datarekha

Subqueries

Queries inside queries — scalar, IN, EXISTS, and the correlated subquery every analyst eventually writes.

6 min read Intermediate SQL Lesson 10 of 27

What you'll learn

  • When a subquery reads cleaner than a JOIN
  • Scalar vs IN vs EXISTS, and why EXISTS handles NULLs better
  • What makes a subquery 'correlated', and its performance cost
  • Derived tables (a subquery in FROM) for aggregate-then-compare

Before you start

A subquery is just a SELECT nested inside another SELECT. The whole skill is recognising where to put it — each position has its own job. We will use these two tables:

productscategoryprice
PencilStationery2
NotebookStationery6
CoffeeGrocery9
LaptopElectronics900
CableElectronics12
ordersuser_id
1011
1021
1033

In WHERE — a value, a set, or a truth check

A scalar subquery returns exactly one value, usable anywhere a value goes. It runs once, and the outer query compares each row to it:

SELECT name, price
FROM   products
WHERE  price > (SELECT AVG(price) FROM products);   -- average is 185.8
nameprice
Laptop900

IN (subquery) checks membership in the set the subquery returns; EXISTS (subquery) ignores what the subquery returns and asks only whether it returns any row. EXISTS is the NULL-safe favourite — “users who have placed at least one order”:

SELECT u.name
FROM   users u
WHERE  EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

That o.user_id = u.id references a column from the outer query, which makes it a correlated subquery — one that logically re-runs for every outer row, rather than once. It is the source of both its power and its cost.

In SELECT — annotate each row (the slow way)

A scalar subquery in SELECT computes a per-row value:

SELECT u.name,
       (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM   users u
ORDER BY order_count DESC;

This works, but a correlated subquery in SELECT fires a fresh scan for every row — O(n²) in the worst case, fine on small data and ruinous on a 100-million-row table. The senior move is to spot the pattern (“a per-row aggregate over a related set”) and rewrite it as a window function: COUNT(*) OVER (PARTITION BY user_id) does the same job in a single pass.

In FROM — the derived table

When you need to aggregate first and then compare, wrap the aggregate in a subquery, alias it, and treat it like a table. “Products priced above the average for their own category”:

SELECT p.name, p.category, p.price, c.avg_price
FROM   products p
JOIN  (SELECT category, AVG(price) AS avg_price
       FROM products GROUP BY category) c
  ON   c.category = p.category
WHERE  p.price > c.avg_price;
namecategorypriceavg_price
NotebookStationery64
LaptopElectronics900456

The inner query collapses to one row per category (Stationery 4, Grocery 9, Electronics 456); the outer joins each product to its category’s average and keeps the ones above it. That “aggregate, then compare back” shape is the skeleton of most non-trivial analytics SQL.

Use a subquery when…Use a JOIN when…
Filtering by membership (IN/EXISTS)You need columns from both sides
Comparing to a single aggregateYou’re combining rows row-for-row

Practice

Quick check

0/3
Q1Which form returns no rows if the inner result contains a NULL?
Q2What makes a subquery 'correlated'?
Q3You want each product's price compared to its own category's average. Cleanest pattern?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
What is the difference between a correlated and an uncorrelated subquery, and when does the distinction matter for performance?

An uncorrelated subquery executes once and its result is fed into the outer query; a correlated subquery references columns from the outer query and re-executes for every row the outer query processes. The distinction matters because correlated subqueries can silently turn an O(n) query into O(n²).

What is a derived table, and how does it differ from a correlated subquery or a CTE?

A derived table is an inline subquery in the FROM clause that acts as a virtual table for the duration of the query; it is not correlated to the outer query and has no name reuse. A CTE is named and can be referenced multiple times, while a correlated subquery executes per-row in WHERE or SELECT.

What is a scalar subquery, where can it appear in a SQL statement, and what happens if it returns more than one row?

A scalar subquery is a subquery that returns exactly one column and one row, and can appear anywhere a single value is valid — SELECT list, WHERE clause, HAVING clause, or even a JOIN ON condition. If it returns more than one row at runtime the database raises a runtime error, not a compile-time error.

What are the risks of placing a correlated subquery in the SELECT list, and what is the preferred rewrite?

A correlated subquery in the SELECT list executes once per output row, turning what looks like a simple projection into an O(n) nested loop. The preferred rewrites are a window function or a pre-aggregating JOIN, both of which the optimizer can execute in a single pass.

Related lessons

Explore further

Skip to content