SQL Subqueries & CTEs
Break complex problems into steps: nest one query inside another (subqueries), or give intermediate results a name you can reuse (Common Table Expressions).
Learning Objectives
- Explain what a subquery is and where it can appear (SELECT, WHERE, FROM).
- Distinguish non-correlated vs correlated subqueries.
- Solve the classic "second-highest value" problem using a subquery.
- Write a Common Table Expression (CTE) with the
WITHclause. - Explain when to use a CTE vs a subquery vs a join.
- Recognize recursive CTEs for hierarchy problems (conceptual).
0. The Sample Tables
We continue with the familiar customers and transactions tables.
| customer_id | customer_name | city | account_type | balance |
|---|---|---|---|---|
| 1 | Aarav Sharma | Mumbai | Savings | 25000 |
| 2 | Priya Verma | Delhi | Current | 120000 |
| 3 | Rahul Mehta | Mumbai | Savings | 8000 |
| 4 | Sneha Iyer | Bengaluru | Salary | 95000 |
| 5 | Vikram Singh | Delhi | Current | 45000 |
| 6 | Ananya Das | Chennai | Savings | 15000 |
| txn_id | customer_id | amount |
|---|---|---|
| 101 | 1 | 500 |
| 102 | 1 | 1200 |
| 103 | 2 | 800 |
| 104 | 3 | 300 |
| 105 | 1 | 950 |
| 106 | 4 | 1500 |
1. What Is a Subquery?
A subquery (or "nested query") is a query written inside another query.
It returns a result that the outer query then uses. Subqueries can appear in the
SELECT, WHERE, or FROM clause.
In WHERE โ filter using another query's result
-- Customers whose balance is above the overall average
SELECT customer_name, balance
FROM customers
WHERE balance > (
SELECT AVG(balance)
FROM customers
);
In SELECT โ a scalar (single-value) subquery
-- Show each customer's transaction count as a column
SELECT c.customer_name,
(SELECT COUNT(*)
FROM transactions t
WHERE t.customer_id = c.customer_id) AS txn_count
FROM customers c;
SELECT or a comparison (=, >)
must return a single value (one row, one column). A subquery used with
IN can return multiple values.
2. Correlated vs Non-Correlated Subqueries
This distinction is a favourite interview question. There are exactly two kinds:
| Non-correlated | Correlated | |
|---|---|---|
| Depends on outer query? | No โ independent | Yes โ references outer columns |
| How many times it runs | once | once per outer row |
| Typical use | fixed threshold / list | row-by-row comparison |
Non-correlated (runs once)
-- The inner query runs once and returns a single average
SELECT customer_name, balance
FROM customers
WHERE balance > (SELECT AVG(balance) FROM customers);
Correlated (runs once per row)
-- Transactions above THAT customer's own average amount
SELECT t.txn_id, t.customer_id, t.amount
FROM transactions t
WHERE t.amount > (
SELECT AVG(t2.amount)
FROM transactions t2
WHERE t2.customer_id = t.customer_id -- references outer 't'
);
t.customer_id). That dependency is what forces it to re-run for every outer row.
3. The Classic: Second-Highest Value
"Find the second-highest value" is one of the most-recycled SQL interview problems, and the cleanest solution uses a subquery.
-- Second-highest balance: highest value that is below the max
SELECT MAX(balance) AS second_highest
FROM customers
WHERE balance < (SELECT MAX(balance) FROM customers);
The inner query finds the maximum (120000). The outer query finds the largest value below that โ which is 95000 (the second highest).
ROW_NUMBER() โ covered in the next session.
4. Common Table Expressions (CTEs)
A CTE is a named, temporary result set defined with the WITH clause.
You can reference it like a table in the same query โ which makes multi-step logic far more readable.
-- Basic CTE: high-balance customers
WITH high_balance AS (
SELECT customer_name, balance
FROM customers
WHERE balance > 40000
)
SELECT *
FROM high_balance
ORDER BY balance DESC;
CTE + aggregation (a common interview pattern)
-- Average number of transactions per customer
WITH txn_counts AS (
SELECT customer_id, COUNT(*) AS no_of_txn
FROM transactions
GROUP BY customer_id
)
SELECT AVG(no_of_txn) AS avg_txn_per_customer
FROM txn_counts;
Multiple CTEs, chained
WITH txn_counts AS (
SELECT customer_id, COUNT(*) AS no_of_txn
FROM transactions GROUP BY customer_id
),
active AS (
SELECT customer_id FROM txn_counts WHERE no_of_txn > 1
)
SELECT c.customer_name
FROM customers c
JOIN active a ON c.customer_id = a.customer_id;
5. CTE vs Subquery vs Join โ When to Use Which
| Tool | Best for |
|---|---|
| Subquery | a quick, one-off filter or single value; simple inline checks |
| CTE | multi-step logic you need to reuse or read clearly; recursion |
| JOIN | combining columns from related tables in the output |
Recursive CTEs (conceptual)
A recursive CTE (WITH RECURSIVE) references itself to walk hierarchical data โ e.g.,
finding all employees under a given manager, or a category tree. You don't need to write one from
memory as a fresher, but knowing the concept exists is a strong signal.
6. Interview Questions (with Model Answers)
The subquery/CTE questions interviewers ask most. Self-test before revealing.
IQ1. What is a subquery?
Model answer: "A subquery is a query nested inside another query. It returns a result that
the outer query uses โ for example, in a WHERE condition, in SELECT as a scalar value,
or in FROM as a derived table."
IQ2. What is the difference between a correlated and a non-correlated subquery?
Model answer: "A non-correlated subquery is independent โ it runs once. A correlated subquery references a column from the outer query, so it runs once for each outer row. You can spot a correlated subquery because it mentions the outer query's alias inside the inner query."
IQ3. When would you use a subquery instead of a join?
Model answer: "I'd use a join when I need columns from both tables in the output. I'd use a
subquery when I only need to filter or check existence against another table โ like
WHERE customer_id IN (SELECT ...). They often give the same result, so I choose whichever reads more clearly."
IQ4. What is a Common Table Expression (CTE)?
Model answer: "A CTE is a named, temporary result set defined with a WITH clause.
It lets me break a complex query into readable steps and reference the intermediate result like a table โ
often more than once in the same query."
IQ5. What's the difference between a CTE and a subquery?
Model answer: "They often return the same result, but a CTE is named and can be referenced multiple times, which makes multi-step logic much more readable and reusable. A subquery is more compact for a simple, one-off condition. I reach for a CTE when the query has several logical steps."
IQ6. How do you find the second-highest value in a column?
Model answer: "I'd find the maximum value that is below the overall maximum:
SELECT MAX(balance) FROM customers WHERE balance < (SELECT MAX(balance) FROM customers).
The inner query gets the top value, and the outer gets the largest value below it."
IQ7. Can a subquery return multiple rows? When?
Model answer: "Yes, but only where the outer query accepts multiple values โ typically with
IN or EXISTS, or in the FROM clause. If a subquery is compared with
= or placed in SELECT, it must return exactly one value."
IQ8. What is a recursive CTE, and when would you use one?
Model answer: "A recursive CTE uses WITH RECURSIVE and references itself to walk
hierarchical data โ like finding all employees under a manager, or a category tree. It's the standard way to
traverse self-referencing tables."
WITH step1 AS (...), step2 AS (...)) is both clearer to the interviewer and harder to get wrong.
Hands-On Project: Subqueries and CTEs
Using customers and transactions, write a query for each task.
Steps
- List customers whose balance is above the overall average balance (subquery).
- Find the second-highest balance (subquery).
- List each customer with the number of transactions they made (correlated subquery in SELECT).
- Using a CTE, list customers who have made more than one transaction.
- Using a CTE, calculate the average number of transactions per customer.
View Solution / Walkthrough
-- 1. Above-average balance
SELECT customer_name, balance
FROM customers
WHERE balance > (SELECT AVG(balance) FROM customers);
-- โ Priya Verma (120000), Sneha Iyer (95000) [avg = ~51333]
-- 2. Second-highest balance
SELECT MAX(balance) AS second_highest
FROM customers
WHERE balance < (SELECT MAX(balance) FROM customers);
-- โ 95000
-- 3. Transaction count per customer (correlated)
SELECT c.customer_name,
(SELECT COUNT(*) FROM transactions t
WHERE t.customer_id = c.customer_id) AS txn_count
FROM customers c;
-- 4. Customers with more than one transaction (CTE)
WITH txn_counts AS (
SELECT customer_id, COUNT(*) AS no_of_txn
FROM transactions GROUP BY customer_id
)
SELECT c.customer_name
FROM customers c
JOIN txn_counts tc ON c.customer_id = tc.customer_id
WHERE tc.no_of_txn > 1;
-- โ Aarav Sharma (3 transactions)
-- 5. Average transactions per customer (CTE)
WITH txn_counts AS (
SELECT customer_id, COUNT(*) AS no_of_txn
FROM transactions GROUP BY customer_id
)
SELECT AVG(no_of_txn) AS avg_txn
FROM txn_counts;
-- โ 1.5 (6 transactions / 4 active customers)
Key Takeaways
A subquery nests one query inside another โ in SELECT, WHERE, or FROM.
Non-correlated runs once; correlated references the outer query and runs per row.
"Second-highest value" = max below the max โ a classic subquery problem.
A CTE (WITH) names an intermediate result for cleaner multi-step logic.
Use subqueries for quick filters, CTEs for readable steps, joins for combining columns.
Objective Questions โ Test Your Understanding
Q1. A subquery that runs once, independent of the outer query, is called?
Q2. Which keyword introduces a Common Table Expression (CTE)?
Q3. A subquery that references a column from the outer query and runs once per outer row is?
Q4. What must a scalar subquery (in SELECT or with =) return?
Q5. Which classic problem is most cleanly solved using a subquery?