SQLで重複を除くときは、まず SELECT DISTINCT が思い浮かびます。ただし、DISTINCTが1行にまとめるのは「SELECTに書いた列がすべて同じ行」だけです。「顧客ごとに1行だけ取り出し、その顧客の最新の注文日と金額も表示したい」という場合は、DISTINCTでは目的の結果を得られません。

この記事では、テーブルの中身は変更せず、SELECTの段階で重複を除いて取得する方法を目的別に整理します。テーブルから重複行を実際に削除する作業は扱いません。

先に決めること:何を重複とみなし、どの行を残すか

どの方法を使うか決める前に、次の2点を言葉にしておきます。ここが曖昧なままだと、件数は合っていても中身が意図と違うSQLになることがあります。

  1. 重複のキー:どの列が同じなら「同じもの」とみなすか(例:customer_id が同じなら同じ顧客)
  2. 残す行の基準:キーが同じ行が複数あるとき、どの行を残すか(例:注文日が最も新しい行。同じ日なら注文番号が大きい行)

「どの行でもよい」と考えている場合でも、実行するたびに結果が変わってよいのかは確認しておきます。

目的別の使い分け

やりたいこと 使う方法
SELECTした列の組み合わせが完全に同じ行を1行にしたい DISTINCT
キーごとの件数・合計・最大値などの集計値がほしい GROUP BY
キーごとに1行だけ、その行のほかの列もそのまま取りたい ROW_NUMBER
2つのSELECTの結果を縦につなぎ、重複を除きたい UNION
2つのSELECTの結果を縦につなぎ、すべて残したい UNION ALL

以下では、次の注文テーブル orders を例に説明します。

order_id customer_id order_date amount staff
1001 C01 2026-04-03 12000 佐藤
1002 C01 2026-05-10 8000 鈴木
1003 C02 2026-05-10 5000 佐藤
1004 C02 2026-05-10 7000 田中
1005 C03 2026-03-21 3000 鈴木

DISTINCTで足りる場合・足りない場合

顧客IDの一覧がほしいだけなら、DISTINCTで足ります。

SELECT DISTINCT customer_id
FROM orders;

結果は C01・C02・C03 の3行です。

ところが、注文日も一緒に表示しようとして列を追加すると、結果が変わります。

SELECT DISTINCT customer_id, order_date
FROM orders;

C01は注文日が2種類あるので2行になります。C02は2件とも同じ日なので1行です。さらに amount を追加すると、C02も2行になります。DISTINCTは「列の組み合わせ」で重複を判定するため、列を増やすほど重複とみなされる行は減ります。これでは「顧客ごとに1行」にはなりません。

GROUP BY:キーごとの集計値を取る

顧客ごとの件数・最新の注文日・合計金額のような集計値がほしい場合は、GROUP BYを使います。

SELECT
    customer_id,
    COUNT(*)        AS order_count,
    MAX(order_date) AS last_order_date,
    SUM(amount)     AS total_amount
FROM orders
GROUP BY customer_id;

GROUP BYを使うときは、次の2点に注意します。

  • GROUP BYに書いていない列は、集計関数で囲まないとSELECTに書けません。 多くのデータベースではエラーになります。MySQLは設定によってエラーにならず、どの行から来たか分からない値を返すことがあります。エラーが出なかったことを、正しいことの根拠にしないでください。
  • 集計関数は列ごとに別々に計算されます。 たとえば MAX(order_date) と MAX(amount) を並べると、C01については最新日 2026-05-10 と最大金額 12000 が返ります。しかし、12000は 2026-04-03 の注文の金額です。別々の行の値が1行に混ざるので、「最新の注文の金額」にはなりません。

最新の注文日を求め、その日付の行を元のテーブルから結合して取り出す方法もあります。ただ、この例ではC02の最新日 2026-05-10 に注文が2件あるため、C02が2行返ります。同じ値の行が複数ありうる場合は、次に説明するROW_NUMBERのほうが確実です。

ROW_NUMBER:キーごとに1行だけ、ほかの列も一緒に取る

「顧客ごとに最新の注文を1行ずつ、注文番号・金額・担当者も含めて取りたい」という場合は、ROW_NUMBERで各行に番号を振り、1番の行だけを取り出します。

WITH ranked AS (
    SELECT
        order_id,
        customer_id,
        order_date,
        amount,
        staff,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY order_date DESC, order_id DESC
        ) AS rn
    FROM orders
)
SELECT order_id, customer_id, order_date, amount, staff
FROM ranked
WHERE rn = 1;

各部分の意味は次のとおりです。

  • PARTITION BY customer_id:重複のキーです。顧客ごとに番号を1から振り直します。
  • ORDER BY order_date DESC, order_id DESC:残す行の基準です。注文日が新しい順、同じ日なら注文番号が大きい順に1、2、3…と番号を振ります。
  • WHERE rn = 1:各顧客の1番目の行だけを残します。

結果は次の3行です。

order_id customer_id order_date amount staff
1002 C01 2026-05-10 8000 鈴木
1004 C02 2026-05-10 7000 田中
1005 C03 2026-03-21 3000 鈴木

ORDER BYには順番を1つに決める列を必ず入れる

ORDER BY order_date DESC だけでは、C02の2件が同じ順位になります。どちらが1番になるかはデータベース次第になり、実行するたびに結果が変わる可能性があります。注文番号のように値が重ならない列を最後に追加して、順番が1つに決まるようにします。

WITH(またはサブクエリ)で囲む理由

ROW_NUMBERのようなウィンドウ関数は、WHERE句より後に計算されます。そのため WHERE ROW_NUMBER() OVER (...) = 1 とは書けません。いったんWITH句(共通テーブル式)かサブクエリで番号付きの結果を作り、外側のSELECTで rn = 1 に絞り込みます。SQL Serverでは、WITHの直前の文がセミコロンで終わっていないとエラーになることがあります。

ROW_NUMBER・RANK・DENSE_RANKの違い

関数 同じ順位の行 1番の行だけを取ったときの件数
ROW_NUMBER 必ず別々の番号になる キーごとに必ず1行
RANK 同じ番号になり、その次の番号が飛ぶ 同じ順位があれば複数行
DENSE_RANK 同じ番号になり、その次の番号は飛ばない 同じ順位があれば複数行

「同じ日の注文はすべて見たい」ならRANK、「必ず1行に絞りたい」ならROW_NUMBERを選びます。

使っているデータベースで使えるか確認する

ROW_NUMBERは、SQL Server・PostgreSQL・Oracle・MySQL 8.0以降・SQLite 3.25以降など、多くのデータベースで使えます。ただし、古いバージョンや、AccessのSQLのようにウィンドウ関数に対応していない製品もあります。使っている製品とバージョンの公式ドキュメントで、ウィンドウ関数に対応しているかを確認してください。

UNIONとUNION ALL:2つの結果をつなぐときの重複

別々のテーブルや条件から取得した結果を縦につなぐときは、UNIONかUNION ALLを使います。

-- 店舗の顧客とWebの顧客を1つの一覧にする(重複は除く)
SELECT customer_id, customer_name FROM store_customers
UNION
SELECT customer_id, customer_name FROM web_customers;
比較項目 UNION UNION ALL
重複の扱い つないだ結果全体から、すべての列が同じ行を除く 除かずにすべて残す
片方の中だけにある重複 それも除かれる 残る
処理 重複を判定する処理が加わる 判定しない

押さえておきたい点は次のとおりです。

  • UNIONで除かれるのは、選択したすべての列が同じ行です。考え方はDISTINCTと同じで、キーが同じでも名前の表記が違う行は残ります。
  • どちらのテーブルから来た行かを示す列(例:'店舗' AS source)を追加すると、その列の値が違うため、両方に登録されている顧客は2行とも残ります。出どころを確認したいならUNION ALLを使うと割り切り、重複を除きたいなら出どころの列は付けません。
  • 2つのSELECTは列の数をそろえ、対応する列の型も合わせます。結果の列名には、1つ目のSELECTの列名が使われます。
  • 期間がまったく重ならない2か月分のデータのように、そもそも重複が起きない組み合わせなら、UNION ALLで十分です。

取得件数で結果を確かめる

重複を除くSQLは、エラーなく動いても意図どおりとは限りません。次の件数を並べて確認します。

1. 元の件数とキーの種類数

SELECT
    COUNT(*)                    AS total_rows,
    COUNT(DISTINCT customer_id) AS distinct_customers
FROM orders;

ROW_NUMBERやGROUP BYで「キーごとに1行」にした結果の件数は、原則として distinct_customers と一致します。

ただし、COUNT(DISTINCT 列) はNULLを数えません。一方、GROUP BYとPARTITION BYはNULLを1つのグループとして扱います。キー列にNULLがあると、結果の件数のほうが1件多くなります。件数が1件だけずれるときは、まずNULLを疑ってください。

2. 重複しているキーの一覧

SELECT customer_id, COUNT(*) AS cnt
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 1
ORDER BY cnt DESC;

どのキーで何件重複しているかが分かります。ここに出たキーをいくつか選び、残った行が基準どおりかを元のデータと見比べます。

3. 結果にキーの重複が残っていないか

ROW_NUMBERで絞り込んだ結果を、もう一度キーで集計します。

WITH ranked AS (
    SELECT
        customer_id,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY order_date DESC, order_id DESC
        ) AS rn
    FROM orders
)
SELECT customer_id, COUNT(*) AS cnt
FROM ranked
WHERE rn = 1
GROUP BY customer_id
HAVING COUNT(*) > 1;

結果が0行なら、キーごとに1行になっています。

4. UNIONで除かれた件数

UNIONを使う前と後の件数を比べます。

  • 1つ目のSELECTの件数+2つ目のSELECTの件数=UNION ALLの件数
  • UNION ALLの件数−UNIONの件数=重複として除かれた件数

除かれた件数が想定より多い、または少ない場合は、どの行が重複と判定されたかをGROUP BYとHAVINGで確認します。

重複が残る・除かれすぎるときの確認点

  • 大文字・小文字、末尾の空白:同じ文字列とみなすかどうかは、データベースや列の照合順序(collation)の設定によって変わります。比較のルールを確認し、必要ならキーにする式でUPPERやTRIMなどを使ってそろえます。使える関数名と対応バージョンは、データベースごとに確認してください。
  • 全角・半角、表記ゆれ:「株式会社」と「(株)」のような違いは、SQLで重複を除いても同じものになりません。表記ゆれの統一は、別の作業として考えます。
  • 型の違い:文字列の '001' と数値の 1 のように型が違う列どうしを比べたりつないだりすると、比較の結果が想定と変わることがあります。キー列の型をそろえておきます。
  • NULL:DISTINCT・GROUP BY・PARTITION BYは、NULLどうしを同じものとして1つにまとめます。NULLのキーを結果に含めるか、WHERE句で除くかを決めておきます。

まとめ

  • 列の組み合わせが完全に同じ行をまとめるだけなら、DISTINCT
  • キーごとの集計値がほしいなら、GROUP BY(列ごとの最大値が別々の行から来る点に注意)
  • キーごとに1行、ほかの列もそのまま取りたいなら、ROW_NUMBERと順番を1つに決める列
  • 結果を縦につなぐなら、重複を除くUNIONか、すべて残すUNION ALL
  • どの方法でも、元の件数・キーの種類数・結果の件数を並べて確かめる
おすすめの記事