SQL Subqueries: WHERE, SELECT and FROM Examples
A subquery is a query written inside another query, in brackets. The inner query produces a value, a list or a small table, and the outer query uses it: to filter rows, to add a calculated column, or as a table to select from.
What Is a Subquery?
Who is paid the most? You cannot write WHERE salary = MAX(salary), because aggregates are not allowed in WHERE. A subquery solves it in two steps: the inner query finds the highest salary, the outer query finds who earns it:
SELECT first_name, last_name, department, salary
FROM employees
WHERE salary = (SELECT MAX(salary) FROM employees);
▶ Run this query in the SQL Playground
| first_name | last_name | department | salary |
|---|---|---|---|
| Steven | Nguyen | Marketing | 204449 |
The inner query, SELECT MAX(salary) FROM employees, returns one number. The outer query then reads as WHERE salary = 204449. Subqueries are also called inner queries or nested queries.
SELECT columns
FROM table
WHERE column operator (SELECT column FROM table WHERE ...);
Types of Subqueries
| Type | Returns | Used with | Example |
|---|---|---|---|
| Single-value (scalar) | One row, one column | =, <, >, or as a column in SELECT | (SELECT AVG(salary) FROM employees) |
| Multi-row | One column, many rows | IN, NOT IN, EXISTS | (SELECT id FROM users WHERE is_premium = 1) |
| Table (derived table) | Rows and columns | In FROM, with an alias | (SELECT department, AVG(salary) ...) d |
| Correlated | Depends on the outer row | Any of the above | WHERE x.department = e.department |
Most subqueries are not correlated: they do not refer to the outer query, so they can be run on their own and are evaluated once. Copy the bracketed part into the Playground and run it to see what the outer query will receive.
Subqueries in WHERE
A single value
Employees who earn more than the company average:
SELECT first_name, last_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC
LIMIT 6;
▶ Run this query in the SQL Playground
| first_name | last_name | salary |
|---|---|---|
| Steven | Nguyen | 204449 |
| Nancy | Thompson | 203999 |
| John | Lee | 203467 |
| Donna | Johnson | 199029 |
| Matthew | Wilson | 196530 |
| Paul | Robinson | 196032 |
A list of values with IN
When the subquery can return several rows, use IN. These are the orders placed by premium customers:
SELECT COUNT(*) AS orders_by_premium_customers
FROM orders
WHERE user_id IN (SELECT id FROM users WHERE is_premium = 1);
▶ Run this query in the SQL Playground
| orders_by_premium_customers |
|---|
| 85 |
Using = instead of IN with a multi-row subquery is an error in PostgreSQL, SQL Server, MySQL and Oracle. SQLite does not complain; it silently uses the first row, which gives a wrong answer without warning.
NOT IN and NOT EXISTS
To find rows with no match, NOT EXISTS is safer than NOT IN, which returns nothing if its list contains a NULL. Customers who have never ordered:
SELECT COUNT(*) AS never_ordered
FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
▶ Run this query in the SQL Playground
| never_ordered |
|---|
| 36 |
See EXISTS and IN vs EXISTS for the details.
salary > ALL (SELECT ...) (greater than every value in the list) and salary > ANY (SELECT ...) (greater than at least one). SQLite does not have them; write > (SELECT MAX(...)) and > (SELECT MIN(...)) instead, which also read more clearly.Subqueries in SELECT
A subquery that returns a single value can be a column. Here every product is shown with the average price of the whole catalogue, and its difference from it:
SELECT name, price,
(SELECT ROUND(AVG(price), 2) FROM products) AS catalogue_avg,
ROUND(price - (SELECT AVG(price) FROM products), 2) AS difference
FROM products
ORDER BY id
LIMIT 6;
▶ Run this query in the SQL Playground
| name | price | catalogue_avg | difference |
|---|---|---|---|
| Play Kitchen Deluxe | 1786.38 | 1048.2 | 738.18 |
| Micro-Cut Paper Shredder 8-Sheet | 907.93 | 1048.2 | -140.27 |
| 4K Dual Dash Camera | 568.89 | 1048.2 | -479.31 |
| Ergonomic Desk Mat XL | 1618.64 | 1048.2 | 570.44 |
| Premium Beard Oil 60ml | 1428.31 | 1048.2 | 380.11 |
| Retinol Eye Cream 30ml | 235.11 | 1048.2 | -813.09 |
If the subquery refers to the outer row, it becomes correlated and can give a different value per row, such as each customer's number of orders:
SELECT u.first_name, u.last_name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS orders
FROM users u
ORDER BY orders DESC, u.id
LIMIT 6;
▶ Run this query in the SQL Playground
| first_name | last_name | orders |
|---|---|---|
| Andrew | Wright | 5 |
| Joseph | King | 5 |
| Laura | Martin | 4 |
| Steven | Rivera | 4 |
| Paul | Young | 4 |
| Michael | Rodriguez | 4 |
Subqueries in FROM (Derived Tables)
A subquery in FROM acts as a temporary table. It must have an alias. This is the usual way to filter or join on aggregated results. Average salary per department, keeping only departments above 130,000:
SELECT d.department, d.employees, d.avg_salary
FROM (SELECT department,
COUNT(*) AS employees,
ROUND(AVG(salary)) AS avg_salary
FROM employees
GROUP BY department) AS d
WHERE d.avg_salary > 130000
ORDER BY d.avg_salary DESC;
▶ Run this query in the SQL Playground
| department | employees | avg_salary |
|---|---|---|
| Customer Success | 15 | 134140 |
| Design | 10 | 133965 |
| Engineering | 11 | 131028 |
The same result could come from GROUP BY with HAVING. Derived tables become necessary when you aggregate twice, for example the average of per-customer totals:
SELECT COUNT(*) AS customers,
ROUND(AVG(spent), 2) AS avg_spend_per_customer,
MAX(spent) AS top_spend
FROM (SELECT user_id, SUM(total) AS spent
FROM orders
GROUP BY user_id) AS per_customer;
▶ Run this query in the SQL Playground
| customers | avg_spend_per_customer | top_spend |
|---|---|---|
| 114 | 4482.2 | 15384.26 |
When a derived table gets long, or you need it twice, a CTE says the same thing more readably.
Nested Subqueries
A subquery can contain another subquery. Read them from the inside out. This counts the customers who have bought something in the category with the highest average price:
SELECT COUNT(DISTINCT o.user_id) AS customers
FROM orders o
WHERE o.id IN (
SELECT oi.order_id
FROM order_items oi
WHERE oi.product_id IN (
SELECT p.id
FROM products p
WHERE p.category = (
SELECT category
FROM products
GROUP BY category
ORDER BY AVG(price) DESC
LIMIT 1)));
▶ Run this query in the SQL Playground
| customers |
|---|
| 45 |
| Level | Returns |
|---|---|
| Innermost | The category with the highest average price: Food & Beverage |
| Next | The ids of the products in that category |
| Next | The ids of orders that contain one of those products |
| Outer | The number of distinct customers who placed those orders: 45 |
SELECT category
FROM products
GROUP BY category
ORDER BY AVG(price) DESC
LIMIT 1;
▶ Run this query in the SQL Playground
| category |
|---|
| Food & Beverage |
Three levels is about the limit for readability. Beyond that, the same logic written with JOINs or CTEs is easier to follow.
Subquery vs JOIN
Many subqueries can be written as joins. The premium-orders count from above, as a JOIN:
SELECT COUNT(*) AS orders_by_premium_customers
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE u.is_premium = 1;
▶ Run this query in the SQL Playground
| orders_by_premium_customers |
|---|
| 85 |
| Subquery | JOIN | |
|---|---|---|
| Output columns | Only from the outer table | From all joined tables |
| Duplicates | IN and EXISTS never repeat outer rows | One row per match |
| Reads well for | "rows where something about another table is true" | "rows combined with data from another table" |
| Performance | Usually the same; optimizers rewrite both | Usually the same |
Choose the form that says what you mean. Use a JOIN when you need columns from the other table; use a subquery when the other table only decides which rows to keep.
Common Subquery Mistakes
Five errors and how to avoid them.
1. = with a subquery that returns several rows
SELECT *
FROM orders
WHERE user_id = (SELECT id FROM users
WHERE is_premium = 1);
SELECT *
FROM orders
WHERE user_id IN (SELECT id FROM users
WHERE is_premium = 1);
There are many premium users. Most databases reject the first query; SQLite compares with just the first one.
2. An aggregate in WHERE instead of a subquery
SELECT *
FROM employees
WHERE salary > AVG(salary);
SELECT *
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
WHERE works on single rows, so it cannot see an average over many rows.
3. A derived table without an alias
SELECT *
FROM (SELECT department, AVG(salary) AS a
FROM employees GROUP BY department);
SELECT *
FROM (SELECT department, AVG(salary) AS a
FROM employees GROUP BY department) AS d;
SQLite accepts the first version, but most databases require an alias for every subquery in FROM.
4. NOT IN with NULLs in the list
WHERE id NOT IN (SELECT manager_id FROM employees)
WHERE NOT EXISTS (SELECT 1 FROM employees r
WHERE r.manager_id = e.id)
manager_id contains NULLs in the Playground, so the NOT IN version matches nothing.
5. Deeply nested subqueries
WHERE id IN (SELECT ... WHERE x IN (SELECT ...
WHERE y = (SELECT ... WHERE z IN (SELECT ...))))
WITH step1 AS (...),
step2 AS (SELECT ... FROM step1)
SELECT ... FROM step2;
Past two or three levels, CTEs or joins make the logic much easier to follow and to test.
Subquery Cheat Sheet
| Task | Pattern |
|---|---|
| Compare with one value | WHERE salary > (SELECT AVG(salary) FROM employees) |
| Match a list | WHERE user_id IN (SELECT id FROM users WHERE is_premium = 1) |
| No match | WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) |
| Value as a column | SELECT name, (SELECT AVG(price) FROM products) AS avg_price FROM products |
| Derived table | FROM (SELECT ... GROUP BY ...) AS d |
| Aggregate twice | SELECT AVG(spent) FROM (SELECT SUM(total) AS spent ... GROUP BY user_id) t |
Practice Subqueries Queries
6 exercises on the real Playground tables.
Write each query yourself first. Every solution has been run against the Playground database.
-
1Easy List the products that cost more than the average product price. Show the 6 cheapest of them. Table:
productsShow solution
SELECT name, price FROM products WHERE price > (SELECT AVG(price) FROM products) ORDER BY price LIMIT 6;▶ Run this query in the SQL Playground
The subquery returns one number, so a plain > comparison works.
-
2Easy Find the employee or employees hired earliest. Table:
employeesShow solution
SELECT first_name, last_name, hire_date FROM employees WHERE hire_date = (SELECT MIN(hire_date) FROM employees);▶ Run this query in the SQL Playground
MIN in a subquery gives the earliest date; = keeps everyone hired that day.
-
3Medium List the products that have at least one 1-star review. Show 8 by name. Table:
reviews, productsShow solution
SELECT name FROM products WHERE id IN (SELECT product_id FROM reviews WHERE rating = 1) ORDER BY name LIMIT 8;▶ Run this query in the SQL Playground
The subquery returns many product ids, so IN is needed.
-
4Medium Using a derived table, find how many customers have placed more than 2 orders. Table:
ordersShow solution
SELECT COUNT(*) AS customers_with_3_plus_orders FROM (SELECT user_id, COUNT(*) AS orders FROM orders GROUP BY user_id) AS t WHERE t.orders > 2;▶ Run this query in the SQL Playground
Count per customer first, then count the customers that pass the filter.
-
5Medium Show each department with its number of employees and the company-wide employee count. Table:
departments, employeesShow solution
SELECT d.name, (SELECT COUNT(*) FROM employees e WHERE e.department_id = d.id) AS employees, (SELECT COUNT(*) FROM employees) AS company_total FROM departments d ORDER BY employees DESC, d.name;▶ Run this query in the SQL Playground
One correlated and one plain subquery in the same SELECT.
-
6Hard Find the cities whose customers spent more in total than the average city. Table:
orders, usersShow solution
SELECT city, spent FROM (SELECT u.city, ROUND(SUM(o.total), 2) AS spent FROM orders o JOIN users u ON u.id = o.user_id GROUP BY u.city) AS c WHERE spent > (SELECT AVG(city_spent) FROM (SELECT SUM(o.total) AS city_spent FROM orders o JOIN users u ON u.id = o.user_id GROUP BY u.city) AS x) ORDER BY spent DESC;▶ Run this query in the SQL Playground
A derived table of city totals, compared with the average of the same totals. A CTE would avoid writing the city totals twice.
📝 Quiz — Subqueries
Test your understanding · 0/15 answered
What is a subquery in SQL?
Also called an inner or nested query.
Which query finds employees earning more than the company average?
Aggregates cannot be used directly in WHERE; the subquery computes the average first.
Which kind of subquery returns exactly one value?
One row, one column, so it works with =, < and >.
Which operator should you use when the subquery can return several rows?
IN (or EXISTS) accepts a list of values.
Where can a subquery appear?
Also in HAVING, and before INSERT or UPDATE values.
What is a subquery in FROM called?
It acts as a temporary table and needs an alias in most databases.
What makes a subquery correlated?
See the correlated subqueries lesson.
Which operator checks that a subquery returns no rows?
Used to find customers with no orders, for example.
Can a subquery be nested inside another subquery?
Read nested subqueries from the inside out.
What is the structural difference between a subquery and a JOIN?
Use a JOIN when you need the other table's columns.
Which clause lets a subquery add a calculated column to every row?
A scalar subquery in SELECT.
What happens if a subquery used with = returns more than one row?
SQLite's behaviour is the dangerous one: no error, possibly a wrong result.
Which condition finds the employees with the highest salary?
In the Playground that is Steven Nguyen with 204,449.
Why might a subquery be rewritten as a JOIN?
Modern optimizers often produce the same plan; check with EXPLAIN.
Why are subqueries common in interviews?
"Salary above average" and "second-highest salary" are classic subquery questions.
Subqueries Interview Questions
Q1. What is the difference between a correlated and a non-correlated subquery?
A non-correlated subquery is independent and runs once. A correlated subquery refers to the outer row and is logically evaluated once per outer row.
Q2. Why can you not write WHERE salary > AVG(salary)?
WHERE evaluates one row at a time and runs before aggregation. Compute the average in a subquery: WHERE salary > (SELECT AVG(salary) FROM employees).
Q3. When do you need a derived table?
When you have to aggregate an aggregate, such as the average of per-customer totals, or filter or join on grouped results before the next step.
Q4. How do you find the second-highest salary with a subquery?
SELECT MAX(salary) FROM employees WHERE salary < (SELECT MAX(salary) FROM employees). For the Nth value in general, DENSE_RANK in a CTE scales better.
Q5. Subquery or JOIN?
A JOIN when you need columns from the other table; a subquery (IN or EXISTS) when the other table only decides which rows to keep. Performance is usually the same in modern databases.
Q6. What do ANY and ALL do?
x > ALL (subquery) is true when x is greater than every value returned, x > ANY (subquery) when it is greater than at least one. They are equivalent to comparing with MAX or MIN of the subquery. SQLite does not support them.
Frequently Asked Questions
What is a subquery in SQL?
A subquery is a query written inside another query. Its result is used by the outer query to filter rows, calculate a value or act as a temporary table. Subqueries are also called inner queries or nested queries.
How do subqueries work?
For a normal (non-correlated) subquery, the database works out the inner query, which gives a value, a list or a set of rows, and the outer query uses that result in its WHERE, SELECT or FROM clause.
What is the difference between a subquery and a JOIN?
A JOIN combines columns from several tables into one result. A subquery nests one query inside another to produce a value, a list or a table for the outer query. Many problems can be solved either way.
Can a subquery return multiple rows?
Yes. A multi-row subquery must be used with operators that accept a list, such as IN, NOT IN or EXISTS. Using it with = is an error in most databases; SQLite silently uses the first row.
What is a nested query?
Another name for a subquery. SQL allows subqueries inside subqueries to several levels.
Are subqueries slow?
Not in general. A non-correlated subquery is evaluated once. A correlated one can be slow on large tables without an index on the column it matches on, and optimizers often rewrite subqueries as joins anyway.
Can subqueries be used in SELECT?
Yes. A subquery that returns one value can be a column, for example the catalogue's average price shown next to every product.
Can subqueries be used in FROM?
Yes. A subquery in FROM is called a derived table. It produces a temporary result, often grouped, that the outer query can filter, join or select from like a real table. It needs an alias in most databases.
What is a correlated subquery?
A subquery that refers to a column of the outer query. It cannot run on its own and is evaluated for every outer row.
Are subqueries important for SQL interviews?
Yes. Questions test single-value versus multi-row subqueries, correlated subqueries, derived tables and when a JOIN is the better choice.