SQL 重複データ管理 — DISTINCT・ROW_NUMBER・UNIONの基礎

基礎重複データ管理DISTINCTROW_NUMBERUNIONPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

重複行を除去する基本 — SELECT DISTINCT でユニークな一覧を得る

DISTINCT重複除去射影ORDER BY
前提知識

同じ内容の行が何度も現れるテーブルから「種類の一覧」を得たい場面はよくあります。DISTINCT は SELECT で取り出した列の組み合わせが重複する行をまとめ、ユニークな行だけを返します。

-- DISTINCT は「SELECTした列の組み合わせ」で重複を判定する
SELECT DISTINCT user_id FROM access_logs;        -- user_id の種類だけが残る
SELECT DISTINCT user_id, page FROM access_logs;  -- (user_id, page) の組で判定
DISTINCT は全列に効く:DISTINCT は直後の1列だけでなく、SELECT句に並べたすべての列の組み合わせに対して重複を判定します。列を増やすほど「ユニーク」の条件はゆるくなり、残る行数は増えます。
問題

access_logs には同じユーザーの複数回のアクセスが記録されています。一度でも訪問したことのあるユーザーIDの一覧を、重複なく取得してください。

使用テーブル
▸ access_logs
log_iduser_idpage
1101/home
2102/home
3101/mypage
4101/home
5103/home
6102/mypage
期待出力
user_id
101
102
103
模範解答コード
SELECT DISTINCT  -- 重複する user_id をまとめる
  user_id
FROM access_logs
ORDER BY user_id;

/*
  実行順序:
  1. FROM access_logs  → 全6行を読み込む
  2. SELECT user_id    → user_id 列だけを射影
  3. DISTINCT          → 重複する user_id をまとめる
  4. ORDER BY user_id  → user_id 昇順で出力
  */
解説(テーブル変化・ポイント)
SELECT DISTINCT user_id FROM access_logs ORDER BY user_id;
LEGEND
データ取得・読込対象
① 元データ
FROM access_logsaccess_logs テーブル(6行)を読み込みます。user_id 列には 101 が3回、102 が2回と、重複した値が含まれています。
1 / 4
log_iduser_idpage
1101/home
2102/home
3101/mypage
4101/home
5103/home
6102/mypage
access_logs: 6行(user_idに重複あり)
学習ポイント
6 RowsDISTINCT3 Unique
DISTINCT
重複行の除去
SELECT列の組み合わせでユニーク化
DISTINCT の判定単位は「選択列の組み合わせ」:DISTINCT は SELECT 句に書いたすべての列をひとまとめにして重複を判定します。SELECT DISTINCT user_id なら user_id だけ、SELECT DISTINCT user_id, page なら (user_id, page) のペアで判定されます。後者は「ユーザーが訪れたユニークなページの組」になり、結果の行数も変わります。
DISTINCT と GROUP BY の関係:SELECT DISTINCT user_id FROM access_logsSELECT user_id FROM access_logs GROUP BY user_id は同じ結果を返します。集計(COUNTやSUM)を伴わない単純な重複除去なら DISTINCT が簡潔で読みやすく、集計が必要なら GROUP BY を選ぶ、という使い分けが基本です。
アンチパターン
DISTINCT を関数だと思って DISTINCT(user_id) と書く:DISTINCT は関数ではなくキーワードです。SELECT DISTINCT(user_id), page と書いても括弧は単なるグループ化扱いで、結局 user_id と page の両方に DISTINCT が効きます。意図せず (user_id, page) の組で判定され、想定より行数が増えてしまいます。
一意な列を混ぜて重複が消えない:SELECT DISTINCT log_id, user_id のように主キー級の一意な列(log_id)を含めると、組み合わせがすべてユニークになり重複除去がまったく効きません。重複を消したいときは判定したいキー列だけを SELECT するのが鉄則です。
実務コラム
「重複を除いた一覧」は実務で頻出します。応用例: サイトのユニーク訪問ユーザー一覧、重複を除いた取引先マスタの抽出、使われているタグ・カテゴリの一覧化。現場では「想定外の列が SELECT に混ざって DISTINCT が効かない」事故が多いため、まず重複判定したいキーは何かを明確にしてから列を選びましょう。
QUESTION 2

重複を除いて数える — COUNT(*) と COUNT(DISTINCT 列) の違い

COUNT(DISTINCT)集計GROUP BYユニーク数
前提知識

「アクセス数」と「訪問人数」は別物です。同じ人が何度訪れても訪問人数は1人。COUNT には3つの形があり、それぞれ数える対象が異なります。

COUNT(*)                  -- 行数(重複もNULLも全部数える)
COUNT(user_id)            -- user_id が NULL でない行数
COUNT(DISTINCT user_id)   -- user_id の種類数(重複を除く)
PV と UU の違い:COUNT(*) は延べアクセス数(PV)、COUNT(DISTINCT user_id) はユニークユーザー数(UU)。同じユーザーの重複アクセスは COUNT(DISTINCT) で1人にまとまります。なお、PV はアクセスログの行数そのものを数えたいため、一般的には COUNT(user_id) ではなく COUNT(*) を使用します。
問題

access_logs から、ページごとに「総アクセス数(pv)」と「ユニークユーザー数(uu)」を取得してください。

使用テーブル
▸ access_logs
log_iduser_idpage
1101/home
2102/home
3101/home
4103/home
5101/products
6102/products
7102/products
期待出力
pagepvuu
/home43
/products32
模範解答コード
SELECT
  page,
  COUNT(*)                AS pv,  -- 延べアクセス数(行数)
  COUNT(DISTINCT user_id) AS uu   -- 重複を除いた訪問者数
FROM access_logs
GROUP BY page
ORDER BY page;

/*
  実行順序:
  1. FROM access_logs            → 全7行を読み込む
  2. GROUP BY page               → page ごとに2グループへ集約
  3. COUNT(*)                    → 各グループの行数(PV)を数える
  4. COUNT(DISTINCT user_id)     → 各グループの user_id の種類数(UU)を数える
  5. SELECT page, pv, uu         → 3列を射影
  6. ORDER BY page               → page 昇順で出力
*/
解説(テーブル変化・ポイント)
SELECT page, COUNT(*) AS pv, COUNT(DISTINCT user_id) AS uu FROM access_logs GROUP BY page ORDER BY page;
LEGEND
データ取得・読込対象
① 元データ
FROM access_logsaccess_logs テーブル(7行)を読み込みます。同じ user_id が同じ page に複数回アクセスしている行(重複)が含まれています。
1 / 4
log_iduser_idpage
1101/home
2102/home
3101/home
4103/home
5101/products
6102/products
7102/products
access_logs: 7行
学習ポイント
COUNT(*)Total: 4vsDISTINCTUnique: 3
COUNT(DISTINCT)
重複を除いた件数
PV(延べ)とUU(ユニーク)の区別
3つの COUNT の違い:COUNT(*)行の存在を数えます(NULLも含む全行)。COUNT(user_id)user_id が NULL でない行を数えます。COUNT(DISTINCT user_id)重複を除いた値の種類数を数えます。この3つを混同すると、PV・有効行数・UU を取り違える集計ミスにつながります。
COUNT(DISTINCT) は内部で重複除去が走る:COUNT(DISTINCT) は値を一意化するためのソートやハッシュ処理が内部で発生し、COUNT(*) より重くなりがちです。大規模データで「おおよそのユニーク数」で良い場合は、近似集計(PostgreSQL の拡張など)を使う選択肢もある、と頭の片隅に置いておくと実務で役立ちます。
アンチパターン
複数列の COUNT(DISTINCT a, b) の移植性に注意:「(user_id, page) の組のユニーク数」を数えたいとき、MySQL では COUNT(DISTINCT user_id, page) と書けますが、PostgreSQL では複数列を直接渡せず COUNT(DISTINCT (user_id, page)) のようにROW式で囲む必要があります。DBによって構文が違う点に注意しましょう。
GROUP BY を忘れて全体が1グループになる:ページ別に出したいのに GROUP BY page を書き忘れると、テーブル全体が1グループとして集計され、ページの区別が消えた合計値だけが返ります。「〜ごと」という要件は GROUP BY のサインです。SELECT に並べた非集計列は原則すべて GROUP BY にも書きます。
実務コラム
PV と UU の区別はアクセス解析の基本そのものです。応用例: DAU / MAU(日次・月次のアクティブユーザー数)、ユニーク購入者数(延べ注文件数と区別)、重複を除いた取扱SKU数。レポートで「件数」と言われたら、それが延べ(COUNT(*))なのか、ユニーク(COUNT(DISTINCT))なのかを最初に確認する習慣が、集計事故を防ぎます。
QUESTION 3

重複データを検出する — GROUP BY + HAVING COUNT(*) > 1

HAVING重複検出データ品質COUNT(*)
前提知識

同じメールアドレスで二重登録されてしまった、といった重複データの検出は実務で頻出します。GROUP BY で同じ値の行をまとめ、HAVING COUNT(*) > 1 で「2件以上あるグループ」だけを残すのが定石です。

GROUP BY email              -- email が同じ行をまとめる
HAVING COUNT(*) > 1     -- グループの行数が2以上=重複
WHERE と HAVING の違い:WHERE は集約の個々の行を絞り込み、HAVING は集約のグループを絞り込みます。COUNT(*) のような集計関数は WHERE では使えず、HAVING でのみ使えます。
問題

users テーブルで、同じ email が複数回登録されているメールアドレスと、その登録件数(cnt)を検出してください。

使用テーブル
▸ users
user_idnameemail
1田中tanaka@example.com
2佐藤sato@example.com
3田中tanaka@example.com
4鈴木suzuki@example.com
5佐藤sato@example.com
6山田yamada@example.com
期待出力
emailcnt
sato@example.com2
tanaka@example.com2
模範解答コード
SELECT
  email,
  COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1  -- 2件以上のグループ=重複のみ残す
ORDER BY email;

/*
  実行順序:
  1. FROM users           → 行を読み込む
  2. GROUP BY email       → email ごとにグループ化
  3. HAVING COUNT(*) > 1  → 重複グループだけ残す
  4. SELECT email, cnt    → 2列を射影
  5. ORDER BY email       → 並び替えて出力
  */
解説(テーブル変化・ポイント)
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1 ORDER BY email;
LEGEND
データ取得・読込対象
① 元データ
FROM usersusers テーブル(6行)を読み込みます。email 列を見ると、tanaka@example.com と sato@example.com がそれぞれ2回登場しており、二重登録が疑われます。
1 / 4
user_idnameemail
1田中tanaka@example.com
2佐藤sato@example.com
3田中tanaka@example.com
4鈴木suzuki@example.com
5佐藤sato@example.com
6山田yamada@example.com
users: 6行
学習ポイント
GroupsCOUNT>1FilterDupes
DUPLICATE DETECTION
重複データの検出
GROUP BY + HAVING COUNT(*) > 1
なぜ WHERE ではなく HAVING なのか:WHERE は集約される前の1行ずつを評価するため、まだ COUNT(*) の値が決まっていません。「グループの件数が2以上」という条件はグループを作った後でないと判定できないので、集約後に効く HAVING を使います。実行順序は WHERE → GROUP BY → HAVING の順だと覚えると整理しやすいです。
「どの行が重複か」までは別途必要:このクエリは「重複している値(email)と件数」を返しますが、具体的にどの user_id の行を残し、どれを消すかまでは決めません。削除対象の行を1件ずつ特定するには、次の Q4 で学ぶ ROW_NUMBER() による行単位の番号付けが必要になります。
アンチパターン
WHERE COUNT(*) > 1 と書いてエラーになる:集計関数は WHERE 句では使えません。WHERE COUNT(*) > 1 は構文エラーになります。集計値での絞り込みは必ず HAVING に書きます。逆に「集約前の行を絞る条件」(例: 退会済みを除く)は WHERE に書く、と役割を分けて考えましょう。
複数列の組み合わせ重複を1列だけで見て見落とす:「同じ氏名かつ同じ生年月日」のような複合的な重複は、GROUP BY name, birth_date のように複数列でグループ化して判定します。email だけ、name だけと1列で見ると、本来別人を重複扱いしたり、逆に重複を見逃したりします。重複の定義(どの列の組で同一とみなすか)を最初に決めることが重要です。
実務コラム
重複検出はデータクレンジングの第一歩です。応用例: ユニーク制約(UNIQUE)を貼る前の事前チェック(既存データに重複があると制約追加が失敗する)、二重登録・二重決済の調査、名寄せ前の重複候補の洗い出し。HAVING COUNT(*) > 1 は「データがきれいか」を確かめる健康診断のような定番クエリとして、まず手に馴染ませておきたいパターンです。
QUESTION 4

重複を1件に絞る — ROW_NUMBER() で代表行だけを残す(重複排除)

ROW_NUMBERCTE重複排除PARTITION BY
前提知識

重複を「検出」した次は「排除」です。重複行のうちどれか1件だけを代表として残したいとき、ROW_NUMBER() が活躍します。重複の判定キーを PARTITION BY、どれを残すかの優先順位を ORDER BY で指定し、rn = 1 の行だけを採用します。

ROW_NUMBER() OVER (
  PARTITION BY email            -- 重複を判定するキー
  ORDER BY created_at DESC     -- 新しいものを優先(=残す)
) AS rn                          -- rn=1 が各グループの代表行
DISTINCT では足りない理由:DISTINCT は全列が完全一致する行しか除去できません。「email は同じだが name や created_at が違う」部分的な重複では DISTINCT は効きません。ROW_NUMBER なら「email が同じものを1件に」とキーを指定した重複排除ができます。
問題

customers には同じ email で複数回登録された顧客がいます。email ごとに最新(created_at が新しい)の1件だけを残し、ユニークな顧客一覧を取得してください。

使用テーブル
▸ customers
customer_idemailnamecreated_at
1tanaka@ex.com田中2024-01-10
2sato@ex.com佐藤2024-01-12
3tanaka@ex.com田中T2024-02-05
4suzuki@ex.com鈴木2024-01-20
5sato@ex.com佐藤S2024-03-01
期待出力
customer_idemailnamecreated_at
3tanaka@ex.com田中T2024-02-05
4suzuki@ex.com鈴木2024-01-20
5sato@ex.com佐藤S2024-03-01
模範解答コード
WITH ranked AS (
  SELECT
    customer_id, email, name, created_at,
    ROW_NUMBER() OVER (
      PARTITION BY email
      ORDER BY created_at DESC    -- 最新が rn=1 になる
    ) AS rn
  FROM customers
)
SELECT
  customer_id, email, name, created_at
FROM ranked
WHERE rn = 1                  -- 各 email の代表1行のみ採用
ORDER BY customer_id;

/*
  実行順序:
  1. CTE(ranked)              → customers を読み込む
  2. ROW_NUMBER() OVER (...)  → email ごとに最新順で番号付け
  3. FROM ranked              → 派生テーブルを参照
  4. WHERE rn = 1             → 各 email の最新1件に絞る
  5. SELECT ...               → 列を射影
  6. ORDER BY customer_id     → 並び替えて出力
  */
解説(テーブル変化・ポイント)
WITH ranked AS ( SELECT customer_id, email, name, created_at, ROW_NUMBER() OVER ( PARTITION BY email ORDER BY created_at DESC ) AS rn FROM customers ) SELECT customer_id, email, name, created_at FROM ranked WHERE rn = 1 ORDER BY customer_id;
LEGEND
データ取得・読込対象
① CTE — 元データ
FROM customerscustomers テーブル(5行)を読み込みます。email を重複の判定キーとして見ると、tanaka@ex.com(2件)と sato@ex.com(2件)が重複しています。各重複から最新の1件を選びます。
1 / 4
customer_idemailnamecreated_at
1tanaka@ex.com田中2024-01-10
2sato@ex.com佐藤2024-01-12
3tanaka@ex.com田中T2024-02-05
4suzuki@ex.com鈴木2024-01-20
5sato@ex.com佐藤S2024-03-01
customers: 5行(email に重複あり)
学習ポイント
Partitionrn=1rn=2WHERE rn=1Top 1
DEDUPLICATION
代表1件を残す重複排除
PARTITION BY + ORDER BY + WHERE rn=1
PARTITION BY と ORDER BY の役割分担:PARTITION BY何を重複とみなすか(判定キー)ORDER BY重複のうちどれを残すか(優先順位)を決めます。「最新を残す」なら ORDER BY created_at DESC、「最古を残す」なら ASC、「優先度の高いものを残す」なら任意の評価列で並べます。この2つを設計するだけで重複排除のルールを自由に表現できます。
DISTINCT / GROUP BY では代替できない理由:DISTINCT は全列一致の重複しか消せず、GROUP BY は集計しない他の列(name など)を一緒に取り出せません。ROW_NUMBER は行をまるごと保持したまま、指定キーの重複を1件に絞れる唯一の方法です。これが「重複排除といえば ROW_NUMBER」と言われる理由です。
Window関数は WHERE で直接使えない:WHERE ROW_NUMBER() OVER(...) = 1 とは書けません。Window関数は SELECT のタイミングで計算されるため、先に CTE やサブクエリで rn を列として作り、その外側で WHERE rn = 1 と絞る2段構えが必要です。
アンチパターン
ROW_NUMBER と RANK / DENSE_RANK の取り違え:もし created_at が同値の行が2件あると、RANK() では両方が rn=1 になり重複が残ってしまいます。必ず1件に絞りたいなら ROW_NUMBER を使い、さらに同値を確実に割るために ORDER BY created_at DESC, customer_id DESC のようにタイブレーク列を足すと安全です。
ORDER BY を省略して「どれが残るか不定」になる:PARTITION BY だけ書いて ORDER BY を省くと、どの行が rn=1 になるかが実行のたびに変わりうる不定動作になります。「最新を残したい」など残す基準が必ずあるはずなので、ORDER BY は明示的に指定しましょう。
実務コラム
ROW_NUMBER による重複排除は、データクレンジングや名寄せの現場で最頻出のパターンです。実際に重複行を削除するには、DELETE FROM customers WHERE customer_id IN (SELECT customer_id FROM ranked WHERE rn > 1) のように「代表以外(rn>1)」を消します。いきなり DELETE せず、まず SELECT で rn>1 の行を確認してから削除するのが、事故を防ぐ実務の鉄則です。
QUESTION 5

リスト統合の重複制御 — UNION と UNION ALL の使い分け

UNION集合演算UNION ALL重複排除
前提知識

複数のリストを縦に積み上げるのが集合演算です。UNION は積み上げたうえで重複行を自動的に除去し、UNION ALL重複もそのまま全件残します。

SELECT email FROM campaign_a
UNION          -- 重複を除去(DISTINCT相当)
SELECT email FROM campaign_b;

-- UNION ALL なら重複もそのまま全件残る(除去処理がない分だけ高速)
列を合わせる:UNION する各 SELECT は列の数・並び順・データ型を揃える必要があります。重複判定は DISTINCT と同じく全列の組み合わせで行われます。
問題

2つのキャンペーンの応募者メール(campaign_acampaign_b)を、重複なく1つのリストに統合してください。

使用テーブル
▸ campaign_a
email
tanaka@ex.com
sato@ex.com
suzuki@ex.com
▸ campaign_b
email
sato@ex.com
yamada@ex.com
tanaka@ex.com
期待出力
email
sato@ex.com
suzuki@ex.com
tanaka@ex.com
yamada@ex.com
模範解答コード
SELECT email FROM campaign_a
UNION                       -- 両リストを統合し、重複を除去
SELECT email FROM campaign_b
ORDER BY email;

/*
  実行順序:
  1. SELECT FROM campaign_a  → 上側を取得
  2. SELECT FROM campaign_b  → 下側を取得
  3. UNION                   → 縦に連結し重複除去
  4. ORDER BY email          → 全体を並べ替え
  */
解説(テーブル変化・ポイント)
SELECT email FROM campaign_a UNION SELECT email FROM campaign_b ORDER BY email;
LEGEND
データ取得・読込対象
① 上のリスト — campaign_a
SELECT email FROM campaign_a1つ目のキャンペーンの応募者メール(3行)です。これと campaign_b を縦に統合していきます。
1 / 5
email
tanaka@ex.com
sato@ex.com
suzuki@ex.com
campaign_a: 3行
学習ポイント
ABUNIONUniqueA ∪ B
SET OPERATION
UNION vs UNION ALL
重複除去(UNION)か全件保持(UNION ALL)か
UNION と UNION ALL の本質的な違い:UNION は連結後に重複除去を行うため、内部でソートやハッシュのコストがかかります。UNION ALL は除去処理をせず全件をそのまま返すため高速です。重複が出ないと分かっている、あるいは重複も残したい場合は UNION ALL を選ぶのがパフォーマンス上のベストプラクティスです。
重複判定は「全列の組み合わせ」:UNION の重複除去は DISTINCT と同じく、SELECT したすべての列の組み合わせで判定されます。今回は email 1列なので分かりやすいですが、複数列を SELECT する場合は「全列が一致する行」だけが重複扱いになります。各 SELECT の列数・並び・型を揃えることも必須条件です。
ORDER BY は最後に1回だけ:UNION 全体の並べ替えは、最後の SELECT のあとに ORDER BY を1回書きます。各 SELECT ごとに ORDER BY を付けることはできません。なお UNION(重複除去)の仲間に、共通部分を取る INTERSECT、差を取る EXCEPT もあり、同じ集合演算ファミリーです。
アンチパターン
何でも UNION を使って無駄なコストを払う:重複しないと分かっているデータ(例: 月別に分かれたテーブルの縦結合)にまで UNION を使うと、不要な重複除去のソートコストを毎回支払うことになります。さらに明細を積み上げる用途で UNION を使うと、本来別々の正当な行まで「同じ値だから」と消えてしまいます。明細の積み上げは UNION ALL が正解です。
列数・型の不一致でエラー、各SELECTに ORDER BY を付ける:UNION する SELECT 同士で列数が違ったり型が合わなかったりするとエラーになります。また、途中の SELECT に ORDER BY を書くのも構文として不正です。並べ替えは必ず全体の末尾に1回だけ書きましょう。
実務コラム
リスト統合は実務の定番です。応用例: EC と実店舗の顧客リストを重複なく統合、複数データソースのマスタ統合、月別・年別に分割されたテーブルの縦結合。判断基準はシンプルで、名簿を統合して重複を消したいなら UNION、明細をそのまま積み上げたいなら UNION ALL。「とりあえず UNION」ではなく、重複を消すべきかどうかを意識して選ぶことが、正確さとパフォーマンスの両立につながります。