Oracle テーブルに意図せず重複データが混入することは実務でよく起きます。CSV の二重取り込み、バッチ処理の再実行、UNIQUE 制約の付け忘れなどが主な原因です。
重複の削除は「どの行を残してどの行を消すか」を正確に制御しないと、必要なデータまで消してしまうリスクがあります。本記事では、重複の特定方法から、ROWID を使った削除(Oracle 固有)、ROW_NUMBER() で残す行を制御する削除、CTAS(テーブル再作成)による高速除去、そしてUNIQUE 制約による再発防止まで体系的に解説します。
この記事でわかること
・GROUP BY + HAVING で重複データを特定する方法
・ROWID を使った重複削除(Oracle 固有で最もシンプル)
・ROW_NUMBER() で「どの行を残すか」を制御する方法
・CTAS でテーブルごと再作成する方法(大量重複時に最速)
・完全一致 vs 特定列のみ一致の重複判定
・最新行/最古行/特定条件で残す行を選ぶ方法
・大量データのバッチ削除
・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 が小さい方を残す」「最新の行を残す」など、残す行の基準をチーム内で決めてから作業に入るのが安全です。
重複データの中身を 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() 方式の柔軟性
・
・
・
・
PARTITION BY で重複判定の列、ORDER BY で残す行の優先順位を指定します。
・
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 行が許容される)点に注意してください。
制約なしだと同じ重複が再び発生します。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 とは何ですか?
A
ROWID は 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() のパーティションに指定する列は何を基準に選べばいいですか?
A
PARTITION 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 (…) |
