Think of a nested subquery like a note inside a note: the inner query answers a smaller question first, and the outer query uses that answer to make the final decision.
Question: What is a nested subquery in SQL?
Answer: A nested subquery is a SQL query written inside another SQL query. The inner query can produce a single value, a list of values, or a temporary row set, and the outer query uses that result to filter, compare, or calculate something else. It is useful when one question depends on the answer to another question.
Interview-Ready Answer: “A nested subquery is a query inside another query. I use it when the outer query needs a value or set of rows produced by an inner query, like comparing to an average or filtering by a computed list. One detail I always mention is that correlated subqueries can be much slower because the inner query may run once per outer row, while an uncorrelated subquery usually runs once.”
A nested subquery is just a query inside another query. The inner query can be a scalar subquery, meaning it returns one value; a row or table subquery, meaning it returns multiple rows or columns; or a correlated subquery, meaning the inner query refers to columns from the outer query. If it does not depend on the outer query, it is uncorrelated.
WHERE, SELECT, HAVING, or FROM.EXISTS or IN, and for anti-filters with NOT EXISTS.| Approach | Best for | Trade-off |
|---|---|---|
| Nested subquery | One-off filtering | Concise, but can be harder to read |
| JOIN | Matching and returning columns | Often clearer for row-to-row relationships |
| CTE | Step-by-step logic | Readable and reusable; some databases may materialize it, while others inline it |
In newer PostgreSQL versions, many non-recursive CTEs can be inlined instead of forced into a separate step; older versions treated them more like an optimization fence, which could block rewrites. That is why a CTE can be easier to read without always being slower, but the exact behavior depends on the database and version.
O(n + m) overall because the inner query runs once and the outer query scans once.O(n × m) in the worst case if the inner query is repeated for each outer row and no useful index exists.orders(customer_id), the same logic can shrink to many cheap index lookups instead of huge full scans.NOT IN and NULL. If the subquery returns even one NULL, the comparison becomes unknown and can eliminate every row.Memory wise, a good way to think about nested subqueries is: the inner query is the calculator, the outer query is the decision-maker. If the calculator is reused once, great. If it is run again for every row, you should suspect a performance problem.
Imagine a checkout service for an online store. The product team asks for a report of customers whose lifetime spend is above the average customer spend, so the team writes a nested subquery to compute each customer total and compare it with the overall average.
It works in staging, but in production the query is correlated and runs against millions of order rows. The database starts doing repeated scans, CPU jumps to 100%, and the checkout API p95 latency climbs from 120 ms to 8 seconds. In logs, you see statement timeout errors and a slow-query alert tied to the report endpoint. Customers notice spinning loaders, delayed checkout confirmations, and retry storms that make the database even hotter.
The fix is usually to add the right index, rewrite the nested subquery into a clearer join or pre-aggregated CTE, and test on production-sized data. The lesson: nested subqueries are fine, but you need to know whether the inner query is run once or many times.
-- Fresh demo data
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount DECIMAL(10,2) NOT NULL
);
INSERT INTO customers (customer_id, name) VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Cara'),
(4, 'Dev');
INSERT INTO orders (order_id, customer_id, amount) VALUES
(101, 1, 120.00),
(102, 1, 80.00),
(103, 2, 60.00),
(104, 3, 200.00),
(105, NULL, 999.00); -- NULL is here on purpose to show a real edge case
-- Nested subquery: find customers whose total spend is above the average customer total.
-- The inner derived table first computes each customer's lifetime spend.
-- The outer HAVING compares that spend against the average of all customer totals.
SELECT c.customer_id, c.name
FROM customers c
WHERE c.customer_id IN (
SELECT o.customer_id
FROM orders o
GROUP BY o.customer_id
HAVING SUM(o.amount) > (
SELECT AVG(customer_total)
FROM (
SELECT customer_id, SUM(amount) AS customer_total
FROM orders
GROUP BY customer_id
) totals
)
)
ORDER BY c.customer_id;
-- Scalar nested subquery: one value per outer row.
-- This is a common pattern when you want a derived metric next to each row.
SELECT c.customer_id, c.name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.customer_id
) AS order_count
FROM customers c
ORDER BY c.customer_id;
-- Failure path: NOT IN + NULL can eliminate every row.
-- Because the subquery returns a NULL customer_id, the comparison becomes unknown.
SELECT c.customer_id, c.name
FROM customers c
WHERE c.customer_id NOT IN (
SELECT o.customer_id
FROM orders o
WHERE o.amount > 150 OR o.customer_id IS NULL
)
ORDER BY c.customer_id;
-- Safer version: NOT EXISTS avoids the NULL trap and expresses the intent directly.
SELECT c.customer_id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.amount > 150
AND o.customer_id = c.customer_id
)
ORDER BY c.customer_id;Follow-up & Tricky Questions:
EXISTS or IN. I choose a JOIN when I need to return columns from both sides or make the relationship between rows explicit.WHERE, SELECT, HAVING, and FROM. The exact placement changes whether you are filtering rows, computing a value, or building a derived table.NOT IN return no rows when the subquery contains NULL? SQL treats that comparison as unknown, not true or false, so every candidate row can fail the test. NOT EXISTS is usually the safer choice.Common Mistakes:
NOT EXISTS or filter NULLs explicitly.Memory Hook: Think: inner answers, outer decides. The inner query is the helper; the outer query is the boss.
Cheat Sheet:
EXISTS is great for existence checks.NOT EXISTS is safer than NOT IN with NULLs.Practice Tasks:
NOT IN behaves versus NOT EXISTS.