SQLで重複を削除するとは、次のどちらかを意味します。DISTINCTでクエリ結果から重複行を隠すか、テーブルから余分なコピーを削除して1件ずつ残すかです。例はPostgreSQL、MySQL、SQLite、SQL Serverで動作します。
SQLで重複を削除する方法
SELECT DISTINCTは指定した列の組み合わせを1回ずつ返し、テーブル自体は変更しません。テーブルそのものをクリーンアップするには、重複グループごとに1行(通常はidが最小の行)を残して、それ以外をすべて削除します。
SELECT DISTINCT column_list FROM table_name;
DELETE FROM table_name
WHERE id NOT IN (
SELECT id FROM (
SELECT MIN(id) AS id FROM table_name GROUP BY column_list
) AS keepers
);
この例では、Maria Lopezが3回登録されています。
-- Input: customers
-- id | name | email | phone
-- ---+-------------+-------------------+---------
-- 1 | Maria Lopez | maria@example.com | 555-0142
-- 2 | Sam Patel | sam@example.com | 555-0178
-- 3 | Maria Lopez | maria@example.com | 555-0142
-- 4 | Lena Novak | lena@example.com | 555-0121
-- 5 | Maria Lopez | maria@example.com | 555-0142
SELECT DISTINCT name, email, phone
FROM customers
ORDER BY name;
-- Output:
-- name | email | phone
-- ------------+-------------------+---------
-- Lena Novak | lena@example.com | 555-0121
-- Maria Lopez | maria@example.com | 555-0142
-- Sam Patel | sam@example.com | 555-0178
DELETE FROM customers
WHERE id NOT IN (
SELECT id FROM (
SELECT MIN(id) AS id FROM customers GROUP BY name, email, phone
) AS keepers
);
-- Output (customers after the DELETE):
-- id | name | email | phone
-- ---+-------------+-------------------+---------
-- 1 | Maria Lopez | maria@example.com | 555-0142
-- 2 | Sam Patel | sam@example.com | 555-0178
-- 4 | Lena Novak | lena@example.com | 555-0121
知っておくべきルール
-
2つの行を重複とみなす列を指定します。idや、
created_atタイムスタンプのように常に異なる値は除外してください。指定した列のいずれかが異なる行はすべて残ります。 -
SELECT id FROM (...) AS keepersのラッパーはMySQL用です。これがないと、MySQLはDELETEがサブクエリで自身のテーブルを読むことを拒否し、「You can’t specify target table ‘customers’ for update in FROM clause」というエラーになります。 -
削除する前に確認しましょう。まず
GROUP BYまたはROW_NUMBER()のクエリを実行して削除対象を確認し、ロールバックできるトランザクション内で削除を行ってください。 - テーブルがきれいになったら、ユニークインデックスで新たな重複を防ぎます。
CREATE UNIQUE INDEX customers_email_key ON customers (email);
INSERT INTO customers VALUES (6, 'Maria Lopez', 'maria@example.com', '555-0142');
-- ERROR: duplicate key value violates unique constraint "customers_email_key"
別の構文
SQL Serverのコードでは、CTEでコピーに番号を振り、CTE経由で削除する書き方をよく見かけます。これはSQL Serverでのみ可能です。元のcustomersテーブルの場合:
-- SQL Server
WITH numbered AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY name, email, phone ORDER BY id
) AS row_num
FROM customers
)
DELETE FROM numbered WHERE row_num > 1;
-- Output (customers after the DELETE):
-- id | name | email | phone
-- ---+-------------+-------------------+---------
-- 1 | Maria Lopez | maria@example.com | 555-0142
-- 2 | Sam Patel | sam@example.com | 555-0178
-- 4 | Lena Novak | lena@example.com | 555-0121
2つのクエリを行の重複なしで結合するには、重複を残すUNION ALLではなく、重複を除去するUNIONを使います。
SELECT 'apple' AS fruit UNION ALL SELECT 'apple';
-- Output:
-- fruit
-- -----
-- apple
-- apple
SELECT 'apple' AS fruit UNION SELECT 'apple';
-- Output: apple
重複はいつ削除すべきか?
結果の重複を隠すだけで済む場合もあれば、テーブル自体のクリーンアップが必要な場合もあります。確認から削除までの一般的な手順を紹介します。
各値を1回ずつ表示する
レポートやドロップダウン、列の中身をざっと確認したいときは、SELECT DISTINCTで結果から重複を除けます。テーブルはそのままです。
削除前にコピーの数を数える
重複を定義する列でグループ化し、HAVING COUNT(*) > 1でグループを絞り込みます。これで、何かを削除する前に、何が何回重複しているかがわかります。確認方法の詳細はSQLで重複を見つけるガイドをご覧ください。
残すコピーを選ぶ
ROW_NUMBER()は重複グループごとに行へ番号を振ります。OVER (...)内のORDER BYによって、どの行が1になって残るか(最も古い行、最も新しい行、最も情報が揃った行)が決まります。
余分なコピーを削除する
残す行が決まったら、それ以外を削除します。最も古い行ではなく最も新しい行を残すには、日付の新しい順に番号を振り、番号が1より大きい行をすべて削除します。
重複削除の実例
配送先のすべての国と配送業者
DISTINCTは選択した列の行全体に適用されるため、Canadaは配送業者ごとに1回表示されます。
-- Input: shipments
-- order_id | country | carrier
-- ---------+---------+--------
-- 1001 | Canada | UPS
-- 1002 | Germany | DHL
-- 1003 | Canada | FedEx
-- 1004 | Canada | UPS
-- 1005 | Germany | DHL
SELECT DISTINCT country, carrier
FROM shipments
ORDER BY country, carrier;
-- Output:
-- country | carrier
-- --------+--------
-- Canada | FedEx
-- Canada | UPS
-- Germany | DHL
大文字・小文字だけが異なるメールアドレス
小文字化したメールでグループ化すると、Jon@Example.comとjon@example.comがコピーとして扱われます。(MySQLとSQL Serverはデフォルトで大文字・小文字を区別しませんが、LOWER()を使っても問題ありません。)
-- Input: accounts
-- id | email
-- ---+----------------------
-- 1 | Ana.Silva@example.com
-- 2 | jon@example.com
-- 3 | ana.silva@example.com
-- 4 | Jon@Example.com
-- 5 | mei@example.com
SELECT LOWER(email) AS email, COUNT(*) AS copies
FROM accounts
GROUP BY LOWER(email)
HAVING COUNT(*) > 1
ORDER BY email;
-- Output:
-- email | copies
-- ----------------------+-------
-- ana.silva@example.com | 2
-- jon@example.com | 2
電話番号のあるコピーを残す
電話番号のある行を先に、次にid順で並べると、各グループで最も情報が揃った行が1番になります。
-- Input: contacts
-- id | email | phone
-- ---+------------------+---------
-- 1 | ravi@example.com | NULL
-- 2 | ravi@example.com | 555-0110
-- 3 | ines@example.com | 555-0133
-- 4 | ines@example.com | NULL
SELECT
id,
email,
phone,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY CASE WHEN phone IS NULL THEN 1 ELSE 0 END, id
) AS row_num
FROM contacts
ORDER BY email, row_num;
-- Output:
-- id | email | phone | row_num
-- ---+------------------+----------+--------
-- 3 | ines@example.com | 555-0133 | 1
-- 4 | ines@example.com | NULL | 2
-- 2 | ravi@example.com | 555-0110 | 1
-- 1 | ravi@example.com | NULL | 2
番号が2以上の行(id 1と4)を削除すると、メールごとに完全な連絡先が1件ずつ残ります。
idのないテーブルから完全なコピーを削除する
すべての列が同一のコピーでは、最小値を残すためのidがありません。重複を除いた行を新しいテーブルにコピーし、置き換えます。コピーではインデックスや制約は引き継がれないため、元のテーブルにあったものを追加し直してください。
-- Input: page_views
-- url | viewed_at
-- ---------+--------------------
-- /pricing | 2026-10-01 09:00:00
-- /pricing | 2026-10-01 09:00:00
-- /docs | 2026-10-01 09:05:00
-- /pricing | 2026-10-01 09:00:00
-- In SQL Server, use SELECT DISTINCT * INTO page_views_clean FROM page_views,
-- then EXEC sp_rename 'page_views_clean', 'page_views'
CREATE TABLE page_views_clean AS SELECT DISTINCT * FROM page_views;
DROP TABLE page_views;
ALTER TABLE page_views_clean RENAME TO page_views;
-- Output (page_views afterwards):
-- url | viewed_at
-- ---------+--------------------
-- /pricing | 2026-10-01 09:00:00
-- /docs | 2026-10-01 09:05:00
重複削除に関する関連記事
このブログの記事:
- SQLで重複を見つけるガイド:削除前に重複行を見つけるその他の方法
- SQL Serverテーブルの重複行を検出する:SQL Serverの観点から
- SQL COUNT DISTINCTガイド:重複を削除せずにユニークな値を数える
- SQL DELETE文の構文と例:行削除の基本
- PostgreSQLでユニーク制約を作成する:重複を恒久的に防ぐ
- SQL CTEとサブクエリの比較ガイド:「どの行を残すか」のステップの2通りの書き方
公式ドキュメント:
- PostgreSQL: DELETEとウィンドウ関数
- MySQL: DELETEとROW_NUMBER()
- SQLite: DELETEとウィンドウ関数
- SQL Server: DELETEとROW_NUMBER
Beekeeper Studioは無料のオープンソースのデータベースGUIです
これまで使った中で最高のSQLクエリ&エディタツールです。データベースの管理に必要なものがすべて揃っています。 - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studioは高速で直感的、そして使いやすいツールです。多数のデータベースに対応し、Windows、Mac、Linuxで快適に動作します。
Beekeeper Studio利用者の声
「Beekeeper Studioで以前のSQLワークフローを完全に置き換えました。高速で直感的で、データベース作業がまた楽しくなりました。」
「多くのデータベースGUIを試しましたが、Beekeeperは機能とシンプルさのバランスが完璧です。とにかくちゃんと動きます。」