Subqueries — Learn CASE × SQ, Multiple EXISTS, Top-N Correlation, and Multi-Level CTEs in Practice
AdvancedSubqueryCASE × subqueryMultiple EXISTSTop-N correlated SQMulti-level CTEPostgreSQL-compatible5 questions
QUESTION 6
CASE × subquery — fetch the overall average with CROSS JOIN and classify users into three tiers
CASE+SQCROSS JOINFROM-clause SQUser segments
Background

Using a subquery as the threshold in a CASE expression enables data-driven, dynamic classification. By CROSS JOINing a subquery that returns only one row, you can efficiently attach the same aggregate value (a constant) to every row.

SELECT  s.user_id, s.total_spent, g.overall_avg,
  CASE
    WHEN s.total_spent >= 2 * g.overall_avg THEN 'HIGH_VALUE'
    WHEN s.total_spent >= g.overall_avg     THEN 'NORMAL'
    ELSE                                          'LIGHT'
  END AS segment
FROM (...) AS s        -- per-user aggregation (FROM-clause SQ)
CROSS JOIN (...) AS g; -- attach the overall average (1 row) to every row
Why CROSS JOIN works here: when you CROSS JOIN a subquery that returns only one row, the Cartesian product is "original row count × 1 = original row count." You attach the same aggregate value to every row with a single evaluation. Writing a scalar SQ inside each CASE WHEN re-evaluates the same aggregate multiple times, but this pattern evaluates it only once.
Problem

Classify users who have a total of completed orders (total_spent) in orders into HIGH_VALUE / NORMAL / LIGHT relative to the overall average. Return user_id, name, plan, total_spent, overall_avg, segment, ordered by total_spent descending. Exclude users with no completed orders.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
▸ orders
order_iduser_idamountstatus
10118000completed
102112000completed
10323500pending
10439500completed
10534000completed
10642000cancelled
10754500completed
10823000completed
6 completed rows, total = 41,000 → ROUND(AVG) = 6,833 | per user: user1 = 20,000 / user3 = 13,500 / user5 = 4,500 / user2 = 3,000 | user4 has no completed → excluded
Expected Output

Expected output (total_spent descending):

user_idnameplantotal_spentoverall_avgsegment
1Taro Tanakapremium200006833HIGH_VALUE
3Ichiro Suzukipremium135006833NORMAL
5Saburo Itofree45006833LIGHT
2Hanako Satofree30006833LIGHT

※ HIGH_VALUE = avg×2 (13,666) or more / NORMAL = avg or more / LIGHT = below avg. user1 (20000 ≥ 13666) → HIGH / user3 (13500 < 13666 and ≥ 6833) → NORMAL / user5 and user2 → LIGHT

QUESTION 7
Chaining multiple EXISTS — combine AND/NOT EXISTS to narrow down target customers
Multiple EXISTSNOT EXISTSCorrelated SQCampaign targets
Background

By chaining EXISTS / NOT EXISTS with AND in the WHERE clause, you can chain multiple independent conditions. Because each EXISTS is evaluated as a completely independent correlated subquery, you can build a compound filter such as "has A, and has B, and does not have C."

SELECT u.user_id, u.name
FROM   users u
WHERE  EXISTS     (SELECT 1 FROM orders o1 WHERE o1.user_id = u.user_id AND ...)
  AND  EXISTS     (SELECT 1 FROM orders o2 WHERE o2.user_id = u.user_id AND ...)
  AND  NOT EXISTS (SELECT 1 FROM orders o3 WHERE o3.user_id = u.user_id AND ...);
AND short-circuit evaluation: once the first EXISTS is FALSE, evaluation of the subsequent conditions for that row is skipped. user4 becomes FALSE at ①, so ② and ③ are not evaluated. Placing low-cost conditions first improves efficiency.
Problem

From the users table, retrieve users who satisfy all three of the following conditions:
has at least one completed order (EXISTS)
has at least one order on or after 2024-05-16 (EXISTS)
has no cancelled order at all (NOT EXISTS)
Return user_id, name, plan, ordered by user_id ascending.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
▸ orders (with ordered_at)
order_iduser_idamountstatusordered_at
10118000completed2024-04-10
102112000completed2024-05-20
10323500pending2024-05-10
10439500completed2024-05-15
10534000completed2024-06-01
10642000cancelled2024-05-08
10754500completed2024-04-20
10825500completed2024-06-10
Expected Output

Expected output (all three conditions AND, user_id ascending):

user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium

※ user4: ①completed=0 → excluded | user5: ②latest order 107 (04-20) < 05-16 → excluded | user1, 2, 3 pass all three conditions

QUESTION 8
Top-N correlated subquery — fetch the top 2 per category with a subquery
Correlated SQTop-N extractionRow numberingRanking
Background

Extracting "the top N per category" is extremely common in practice. By counting "how many rows are higher than me" with a correlated subquery, you can achieve Top-N extraction even without the ROW_NUMBER() window function.

SELECT p.*
FROM   products p
WHERE  (
  SELECT COUNT(*)
  FROM   products p2
  WHERE  p2.category = p.category   -- within the same category
    AND  p2.price    >  p.price      -- products priced higher than me
) < 2;                               -- fewer than 2 → I am in the top 2
How it works: "within the same category, 0 rows priced higher than me → 1st place, 1 row → 2nd place, 2 or more → 3rd place or below." To take the top 2, the condition is "fewer than 2 rows priced higher than me (COUNT(*) < 2)." When multiple rows share the same price, they are all returned as "tied."
Problem

From the products table, retrieve the top 2 products per category by price descending. Return product_id, name, category, price, ordered by category ascending, and within the same category by price descending.
Implement it with subqueries only; do not use window functions.

Tables used
▸ products
product_idnamecategoryprice
1Plan Aservice9800
2Plan Bservice4900
3Plan Cservice2500
4Template Xcontent5500
5Template Ycontent3800
6Template Zcontent1200
7API Add-onoption3500
8Support Extensionoption2200
9Storage Add-onoption1500
Expected Output

Expected output (top 2 per category, category ascending, price descending):

product_idnamecategoryprice
4Template Xcontent5500
5Template Ycontent3800
7API Add-onoption3500
8Support Extensionoption2200
1Plan Aservice9800
2Plan Bservice4900

※ service: 3 rows → Plan A (9800) and Plan B (4900) are the top 2 / content: Template X, Y / option: API Add-on, Support Extension

QUESTION 9
Multi-level CTE — stack WITH clauses in three stages to implement cohort analysis
Multi-level CTEWITH clauseCohort analysisUser behavior analysis
Background

A multi-level CTE that chains multiple CTEs (WITH clauses) is a standard practical technique for decomposing complex analytical queries step by step. Because each CTE can reference the previous CTE, you can intuitively write a pipeline of "narrow down the result of step 1 in step 2, then aggregate that result in step 3."

WITH
  step1 AS (                         -- stage 1: base aggregation
    SELECT ... FROM source_table
  ),
  step2 AS (                         -- stage 2: reference step1 and narrow down
    SELECT ... FROM step1 WHERE ...
  ),
  step3 AS (                         -- stage 3: further process step2
    SELECT ... FROM step2 JOIN ...
  )
SELECT * FROM step3;
The value of multi-level CTEs: decomposing complex nesting into "named stages" gives you: ① each step can be run and debugged on its own, ② intent is easier to convey to the team, and ③ when specs change, the affected scope is easier to identify.
Problem

Implement the following three steps with a multi-level CTE:
user_stats: aggregate per user the count of completed orders (order_cnt) and the total amount (total_spent)
active_users: from ①, extract only users with order_cnt of 2 or more
③ Final SELECT: JOIN ② with the users table and return user_id, name, plan, order_cnt, total_spent in total_spent descending order.

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
▸ orders
order_iduser_idamountstatus
10118000completed
102112000completed
10323500pending
10439500completed
10534000completed
10642000cancelled
10754500completed
10816500completed
Expected Output

Expected output (users with order_cnt ≥ 2, total_spent descending):

user_idnameplanorder_cnttotal_spent
1Taro Tanakapremium326500
3Ichiro Suzukipremium213500

※ user1: 101(8000)+102(12000)+108(6500)=26500, cnt=3 / user3: 104(9500)+105(4000)=13500, cnt=2 / user5: cnt=1 → excluded / user2, 4: no completed → excluded

QUESTION 10
CTE scoring — a user-evaluation query that aggregates multiple metrics independently and sums the scores
CTE scoringCASE+CTEComposite applicationRetention analysis
Background

In practice, there are many cases where a customer rank is decided by a score that sums multiple metrics such as total amount, order count, and activity frequency. Aggregating each metric in an independent CTE and summing the scores in a final CTE is a design excellent for maintainability and readability.

Design points: when you want to change the score calculation logic (e.g., change the weight of the total amount), you only need to modify the relevant CTE. Compared with embedding all the logic in one huge SQL statement, the place to change is clear, and team review is easier.
Problem

Using the steps below, compute each user's total score and return a ranking in score descending order, with ties broken by user_id ascending:
spend_score: based on each user's completed total amount (total_spent), 10,000 or more → 3 points / 5,000 or more → 2 points / otherwise → 1 point (no completed → 0 points)
freq_score: based on each user's completed order count (order_cnt), 3 or more → 3 points / 2 or more → 2 points / otherwise → 1 point (no completed → 0 points)
③ Final SELECT: for all users, return user_id, name, spend_score, freq_score, total_score (the sum).

Tables used
▸ users
user_idnameplan
1Taro Tanakapremium
2Hanako Satofree
3Ichiro Suzukipremium
4Jiro Yamadastandard
5Saburo Itofree
▸ orders
order_iduser_idamountstatus
10118000completed
102112000completed
10323500pending
10439500completed
10534000completed
10642000cancelled
10754500completed
10816500completed
Aggregation: user1 = 3 completed, 26500 yen / user3 = 2 completed, 13500 yen / user5 = 1 completed, 4500 yen / user2 = no completed / user4 = no completed
Expected Output

Expected output (total_score descending, ties broken by user_id ascending):

user_idnamespend_scorefreq_scoretotal_score
1Taro Tanaka336
3Ichiro Suzuki325
5Saburo Ito112
2Hanako Sato000
4Jiro Yamada000

※ user1: 26500≥10000→spend=3, cnt=3≥3→freq=3, total=6 | user3: 13500≥10000→spend=3, cnt=2≥2→freq=2, total=5 | user5: 4500<5000→spend=1, cnt=1→freq=1, total=2 | user2, 4: no completed → 0+0=0