SQLab Hub
Lesson 19 · Subqueries
Lesson 19 · Subqueries

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.

( ) Query inside a query ▶ Runnable Playground examples 🧪 6 practice exercises 📝 15-question quiz ⏱ ~25 min

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:

SQLite · runs in the Playground
SELECT first_name, last_name, department, salary
FROM employees
WHERE salary = (SELECT MAX(salary) FROM employees);

▶ Run this query in the SQL Playground

The highest-paid employee
first_namelast_namedepartmentsalary
StevenNguyenMarketing204449
✓ 1 row · real output from the Playground database

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.

SQL · syntax
SELECT columns
FROM table
WHERE column operator (SELECT column FROM table WHERE ...);

Types of Subqueries

TypeReturnsUsed withExample
Single-value (scalar)One row, one column=, <, >, or as a column in SELECT(SELECT AVG(salary) FROM employees)
Multi-rowOne column, many rowsIN, NOT IN, EXISTS(SELECT id FROM users WHERE is_premium = 1)
Table (derived table)Rows and columnsIn FROM, with an alias(SELECT department, AVG(salary) ...) d
CorrelatedDepends on the outer rowAny of the aboveWHERE 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:

SQLite · runs in the Playground
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

Employees above the average salary
first_namelast_namesalary
StevenNguyen204449
NancyThompson203999
JohnLee203467
DonnaJohnson199029
MatthewWilson196530
PaulRobinson196032
✓ 6 rows · real output from the Playground database

A list of values with IN

When the subquery can return several rows, use IN. These are the orders placed by premium customers:

SQLite · runs in the Playground
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 from premium customers
orders_by_premium_customers
85
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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

Customers with no orders
never_ordered
36
✓ 1 row · real output from the Playground database

See EXISTS and IN vs EXISTS for the details.

ANY and ALL. PostgreSQL, MySQL, SQL Server and Oracle also accept 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:

SQLite · runs in the Playground
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

Products next to the catalogue average
namepricecatalogue_avgdifference
Play Kitchen Deluxe1786.381048.2738.18
Micro-Cut Paper Shredder 8-Sheet907.931048.2-140.27
4K Dual Dash Camera568.891048.2-479.31
Ergonomic Desk Mat XL1618.641048.2570.44
Premium Beard Oil 60ml1428.311048.2380.11
Retinol Eye Cream 30ml235.111048.2-813.09
✓ 6 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Customers with their order count
first_namelast_nameorders
AndrewWright5
JosephKing5
LauraMartin4
StevenRivera4
PaulYoung4
MichaelRodriguez4
✓ 6 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Departments with an average salary above 130,000
departmentemployeesavg_salary
Customer Success15134140
Design10133965
Engineering11131028
✓ 3 rows · real output from the Playground database

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:

SQLite · runs in the Playground
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

Average spend per customer, from per-customer totals
customersavg_spend_per_customertop_spend
1144482.215384.26
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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 who bought from the priciest category
customers
45
✓ 1 row · real output from the Playground database
LevelReturns
InnermostThe category with the highest average price: Food & Beverage
NextThe ids of the products in that category
NextThe ids of orders that contain one of those products
OuterThe number of distinct customers who placed those orders: 45
SQLite · runs in the Playground
SELECT category
FROM products
GROUP BY category
ORDER BY AVG(price) DESC
LIMIT 1;

▶ Run this query in the SQL Playground

The innermost subquery on its own
category
Food & Beverage
✓ 1 row · real output from the Playground database

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:

SQLite · runs in the Playground
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 from premium customers with a JOIN
orders_by_premium_customers
85
✓ 1 row · real output from the Playground database
SubqueryJOIN
Output columnsOnly from the outer tableFrom all joined tables
DuplicatesIN and EXISTS never repeat outer rowsOne row per match
Reads well for"rows where something about another table is true""rows combined with data from another table"
PerformanceUsually the same; optimizers rewrite bothUsually 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

❌ Error, or a wrong answer in SQLite
SQL
SELECT *
FROM orders
WHERE user_id = (SELECT id FROM users
                 WHERE is_premium = 1);
✅ IN accepts a list
SQL
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

❌ Error: misuse of aggregate
SQL
SELECT *
FROM employees
WHERE salary > AVG(salary);
✅ Compute it in a subquery
SQL
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

❌ Error in MySQL, PostgreSQL before 16, SQL Server
SQL
SELECT *
FROM (SELECT department, AVG(salary) AS a
      FROM employees GROUP BY department);
✅ Give it a name
SQL
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

❌ Returns no rows
SQL
WHERE id NOT IN (SELECT manager_id FROM employees)
✅ NOT EXISTS
SQL
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

❌ Hard to read and debug
SQL
WHERE id IN (SELECT ... WHERE x IN (SELECT ...
  WHERE y = (SELECT ... WHERE z IN (SELECT ...))))
✅ Name the steps
SQL
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

TaskPattern
Compare with one valueWHERE salary > (SELECT AVG(salary) FROM employees)
Match a listWHERE user_id IN (SELECT id FROM users WHERE is_premium = 1)
No matchWHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
Value as a columnSELECT name, (SELECT AVG(price) FROM products) AS avg_price FROM products
Derived tableFROM (SELECT ... GROUP BY ...) AS d
Aggregate twiceSELECT 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.

  1. 1Easy List the products that cost more than the average product price. Show the 6 cheapest of them. Table: products

    Show solution
    SQLite · runs in the Playground
    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.

  2. 2Easy Find the employee or employees hired earliest. Table: employees

    Show solution
    SQLite · runs in the Playground
    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.

  3. 3Medium List the products that have at least one 1-star review. Show 8 by name. Table: reviews, products

    Show solution
    SQLite · runs in the Playground
    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.

  4. 4Medium Using a derived table, find how many customers have placed more than 2 orders. Table: orders

    Show solution
    SQLite · runs in the Playground
    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.

  5. 5Medium Show each department with its number of employees and the company-wide employee count. Table: departments, employees

    Show solution
    SQLite · runs in the Playground
    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.

  6. 6Hard Find the cities whose customers spent more in total than the average city. Table: orders, users

    Show solution
    SQLite · runs in the Playground
    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

Question 1 of 15

What is a subquery in SQL?

Question 2 of 15

Which query finds employees earning more than the company average?

Question 3 of 15

Which kind of subquery returns exactly one value?

Question 4 of 15

Which operator should you use when the subquery can return several rows?

Question 5 of 15

Where can a subquery appear?

Question 6 of 15

What is a subquery in FROM called?

Question 7 of 15

What makes a subquery correlated?

Question 8 of 15

Which operator checks that a subquery returns no rows?

Question 9 of 15

Can a subquery be nested inside another subquery?

Question 10 of 15

What is the structural difference between a subquery and a JOIN?

Question 11 of 15

Which clause lets a subquery add a calculated column to every row?

Question 12 of 15

What happens if a subquery used with = returns more than one row?

Question 13 of 15

Which condition finds the employees with the highest salary?

Question 14 of 15

Why might a subquery be rewritten as a JOIN?

Question 15 of 15

Why are subqueries common in interviews?

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.