【完全ガイド】ORA-00001: unique constraint violated の原因と解決方法|MERGE・IGNORE_ROW_ON_DUPKEY_INDEX・IDENTITY まで徹底解説
- 作成日 2026.07.21
- Oracle Database
- 1. Oracle 開発者・DBA が日常的に遭遇する最頻出エラーの一つ:
- 2. 結論:エラーメッセージから即対応
- 3. まず理解する:3種類の一意制約
- 4. エラーメッセージの詳細解読
- 5. 【原因①】INSERT で明示的な重複
- 6. 【原因②】UPDATE で既存値と衝突
- 7. 【原因③】MERGE + 並行実行(マニアック)
- 8. 【原因④】シーケンス + トリガーの誤設計
- 9. 【原因⑤】複合キーの一部一致
- 10. 【原因⑥】バルク INSERT で一部行が重複
- 11. 7つの解決策 完全リファレンス
- 12. Rails / Java / Python 対応
- 13. 実践シナリオ
- 14. 予防のベストプラクティス
- 15. トラブルシューティング
- 16. よくある質問(FAQ)
- 16.1. Q1. NULL の扱いは?
- 16.2. Q2. ORA-00001 と ORA-02290 の違い
- 16.3. Q3. IGNORE_ROW_ON_DUPKEY_INDEX の制約
- 16.4. Q4. MERGE と INSERT の性能
- 16.5. Q5. Rails での upsert
- 16.6. Q6. DUP_VAL_ON_INDEX の対応範囲
- 16.7. Q7. Constraint DEFERRED
- 16.8. Q8. 大文字小文字を区別しない一意
- 16.9. Q9. 部分一意(条件付き)
- 16.10. Q10. Autonomous DB での対応
- 16.11. Q11. AWS RDS Oracle
- 16.12. Q12. 移行時の対応
- 17. 参考リンク
- 18. まとめ
Oracle 開発者・DBA が日常的に遭遇する最頻出エラーの一つ:
INSERT INTO employees (id, name, email)
VALUES (100, 'Alice', 'alice@example.com');
-- 既に id=100 が存在
ORA-00001: unique constraint (SCOTT.EMP_PK) violated
「一意制約に違反した」というシンプルな見た目に反し、実は発生パターンが多岐にわたる複雑なエラー:
- INSERT で明示的な重複
- UPDATE で既存値と衝突
- MERGE 文でも発生する(意外に多い)
- シーケンス + トリガーの誤設計
- 並行実行での race condition
- NULL の扱いが Oracle 独特
現場では:
- 本番の月次バッチで発生
- 並行アクセスで再現困難な発生
- シーケンスなのに重複エラー(!?)
- MERGE で既存チェックしたのに ORA-00001(!!)
- Rails の save が失敗
- CI/CD テストで散発的に失敗
さらに、Oracle 特有の落とし穴として:
MERGEは複数セッションでの並行実行時に ORA-00001 が発生する(Read Consistency の副作用)IGNORE_ROW_ON_DUPKEY_INDEXヒント(11gR2+)の存在を知らないDUP_VAL_ON_INDEX例外の適切な使い方- 12c+ の IDENTITY 列でシーケンスの複雑さから解放
これらを正しく理解しないと、「同じ問題を何度も対処する」無限ループにハマります。特に、既存 ORA-01400 (NOT NULL) と対をなすOracle 制約エラー2大巨頭として、確実にマスターすべきエラーです。
本記事では、ORA-00001: unique constraint violated の完全な原因と解決方法を、リファレンスとして実用的に整理します。PRIMARY KEY vs UNIQUE 制約 vs UNIQUE INDEX の違い、エラーメッセージの読み方、6大原因、7つの解決策(MERGE・IGNORE_ROW_ON_DUPKEY_INDEX・IDENTITY・LOG ERRORS等)、Rails/Java/Python 対応、実践シナリオ、予防のベストプラクティス、FAQまで完全網羅。この1本で ORA-00001 を根本から解決できるようになります。
結論:エラーメッセージから即対応
時間がない方向けに、最速の対処を先に示します。
エラーメッセージから制約名を読み取る
ORA-00001: unique constraint (SCOTT.EMP_PK) violated
↑ ↑
スキーマ 制約名
制約の詳細を確認
-- 制約と対象カラム
SELECT ac.constraint_name, ac.constraint_type, ac.table_name,
LISTAGG(acc.column_name, ', ') WITHIN GROUP (ORDER BY acc.position) AS columns
FROM all_constraints ac
JOIN all_cons_columns acc
ON ac.constraint_name = acc.constraint_name
AND ac.owner = acc.owner
WHERE ac.constraint_name = 'EMP_PK'
GROUP BY ac.constraint_name, ac.constraint_type, ac.table_name;
6つの解決策の使い分け
| # | 解決策 | 使う場面 |
|---|---|---|
| ① | MERGE 文 | UPSERT(更新 or 挿入) |
| ② | IGNORE_ROW_ON_DUPKEY_INDEX ヒント | 重複を無視して INSERT |
| ③ | DUP_VAL_ON_INDEX 例外処理 | PL/SQL でハンドル |
| ④ | LOG ERRORS | バルク処理でスキップ |
| ⑤ | IDENTITY 列(12c+) | シーケンスの罠回避 |
| ⑥ | 事前 SELECT | 単純だが並行実行に弱い |
最速チェック:重複データ検索
-- 単一カラム
SELECT email, COUNT(*)
FROM employees
GROUP BY email
HAVING COUNT(*) > 1;
-- 複合カラム
SELECT first_name, last_name, COUNT(*)
FROM employees
GROUP BY first_name, last_name
HAVING COUNT(*) > 1;
詳細は以下で解説します。
まず理解する:3種類の一意制約
PRIMARY KEY
CREATE TABLE employees (
id NUMBER PRIMARY KEY,
name VARCHAR2(100)
);
- 1テーブルに1つ
- NOT NULL 自動
- 一意インデックス自動作成
UNIQUE 制約
CREATE TABLE employees (
id NUMBER PRIMARY KEY,
email VARCHAR2(255) UNIQUE
);
-- または後から追加
ALTER TABLE employees
ADD CONSTRAINT emp_email_uq UNIQUE (email);
- 複数持てる
- NULL 許容(Oracle 特有の挙動あり)
- 一意インデックス自動作成
UNIQUE INDEX
CREATE UNIQUE INDEX emp_email_idx ON employees(email);
- 制約ではなくインデックス
- 効果は UNIQUE 制約と類似
- 主に関数インデックスと組み合わせ
-- 大文字小文字を区別しない一意
CREATE UNIQUE INDEX emp_email_lower_idx
ON employees(LOWER(email));
エラーメッセージの違い
どの場合も同じ ORA-00001 だが、制約名が違う:
-- PRIMARY KEY
ORA-00001: unique constraint (SCOTT.EMP_PK) violated
-- UNIQUE 制約
ORA-00001: unique constraint (SCOTT.EMP_EMAIL_UQ) violated
-- UNIQUE INDEX
ORA-00001: unique constraint (SCOTT.EMP_EMAIL_LOWER_IDX) violated
Oracle の NULL 挙動(重要)
Oracle は NULL 同士を「別」と扱う:
CREATE TABLE t (col NUMBER UNIQUE);
INSERT INTO t VALUES (NULL); -- OK
INSERT INTO t VALUES (NULL); -- OK! ORA-00001 発生しない
INSERT INTO t VALUES (1); -- OK
INSERT INTO t VALUES (1); -- ORA-00001
他 DB(PostgreSQL 等)とは違うため、移行時に注意。
複合キーで一部が NULL のケースも独特:
CREATE TABLE t (a NUMBER, b NUMBER, UNIQUE (a, b));
INSERT INTO t VALUES (1, NULL); -- OK
INSERT INTO t VALUES (1, NULL); -- ORA-00001(一部一致は違反)
複雑な NULL 処理には注意。
エラーメッセージの詳細解読
メッセージから3つの情報を抽出
ORA-00001: unique constraint (SCOTT.EMP_PK) violated
抽出できるのは:
- エラー種別:
ORA-00001= 一意制約違反 - スキーマ:
SCOTT - 制約名:
EMP_PK
制約名から詳細取得
-- 制約詳細
SELECT constraint_name, constraint_type, table_name,
search_condition, r_constraint_name
FROM all_constraints
WHERE constraint_name = 'EMP_PK';
-- 制約のカラム
SELECT column_name, position
FROM all_cons_columns
WHERE constraint_name = 'EMP_PK'
ORDER BY position;
constraint_type:
P= PRIMARY KEYU= UNIQUEC= CHECKR= FOREIGN KEY
Oracle 19c+ の詳細メッセージ
Oracle 19c 以降、ERROR_MESSAGE_DETAILS=ON で詳細情報が付く:
ALTER SESSION SET ERROR_MESSAGE_DETAILS = ON;
INSERT INTO employees VALUES (100, 'Alice', 'alice@example.com');
-- ORA-00001: unique constraint (SCOTT.EMP_PK) violated on table SCOTT.EMPLOYEES
-- columns (ID)
カラム名まで表示され、対処が簡単に。
【原因①】INSERT で明示的な重複
症状
INSERT INTO employees (id, name) VALUES (100, 'Alice');
-- 既に id=100 が存在 → ORA-00001
解決A: MERGE 文(UPSERT)
MERGE INTO employees dst
USING (SELECT 100 AS id, 'Alice' AS name FROM DUAL) src
ON (dst.id = src.id)
WHEN MATCHED THEN
UPDATE SET dst.name = src.name
WHEN NOT MATCHED THEN
INSERT (id, name) VALUES (src.id, src.name);
存在すれば UPDATE、なければ INSERT。UPSERT の Oracle 標準実装。
解決B: IGNORE_ROW_ON_DUPKEY_INDEX ヒント(11gR2+)
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(employees, EMP_PK) */
INTO employees (id, name) VALUES (100, 'Alice');
-- 重複時は静かにスキップ、エラーなし
単一 INSERT のみ、UPDATE や MERGE には使えない。
解決C: 事前 SELECT
DECLARE
v_exists NUMBER;
BEGIN
SELECT COUNT(*) INTO v_exists FROM employees WHERE id = 100;
IF v_exists = 0 THEN
INSERT INTO employees VALUES (100, 'Alice');
END IF;
END;
/
問題点: 並行実行に弱い(race condition)。
解決D: DUP_VAL_ON_INDEX 例外
BEGIN
INSERT INTO employees VALUES (100, 'Alice');
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
UPDATE employees SET name = 'Alice' WHERE id = 100;
END;
/
シンプル、単発 INSERT に有効。
【原因②】UPDATE で既存値と衝突
症状
-- id=200 に既に email='alice@example.com' が存在
UPDATE employees
SET email = 'alice@example.com'
WHERE id = 100;
-- ORA-00001: unique constraint (SCOTT.EMP_EMAIL_UQ) violated
INSERT だけでなく UPDATE でも発生。
解決
方法A: 事前チェック:
UPDATE employees
SET email = 'alice@example.com'
WHERE id = 100
AND NOT EXISTS (
SELECT 1 FROM employees
WHERE email = 'alice@example.com'
AND id != 100
);
方法B: 例外処理:
BEGIN
UPDATE employees SET email = 'alice@example.com' WHERE id = 100;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
RAISE_APPLICATION_ERROR(-20001, 'Email already exists');
END;
/
【原因③】MERGE + 並行実行(マニアック)
症状
MERGE 文でも並行実行で ORA-00001 が発生する:
-- Session 1:
MERGE INTO t dst USING (SELECT 1 AS id, 100 AS val FROM DUAL) src
ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.val = src.val
WHEN NOT MATCHED THEN INSERT VALUES (src.id, src.val);
-- id=1 の行を INSERT 開始(未コミット)
-- Session 2: 同時に同じ MERGE を実行
MERGE INTO t dst USING (SELECT 1 AS id, 100 AS val FROM DUAL) src
ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.val = src.val
WHEN NOT MATCHED THEN INSERT VALUES (src.id, src.val);
-- Session 1 の未コミットデータは「見えない」
-- → NOT MATCHED と判定 → INSERT
-- → Session 1 の commit を待つ
-- → Session 1 commit 後、Session 2 の INSERT が衝突
-- → ORA-00001
なぜ発生するか
Oracle の Read Consistency(読み取り一貫性) モデルの副作用:
Session 1: INSERT 開始(未コミット)
↓
Session 2: 同じ MERGE 実行
↓ Session 1 の未コミット行は見えない
Session 2: NOT MATCHED と判定 → INSERT 試行
↓ Session 1 のロックで待機
↓ Session 1 が COMMIT
↓ Session 2 の INSERT が実行 → 重複 → ORA-00001
解決
方法A: リトライ:
DECLARE
v_retry NUMBER := 0;
BEGIN
<<merge_loop>>
BEGIN
MERGE INTO t dst USING ...;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
v_retry := v_retry + 1;
IF v_retry < 3 THEN GOTO merge_loop; END IF;
RAISE;
END;
END;
/
方法B: SELECT FOR UPDATE で排他制御:
SELECT id INTO v_id FROM t WHERE id = 1 FOR UPDATE;
-- 存在チェック + ロック
方法C: ロック取得後 MERGE(トランザクション分離レベル SERIALIZABLE):
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;
問題: パフォーマンス低下、ORA-08177 の可能性。
Oracle のロック関連は、ORA-00054 リソースビジーの記事、デッドロック関連は ORA-00060 デッドロックの記事も参照してください。
【原因④】シーケンス + トリガーの誤設計
症状
シーケンスで PK 採番しているはずが、なぜか重複:
CREATE SEQUENCE emp_seq;
CREATE TRIGGER emp_bi
BEFORE INSERT ON employees FOR EACH ROW
BEGIN
:NEW.id := emp_seq.NEXTVAL;
END;
/
INSERT INTO employees VALUES (1, 'Alice'); -- id=1 明示
INSERT INTO employees (name) VALUES ('Bob'); -- 自動採番
-- どこかで ORA-00001 発生
原因
シーケンスで 1, 2, 3… と採番されるが、既存レコードに手動で入れた値と衝突:
-- 過去に手動で入れた
INSERT INTO employees VALUES (100, 'X');
-- シーケンスは 1 から
-- そのうち NEXTVAL が 100 になった時 → ORA-00001
解決A: IDENTITY 列(12c+、最推奨)
CREATE TABLE employees (
id NUMBER GENERATED ALWAYS AS IDENTITY,
name VARCHAR2(100)
);
-- INSERT では id を指定しない
INSERT INTO employees (name) VALUES ('Alice');
Oracle が自動採番、重複なし。シーケンス + トリガーの複雑さから解放。
解決B: シーケンスを最大値+1 に
-- 現在の最大値確認
SELECT MAX(id) FROM employees;
-- シーケンスを進める
ALTER SEQUENCE emp_seq RESTART START WITH 200;
-- または
ALTER SEQUENCE emp_seq INCREMENT BY 100;
SELECT emp_seq.NEXTVAL FROM DUAL; -- 100 進む
ALTER SEQUENCE emp_seq INCREMENT BY 1;
解決C: GENERATED BY DEFAULT
CREATE TABLE employees (
id NUMBER GENERATED BY DEFAULT AS IDENTITY,
name VARCHAR2(100)
);
-- 明示的な値も許容、指定しなければ自動
INSERT INTO employees (id, name) VALUES (100, 'Alice'); -- OK
INSERT INTO employees (name) VALUES ('Bob'); -- 自動採番
【原因⑤】複合キーの一部一致
症状
CREATE TABLE order_items (
order_id NUMBER,
product_id NUMBER,
quantity NUMBER,
CONSTRAINT ord_item_pk PRIMARY KEY (order_id, product_id)
);
INSERT INTO order_items VALUES (1, 10, 5);
INSERT INTO order_items VALUES (1, 10, 3); -- ORA-00001
(1, 10) の複合キーが重複。
解決
方法A: 意図的なら UPDATE:
UPDATE order_items SET quantity = 3 WHERE order_id = 1 AND product_id = 10;
方法B: 集計してから INSERT:
INSERT INTO order_items
SELECT order_id, product_id, SUM(quantity)
FROM staging_items
GROUP BY order_id, product_id;
方法C: MERGE で数量を合算:
MERGE INTO order_items dst
USING (SELECT 1 AS order_id, 10 AS product_id, 5 AS qty FROM DUAL) src
ON (dst.order_id = src.order_id AND dst.product_id = src.product_id)
WHEN MATCHED THEN
UPDATE SET dst.quantity = dst.quantity + src.qty
WHEN NOT MATCHED THEN
INSERT VALUES (src.order_id, src.product_id, src.qty);
【原因⑥】バルク INSERT で一部行が重複
症状
INSERT INTO customers (id, email)
SELECT id, email FROM staging_customers;
-- 5000 行中 3 行が重複 → 全ロールバック
解決A: LOG ERRORS(エラーログテーブル)
-- エラーログ作成
BEGIN
DBMS_ERRLOG.CREATE_ERROR_LOG(
dml_table_name => 'CUSTOMERS',
err_log_table_name => 'CUSTOMERS_ERR'
);
END;
/
-- エラー行を分離
INSERT INTO customers (id, email)
SELECT id, email FROM staging_customers
LOG ERRORS INTO customers_err ('Batch 2026-06') REJECT LIMIT UNLIMITED;
-- エラー行確認
SELECT ora_err_number$, ora_err_mesg$, id, email
FROM customers_err
WHERE ora_err_number$ = 1; -- ORA-00001
エラー行を分離、成功行は投入。
解決B: 事前に重複除去
INSERT INTO customers (id, email)
SELECT id, email FROM staging_customers sc
WHERE NOT EXISTS (
SELECT 1 FROM customers c WHERE c.id = sc.id
);
解決C: MERGE で UPSERT
MERGE INTO customers dst
USING staging_customers src
ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.email = src.email
WHEN NOT MATCHED THEN INSERT (id, email) VALUES (src.id, src.email);
7つの解決策 完全リファレンス
解決策① MERGE 文(推奨)
MERGE INTO dst_table dst
USING (SELECT ... FROM src_table) src
ON (dst.key = src.key)
WHEN MATCHED THEN
UPDATE SET dst.col = src.col
WHEN NOT MATCHED THEN
INSERT (key, col) VALUES (src.key, src.col);
UPSERT の Oracle 標準。
解決策② IGNORE_ROW_ON_DUPKEY_INDEX ヒント(11gR2+)
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(customers, customers_email_uq) */
INTO customers (email, name)
SELECT email, name FROM staging_customers;
制約/インデックス名を指定、重複行を静かにスキップ。単一 INSERT のみ。
解決策③ DUP_VAL_ON_INDEX 例外処理
BEGIN
INSERT INTO customers (id, email) VALUES (100, 'alice@x.com');
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
-- 既存の処理
UPDATE customers SET email = 'alice@x.com' WHERE id = 100;
END;
/
PL/SQL での単純な UPSERT に有効。
解決策④ LOG ERRORS
INSERT INTO target
SELECT * FROM source
LOG ERRORS INTO target_err REJECT LIMIT UNLIMITED;
バルク処理でエラー行を分離。
解決策⑤ IDENTITY 列(12c+)
CREATE TABLE employees (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR2(100)
);
シーケンス + トリガーの罠から解放。
解決策⑥ 事前 SELECT + INSERT
DECLARE
v_exists NUMBER;
BEGIN
SELECT COUNT(*) INTO v_exists FROM employees WHERE id = 100;
IF v_exists = 0 THEN
INSERT INTO employees VALUES (100, 'Alice');
END IF;
END;
/
シンプルだが並行実行に弱い。
解決策⑦ INSERT … WHERE NOT EXISTS
INSERT INTO employees (id, name)
SELECT 100, 'Alice' FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM employees WHERE id = 100);
単一クエリで判定 + 挿入。
Rails / Java / Python 対応
Rails ActiveRecord
class User < ApplicationRecord
validates :email, uniqueness: true
end
# バリデーション + 例外処理
begin
User.create!(email: 'alice@x.com')
rescue ActiveRecord::RecordNotUnique => e
# ORA-00001 に対応
puts "Email already exists"
end
# UPSERT
User.upsert({ email: 'alice@x.com', name: 'Alice' }, unique_by: :email)
# find_or_create_by
User.find_or_create_by(email: 'alice@x.com') do |u|
u.name = 'Alice'
end
Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事、Solid Queue 使い方の記事も参照してください。
Java (JDBC)
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO employees (id, name) VALUES (?, ?)")) {
ps.setInt(1, 100);
ps.setString(2, "Alice");
ps.executeUpdate();
} catch (SQLIntegrityConstraintViolationException e) {
if (e.getErrorCode() == 1) { // ORA-00001
// 既存 UPDATE
updateExistingEmployee(100, "Alice");
}
}
Python (oracledb)
import oracledb
try:
cursor.execute(
"INSERT INTO employees (id, name) VALUES (:id, :name)",
id=100, name="Alice"
)
except oracledb.IntegrityError as e:
error_obj = e.args[0]
if error_obj.code == 1: # ORA-00001
cursor.execute(
"UPDATE employees SET name = :name WHERE id = :id",
id=100, name="Alice"
)
実践シナリオ
シナリオ1:Web フォームからの登録
# Rails Controller
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new # バリデーションエラー表示
end
end
Rails のバリデーション + DB制約の二重防御。
シナリオ2:バッチ INSERT でエラー分離
DECLARE
TYPE t_batch IS TABLE OF customers%ROWTYPE;
v_batch t_batch;
BEGIN
-- staging から取得
SELECT id, email, name BULK COLLECT INTO v_batch
FROM staging_customers;
FORALL i IN 1 .. v_batch.COUNT SAVE EXCEPTIONS
INSERT INTO customers VALUES v_batch(i);
EXCEPTION
WHEN OTHERS THEN
FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(
'Error at row ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX ||
': ' || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE)
);
END LOOP;
END;
/
シナリオ3:ETL パイプライン
-- LOG ERRORS で継続
INSERT INTO fact_sales (sale_id, product_id, amount)
SELECT sale_id, product_id, amount
FROM staging_sales
LOG ERRORS INTO fact_sales_err REJECT LIMIT UNLIMITED;
-- ORA-00001 の行を確認
SELECT * FROM fact_sales_err
WHERE ora_err_number$ = 1;
-- 重複除去してリトライ
INSERT INTO fact_sales
SELECT DISTINCT sale_id, product_id, amount
FROM staging_sales
WHERE sale_id IN (SELECT sale_id FROM fact_sales_err);
シナリオ4:ユーザー登録での race condition
// 悪い例:並行アクセスで問題
public User createUser(String email) {
User existing = repo.findByEmail(email); // check
if (existing == null) {
return repo.save(new User(email)); // then insert
}
return existing;
}
// 良い例:楽観的
public User createUser(String email) {
try {
return repo.save(new User(email));
} catch (DataIntegrityViolationException e) {
return repo.findByEmail(email); // 既存を取得
}
}
シナリオ5:シーケンスの Reset
-- 開発環境で全データクリア後
TRUNCATE TABLE employees;
-- シーケンスもリセット
ALTER SEQUENCE emp_seq RESTART START WITH 1;
シナリオ6:Rails マイグレーションでの一意インデックス追加
class AddUniqueIndexToUsers < ActiveRecord::Migration[8.0]
def change
# 既存の重複を先に除去
execute <<-SQL
DELETE FROM users
WHERE ROWID NOT IN (
SELECT MIN(ROWID) FROM users GROUP BY email
);
SQL
# ユニークインデックス追加
add_index :users, :email, unique: true
end
end
Rails migration の詳細は rails db:migrate 使い方の記事、Rails 8 アップグレードガイドの記事も参照してください。
シナリオ7:Kamal デプロイでの制約追加
# デプロイ時のマイグレーションでALTER TABLE ADD CONSTRAINT
# 既存データに重複があるとエラー
事前にデータクレンジング必要。Kamal 2 デプロイの詳細は Kamal 2 デプロイの記事を参照してください。
シナリオ8:Docker Oracle での動作確認
docker exec -it oracle-xe sqlplus scott/tiger
SQL> INSERT INTO employees VALUES (100, 'test');
SQL> INSERT INTO employees VALUES (100, 'test2');
-- ORA-00001 確認
Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。
シナリオ9:Solid Queue ジョブテーブル
Solid Queue のジョブ ID 重複対策:
-- 重複挿入を防ぐ
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(solid_queue_jobs, sq_jobs_pk) */
INTO solid_queue_jobs (...)
VALUES (...);
Solid Queue の詳細は Solid Queue 使い方の記事も参照してください。
シナリオ10:Analytics での日次集計
-- 日次サマリテーブル(UNIQUE (date, category))
MERGE INTO daily_summary dst
USING (
SELECT TRUNC(order_date) date, category, SUM(amount) total
FROM orders
WHERE TRUNC(order_date) = TRUNC(SYSDATE - 1)
GROUP BY TRUNC(order_date), category
) src
ON (dst.date = src.date AND dst.category = src.category)
WHEN MATCHED THEN UPDATE SET dst.total = src.total
WHEN NOT MATCHED THEN
INSERT (date, category, total) VALUES (src.date, src.category, src.total);
日次バッチの再実行でも安全。
予防のベストプラクティス
1. アプリ側バリデーション先行
validates :email, uniqueness: true, presence: true
DB エラーになる前にアプリで防ぐ。
2. IDENTITY 列を使う(12c+)
CREATE TABLE t (id NUMBER GENERATED ALWAYS AS IDENTITY);
シーケンス + トリガーより安全。
3. MERGE を UPSERT の標準に
MERGE INTO ... WHEN MATCHED ... WHEN NOT MATCHED ...
存在チェック + INSERT より安全。
4. LOG ERRORS で本番バルク処理
INSERT ... LOG ERRORS INTO ... REJECT LIMIT UNLIMITED;
エラーで停止しない設計。
5. 制約に意味のある名前
-- ❌ 自動生成の名前
CREATE TABLE t (id NUMBER PRIMARY KEY);
-- SYS_C001234
-- ✅ 意味ある名前
CREATE TABLE t (id NUMBER, CONSTRAINT t_pk PRIMARY KEY (id));
エラーメッセージで分かりやすい。
6. UNIQUE 制約の適切な設計
-- 論理的な一意性を明示
ALTER TABLE users ADD CONSTRAINT users_email_uq UNIQUE (email);
ALTER TABLE orders ADD CONSTRAINT orders_natural_key_uq
UNIQUE (customer_id, order_date, order_no);
7. 例外処理の一貫性
BEGIN
INSERT ...;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
-- 明示的な処理
NULL;
END;
サイレント無視は避け、明示的にログ or 対処。
8. 並行実行の考慮
-- 楽観的ロック(バージョン列使用)
UPDATE t SET col = 'X', version = version + 1
WHERE id = 1 AND version = :old_version;
-- 悲観的ロック
SELECT ... FROM t WHERE id = 1 FOR UPDATE;
9. 定期的な重複チェック
-- 監視スクリプト
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- 通常は0行、あれば異常
10. ERROR_MESSAGE_DETAILS を ON
ALTER SESSION SET ERROR_MESSAGE_DETAILS = ON;
カラム名まで表示され、デバッグが楽。
トラブルシューティング
制約名から特定できない
-- 全一意制約
SELECT ac.owner, ac.constraint_name, ac.table_name,
LISTAGG(acc.column_name, ', ') WITHIN GROUP (ORDER BY acc.position) cols
FROM all_constraints ac
JOIN all_cons_columns acc
ON ac.constraint_name = acc.constraint_name
AND ac.owner = acc.owner
WHERE ac.constraint_type IN ('U', 'P')
GROUP BY ac.owner, ac.constraint_name, ac.table_name;
重複データが分からない
-- 重複データの詳細
SELECT * FROM (
SELECT t.*, COUNT(*) OVER (PARTITION BY email) cnt
FROM users t
)
WHERE cnt > 1
ORDER BY email;
MERGE で ORA-00001 が発生
並行実行が原因の可能性:
-- リトライロジック追加
BEGIN
<<retry>>
DECLARE
v_attempt NUMBER := 0;
BEGIN
MERGE INTO ...;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
v_attempt := v_attempt + 1;
IF v_attempt < 3 THEN
DBMS_LOCK.SLEEP(0.1);
GOTO retry;
END IF;
RAISE;
END;
END;
/
一意インデックス削除できない
DROP INDEX emp_email_idx;
-- ORA-02429: cannot drop index used for unique/primary key
制約が使用中。制約を先に削除:
ALTER TABLE employees DROP CONSTRAINT emp_email_uq;
DROP INDEX emp_email_idx;
PRIMARY KEY を無効化したい
-- 一時的に無効化
ALTER TABLE employees DISABLE PRIMARY KEY;
-- 大量 INSERT
-- 有効化(重複あるとエラー)
ALTER TABLE employees ENABLE VALIDATE PRIMARY KEY;
制約を後から追加
-- 事前に重複除去
DELETE FROM users WHERE ROWID NOT IN (
SELECT MIN(ROWID) FROM users GROUP BY email
);
-- 追加
ALTER TABLE users ADD CONSTRAINT users_email_uq UNIQUE (email);
よくある質問(FAQ)
Q1. NULL の扱いは?
Oracle は NULL 同士を別として扱う:
INSERT INTO t (uniq_col) VALUES (NULL);
INSERT INTO t (uniq_col) VALUES (NULL); -- OK!
Q2. ORA-00001 と ORA-02290 の違い
- ORA-00001: UNIQUE 制約違反
- ORA-02290: CHECK 制約違反
- ORA-02291: 外部キー制約違反(親なし)
- ORA-02292: 外部キー制約違反(子あり)
- ORA-01400: NOT NULL 制約違反
Q3. IGNORE_ROW_ON_DUPKEY_INDEX の制約
- 単一 INSERT のみ
- UPDATE / MERGE では使えない
- 制約名 or インデックス名の指定必要
Q4. MERGE と INSERT の性能
- 単一行: INSERT 高速
- 大量: MERGE も遜色なし
- UPSERT 用途は MERGE
Q5. Rails での upsert
# Rails 6+
User.upsert({ email: 'alice@x.com', name: 'Alice' }, unique_by: :email)
# Bulk
User.upsert_all([
{ email: 'a@x.com', name: 'A' },
{ email: 'b@x.com', name: 'B' }
], unique_by: :email)
Q6. DUP_VAL_ON_INDEX の対応範囲
UNIQUE 制約 or UNIQUE INDEX 違反時に発生。他の制約違反は別例外(ORA-02290等)。
Q7. Constraint DEFERRED
CREATE TABLE t (
id NUMBER,
CONSTRAINT t_pk PRIMARY KEY (id) INITIALLY DEFERRED DEFERRABLE
);
-- トランザクション内で一時的に重複OK
SET CONSTRAINTS ALL DEFERRED;
INSERT INTO t VALUES (1);
INSERT INTO t VALUES (1); -- 一時的に OK
UPDATE t SET id = 2 WHERE ROWNUM = 1;
COMMIT; -- ここで検証
循環参照など特殊ケース用。
Q8. 大文字小文字を区別しない一意
CREATE UNIQUE INDEX u_email_idx ON users (LOWER(email));
関数インデックスで対応。
Q9. 部分一意(条件付き)
-- status='active' の行だけ email 一意
CREATE UNIQUE INDEX u_active_email
ON users (CASE WHEN status='active' THEN email END);
関数インデックスで条件付き一意を実現。
Q10. Autonomous DB での対応
同様の挙動。マイクロサービス化などで並行実行が増えるため、リトライ等の対処強化推奨。
Q11. AWS RDS Oracle
同様。特別な差異なし。
Q12. 移行時の対応
MySQL / PostgreSQL からの移行時、INSERT ON CONFLICT / INSERT ... ON DUPLICATE KEY UPDATE は MERGE 文に書き換え。
参考リンク
Oracle 公式
- Oracle Database Error Messages: ORA-00001
- MERGE Statement
- CREATE TABLE: IDENTITY
- Managing Integrity Constraints
まとめ
ORA-00001: unique constraint violated の解決、要点を再整理します。
エラーメッセージから抽出
ORA-00001: unique constraint (SCOTT.EMP_PK) violated
↑ここから制約特定
3種類の一意制約
- PRIMARY KEY(1つ、NOT NULL 自動)
- UNIQUE 制約(複数可、NULL 許容)
- UNIQUE INDEX(関数含む)
6大原因
| # | 原因 | 対処 |
|---|---|---|
| ① | INSERT の重複 | MERGE or IGNORE ヒント |
| ② | UPDATE で衝突 | 事前チェック or 例外処理 |
| ③ | MERGE + 並行実行 | リトライ or ロック |
| ④ | シーケンス誤設計 | IDENTITY 列 |
| ⑤ | 複合キー一部一致 | UPDATE or 集計 MERGE |
| ⑥ | バルク重複 | LOG ERRORS |
7つの解決策
-- ① MERGE(UPSERT の標準)
MERGE INTO t dst USING src ON (...) WHEN MATCHED ... WHEN NOT MATCHED ...
-- ② IGNORE_ROW_ON_DUPKEY_INDEX(11gR2+)
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(t, t_uk) */ INTO t ...
-- ③ DUP_VAL_ON_INDEX 例外処理
BEGIN INSERT ...; EXCEPTION WHEN DUP_VAL_ON_INDEX THEN ...; END;
-- ④ LOG ERRORS(バルク)
INSERT ... LOG ERRORS INTO t_err REJECT LIMIT UNLIMITED;
-- ⑤ IDENTITY 列(12c+)
CREATE TABLE t (id NUMBER GENERATED ALWAYS AS IDENTITY);
-- ⑥ 事前 SELECT + INSERT
IF NOT EXISTS ... THEN INSERT ...;
-- ⑦ INSERT ... WHERE NOT EXISTS
INSERT INTO t SELECT ... FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM t WHERE ...);
診断クエリ
-- 制約と対象カラム
SELECT constraint_name, column_name, position
FROM all_cons_columns
WHERE constraint_name = 'EMP_PK';
-- 重複データ検索
SELECT email, COUNT(*) FROM users
GROUP BY email HAVING COUNT(*) > 1;
アプリ言語別
- Rails:
validates :col, uniqueness: true,find_or_create_by,upsert - Java:
SQLIntegrityConstraintViolationException, error code 1 - Python:
oracledb.IntegrityError,code == 1
Oracle 特有の落とし穴
- NULL 同士は別(他 DB と挙動違い)
- MERGE でも並行実行時に発生
- シーケンス + トリガーの罠
- DEFERRED 制約(トランザクション末尾検証)
予防策
- アプリバリデーション先行
- IDENTITY 列を使う(12c+)
- MERGE を UPSERT の標準
- LOG ERRORS でバルク対応
- 制約に意味ある名前
- 例外処理の一貫性
- 並行実行の考慮
- 定期的な重複チェック
- ERROR_MESSAGE_DETAILS を ON
事故防止
- サイレント無視(EXCEPTION 内 NULL)は避ける
- 並行実行で MERGE が失敗しうる
- NULL の挙動を意識
- シーケンスの現在値を確認
- 本番の制約追加前に重複除去
これらの知識は、Oracle での日常開発・ETL・データ移行・Web アプリ・バッチ処理・Rails / Java / Python 開発など、あらゆる場面で活用できます。本記事をブックマークしておけば、ORA-00001 に出会っても迷わず的確に対処できるようになります。
本記事は2026年6月時点の情報をもとに、Oracle Database 19c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。
-
前の記事
【完全ガイド】Oracle パラメータ確認(V$PARAMETER)|V$系ビュー・ALTER SYSTEM・重要パラメータまで徹底解説 2026.07.20
-
次の記事
【完全ガイド】Oracle MERGE 文 使い方|UPSERT・WHEN MATCHED・DELETE 句・差分同期まで徹底解説 2026.07.21
コメントを書く