Intermediate

SQL Subqueries Explained: Nested SELECT Statements

8 min readUpdated September 24, 2026Every example verified
On this page 10 sections ▾
  1. Scalar subqueries — a single value
  2. Subqueries with IN — a list of values
  3. EXISTS — does at least one row match?
  4. Subqueries in FROM — a virtual table
  5. Correlated subqueries — one run per row
  6. Subqueries in the SELECT list
  7. Subquery vs JOIN: which one?
  8. Practice this topic
  9. Frequently asked questions
  10. Related reading

A subquery is a SELECT statement nested inside another query, wrapped in parentheses. The inner query runs first (conceptually), and its result feeds into the outer query — as a single value, a list, or an entire virtual table. They're most useful when a JOIN would be awkward or when you only need an aggregate result to compare against, not the joined rows themselves.

Scalar subqueries — a single value

A scalar subquery returns exactly one row, one column — a single value you can use anywhere an expression is allowed, including right inside a comparison:

-- Employees earning more than the company average
SELECT first_name, salary
FROM Employees
WHERE salary > (
    SELECT AVG(salary) FROM Employees
);
first_namesalary
Ananya142000
Marcus118000
Tomas131000
Priya105000
Chen156000

● 5 rows · produced by running this query on the sample database

The inner query computes one number (the average salary across the whole table); the outer query then compares every employee's salary against that single number.

Subqueries with IN — a list of values

When the inner query returns multiple rows of a single column, pair it with IN (or NOT IN):

-- Products that have never been in a shipped order
SELECT product_id, name
FROM Products
WHERE product_id NOT IN (
    SELECT DISTINCT product_id FROM Orders WHERE status = 'Shipped'
);
product_idname
207Harbor Support Plan

● 1 row · produced by running this query on the sample database

Watch out: if the inner query's column can contain NULL, NOT IN can silently return zero rows for the whole query, because comparing anything to NULL is neither true nor false. Every major database does this, SQLite and therefore this playground included; the EXISTS vs IN guide shows it happening on the sample data. If NULLs are possible, filter them out inside the subquery (WHERE product_id IS NOT NULL) or use NOT EXISTS instead.

EXISTS — does at least one row match?

EXISTS doesn't care what the subquery returns, only whether it returns any row at all. You will hear that this makes it faster than IN, since it can stop at the first match, but modern query planners usually turn both into the same plan. Choose between them for readability and for how they treat NULLs, which the EXISTS vs IN guide covers in detail.

-- Customers who have placed at least one order
SELECT company
FROM Customers c
WHERE EXISTS (
    SELECT 1 FROM Orders o WHERE o.customer_id = c.customer_id
);
company
Meridian Trading Co.
Acme Robotics
Sakura Logistics
Kilimanjaro Foods
Andes Textiles
Baltic Systems
Cedar Health
Fjord Analytics

● 8 rows · produced by running this query on the sample database

This is a correlated subquery — notice it references c.customer_id from the outer query. Unlike the earlier examples, this inner query can't run on its own; it re-evaluates once per outer row.

Subqueries in FROM — a virtual table

A subquery can also stand in for a table, letting you query the results of another query:

SELECT dept_id, avg_salary
FROM (
    SELECT dept_id, AVG(salary) AS avg_salary
    FROM Employees
    GROUP BY dept_id
) dept_averages
WHERE avg_salary > 90000;
dept_idavg_salary
1099200
20100000
50124000

● 3 rows · produced by running this query on the sample database

Here the inner query pre-aggregates by department, and the outer query filters those already-computed averages — something you couldn't do directly with WHERE (see our GROUP BY vs HAVING guide for why).

Correlated subqueries — one run per row

Every subquery so far could run on its own. A correlated subquery cannot: it references a column from the outer query, so it has to be evaluated once for each row the outer query considers. That is what makes it powerful, and also why it can be slow on a large table, especially when the column it matches on has no index.

This finds everyone paid above the average for their own department — a question a single plain aggregate cannot answer, because the comparison value changes from row to row:

SELECT e.first_name, e.dept_id, e.salary
FROM Employees e
WHERE e.salary > (SELECT AVG(salary) FROM Employees x WHERE x.dept_id = e.dept_id);
first_namedept_idsalary
Ananya10142000
Marcus10118000
Tomas20131000
Priya20105000
Chen50156000

● 5 rows · produced by running this query on the sample database

The alias e on the outside and x on the inside are doing essential work here. Inside the subquery, x.dept_id means the row being averaged and e.dept_id means the row the outer query is currently testing. Without distinct aliases there would be no way to say which is which.

Subqueries in the SELECT list

A subquery that returns exactly one value can sit in the SELECT list and become a column of its own. This is an easy way to attach a per-row summary without changing the shape of the main query:

SELECT c.company,
       (SELECT COUNT(*) FROM Orders o WHERE o.customer_id = c.customer_id) AS order_count
FROM Customers c
ORDER BY order_count DESC;
companyorder_count
Meridian Trading Co.3
Acme Robotics2
Sakura Logistics2
Andes Textiles2
Cedar Health2
Fjord Analytics2
Kilimanjaro Foods1
Baltic Systems1

● 8 rows · produced by running this query on the sample database

It must return one column and at most one row (no row at all gives NULL). If it returns several rows, PostgreSQL, MySQL, SQL Server and Oracle stop with an error. SQLite, which runs this playground, does not: it quietly uses the first row it finds, so WHERE salary = (SELECT salary FROM Employees WHERE dept_id = 10) returns Ananya alone here instead of failing. The fix is the same everywhere: make sure the subquery can only produce one row, or compare with IN. For the order counts above, a LEFT JOIN with GROUP BY gives the same answer and can run faster over large tables, but this form is often clearer when you only need one such figure.

Subquery vs JOIN: which one?

Both often solve the same problem. General rule of thumb:

  • Need columns from both tables in the result? Use a JOIN.
  • Only need to filter or compare against a value/list computed from another table? A subquery is usually more readable.
  • Checking existence without needing any data from the other table? EXISTS reads more clearly than a JOIN + DISTINCT.

Try it yourself

Every subquery example above runs against the pre-loaded Employees, Products, Orders and Customers tables.

Open the Playground

Practice this topic

Reading explains it; solving it is what makes it stick. These exercises use exactly what this page covered, and your answer is checked by running it against the same database:

See all 42 exercises →

Frequently asked questions

What is a subquery in SQL?

A subquery is a SELECT statement nested inside another statement. It runs first and hands its result to the outer query, which can use that result as a value, as a list to match against, or as a table to select from.

What is the difference between a correlated and a non-correlated subquery?

A non-correlated subquery is self-contained: it runs once and its result is reused. A correlated subquery references a column from the outer query, so it is logically evaluated once per outer row — more flexible, but it can be slow on large tables.

Should I use a subquery or a JOIN?

If you need columns from both tables in the output, use a JOIN. If you only need one table’s columns and the other is there purely to filter, a subquery with IN or EXISTS often reads more clearly. On large tables, measure rather than assume.

What is the difference between IN and EXISTS?

IN compares a value against a list of results. EXISTS only checks whether the subquery returns any row at all and stops at the first one it finds. EXISTS is also safe when the inner query can produce NULLs, which is where NOT IN goes wrong.

Why does my subquery return a “more than one row” error?

Because it is being used where a single value is expected — typically on the right-hand side of =. PostgreSQL, MySQL, SQL Server and Oracle raise that error; SQLite does not, and silently uses the first row instead, so there the same mistake goes unnoticed. Either add a condition so it returns exactly one row, or switch the operator to IN, which is designed to accept many.

Can a subquery be nested inside another subquery?

Yes, and most databases allow several levels. Readability degrades quickly, though. Past two levels a common table expression written with WITH usually expresses the same logic far more clearly.

About the author

SQL Practice

SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.