Subqueries in SQL
A subquery is a query nested inside another query. It lets you use the result of one SELECT as an input to another — to filter, compare, or compute. Subqueries are how you answer layered questions like "which students are older than the average?" where you need one result (the average) to evaluate another (each student).
They can appear in the WHERE, FROM, or SELECT clause, and they come in a few distinct flavours.
Scalar subquery: returns a single value
A scalar subquery returns exactly one row and one column. You can use it anywhere a single value is expected:
-- PostgreSQL / MySQL compatible
-- Students older than the overall average age
SELECT full_name, age
FROM student
WHERE age > (SELECT AVG(age) FROM student);
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ scalar subquery: one number
The inner query computes the average once; the outer query compares each student to it. If a "scalar" subquery accidentally returns more than one row, the database raises an error — so make sure it truly yields one value.
Subquery with IN: membership
A subquery returning a list of values pairs with IN:
-- Students who are enrolled in at least one course
SELECT full_name
FROM student
WHERE student_id IN (SELECT student_id FROM enrolment);
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ list of student_ids
This keeps students whose ID appears anywhere in the enrolment table.
Be careful with NOT IN and NULLs: if the subquery's list contains any NULL, NOT IN returns no rows at all (a well-known SQL gotcha). Prefer NOT EXISTS for the "not present" case.
EXISTS: does a matching row exist?
EXISTS checks whether a subquery returns any row — it cares about existence, not values. It is often the clearest and most efficient way to ask "is there a related row?"
-- Students who have at least one enrolment, using EXISTS
SELECT s.full_name
FROM student s
WHERE EXISTS (
SELECT 1
FROM enrolment e
WHERE e.student_id = s.student_id -- correlated: refers to the outer row
);
The inner SELECT 1 is conventional — EXISTS ignores the selected columns and only asks "did any row come back?"
Correlated subqueries
The EXISTS example above is a correlated subquery: the inner query references a column from the outer query (s.student_id). That means it is evaluated once per outer row, not once overall. Correlated subqueries are powerful but can be slower — the database may run them repeatedly.
The opposite is a non-correlated (independent) subquery, like the AVG(age) example, which runs once and is reused.
-- Correlated: find students with NO enrolment (the "anti-join" pattern)
SELECT s.full_name
FROM student s
WHERE NOT EXISTS (
SELECT 1 FROM enrolment e WHERE e.student_id = s.student_id
);
NOT EXISTS is the safe, NULL-proof way to find unmatched rows.
Want to learn this properly?
Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.
Browse coursesSubquery in the FROM clause (derived table)
A subquery in FROM acts as a temporary table — often called a derived table. You must give it an alias:
-- Count students per city, then keep only the busy cities
SELECT city, n
FROM (
SELECT city, COUNT(*) AS n
FROM student
GROUP BY city
) AS city_counts -- alias is mandatory
WHERE n > 5;
For complex cases, a Common Table Expression (CTE) with WITH is a more readable alternative to a FROM-clause subquery, and both PostgreSQL and MySQL 8+ support CTEs.
Subquery in the SELECT list
You can compute a per-row value with a scalar subquery in SELECT:
-- Show each student plus their enrolment count
SELECT
s.full_name,
(SELECT COUNT(*) FROM enrolment e WHERE e.student_id = s.student_id) AS courses
FROM student s;
This is correlated and runs per row. For many rows, a LEFT JOIN with GROUP BY is usually faster — which raises the key question below.
Subquery or JOIN?
Many subqueries can be rewritten as joins, and vice versa. Rough guidance:
- Use a JOIN when you need columns from both tables in the output.
- Use EXISTS / IN when you only need to test for a related row, not display its data.
- Use a scalar subquery for a single computed value used in a comparison.
Modern query optimizers often turn one into the other internally, so write whichever is clearest, then optimise only if measurement shows a problem.
Common mistakes
NOT INwith NULLs. If the subquery can return NULL,NOT INyields no rows. UseNOT EXISTSinstead.- Scalar subquery returning multiple rows. Used where one value is expected, it errors out. Add a
LIMIT 1or an aggregate, or fix the logic. - Forgetting the alias on a
FROMsubquery. Derived tables must be named. - Overusing correlated subqueries in SELECT. Running a subquery per row can be slow on large tables; a join often does the same job in one pass.
- Assuming subqueries are always slower than joins. Not true —
EXISTSis frequently as fast or faster. Measure rather than guess.
FAQ
Can a subquery be inside another subquery?
Yes, subqueries can nest several levels deep. Readability suffers quickly, though — CTEs (WITH) usually express deep nesting more clearly.
What is the difference between IN and EXISTS?
IN compares a value against a list of returned values; EXISTS just checks whether any row exists. For "is there a match?" questions, EXISTS (and NOT EXISTS) handles NULLs more safely.
Are CTEs the same as subqueries?
A CTE (WITH name AS (...)) is a named subquery you can reference, sometimes more than once. It is functionally similar but far more readable for complex queries.
Keep learning
Subqueries build on SQL SELECT basics, often replace or complement SQL joins, and frequently wrap aggregate functions.
Want to master when to reach for a subquery vs a join? Join the waitlist for the DBMS & SQL course at Infoplanet, Jalgaon.
Want to learn this properly?
Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.
Browse coursesFounder, Infoplanet
Atul Kabra founded Infoplanet in 2001 and has spent over two decades teaching programming — C, C++, Java, databases and more — to students across Maharashtra.
Related guides
The ER Model Explained
A plain-language guide to the Entity-Relationship model — the conceptual blueprint you draw before you build any database. Covers entities, attributes, keys, relationships, cardinality, and how to convert an ER diagram into tables.
Keys in DBMS Explained
Every type of key in a relational database explained simply: super, candidate, primary, alternate, composite, foreign, and surrogate keys — with SQL you can run.
Database Normalization (1NF–BCNF)
A step-by-step walk through normalization from 1NF to BCNF, using one example table refactored stage by stage. Covers functional dependencies, the anomalies normalization prevents, and when denormalization makes sense.
