Session 06 ยท Phase 1: Foundations & SQL

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).

โฑ ~2 hrs ๐Ÿ“š Core content ๐ŸŽฏ High priority

Learning Objectives

0. The Sample Tables

We continue with the familiar customers and transactions tables.

customer_idcustomer_namecityaccount_typebalance
1Aarav SharmaMumbaiSavings25000
2Priya VermaDelhiCurrent120000
3Rahul MehtaMumbaiSavings8000
4Sneha IyerBengaluruSalary95000
5Vikram SinghDelhiCurrent45000
6Ananya DasChennaiSavings15000
txn_idcustomer_idamount
1011500
10211200
1032800
1043300
1051950
10641500

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;
๐Ÿ“
Rule of thumb: a subquery in 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-correlatedCorrelated
Depends on outer query?No โ€” independentYes โ€” references outer columns
How many times it runsonceonce per outer row
Typical usefixed threshold / listrow-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'
);
โš ๏ธ
How to spot a correlated subquery: the inner query references a column from the outer query (here 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).

๐Ÿ’ก
Generalize it: for the nth highest value, replace the subquery with one that finds the max below the (nโˆ’1)th highest, or (more elegantly) use a window function like 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

ToolBest for
Subquerya quick, one-off filter or single value; simple inline checks
CTEmulti-step logic you need to reuse or read clearly; recursion
JOINcombining columns from related tables in the output
๐Ÿ“
Key interview answer: "A CTE and a subquery often produce the same result. I prefer a CTE when the logic has multiple steps or the same intermediate result is used more than once โ€” it's easier to read and debug. I use a subquery for simple, one-off conditions."

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.

๐Ÿ“‹ Stable content โ€” Reviewed: August 2026

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."

๐Ÿ’ก
Remember: For any "solve it in steps" question, think CTE first. Naming each step (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

  1. List customers whose balance is above the overall average balance (subquery).
  2. Find the second-highest balance (subquery).
  3. List each customer with the number of transactions they made (correlated subquery in SELECT).
  4. Using a CTE, list customers who have made more than one transaction.
  5. 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

1

A subquery nests one query inside another โ€” in SELECT, WHERE, or FROM.

2

Non-correlated runs once; correlated references the outer query and runs per row.

3

"Second-highest value" = max below the max โ€” a classic subquery problem.

4

A CTE (WITH) names an intermediate result for cleaner multi-step logic.

5

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?