🧚 お知らせ!Beekeeper Studio は高速でモダンなオープンソースのデータベース GUI です ダウンロード
2026年1月9日 著者: Matthew Rathbone

SQL で重複を見つけるには、一意であるべき列で行をグループ化し、2行以上あるグループを残します。さらにウィンドウ関数や自己結合を使えば重複している行そのものを確認でき、どれを残すか判断できます。例は PostgreSQL、MySQL、SQLite、SQL Server で動作します。

SQL で重複を見つける方法

GROUP BY は同じ値の行をまとめ、COUNT(*) は各グループの行数を数え、HAVING は2行以上あるグループだけを残します:

SELECT columns, COUNT(*)
FROM table_name
GROUP BY columns
HAVING COUNT(*) > 1;

たとえば、複数の顧客が使っているメールアドレスを見つけるには次のようにします:

-- Input: customers
-- id | name           | email
-- ---+----------------+----------------------
-- 1  | Maria Lopez    | maria.lopez@gmail.com
-- 2  | Sam Patel      | sam@patel.dev
-- 3  | M. Lopez       | maria.lopez@gmail.com
-- 4  | Kenji Watanabe | kenji@watanabe.jp

SELECT email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;

-- Output:
-- email                 | copies
-- ----------------------+-------
-- maria.lopez@gmail.com | 2

複数の列がすべて一致する場合のみ重複とみなすには、それらの列をすべて SELECT と GROUP BY の両方に記述します。

知っておくべきルール

  • SELECT する列はすべて GROUP BY に含めるか、集計関数の中に入れる必要があります。上のクエリに name を追加しても、重複している人の名前は表示されません。エラーになります。ただし SQLite ではメールごとに1つの名前が表示されます:
-- Input: the customers table above

SELECT name, email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;

-- ERROR: column "customers.name" must appear in the GROUP BY clause or be used in an aggregate function

重複している行をすべて確認するには、「別の書き方」にあるクエリを使ってください。

  • NULL は1つのグループにまとめられ、大文字と小文字は区別されます。NULL のメールはすべて同じグループに入るため互いに重複しているように見えますが、Ana@Silva.dev と ana@silva.dev は重複とみなされません:
-- Input: users
-- id | email
-- ---+--------------
-- 1  | ana@silva.dev
-- 2  | NULL
-- 3  | Ana@Silva.dev
-- 4  | NULL

SELECT email, COUNT(*) AS copies
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- Output:
-- email | copies
-- ------+-------
-- NULL  | 2

-- MySQL and SQL Server ignore case by default, so they also return ana@silva.dev | 2

-- Skip the NULLs and ignore case:
SELECT LOWER(email) AS email, COUNT(*) AS copies
FROM users
WHERE email IS NOT NULL
GROUP BY LOWER(email)
HAVING COUNT(*) > 1;

-- Output:
-- email         | copies
-- --------------+-------
-- ana@silva.dev | 2

別の書き方

GROUP BY は重複した値ごとに1行を返します。重複している行そのものを取得するには、代わりにウィンドウで数えます。COUNT(*) OVER (PARTITION BY email) は、各メールの件数をそのメールを持つすべての行に付けます:

-- Input: the customers table above

SELECT id, name, email, copies
FROM (
  SELECT id, name, email, COUNT(*) OVER (PARTITION BY email) AS copies
  FROM customers
) AS c
WHERE copies > 1
ORDER BY id;

-- Output:
-- id | name        | email                 | copies
-- ---+-------------+-----------------------+-------
-- 1  | Maria Lopez | maria.lopez@gmail.com | 2
-- 3  | M. Lopez    | maria.lopez@gmail.com | 2

EXISTS を使えば、ウィンドウ関数なしで同じことができます。同じメールを持つ別の行が存在する行だけを残します:

-- Input: the customers table above

SELECT id, name, email
FROM customers c
WHERE EXISTS (
  SELECT 1 FROM customers other
  WHERE other.email = c.email AND other.id <> c.id
);

-- Output:
-- id | name        | email
-- ---+-------------+----------------------
-- 1  | Maria Lopez | maria.lopez@gmail.com
-- 3  | M. Lopez    | maria.lopez@gmail.com

重複を探すべきなのはどんなとき?

重複は、インポート、ダブルクリック、リトライ、制約の不足などによって入り込みます。重複に関してよく必要になる作業は次の4つです。

列が一意かどうかを確認する

UNIQUE 制約を追加する前や、ログインや検索で2行返されるようになったときは、複数回出現する値を一覧にします。GROUP BY と HAVING でそのまま確認できます。

関係するすべての行を確認する

どの値が重複しているかがわかっても、それだけでは修正できません。行全体を見て、どこが違うのか、どれが正しいのかを確認する必要があります。重複した値を同じテーブルに対するクエリに渡しましょう。

余分なコピーに印を付ける

クリーンアップするには、重複の各セットから1行を残して残りを削除します。ROW_NUMBER() は各セット内の行に番号を振るので、1番目以降の行はすべて余分なコピーです。

二重送信を検出する

ボタンのダブルクリックや支払いのリトライでは、ID 以外がすべて一致する2行が残ります。テーブルを自己結合するとそれらがペアになり、t1.id < t2.id で各ペアを1回だけ表示できます。

重複を見つける実例

ダブルブッキングされたホテルの部屋を見つける

1つの部屋は1泊につき1回しか予約できないので、重複とは同じ部屋と同じ日の組み合わせです。両方の列でグループ化します:

-- Input: bookings
-- booking_id | room | night      | guest
-- -----------+------+------------+----------
-- 1          | 204  | 2026-11-06 | R. Okafor
-- 2          | 204  | 2026-11-07 | R. Okafor
-- 3          | 310  | 2026-11-06 | L. Chen
-- 4          | 204  | 2026-11-06 | P. Novak

SELECT room, night, COUNT(*) AS bookings
FROM bookings
GROUP BY room, night
HAVING COUNT(*) > 1;

-- Output:
-- room | night      | bookings
-- -----+------------+---------
-- 204  | 2026-11-06 | 2

メールアドレスを共有しているアカウントを一覧にする

同じアドレスを大文字・小文字を変えて二重に登録する人がいます。LOWER(email) でパーティション分割すると、両方のアカウントを並べて確認できます:

-- Input: accounts
-- id | username | email
-- ---+----------+----------------
-- 1  | jkim     | jin@kim.kr
-- 2  | olivia_r | olivia@reyes.mx
-- 3  | jin.kim  | Jin@Kim.kr
-- 4  | noah_b   | noah@baker.uk

SELECT id, username, email, accounts_with_email
FROM (
  SELECT id, username, email,
         COUNT(*) OVER (PARTITION BY LOWER(email)) AS accounts_with_email
  FROM accounts
) AS a
WHERE accounts_with_email > 1
ORDER BY id;

-- Output:
-- id | username | email      | accounts_with_email
-- ---+----------+------------+--------------------
-- 1  | jkim     | jin@kim.kr | 2
-- 3  | jin.kim  | Jin@Kim.kr | 2

クリーンアップ前に重複した人物に印を付ける

姓と名が同じユーザーを同一人物とみなし、最も古いレコードを残して、それ以外にラベルを付けます。CTE を使うと番号付けとラベル付けを分けて書けます:

-- Input: users
-- id | first_name | last_name | email
-- ---+------------+-----------+----------------
-- 1  | Priya      | Shah      | priya@shah.net
-- 2  | Tom        | Becker    | tom@becker.de
-- 3  | Priya      | Shah      | pshah@work.com
-- 4  | Lena       | Kowalski  | lena@kowal.pl
-- 5  | Tom        | Becker    | tbecker@mail.de

WITH numbered AS (
  SELECT id, first_name, last_name, email,
         ROW_NUMBER() OVER (PARTITION BY first_name, last_name ORDER BY id) AS copy_number
  FROM users
)
SELECT id, first_name, last_name, email,
       CASE WHEN copy_number > 1 THEN 'Duplicate' ELSE 'Original' END AS status
FROM numbered
ORDER BY id;

-- Output:
-- id | first_name | last_name | email           | status
-- ---+------------+-----------+-----------------+----------
-- 1  | Priya      | Shah      | priya@shah.net  | Original
-- 2  | Tom        | Becker    | tom@becker.de   | Original
-- 3  | Priya      | Shah      | pshah@work.com  | Duplicate
-- 4  | Lena       | Kowalski  | lena@kowal.pl   | Original
-- 5  | Tom        | Becker    | tbecker@mail.de | Duplicate

Duplicate と印の付いた行が、統合または削除の対象です。削除方法は重複削除ガイドで紹介しています。

誤って二重に注文されたものを見つける

同じ顧客が同じ日に同じ商品を2回注文している場合、おそらく1つの注文が二重に送信されたものです:

-- Input: orders
-- order_id | customer_id | product   | order_date
-- ---------+-------------+-----------+-----------
-- 1001     | 42          | Desk Lamp | 2026-03-02
-- 1002     | 42          | Desk Lamp | 2026-03-02
-- 1003     | 42          | Desk Lamp | 2026-03-09
-- 1004     | 57          | Desk Lamp | 2026-03-02

SELECT o1.order_id AS first_order, o2.order_id AS repeat_order,
       o1.customer_id, o1.product
FROM orders o1
JOIN orders o2
  ON o1.customer_id = o2.customer_id
 AND o1.product = o2.product
 AND o1.order_date = o2.order_date
 AND o1.order_id < o2.order_id;

-- Output:
-- first_order | repeat_order | customer_id | product
-- ------------+--------------+-------------+----------
-- 1001        | 1002         | 42          | Desk Lamp

重複に関する参考資料

このブログの記事:

公式ドキュメント:

Beekeeper Studioは無料のオープンソースのデータベースGUIです

これまで使った中で最高のSQLクエリ&エディタツールです。データベースの管理に必要なものがすべて揃っています。 - ⭐⭐⭐⭐⭐ Mit

Beekeeper Studioは高速で直感的、そして使いやすいツールです。多数のデータベースに対応し、Windows、Mac、Linuxで快適に動作します。

BeekeeperのLinux版は100%フル機能。手抜きも機能の妥協も一切ありません。

Beekeeper Studio 利用者の声

★★★★★
「Beekeeper Studio は以前の SQL ワークフローを完全に置き換えました。高速で直感的で、データベース作業がまた楽しくなりました。」
— Alex K.、データベース開発者
★★★★★
「多くのデータベース GUI を試してきましたが、Beekeeper は機能とシンプルさのバランスが完璧です。とにかくちゃんと動きます。」
— Sarah M.、フルスタックエンジニア