【完全ガイド】ORA-00001: unique constraint violated の原因と解決方法|MERGE・IGNORE_ROW_ON_DUPKEY_INDEX・IDENTITY まで徹底解説

【完全ガイド】ORA-00001: unique constraint violated の原因と解決方法|MERGE・IGNORE_ROW_ON_DUPKEY_INDEX・IDENTITY まで徹底解説

目次

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

抽出できるのは:

  1. エラー種別: ORA-00001 = 一意制約違反
  2. スキーマ: SCOTT
  3. 制約名: 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 KEY
  • U = UNIQUE
  • C = CHECK
  • R = 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 UPDATEMERGE 文に書き換え。


参考リンク

Oracle 公式


まとめ

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)もあわせてご確認ください。