In Basic Q6, multi-column DISTINCT returned the distinct combinations of (user_id, category). In practice, we often need the entire representative row for each group—for example, each user’s latest purchase, including all its other columns. PostgreSQL’s DISTINCT ON solves this elegantly in a single query.
SELECT DISTINCT ON (user_id) -- keep only the first row for each user_id user_id, order_id, purchased_at FROM purchase_logs ORDER BY user_id, purchased_at DESC; -- the sort order determines the first row
The purchase_logs table stores each user’s purchase history over time. Retrieve exactly one row per user: the latest purchase row, including order_id, category, and amount.
| order_id | user_id | category | amount | purchased_at |
|---|---|---|---|---|
| 1 | 101 | Books | 1500 | 2026-01-10 |
| 2 | 102 | Food | 800 | 2026-01-12 |
| 3 | 101 | Electronics | 3000 | 2026-02-05 |
| 4 | 103 | Books | 1200 | 2026-02-08 |
| 5 | 102 | Electronics | 5000 | 2026-03-01 |
| 6 | 101 | Food | 600 | 2026-03-15 |
| 7 | 103 | Food | 900 | 2026-02-20 |
Note: user_id=101 has 3 purchases, while 102 and 103 have 2 each. Keep only the row with the newest purchased_at (101 → order_id=6, 102 → 5, 103 → 7). Output in ascending user_id order.
| user_id | order_id | category | amount | purchased_at |
|---|---|---|---|---|
| 101 | 6 | Food | 600 | 2026-03-15 |
| 102 | 5 | Electronics | 5000 | 2026-03-01 |
| 103 | 7 | Food | 900 | 2026-02-20 |
- 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
Basic Q7 used EXCEPT to return a set difference, but duplicates are removed internally, so a quantity difference such as “three on the left and two on the right” disappears. EXCEPT ALL preserves duplicates and cancels exactly one matching row for each matching value, performing multiset subtraction.
-- A = {x, x, x, y} / B = {x, y} A EXCEPT B -- → {} both x and y exist in B → all removed A EXCEPT ALL B -- → {x, x} two x remain: 3 − 1 = 2
Reconcile book inventory system_stock (one row per item) with the physical count counted_stock. Retrieve the inventory present in the books but missing from the physical count, with one result row for each missing item.
| stock_id | sku |
|---|---|
| 1 | A001 |
| 2 | A001 |
| 3 | A001 |
| 4 | B002 |
| 5 | B002 |
| 6 | C003 |
| 7 | D004 |
| scan_id | sku |
|---|---|
| 1 | A001 |
| 2 | A001 |
| 3 | B002 |
| 4 | B002 |
| 5 | C003 |
Book inventory is A001×3, B002×2, C003×1, D004×1; the physical count is A001×2, B002×2, C003×1. A001 is short by 1 and D004 by 1, so the result has 2 rows. Output in ascending sku order.
| sku |
|---|
| A001 |
| D004 |
- 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
Basic Q8 used a two-scan pattern: a GROUP BY + HAVING subquery followed by an IN lookup against the original table. With the window function COUNT(*) OVER (PARTITION BY …), we can attach “the number of rows with my key” to every row without collapsing the rows, obtaining the same result plus the duplicate count in one table read.
COUNT(*) OVER (PARTITION BY member_id, class_id) -- Unlike GROUP BY, rows are not reduced; each row receives its group count
WHERE COUNT(*) OVER (…) > 1 is an error. The standard pattern is to calculate the value in a subquery (or CTE), then filter it in the outer query.The fitness club’s reservations table contains duplicate bookings caused by a system problem. Retrieve every reservation row whose (member_id, class_id) pair is duplicated, together with the duplicate count (dup_cnt).
| reservation_id | member_id | class_id | reserved_at |
|---|---|---|---|
| 1 | 201 | Y01 | 2026-01-05 |
| 2 | 202 | P02 | 2026-01-06 |
| 3 | 201 | Y01 | 2026-01-07 |
| 4 | 203 | Y01 | 2026-01-08 |
| 5 | 202 | P02 | 2026-01-09 |
| 6 | 202 | S03 | 2026-01-10 |
The duplicate pairs are (201, Y01) and (202, P02), with 2 rows each and 4 result rows total. Output in ascending member_id, class_id, reservation_id order.
| reservation_id | member_id | class_id | reserved_at | dup_cnt |
|---|---|---|---|---|
| 1 | 201 | Y01 | 2026-01-05 | 2 |
| 3 | 201 | Y01 | 2026-01-07 | 2 |
| 2 | 202 | P02 | 2026-01-06 | 2 |
| 5 | 202 | P02 | 2026-01-09 | 2 |
- 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
Basic Q9 introduced INTERSECT as the common part of two sets. INTERSECT can be chained any number of times, building a filter for elements common to every set, like A ∩ B ∩ C. “Active for three consecutive months” in retention analysis is a representative use case.
SELECT user_id FROM logins WHERE login_month = '2026-01' INTERSECT -- January ∩ February SELECT user_id FROM logins WHERE login_month = '2026-02' INTERSECT -- (January ∩ February) ∩ March SELECT user_id FROM logins WHERE login_month = '2026-03';
The logins table stores monthly login records, including duplicate logins within a month. Retrieve the user_id of every continuing user who logged in during all three months: January, February, and March 2026.
| login_id | user_id | login_month |
|---|---|---|
| 1 | 101 | 2026-01 |
| 2 | 102 | 2026-01 |
| 3 | 103 | 2026-01 |
| 4 | 101 | 2026-01 |
| 5 | 101 | 2026-02 |
| 6 | 103 | 2026-02 |
| 7 | 104 | 2026-02 |
| 8 | 101 | 2026-03 |
| 9 | 103 | 2026-03 |
| 10 | 105 | 2026-03 |
| 11 | 103 | 2026-03 |
January is {101,102,103}, February is {101,103,104}, and March is {101,103,105}, so the common users are 101 and 103. Output in ascending user_id order.
| user_id |
|---|
| 101 |
| 103 |
- 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
Basic Q10 used MIN(id) + GROUP BY to choose the row to keep. The advanced finale is the practical cleansing procedure: number deletion candidates with ROW_NUMBER() and physically delete them. It combines three points: keep the latest row, make the tie-break deterministic, and record deleted rows with RETURNING.
ROW_NUMBER() OVER ( PARTITION BY email -- duplicate group ORDER BY created_at DESC, contact_id DESC -- make the row to keep rn=1 ) AS rn -- keep rn=1 and delete rn>1
RETURNING. Skipping this procedure is a classic cause of duplicate-cleanup incidents.The customer master contacts contains duplicate registrations for the same email. Keep the newest row for each email (the maximum created_at) and DELETE the older duplicate rows. Return the deleted contact_id, email, and created_at with RETURNING.
| contact_id | name | created_at | |
|---|---|---|---|
| 1 | sato@ex.com | Sato | 2026-01-10 |
| 2 | suzuki@ex.com | Suzuki | 2026-01-12 |
| 3 | sato@ex.com | Sato | 2026-02-01 |
| 4 | tanaka@ex.com | Tanaka | 2026-02-05 |
| 5 | sato@ex.com | Sato | 2026-03-01 |
| 6 | suzuki@ex.com | Suzuki | 2026-02-20 |
sato@ex.com has 3 rows (latest contact_id=5), suzuki@ex.com has 2 (latest 6), and tanaka@ex.com has 1. The rows deleted are contact_id = 1, 2, and 3.
| contact_id | created_at | |
|---|---|---|
| 1 | sato@ex.com | 2026-01-10 |
| 2 | suzuki@ex.com | 2026-01-12 |
| 3 | sato@ex.com | 2026-02-01 |
- 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