Query Go
重複行を消したい — DISTINCT / GROUP BY / ROW_NUMBER() 使い分け
ガイド

重複行を消したい — DISTINCT / GROUP BY / ROW_NUMBER() 使い分け

重複行を取り除きたいとき。DISTINCT / GROUP BY / ROW_NUMBER の使い分け。

重複行を消したい — DISTINCT / GROUP BY / ROW_NUMBER() 使い分け diagram

問題 — 「重複」には2種類ある

「重複行を消したい」という要求は、よく見ると2通りあります。

  • (A) 全列完全に同じ行が複数ある → とにかく1行にまとめたい
  • (B) キー列は同じだが他の列が違う行が複数ある → そのキーで「どれか1件」だけ残したい(最新1件など)

(A) は DISTINCT または GROUP BY で一発ですが、(B) は 残す行の選択基準が必要なので ROW_NUMBER() OVER() やウィンドウ関数が必要になります。最初に自分がどちらかを見極めるのが一番重要です。

解法1: DISTINCT — 全列の完全重複に

最もシンプル。SELECT DISTINCT は指定した列の組合せで重複を1行にまとめます。全列指定なら完全重複の解消。

ただし どの行を残すかを選べないので、(B) のように「最新1件だけ」という要件には向きません。PostgreSQL の DISTINCT ON (...) は拡張機能で、キー + ORDER BY で「最新1件残し」ができる便利構文です(他 RDBMS には無し)。

sql
-- (A) 完全重複を1行に
SELECT DISTINCT * FROM logs;

-- PostgreSQL 拡張: user_id ごとに最新1件
SELECT DISTINCT ON (user_id) user_id, action, created_at
FROM events
ORDER BY user_id, created_at DESC;

解法2: GROUP BY — 集約したいなら自然

キー列で GROUP BY し、他の列には MAX() / MIN() / SUM() などの集約関数を使う方法。「ユーザーごとに最新注文日」など集約値だけで十分なら最短ルートです。

ただし「最新注文日の注文ID」「最新注文日の金額」など同一行の他の列も一緒に欲しいときは、別テーブル化した上で結合が必要になって冗長です。そこは解法3の出番。

sql
-- ユーザーごとに最新注文日だけ欲しい
SELECT user_id, MAX(created_at) AS last_order
FROM orders
GROUP BY user_id;

解法3: ROW_NUMBER() — 「キー単位で1件残し」の本命

もっとも柔軟で標準 SQL。PARTITION BY キー ORDER BY 並び順 で各グループ内に番号を振り、外側で rn = 1 を残します。残したい行の基準(新しい順、金額の大きい順など)を自由に書けるのが強み。

PostgreSQL / MySQL 8 / SQL Server / SQLite 3.25+ で使えるポータブルな書き方なので、RDBMS 非依存で書きたいならこれ一択です。

sql
-- user_id ごとに最新1件だけ残す
SELECT * FROM (
  SELECT
    e.*,
    ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY created_at DESC
    ) AS rn
  FROM events e
) t
WHERE rn = 1;

注意点 — 性能とNULL

DISTINCT は安易に使わない。結合の書き方を間違えて重複が出たのを DISTINCT で隠すのは典型的なアンチパターンで、根本原因(結合条件 or N対N関係)を先に直してください。

大量データで ROW_NUMBER を使うときは、(PARTITION 列, ORDER BY 列) に合わせたインデックスを貼ると劇的に速くなります。

NULL を含む列で重複判定する場合、NULL は等しくないNULL = NULL は UNKNOWN)ので、DISTINCTGROUP BY は「NULL 同士は同じグループ」扱いになる一方、JOINWHERE x = y は一致しない。挙動が違うので注意。

関連トピック