Subqueries — Learn Correlated, HAVING + SQ, Derived Table × JOIN, and Multi-Level Nesting in Practice
AdvancedSubquery (applied)Correlated subqueryHAVING + subqueryDerived table × JOINMulti-level nestingPostgreSQL-compatible5 questions
QUESTION 1
Correlated subquery — extract only each user's "latest order" from the same table
Correlated SQWHERE clauseLatest-row extractionSame-table comparison
Background

A correlated subquery is a subquery whose inner query references columns of the outer query. Because the inner query runs each time the outer query processes a row, it can dynamically compute a per-row value such as "the maximum value for that row's user."

SELECT * FROM orders o1
WHERE ordered_at = (
  SELECT MAX(ordered_at)  -- compute the max for o1's user_id
  FROM   orders o2       -- reference the same table under a different alias
  WHERE  o2.user_id = o1.user_id  -- reference an outer column (this is the "correlation")
);
Difference from a non-correlated SQ: the scalar SQ in the basic edition (Q1) is evaluated only once, independently of the outer query. A correlated SQ is evaluated for each row of the outer query, so it enables row-dependent computation such as "a different maximum per user."
Problem

From the orders table, retrieve only each user's latest order (the row where ordered_at is the maximum). Return order_id, user_id, amount, status, ordered_at ordered by user_id ascending.

Tables used
▸ orders
order_iduser_idamountstatusordered_at
10118,000completed2024-05-01
102112,000completed2024-05-20
10323,500completed2024-05-10
10439,500completed2024-05-15
105311,000completed2024-05-25
10642,000completed2024-05-08
107115,000completed2024-06-10
Expected Output

Expected output (only each user's latest order, ordered by user_id ascending):

order_iduser_idamountstatusordered_at
107115,000completed2024-06-10
10323,500completed2024-05-10
105311,000completed2024-05-25
10642,000completed2024-05-08

※ user1's latest is 107 (06-10) → 101/102 excluded. user3's latest is 105 (05-25) → 104 excluded.

QUESTION 2
HAVING × scalar subquery — filter groups by comparing group aggregates against the overall average
HAVINGScalar SQGroup comparisonSegment analysis
Background

The HAVING clause filters the result of a GROUP BY aggregation. WHERE filters before aggregation (per row); HAVING filters after aggregation (per group). By using a scalar SQ as the comparison value in the HAVING clause, you can dynamically compare an overall aggregate against a group aggregate.

SELECT col, AVG(amount)
FROM   orders
GROUP BY col
HAVING AVG(amount) > (             -- post-aggregation group condition
  SELECT AVG(amount) FROM orders  -- scalar SQ: returns the overall average
);
Choosing WHERE vs HAVING: WHERE AVG(amount) > ... is a syntax error. Because WHERE is evaluated before GROUP BY, you cannot use aggregate functions in its condition. To filter after aggregation, you must use HAVING.
Problem

From the orders table, limited to orders whose status is 'completed', retrieve the users whose per-user average order amount is higher than the overall average. Return user_id, avg_amount (the ROUNDed integer) ordered by avg_amount descending.

Tables used
▸ orders
order_iduser_idamountstatus
10118,000completed
102112,000completed
10323,500completed
10439,500completed
105311,000completed
10642,000completed
107115,000completed
Expected Output

Expected output (users exceeding the overall average ≈ 8,714, ordered by avg_amount descending):

user_idavg_amount
111,667
310,250

※ Overall AVG = (8000+12000+3500+9500+11000+2000+15000)÷7 ≈ 8,714. user2 (3,500) and user4 (2,000) are below the overall average and are excluded.

QUESTION 3
EXISTS + NOT EXISTS — extract "ordered but not yet reviewed" users with a compound condition
NOT EXISTSCompound EXISTSBehavioral funnelSet-difference detection
Background

By combining multiple EXISTS / NOT EXISTS with AND, as in WHERE EXISTS (...) AND NOT EXISTS (...), you can extract in a single query the "rows that satisfy condition A and do not satisfy condition B." This is a field pattern that makes the EXISTS from the basic edition (Q5) compound.

SELECT * FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE  o.user_id = u.user_id  -- an order exists
)
  AND NOT EXISTS (
  SELECT 1 FROM reviews r
  WHERE  r.user_id = u.user_id  -- and no review exists
);
NOT EXISTS is NULL-safe: NOT IN (basic edition Q3) has the trap that a single NULL in the list excludes all rows, but NOT EXISTS is unaffected by NULL. In production data, it is safer to prefer NOT EXISTS over NOT IN as a habit.
Problem

From the users table, retrieve users who have at least one completed order yet have never posted a review. Return user_id, name, plan ordered by user_id ascending.

Tables used
▸ users
user_idnameplan
1Tanaka Taropremium
2Sato Hanakofree
3Suzuki Ichiropremium
4Yamada Jirostandard
5Ito Saburofree
▸ orders (relevant columns only)
order_iduser_idstatus
1011completed
1021completed
1032completed
1043completed
1053completed
1064completed
1071completed
▸ reviews
review_iduser_idorder_idrating
111015
231044
Expected Output

Expected output (has completed / no review, ordered by user_id ascending):

user_idnameplan
2Sato Hanakofree
4Yamada Jirostandard

※ user1 has review1 → excluded. user3 has review2 → excluded. user5 has 0 orders → EXISTS fails → excluded.

QUESTION 4
Derived table × INNER JOIN — join user info onto an aggregate summary to return a list
Derived tableINNER JOINAggregate + master joinAPI response design
Background

In the basic edition Q4, a FROM-clause subquery (derived table) grouped and aggregated, and the outer WHERE filtered it. Extending this pattern further, INNER JOINing the aggregate result (derived table) with another master table to obtain aggregate values and related information in a single query is the most frequent pattern in practice.

SELECT m.name, s.total
FROM   master m
INNER JOIN (
  SELECT id, SUM(amount) AS total
  FROM   transactions
  GROUP BY id
) AS s ON m.id = s.id;  -- join the aggregate result with the master
The exclusion effect of INNER JOIN: INNER JOIN includes rows in the result only when matching rows exist in both tables. Users that do not exist on the derived-table side (users with zero orders) are automatically excluded. To include all users, use a LEFT JOIN.
Problem

Aggregate the orders table to obtain each user's order count, total amount, and average amount, and combine them with the name / plan of the users table to return the result. Consider completed orders only, and return user_id, name, plan, order_count, total_amount, avg_amount ordered by total_amount descending.

Tables used
▸ users
user_idnameplan
1Tanaka Taropremium
2Sato Hanakofree
3Suzuki Ichiropremium
4Yamada Jirostandard
5Ito Saburofree
▸ orders
order_iduser_idamountstatus
10118,000completed
102112,000completed
10323,500completed
10439,500completed
105311,000completed
10642,000completed
107115,000completed
Expected Output

Expected output (aggregated over completed orders only, ordered by total_amount descending):

user_idnameplanorder_counttotal_amountavg_amount
1Tanaka Taropremium335,00011,667
3Suzuki Ichiropremium220,50010,250
2Sato Hanakofree13,5003,500
4Yamada Jirostandard12,0002,000

※ user5 has no completed orders, so no row is generated in the derived table and it is automatically excluded by the INNER JOIN.

QUESTION 5
Multi-level nested SQ — get the product list of the "best-selling category" via nested subqueries
Multi-level SQWHERE INCategory aggregationRanking extraction
Background

A subquery can contain yet another subquery (multi-level nesting). By narrowing step by step from the outside inward — "extract the category's maximum sales → identify that category name → extract that category's products" — you can express complex conditions.

SELECT * FROM products
WHERE category = (
  SELECT category          -- ② find the category name
  FROM   sales_summary
  WHERE  total = (
    SELECT MAX(total)      -- ① fix the maximum value first
    FROM   sales_summary
  )
);
Evaluation order of multi-level nesting: evaluation starts from the innermost subquery. The flow is "find the maximum value → extract the category holding that maximum → extract that category's products." It is easier to understand if you design by working backward, inner to outer.
Problem

Using the following 3 tables, retrieve the product list of the category with the largest total sold quantity (qty) among completed orders (completed). Return product_id, name, category, price ordered by price descending.

Tables used
▸ products
product_idnamecategoryprice
1Wireless Earbudselectronics8,000
2Smartwatchelectronics25,000
3Cotton T-shirtapparel3,500
4Denim Jacketapparel12,000
5Protein Powderhealth5,000
▸ orders (relevant columns only)
order_idstatus
101completed
102completed
103completed
104pending
▸ order_items
item_idorder_idproduct_idqty
110112
210131
310221
410213
510342
610351
710422
Expected Output

Expected output (products of the best-selling category = electronics, ordered by price descending):

product_idnamecategoryprice
2Smartwatchelectronics25,000
1Wireless Earbudselectronics8,000

※ Aggregated over completed orders only. electronics: qty total = 2+3+1 = 6, apparel: 1+2 = 3, health: 1. electronics is the largest. order_id=104 is pending and is excluded.