【Oracle】重複データを削除する方法完全ガイド|ROWID・ROW_NUMBER・CTAS・残す行の選択・再発防止まで解説

【Oracle】重複データを削除する方法完全ガイド|ROWID・ROW_NUMBER・CTAS・残す行の選択・再発防止まで解説 Oracle

Oracle テーブルに意図せず重複データが混入することは実務でよく起きます。CSV の二重取り込み、バッチ処理の再実行、UNIQUE 制約の付け忘れなどが主な原因です。

重複の削除は「どの行を残してどの行を消すか」を正確に制御しないと、必要なデータまで消してしまうリスクがあります。本記事では、重複の特定方法から、ROWID を使った削除(Oracle 固有)、ROW_NUMBER() で残す行を制御する削除CTAS(テーブル再作成)による高速除去、そしてUNIQUE 制約による再発防止まで体系的に解説します。

この記事でわかること
・GROUP BY + HAVING で重複データを特定する方法
・ROWID を使った重複削除(Oracle 固有で最もシンプル)
・ROW_NUMBER() で「どの行を残すか」を制御する方法
・CTAS でテーブルごと再作成する方法(大量重複時に最速)
・完全一致 vs 特定列のみ一致の重複判定
・最新行/最古行/特定条件で残す行を選ぶ方法
・大量データのバッチ削除
・UNIQUE 制約の追加による再発防止
スポンサーリンク

重複データの特定

SQL(重複の検出)
-- 特定列の組み合わせで重複している行を検出
SELECT email, COUNT(*) AS dup_count
FROM customers
GROUP BY email
HAVING COUNT(*) >= 2
ORDER BY dup_count DESC;

-- 複数列の組み合わせで重複検出
SELECT first_name, last_name, birth_date, COUNT(*) AS dup_count
FROM employees
GROUP BY first_name, last_name, birth_date
HAVING COUNT(*) >= 2;
SQL(重複行の中身を確認)
-- 重複している実データを確認(削除前に必ず確認)
SELECT e.*
FROM employees e
WHERE (first_name, last_name) IN (
    SELECT first_name, last_name FROM employees
    GROUP BY first_name, last_name
    HAVING COUNT(*) >= 2
)
ORDER BY last_name, first_name, employee_id;
削除前に必ず確認する
重複データの中身を SELECT で確認してから DELETE を実行してください。「ROWID が小さい方を残す」「最新の行を残す」など、残す行の基準をチーム内で決めてから作業に入るのが安全です。

方法(1): ROWID を使った削除(Oracle 固有)

Oracle 固有の ROWID(行の物理アドレス)を使い、重複グループ内で ROWID が最小の行だけ残して他を削除します。最もシンプルで高速な方法です。

SQL(ROWID + MIN で重複削除)
-- email が重複する行のうち、ROWID が最小の行だけ残す
DELETE FROM customers
WHERE ROWID NOT IN (
    SELECT MIN(ROWID) FROM customers
    GROUP BY email
);

-- 削除件数を確認してから COMMIT
-- SQL%ROWCOUNT で影響行数を確認
COMMIT;
SQL(複数列の組み合わせで重複削除)
-- first_name + last_name + birth_date が重複する行を削除
DELETE FROM employees
WHERE ROWID NOT IN (
    SELECT MIN(ROWID)
    FROM employees
    GROUP BY first_name, last_name, birth_date
);
ROWID 方式は「どの行を残すか」を制御できない
MIN(ROWID) は物理的に最初に格納された行を残しますが、それが「最新の行」や「正しい行」とは限りません。「最新のレコードを残したい」「特定の条件の行を残したい」場合は、次の ROW_NUMBER() 方式を使ってください。

方法(2): ROW_NUMBER() で残す行を制御する

ROW_NUMBER() を使うと、重複グループ内でどの行を残すかを ORDER BY で細かく制御できます。

SQL(最新の行を残して重複削除)
-- email の重複で、updated_at が最新の行を残す
DELETE FROM customers
WHERE ROWID IN (
    SELECT rid FROM (
        SELECT ROWID AS rid,
               ROW_NUMBER() OVER (
                   PARTITION BY email
                   ORDER BY updated_at DESC  -- 最新が 1 位
               ) AS rn
        FROM customers
    )
    WHERE rn > 1   -- 2 位以下を削除
);
SQL(最古の行を残して重複削除)
-- created_at が最も古い行(最初に登録された行)を残す
DELETE FROM customers
WHERE ROWID IN (
    SELECT rid FROM (
        SELECT ROWID AS rid,
               ROW_NUMBER() OVER (
                   PARTITION BY email
                   ORDER BY created_at ASC   -- 最古が 1 位
               ) AS rn
        FROM customers
    )
    WHERE rn > 1
);
SQL(ID が最大の行を残す)
-- customer_id が最大(= 最後にINSERTされた行)を残す
DELETE FROM customers
WHERE ROWID IN (
    SELECT rid FROM (
        SELECT ROWID AS rid,
               ROW_NUMBER() OVER (
                   PARTITION BY email
                   ORDER BY customer_id DESC
               ) AS rn
        FROM customers
    )
    WHERE rn > 1
);
ROW_NUMBER() 方式の柔軟性
ORDER BY updated_at DESC: 最新行を残す
ORDER BY created_at ASC: 最古行を残す
ORDER BY id DESC: ID 最大を残す
ORDER BY status ASC, updated_at DESC: ステータス優先 + 日付で制御
PARTITION BY で重複判定の列、ORDER BY で残す行の優先順位を指定します。

方法(3): CTAS(テーブル再作成)で重複を除去する

重複が大量にある場合、DELETE よりテーブルを丸ごと再作成する方が高速です。UNDO ログが発生しないため、大量データでもパフォーマンスが安定します。

SQL(CTAS で重複を除去して再作成)
-- (1) 重複を除去した新テーブルを作成
CREATE TABLE customers_clean AS
SELECT * FROM (
    SELECT c.*,
           ROW_NUMBER() OVER (PARTITION BY email ORDER BY updated_at DESC) AS rn
    FROM customers c
)
WHERE rn = 1;

-- (2) rn 列を削除
ALTER TABLE customers_clean DROP COLUMN rn;

-- (3) 元テーブルを退避してリネーム
ALTER TABLE customers RENAME TO customers_old;
ALTER TABLE customers_clean RENAME TO customers;

-- (4) インデックス・制約を再作成
-- ALTER TABLE customers ADD CONSTRAINT pk_customers PRIMARY KEY (customer_id);
-- CREATE INDEX idx_customers_email ON customers (email);

-- (5) 確認後に元テーブルを削除
-- DROP TABLE customers_old PURGE;
方式 速度 UNDO 使用 制約/インデックス 適するケース
ROWID + DELETE 大(DELETE 分) 維持される 少量の重複(数百〜数千行)
ROW_NUMBER + DELETE 維持される 残す行の制御が必要な場合
CTAS 再作成 最速 なし 再作成が必要 大量の重複(数万行以上)

完全一致の重複を削除する

全列が完全に同じ行を削除する場合、GROUP BY に全列を指定するのは冗長です。ROWID を使えばシンプルに書けます。

SQL(全列一致の重複を削除)
-- 全列が完全に同じ行のうち、ROWID が最小の行だけ残す
DELETE FROM customers a
WHERE ROWID > (
    SELECT MIN(ROWID) FROM customers b
    WHERE a.customer_id = b.customer_id
      AND a.email = b.email
      AND a.first_name = b.first_name
      AND NVL(a.phone, 'X') = NVL(b.phone, 'X')  -- NULL 対策
);

-- CTAS + DISTINCT の方がシンプル(全列完全一致の場合)
CREATE TABLE customers_clean AS
SELECT DISTINCT * FROM customers;
DISTINCT は「どの行を残すか」を制御できない
SELECT DISTINCT * は全列完全一致の重複を 1 行にまとめますが、どの ROWID の行が残るかは不定です。「最新の行を残したい」場合は ROW_NUMBER() 方式を使ってください。

安全な削除手順

手順 操作 SQL 例
(1) 重複の検出 GROUP BY + HAVING で件数確認 SELECT col, COUNT(*) … HAVING COUNT(*) >= 2
(2) 重複行の確認 実データを SELECT で確認 SELECT * FROM table WHERE (cols) IN (…)
(3) バックアップ テーブルをコピー CREATE TABLE table_bk AS SELECT * FROM table
(4) 削除の実行 DELETE(COMMIT 前) DELETE FROM table WHERE ROWID IN (…)
(5) 件数確認 削除件数と残り件数を確認 SELECT COUNT(*) FROM table
(6) COMMIT 問題なければ確定 COMMIT
(7) 再発防止 UNIQUE 制約を追加 ALTER TABLE … ADD CONSTRAINT … UNIQUE (…)

大量データのバッチ削除

SQL(N 件ずつバッチ削除)
-- UNDO 溢れを防ぐため 10000 件ずつ削除
BEGIN
    LOOP
        DELETE FROM customers
        WHERE ROWID IN (
            SELECT rid FROM (
                SELECT ROWID AS rid,
                       ROW_NUMBER() OVER (PARTITION BY email ORDER BY customer_id DESC) AS rn
                FROM customers
            )
            WHERE rn > 1
              AND ROWNUM <= 10000
        );

        EXIT WHEN SQL%ROWCOUNT = 0;
        COMMIT;
    END LOOP;
    COMMIT;
END;
/

再発防止:UNIQUE 制約の追加

SQL(UNIQUE 制約で重複を防ぐ)
-- 重複削除後に UNIQUE 制約を追加
ALTER TABLE customers
ADD CONSTRAINT uq_customers_email UNIQUE (email);

-- 複数列の組み合わせで UNIQUE
ALTER TABLE employees
ADD CONSTRAINT uq_emp_name_birth UNIQUE (first_name, last_name, birth_date);

-- UNIQUE インデックスとして作成する方法(同じ効果)
CREATE UNIQUE INDEX uq_customers_email ON customers (email);
重複を削除したら必ず UNIQUE 制約を追加する
制約なしだと同じ重複が再び発生します。NULL を含む列に UNIQUE 制約を付ける場合、Oracle では NULL 同士は重複とみなされない(複数の NULL 行が許容される)点に注意してください。

実務パターン集

パターン(1): メールアドレスの重複を最新行で残す

SQL
DELETE FROM users
WHERE ROWID IN (
    SELECT rid FROM (
        SELECT ROWID AS rid,
               ROW_NUMBER() OVER (PARTITION BY email ORDER BY last_login DESC NULLS LAST) AS rn
        FROM users
    ) WHERE rn > 1
);
-- last_login が最新のユーザーを残す(NULL は最後)

パターン(2): 注文データの二重取り込みを修正

SQL
-- order_no + order_date の組み合わせで重複 → order_id 最大を残す
DELETE FROM orders
WHERE ROWID IN (
    SELECT rid FROM (
        SELECT ROWID AS rid,
               ROW_NUMBER() OVER (
                   PARTITION BY order_no, order_date
                   ORDER BY order_id DESC
               ) AS rn
        FROM orders
    ) WHERE rn > 1
);

パターン(3): 特定条件の行を優先して残す

SQL
-- status='ACTIVE' の行を優先、同じなら updated_at 最新を残す
DELETE FROM accounts
WHERE ROWID IN (
    SELECT rid FROM (
        SELECT ROWID AS rid,
               ROW_NUMBER() OVER (
                   PARTITION BY account_no
                   ORDER BY DECODE(status, 'ACTIVE', 0, 1),  -- ACTIVE 優先
                            updated_at DESC
               ) AS rn
        FROM accounts
    ) WHERE rn > 1
);

よくある質問

QROWID とは何ですか?
AROWID は Oracle 固有の行の物理アドレスです。テーブル内の全行で一意なので、重複行の中から特定の行を識別するのに使えます。他の RDBMS(MySQL、PostgreSQL)には存在しないため、Oracle 固有のテクニックです。
QDELETE と CTAS はどちらが速いですか?
A重複が少量(数百〜数千行)なら DELETE で十分です。重複が大量(数万行以上)またはテーブルの半分以上が重複している場合はCTAS(テーブル再作成)の方が圧倒的に速いです。DELETE は UNDO ログが発生しますが、CTAS(DDL)は発生しません。
QNULL を含む列で重複判定するとどうなりますか?
AGROUP BY では NULL 同士は同じグループとして扱われるため、NULL を含む行も正しく重複判定されます。ただし UNIQUE 制約では NULL 同士は重複とみなされないため、NULL を含む列の重複防止には追加の工夫(CHECK 制約やトリガー)が必要です。
Q削除を間違えた場合、元に戻せますか?
ACOMMIT 前であれば ROLLBACK で元に戻せます。COMMIT 後は通常の方法では戻せません。削除前に必ずバックアップテーブルを作成してください。
CREATE TABLE table_bk AS SELECT * FROM table;
問題が起きた場合はバックアップから復元できます。
QROW_NUMBER() のパーティションに指定する列は何を基準に選べばいいですか?
APARTITION BY には重複を判定する列を指定します。例えば「email が重複している」場合は PARTITION BY email、「名前 + 生年月日が重複している」場合は PARTITION BY first_name, last_name, birth_date です。ORDER BY には「どの行を残すか」の優先順位を指定します。
Q主キーがあるテーブルで重複は発生しますか?
A主キー列自体が重複することはありません(UNIQUE 制約により拒否されます)。しかし、主キー以外の列で実質的な重複が発生することはよくあります。例えば「customer_id は異なるが email が同じ」というケースです。ビジネスロジック上の一意性を保証するには、該当列に UNIQUE 制約を追加する必要があります。

まとめ

重複データ削除の要点をまとめます。

やりたいこと 推奨方法
重複の件数を確認 SELECT col, COUNT(*) … GROUP BY col HAVING COUNT(*) >= 2
シンプルに重複を削除(ROWID 最小を残す) DELETE WHERE ROWID NOT IN (SELECT MIN(ROWID) … GROUP BY …)
最新行/最古行を残して削除 ROW_NUMBER() OVER (PARTITION BY … ORDER BY …) で rn > 1 を削除
特定条件の行を優先して残す ROW_NUMBER の ORDER BY に DECODE で優先順位を指定
大量重複を高速除去 CTAS でテーブル再作成(インデックス・制約は再作成が必要)
全列完全一致の重複除去 CREATE TABLE new AS SELECT DISTINCT * FROM old
大量データをバッチ削除 LOOP + ROWNUM <= 10000 + COMMIT
再発防止 ALTER TABLE ADD CONSTRAINT … UNIQUE (…)