Cardinality — Learn ROW_NUMBER, FULL OUTER JOIN, and FILTER in Practice
AdvancedCardinalityROW_NUMBERFULL OUTER JOINFILTERPostgreSQL-compatible5 questions
QUESTION 6
Nth-Row Extraction — Retrieve the second purchase with ROW_NUMBER (rn=2) and LEFT JOIN
ROW_NUMBER in practice1:N→0..1LEFT JOINAdditional ON condition
Background

Basic Q7 used ROW_NUMBER + rn=1 to retrieve the latest row. In this practical set, we retrieve an arbitrary Nth row. Two details matter: the ORDER BY direction determines what “Nth” means, and LEFT JOIN preserves customers for whom that row does not exist.

-- ORDER BY direction changes the meaning of rn
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at DESC)  -- rn=1 means “latest”
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at ASC)   -- rn=2 means “second”
Where to put rn = 2 — the decisive difference between ON and WHERE:
LEFT JOIN ... ON ... AND r.rn = 2 keeps a customer with one purchase as a NULL row.
LEFT JOIN ... WHERE r.rn = 2 evaluates NULL = 2 as UNKNOWN and removes that row, effectively becoming an INNER JOIN.
Problem

For every customer, retrieve the second purchased product as second_product and its timestamp as second_ordered_at. Customers with one or zero purchases must still appear with NULL values.

Tables used
▸ customers
customer_idname
1Tanaka
2Sato
3Yamada
▸ orders
order_idcustomer_idproduct_nameordered_at
1011Keyboard2024-04-01 10:00
1021Mouse2024-04-03 14:30
1031Monitor2024-04-10 09:15
1042Earphones2024-04-02 18:00
1052Charger2024-04-08 12:00
1063Cable2024-04-05 11:00

Note: Yamada (customer_id=3) has only one purchase. If the rn=2 condition is placed in WHERE, Yamada disappears from the result.

Expected Output
namesecond_productsecond_ordered_at
TanakaMouse2024-04-03 14:30
SatoCharger2024-04-08 12:00
YamadaNULLNULL
QUESTION 7
Full Outer Join — Reconcile two product masters and detect differences
FULL OUTER JOINCOALESCEDifference detectionThree-valued logic (NULL comparison)
Background

When the same product master exists in a core system and an e-commerce site, reconciliation means finding the differences. LEFT JOIN preserves only rows from the left, but reconciliation must also find products that exist only on the right. FULL OUTER JOIN retains rows from both sides.

-- The join result has three zones
--   ① Present on both sides (matching key) → values can be compared
--   ② Left side only                     → every right-side column is NULL
--   ③ Right side only                    → every left-side column is NULL
FULL OUTER JOIN ... ON a.product_id = b.product_id
Comparisons with NULL are UNKNOWN (three-valued logic):
For a one-sided row, a.price <> b.price becomes “100 <> NULL”, which is UNKNOWN rather than TRUE or FALSE. WHERE rejects UNKNOWN. A difference condition alone therefore misses missing products; add explicit IS NULL conditions or use a NULL-safe comparison.
Problem

Reconcile products_kikan and products_ec on product_id and return only products with differences. Show the three statuses—price mismatch, missing from EC, and missing from the core system—in a status column, along with both prices. The completely matching product (id=101) must not be returned.

Tables used
▸ products_kikan
product_idnameprice
101Notebook300
102Pen150
103Eraser100
104Ruler200
▸ products_ec
product_idnameprice
101Notebook300
102Pen180
105Marker250

Note: id=102 exists in both tables but has different prices (150 vs 180). IDs 103 and 104 exist only in the core system; id=105 exists only in EC.

Expected Output
product_idnameprice_kikanprice_ecstatus
102Pen150180Price mismatch
103Eraser100NULLMissing from EC
104Ruler200NULLMissing from EC
105MarkerNULL250Missing from core
QUESTION 8
Hierarchical Roll-Up — Roll child-department sales and targets up to parent departments with a self-join
Self-joinHierarchical structurePre-aggregationAvoiding fan traps
Background

When a parent department has many child departments and each child has many sales rows, joining sales and targets directly creates a fan trap: the target is copied once per sale. Pre-aggregate the many-side rows to child-department grain, attach the parent with a self-join, and aggregate once more at parent grain.

-- Two-stage roll-up
--   ① Aggregate sales at child-department grain
--   ② Self-join to attach the parent
--   ③ Aggregate sales and targets at parent grain
Aggregate before joining another one-to-many source:
If a target row is joined while sales still has multiple rows per child, the target is duplicated and the achievement rate becomes wrong.
Problem

Using departments, sales, and dept_targets, roll each child department's sales and target up to its parent department. Return parent_dept_name, total sales, total target, and achievement rate. Use pre-aggregation and a self-join so each target is counted once.

Tables used
▸ departments
dept_iddept_nameparent_id
1Sales HQNULL
2Engineering HQNULL
11Domestic Sales1
12International Sales1
21App Development2
22Platform Development2
▸ sales
sale_iddept_idamount
111300000
211150000
312400000
421500000
521250000
622250000
▸ dept_targets
dept_idtarget
11400000
12500000
21800000
22300000

Note: each child has a unique target, but sales has multiple rows for some children. Aggregate sales before joining targets.

Expected Output
parent_depttotal_salestotal_targetachievement_rate
Sales HQ85000090000094.4
Engineering HQ1000000110000090.9
QUESTION 9
Non-Equi Join — Count usage days within a contract period with BETWEEN
Non-equi joinBETWEENCOUNT(DISTINCT)Range condition in ON
Background

A non-equi join matches rows using a range condition rather than equality. Contract masters are coarse-grained—one row per contract period—while usage logs are fine-grained—one row per event. Put both the user equality and the inclusive date range in the ON clause.

ON s.user_id = l.user_id
AND l.used_on BETWEEN s.start_date AND s.end_date
BETWEEN includes both endpoints:
04-10 and 04-30 are inside the range. Keep the range condition in ON so a contract with no matching usage remains as a NULL row under LEFT JOIN.
Problem

For every subscription, count distinct usage days inside its contract period. Return user_id, plan, and active_days. A subscription with no usage must remain in the output with 0.

Tables used
▸ subscriptions
sub_iduser_idplanstart_dateend_date
11Pro2024-04-012024-04-30
22Lite2024-04-102024-05-09
33Pro2024-04-152024-05-14
▸ usage_logs
log_iduser_idused_on
112024-04-05
212024-04-05
312024-04-20
412024-04-28
512024-05-02
622024-04-08
722024-04-12
822024-04-25

Note: user1 has four events across three days because 04-05 is duplicated. user2's 04-08 event is before the contract starts, and user3 has no usage.

Expected Output
user_idplanactive_days
1Pro3
2Lite2
3Pro0
QUESTION 10
Conditional Aggregation — Use HAVING to find users who bought A or B but never bought C
Conditional aggregationHAVINGFILTER / CASE WHENPredicate on a set
Background

“Has bought coffee or tea” and “has never bought cake” cannot be decided from one row. They are conditions on the user's entire purchase set. WHERE filters individual rows; GROUP BY creates one set per user, conditional aggregation counts the relevant rows, and HAVING evaluates the set-level conditions.

-- Conditional aggregation counts only rows matching a condition
COUNT(*) FILTER (WHERE condition)       -- Standard SQL / PostgreSQL
SUM(CASE WHEN condition THEN 1 ELSE 0 END) -- Portable form (including MySQL)
Row-level negation is not set-level negation:
WHERE product_name <> 'Cake' removes cake rows, not users who bought cake. A user who bought both coffee and cake still has a surviving coffee row. Translate “has never bought” into the set predicate COUNT(condition) = 0.
Problem

Join users and purchases and return each user's user_id and user_name when they have bought coffee or tea at least once and have never bought cake.

Tables used
▸ users
user_iduser_name
1Tanaka
2Sato
3Yamada
4Suzuki
5Takahashi
▸ purchases
purchase_iduser_idproduct_name
11Coffee
21Cake
32Coffee
42Sandwich
53Cake
64Tea
75Sandwich

Note: Tanaka bought coffee and cake; Yamada bought only cake; Takahashi bought only a sandwich. None of those users satisfies both conditions.

Expected Output
user_iduser_name
2Sato
4Suzuki