A subquery is simply a SELECT query nested inside another query, used wherever a single value, a list of values, or an entire mini result set is needed. This lesson covers the three places a subquery commonly appears, and how to tell when a subquery is clearer than a join, or when it's the other way around.
Learning Objectives
- Use a subquery inside WHERE to filter against a computed list of values.
- Use a scalar subquery inside SELECT to pull in a single computed value per row.
- Explain what makes a subquery "correlated," and compare subqueries to joins.
Inner Query Runs
Produces a value, list, or table
Used by Outer Query
In WHERE, SELECT, or FROM
Final Result
One combined query
Subquery in WHERE
The inner query runs first, producing a list of department IDs. The outer query's IN then filters employees against that list, exactly like IN (1, 2, 3), except the values come from a query instead of being typed out by hand.
SELECT first_name, department
FROM employees
WHERE department_id IN (
SELECT id FROM departments WHERE name IN ('Engineering', 'Research')
);Scalar Subquery in SELECT
A scalar subquery returns exactly one value, here, the company-wide average salary, repeated on every row alongside each employee's own salary for easy comparison. If a subquery used this way could return more than one row, the database raises an error, a scalar subquery must collapse to a single value.
SELECT first_name, salary, (SELECT AVG(salary) FROM employees) AS company_avg_salary FROM employees;
Subquery in FROM (a Derived Table)
The inner query produces a complete mini result set, department averages, and the outer query treats it exactly like a regular table, giving it an alias (department_averages) and querying it further. This is useful when you need to filter or sort on a value that itself required aggregation, avg_salary here can't be filtered with WHERE directly, but it can once it's a column in a derived table.
SELECT department, avg_salary FROM ( SELECT department, AVG(salary) AS avg_salary FROM employees GROUP BY department ) AS department_averages WHERE avg_salary > 90000;
Correlated Subqueries
A correlated subquery references a column from the outer query (e.department here), which means it can't run once up front, it has to run again for every row the outer query considers, each time with that row's own department. This finds employees earning more than their own department's average, something a plain (non-correlated) subquery can't express.
SELECT first_name, salary, department FROM employees e WHERE salary > ( SELECT AVG(salary) FROM employees WHERE department = e.department );
Correlated subqueries are powerful but can be slow on large tables, since the inner query potentially runs once per outer row. The same result is often expressible as a join with a derived table, which a database's query optimizer can usually execute more efficiently.
Subquery vs Join
Many queries can be written either way. As a rule of thumb: reach for a join when you need columns from both tables in the final result, and a subquery when you just need to filter or compare against a computed value or list, without actually displaying anything from the inner query.
- Often reads more naturally for "filter by a computed list" or "compare to an aggregate"
- A correlated subquery can be slower, it may re-run per outer row
- Easy to compose and nest for complex, step-by-step logic
- Usually faster for combining and returning columns from multiple tables
- The query optimizer has more room to find an efficient execution plan
- Preferred when you actually need columns from both tables in the result
Frequently Asked Questions
What's the difference between IN (subquery) and EXISTS (subquery)?
IN compares a value against a list the subquery returns. EXISTS just checks whether the subquery returns any rows at all, true or false, and is often faster for a correlated "does a related row exist" check, since the database can stop as soon as it finds one match instead of building a full list.
Can a subquery return more than one column?
Yes, when it's used in FROM as a derived table, or with multi-column IN comparisons on databases that support it. A subquery used as a single value in SELECT or with = must return exactly one column and one row.
Is there a performance difference between a subquery and a CTE (WITH clause)?
A CTE is mostly a readability tool, in most modern databases, a CTE and an equivalent derived subquery produce the same execution plan. CTEs become genuinely necessary for recursive queries, beyond this lesson's scope.
Common Beginner Mistakes
Using = with a subquery that can return more than one row
WHERE department_id = (SELECT id FROM departments WHERE ...) errors at runtime the moment the subquery returns more than one row. Use IN instead of = whenever the subquery might return multiple rows.
Writing a correlated subquery without realizing it'll re-run for every outer row
This can be fine on a small table and seriously slow on a large one. If performance matters, check whether the same logic is expressible as a join, which usually performs better.
Forgetting to alias a derived table in FROM
FROM (SELECT ...) WHERE ... without AS some_name is a syntax error in MySQL and PostgreSQL, every derived table needs a name, even if nothing ever explicitly references it by that name.
Best Practices
- Use IN rather than = when a subquery could plausibly return more than one row.
- Reach for EXISTS instead of IN for a correlated "does a matching row exist" check, it's typically faster.
- Consider rewriting a slow correlated subquery as a join with a derived table before assuming the query just can't be made faster.
- Always alias a derived table in FROM, even a short one-letter alias, it's required syntax, not optional style.
Interview Questions
Write a query that finds employees earning more than the average salary of their own department.
SELECT first_name, salary, department FROM employees e WHERE salary > (SELECT AVG(salary) FROM employees WHERE department = e.department); — a correlated subquery, since the inner query's condition depends on e.department from the outer row.
When would you choose EXISTS over IN?
When you only need to know whether at least one related row exists, not what its values are, and especially in a correlated context. EXISTS can short-circuit as soon as it finds one matching row, while IN typically has to materialize a full list from the subquery first, making EXISTS often the faster choice for existence checks on large tables.
Rewrite this correlated subquery as a join: employees earning above their department's average.
SELECT e.first_name, e.salary FROM employees e JOIN (SELECT department, AVG(salary) AS avg_salary FROM employees GROUP BY department) d ON e.department = d.department WHERE e.salary > d.avg_salary; — the department averages become a derived table, joined once instead of re-queried per row.
Summary
This lesson covered subqueries in WHERE (filtering against a computed list), SELECT (a scalar value per row), and FROM (a derived table), what makes a subquery correlated, and how to decide between a subquery and a join for a given question.
That completes Module 2, Working With Data: writing data with INSERT, UPDATE, and DELETE, choosing correct data types, handling NULL properly, and summarizing and combining data with aggregate functions, GROUP BY, joins, and subqueries.
What's Next?
Module 3, Database Design, is next: primary and foreign keys, creating tables with constraints, indexes and performance, and transactions.
Ready to Practice SQL?
Write and run real SQL queries online, right in your browser, no install or signup required. Pick a playground based on which database you want to practice on.
Both open a third-party SQL playground in a new tab. External site terms and privacy policy apply.