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
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.
| user_id | name | plan |
|---|---|---|
| 1 | Taro Tanaka | premium |
| 2 | Hanako Sato | free |
| 3 | Ichiro Suzuki | premium |
| 4 | Jiro Yamada | standard |
| 5 | Saburo Ito | free |
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 4000 | completed |
| 106 | 4 | 2000 | cancelled |
| 107 | 5 | 4500 | completed |
| 108 | 2 | 3000 | completed |
Expected output (total_spent descending):
| user_id | name | plan | total_spent | overall_avg | segment |
|---|---|---|---|---|---|
| 1 | Taro Tanaka | premium | 20000 | 6833 | HIGH_VALUE |
| 3 | Ichiro Suzuki | premium | 13500 | 6833 | NORMAL |
| 5 | Saburo Ito | free | 4500 | 6833 | LIGHT |
| 2 | Hanako Sato | free | 3000 | 6833 | LIGHT |
※ 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
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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 ...);
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.
| user_id | name | plan |
|---|---|---|
| 1 | Taro Tanaka | premium |
| 2 | Hanako Sato | free |
| 3 | Ichiro Suzuki | premium |
| 4 | Jiro Yamada | standard |
| 5 | Saburo Ito | free |
| order_id | user_id | amount | status | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | completed | 2024-04-10 |
| 102 | 1 | 12000 | completed | 2024-05-20 |
| 103 | 2 | 3500 | pending | 2024-05-10 |
| 104 | 3 | 9500 | completed | 2024-05-15 |
| 105 | 3 | 4000 | completed | 2024-06-01 |
| 106 | 4 | 2000 | cancelled | 2024-05-08 |
| 107 | 5 | 4500 | completed | 2024-04-20 |
| 108 | 2 | 5500 | completed | 2024-06-10 |
Expected output (all three conditions AND, user_id ascending):
| user_id | name | plan |
|---|---|---|
| 1 | Taro Tanaka | premium |
| 2 | Hanako Sato | free |
| 3 | Ichiro Suzuki | premium |
※ user4: ①completed=0 → excluded | user5: ②latest order 107 (04-20) < 05-16 → excluded | user1, 2, 3 pass all three conditions
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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
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.
| product_id | name | category | price |
|---|---|---|---|
| 1 | Plan A | service | 9800 |
| 2 | Plan B | service | 4900 |
| 3 | Plan C | service | 2500 |
| 4 | Template X | content | 5500 |
| 5 | Template Y | content | 3800 |
| 6 | Template Z | content | 1200 |
| 7 | API Add-on | option | 3500 |
| 8 | Support Extension | option | 2200 |
| 9 | Storage Add-on | option | 1500 |
Expected output (top 2 per category, category ascending, price descending):
| product_id | name | category | price |
|---|---|---|---|
| 4 | Template X | content | 5500 |
| 5 | Template Y | content | 3800 |
| 7 | API Add-on | option | 3500 |
| 8 | Support Extension | option | 2200 |
| 1 | Plan A | service | 9800 |
| 2 | Plan B | service | 4900 |
※ service: 3 rows → Plan A (9800) and Plan B (4900) are the top 2 / content: Template X, Y / option: API Add-on, Support Extension
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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;
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.
| user_id | name | plan |
|---|---|---|
| 1 | Taro Tanaka | premium |
| 2 | Hanako Sato | free |
| 3 | Ichiro Suzuki | premium |
| 4 | Jiro Yamada | standard |
| 5 | Saburo Ito | free |
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 4000 | completed |
| 106 | 4 | 2000 | cancelled |
| 107 | 5 | 4500 | completed |
| 108 | 1 | 6500 | completed |
Expected output (users with order_cnt ≥ 2, total_spent descending):
| user_id | name | plan | order_cnt | total_spent |
|---|---|---|---|---|
| 1 | Taro Tanaka | premium | 3 | 26500 |
| 3 | Ichiro Suzuki | premium | 2 | 13500 |
※ 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
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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.
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).
| user_id | name | plan |
|---|---|---|
| 1 | Taro Tanaka | premium |
| 2 | Hanako Sato | free |
| 3 | Ichiro Suzuki | premium |
| 4 | Jiro Yamada | standard |
| 5 | Saburo Ito | free |
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 4000 | completed |
| 106 | 4 | 2000 | cancelled |
| 107 | 5 | 4500 | completed |
| 108 | 1 | 6500 | completed |
Expected output (total_score descending, ties broken by user_id ascending):
| user_id | name | spend_score | freq_score | total_score |
|---|---|---|---|---|
| 1 | Taro Tanaka | 3 | 3 | 6 |
| 3 | Ichiro Suzuki | 3 | 2 | 5 |
| 5 | Saburo Ito | 1 | 1 | 2 |
| 2 | Hanako Sato | 0 | 0 | 0 |
| 4 | Jiro Yamada | 0 | 0 | 0 |
※ 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
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free