Introduction
A subquery is a query nested inside another query. They help solve complex problems step by step — you can use the result of one query as input to another. Subqueries are one of the most powerful tools in SQL.
Key Concepts
- Scalar subquery: A subquery that returns a single value (one row, one column). Used with comparison operators like
=,>,<. - List subquery: A subquery that returns a list of values (multiple rows, one column). Used with
IN,NOT IN,ANY,ALL. - Correlated subquery: A subquery that references columns from the outer query. It runs once per row of the outer query.
- EXISTS: Tests whether a subquery returns any rows. More efficient than IN for large datasets.
- Derived table: A subquery in the FROM clause that acts as a temporary table.
Real World Context
Subqueries solve problems that cannot be expressed in a single flat query: "find users who ordered more than the average", "find products that have never been ordered", "show each employee's salary relative to their department's average". These patterns appear in every real application.
Deep Dive
Scalar Subqueries
Scalar subqueries return a single value and can be used anywhere a single value is expected:
sql-- Books longer than average SELECT title, pages FROM books WHERE pages > (SELECT AVG(pages) FROM books); -- Latest order SELECT * FROM orders WHERE created_at = (SELECT MAX(created_at) FROM orders);
The inner query computes one value (average pages, maximum date), and the outer query uses it as a filter.
Subqueries with IN
IN subqueries return a list of values to filter against:
sql-- Books by authors from the USA SELECT title FROM books WHERE author_id IN ( SELECT id FROM authors WHERE country = 'USA' ); -- Users who have never ordered SELECT name FROM users WHERE id NOT IN ( SELECT DISTINCT user_id FROM orders );
The inner query produces a list of IDs, and the outer query checks membership in that list.
Correlated Subqueries
Correlated subqueries reference the outer query, running once per row:
sql-- Authors with more than 3 books SELECT name FROM authors a WHERE ( SELECT COUNT(*) FROM books b WHERE b.author_id = a.id ) > 3;
Notice b.author_id = a.id — this references the outer query's a.id, making it correlated. The subquery runs for each author.
EXISTS Subqueries
EXISTS checks whether any rows match, and is often more efficient than IN:
sql-- Authors who have written at least one book SELECT name FROM authors a WHERE EXISTS ( SELECT 1 FROM books b WHERE b.author_id = a.id ); -- Products never ordered SELECT name FROM products p WHERE NOT EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.id );
EXISTS stops as soon as it finds one matching row, making it efficient for large tables.
Subqueries in FROM (Derived Tables)
Subqueries in the FROM clause create temporary tables:
sql-- Average of category averages SELECT AVG(category_avg) AS overall_avg FROM ( SELECT category, AVG(price) AS category_avg FROM products GROUP BY category ) AS category_averages;
The inner query groups by category; the outer query aggregates those results. For complex cases, CTEs (next lesson) are often clearer.
Common Pitfalls
- NOT IN with NULLs — If the subquery returns any NULL values,
NOT INreturns zero rows because NULL comparisons are always unknown. UseNOT EXISTSinstead for safety. - Correlated subqueries and performance — Correlated subqueries run once per row in the outer query, which can be very slow on large tables. Consider rewriting with JOINs or CTEs.
Best Practices
- Prefer EXISTS over IN for large datasets — EXISTS stops at the first match, while IN must evaluate the entire list.
- Use NOT EXISTS instead of NOT IN — NOT EXISTS handles NULLs correctly, while NOT IN does not.
- Consider CTEs for complex subqueries — When subqueries get nested or repeated, CTEs (WITH clause) improve readability.
Summary
- Scalar subqueries return one value; list subqueries return multiple values.
- Correlated subqueries reference the outer query and run once per row.
- EXISTS is more efficient than IN for checking existence in large tables.
- NOT EXISTS is safer than NOT IN because it handles NULLs correctly.
- Derived tables (subqueries in FROM) create temporary tables for further querying.
- For complex cases, prefer CTEs over deeply nested subqueries.
Code Examples
-- Find products priced above their category's average
SELECT p.name, p.price, p.category
FROM products p
WHERE p.price > (
SELECT AVG(p2.price)
FROM products p2
WHERE p2.category = p.category -- Correlated!
)
ORDER BY p.category, p.price DESC;