SQL 重複データ管理 — 集合演算・ウィンドウ関数の応用

応用重複データ管理ウィンドウ関数集合演算CTEPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

DISTINCT ON で「グループごとの最新1行」を取る — 組み合わせのユニーク化から代表行の選抜へ

DISTINCT ONPostgreSQLグループ代表行最新1件
前提知識

基礎編の複数列 DISTINCT は「(user_id, category) の組み合わせの種類」を返しましたが、実務では「各ユーザーの最新の購入行そのもの」のように、グループごとに代表1行(他の列も含めた行全体)が欲しい場面が頻出します。PostgreSQL の DISTINCT ON はこれを1クエリでエレガントに解決します。

SELECT DISTINCT ON (user_id)  -- user_id ごとに「先頭の1行」だけ残す
  user_id, order_id, purchased_at
FROM purchase_logs
ORDER BY user_id, purchased_at DESC;  -- 並び順が「先頭」を決める
ORDER BY が「どの1行を残すか」を決める:DISTINCT ON は「ソート後にグループの先頭に来た行」を採用します。そのため ORDER BY の先頭は必ず DISTINCT ON の列と一致させ、その後ろに「残したい行が先頭に来る並び順」(最新なら日付の DESC)を書きます。
問題

purchase_logs には各ユーザーの購入履歴が時系列で蓄積されています。「各ユーザーの最新の購入行」(order_id・category・amount を含む行全体)を、ユーザーごとに1行ずつ取得してください。

使用テーブル
▸ purchase_logs
order_iduser_idcategoryamountpurchased_at
1101Books15002026-01-10
2102Food8002026-01-12
3101Electronics30002026-02-05
4103Books12002026-02-08
5102Electronics50002026-03-01
6101Food6002026-03-15
7103Food9002026-02-20
期待出力
user_idorder_idcategoryamountpurchased_at
1016Food6002026-03-15
1025Electronics50002026-03-01
1037Food9002026-02-20
QUESTION 7

EXCEPT ALL で「件数の差」まで検出する — 多重集合の引き算で在庫を突合

EXCEPT ALL多重集合件数差分在庫突合
前提知識

基礎編の EXCEPT は差集合を返しますが、内部で重複除去が行われるため「左に3件・右に2件」のような数量の差は消えてしまいます。EXCEPT ALL は重複を保持したまま「同じ値1件につき1件だけ打ち消す」多重集合(マルチセット)の引き算を行います。

-- A = {x, x, x, y} / B = {x, y} のとき
A EXCEPT B      -- → {}        値レベルで x も y も B にある → 全消し
A EXCEPT ALL B  -- → {x, x}    x は 3−1=2 件残る(件数で相殺)
「あるか/ないか」と「何件足りないか」は別の問い:在庫突合・台帳照合のように数量がズレた分を検出したい場面で EXCEPT を使うと、差異が1件でも100件でも「1値」に潰れてしまいます。数量の問題には必ず ALL 付きを選びます。
問題

倉庫の棚卸しで、帳簿在庫 system_stock(商品1個 = 1行)と実棚カウント counted_stock を突合します。「帳簿には存在するが実棚で見つからなかった在庫」を、不足している個数分の行数で取得してください。

使用テーブル
▸ system_stock(帳簿)
stock_idsku
1A001
2A001
3A001
4B002
5B002
6C003
7D004
▸ counted_stock(実棚)
scan_idsku
1A001
2A001
3B002
4B002
5C003
期待出力
sku
A001
D004
QUESTION 8

COUNT(*) OVER で重複行を1パスで全取得 — サブクエリ + IN をウィンドウ関数で進化させる

ウィンドウ関数COUNT() OVERPARTITION BY重複行全取得
前提知識

基礎編では「GROUP BY + HAVING のサブクエリ → IN で本体を再検索」という2回スキャンの構成で重複行を取得しました。ウィンドウ関数 COUNT(*) OVER (PARTITION BY …) を使うと、行を畳まずに各行へ「自分と同じキーの行数」を付与でき、テーブル1回の読み取りで同じ結果+重複件数まで得られます。

COUNT(*) OVER (PARTITION BY member_id, class_id)
-- GROUP BY と違い行は減らない。各行に「同じ組の行数」が列として付く
ウィンドウ関数は WHERE に直接書けない:ウィンドウ関数は SELECT 句の評価段階(WHERE より後)で計算されるため、WHERE COUNT(*) OVER (…) > 1 はエラーです。サブクエリ(または CTE)で一度列にしてから外側で絞るのが定石です。
問題

フィットネスジムの reservations には、システム不具合で同じ会員が同じクラスを二重予約したレコードが混在しています。重複している (member_id, class_id) の組を持つすべての予約行を、重複件数(dup_cnt)付きで取得してください。

使用テーブル
▸ reservations
reservation_idmember_idclass_idreserved_at
1201Y012026-01-05
2202P022026-01-06
3201Y012026-01-07
4203Y012026-01-08
5202P022026-01-09
6202S032026-01-10
期待出力
reservation_idmember_idclass_idreserved_atdup_cnt
1201Y012026-01-052
3201Y012026-01-072
2202P022026-01-062
5202P022026-01-092
QUESTION 9

INTERSECT を連結して「全期間に共通」を抽出 — 3ヶ月連続アクティブユーザーの特定

INTERSECT 連結集合演算継続ユーザーN期間共通
前提知識

基礎編の INTERSECT は2つの集合の共通部分でした。INTERSECT は何段でも連結でき、「A ∩ B ∩ C」のようにすべての集合に共通する要素を積み上げて絞り込めます。リテンション分析の「3ヶ月連続アクティブ」はこの型の代表例です。

SELECT user_id FROM logins WHERE login_month = '2026-01'
INTERSECT   -- 1月 ∩ 2月
SELECT user_id FROM logins WHERE login_month = '2026-02'
INTERSECT   -- (1月 ∩ 2月) ∩ 3月
SELECT user_id FROM logins WHERE login_month = '2026-03';
INTERSECT は重複除去込みで「共通」を判定:同じ月に何度ログインしていても集合上は1要素として扱われます。「各期間に1回以上存在するか」という存在判定であり、回数は問いません(回数も問うなら INTERSECT ALL や GROUP BY を使います)。
問題

logins には月次のログイン記録が格納されています(同月内の重複ログインあり)。2026年1月・2月・3月の3ヶ月すべてにログインした継続ユーザーの user_id を取得してください。

使用テーブル
▸ logins
login_iduser_idlogin_month
11012026-01
21022026-01
31032026-01
41012026-01
51012026-02
61032026-02
71042026-02
81012026-03
91032026-03
101052026-03
111032026-03
期待出力
user_id
101
103
QUESTION 10

ROW_NUMBER + DELETE で重複を物理削除 — 最新を残すクレンジングの完成形

ROW_NUMBERCTE + DELETE重複削除RETURNING
前提知識

基礎編では MIN(id) + GROUP BY で「残す行」を選びました。応用編の総仕上げは、ROW_NUMBER() で削除対象を採番し、DELETE で物理削除する実務のクレンジング本番手順です。「最新を残す」「タイブレークで決定的にする」「削除行を RETURNING で記録する」の3点を一度に押さえます。

ROW_NUMBER() OVER (
  PARTITION BY email                      -- 重複グループ
  ORDER BY created_at DESC, contact_id DESC  -- 残す行が rn=1 になる順
) AS rn   -- rn=1 を残し、rn>1 を削除する
DELETE は不可逆 — 必ず3点セットで:①削除対象を同じ条件の SELECT で事前確認、②トランザクション内で実行して件数を確認してから COMMIT、③ RETURNING で削除行を記録。この手順を省いた重複削除は事故の典型例です。
問題

顧客マスタ contacts には同一 email の重複登録が蓄積されています。email ごとに最新(created_at が最大)の1行を残し、それ以外の古い重複行を DELETE してください。削除した行は RETURNING で contact_id・email・created_at を返してください。

使用テーブル
▸ contacts
contact_idemailnamecreated_at
1sato@ex.com佐藤2026-01-10
2suzuki@ex.com鈴木2026-01-12
3sato@ex.com佐藤2026-02-01
4tanaka@ex.com田中2026-02-05
5sato@ex.com佐藤2026-03-01
6suzuki@ex.com鈴木2026-02-20
期待出力
contact_idemailcreated_at
1sato@ex.com2026-01-10
2suzuki@ex.com2026-01-12
3sato@ex.com2026-02-01