🔑

Db2の「SQL0803N 重複キー」はメッセージの索引番号から“どの制約か”を機械的に特定する

に公開

INSERTUPDATE を流したら SQL0803N「一意索引 "…" によって重複が許されません」で弾かれた——アプリ開発でも運用でも遭遇率の高いエラーです。原因は文字どおり「一意なはずの列に重複が生じた」ことなのですが、現場で時間を溶かすのはどのキーで重複したのかの切り分けです。

主キーだけでなく、一意制約・一意索引・(外部キー更新の巻き添え)まで、SQL0803N を出す原因は複数あります。メッセージには手がかりとして索引番号が入っているのに、それを読まずに「たぶん主キーだろう」と当たりをつけて、実は別の一意索引だった、というのがありがちな回り道です。

この記事では、SQL0803N が出たときにメッセージの索引番号から“どの一意制約か”を一意に特定し、重複行を突き止めるまでを機械的に進める手順を解説します。

まず結論:SQL0803N は「一意キーの重複」。索引番号→索引名で原因が確定する

SQL0803N の SQLSTATE は 23505。メッセージには必ず索引番号表名が入っています。

SQL0803N  One or more values in the INSERT statement, UPDATE statement,
or foreign key update caused by a DELETE statement are not valid because
the primary key, unique constraint or unique index identified by "1"
constrains table "DB2INST1.EMPLOYEE" from having duplicate values for
the index key.  SQLSTATE=23505

ここで "1"索引番号(IID)"DB2INST1.EMPLOYEE" が対象表です。切り分けは次の3手で終わります。

  1. 索引番号→索引名SYSCAT.INDEXES を引いて、どの一意索引か(主キーか一意制約か)を確定する
  2. 重複行の特定 … その索引のキー列で GROUP BY ... HAVING COUNT(*) > 1
  3. 原因に応じた対処 … 本当に重複データなのか、リトライの二重投入なのか、制約設計の問題なのかを切り分ける

順に見ていきます。

ステップ1:索引番号から「どの一意制約か」を特定する

SQL0803N の索引番号が整数のときは、次のクエリで索引名を引けます。公式メッセージ(db2 "? SQL0803N")にも案内されている引き方です。

-- 索引番号(IID)から索引名を引く
SELECT INDNAME, INDSCHEMA, UNIQUERULE
  FROM SYSCAT.INDEXES
  WHERE IID = 1
    AND TABSCHEMA = 'DB2INST1'
    AND TABNAME = 'EMPLOYEE';

検証環境の SAMPLE で実際に主キー重複を起こすと、索引番号 "1" が返り、上のクエリはこうなります。

INDNAME       INDSCHEMA   UNIQUERULE
------------- ----------- ----------
PK_EMPLOYEE   DB2INST1    P

UNIQUERULE でキーの種類が分かります。ここが対処方針の分かれ目です。

UNIQUERULE 意味 典型的な原因
P 主キー 主キーの重複。二重投入・採番の衝突が多い
U 一意制約 / 一意索引 主キー以外に張った一意キーの重複。見落としやすい
D 重複可(非一意索引) この索引は SQL0803N の原因にならない

索引名から制約名(主キー名・一意制約名)を確認したいときは SYSCAT.TABCONST を見ます。

SELECT CONSTNAME, TYPE, ENFORCED
  FROM SYSCAT.TABCONST
  WHERE TABSCHEMA = 'DB2INST1' AND TABNAME = 'EMPLOYEE';

TYPEP=主キー / U=一意制約 / F=外部キー / K=チェック制約です。どのキー列で重複したかは、次のクエリで索引のキー列を確定できます。

-- その索引のキー列(重複判定に使う列)
SELECT COLNAME, COLSEQ, COLORDER
  FROM SYSCAT.INDEXCOLUSE
  WHERE INDSCHEMA = 'DB2INST1' AND INDNAME = 'PK_EMPLOYEE'
  ORDER BY COLSEQ;

ステップ2:どの行が重複しているかを突き止める

原因の索引とキー列が分かれば、重複している値は GROUP BY ... HAVING で洗い出せます。EMPLOYEE の主キーが EMPNO なら次の通りです。

-- 重複している主キー値を洗い出す
SELECT EMPNO, COUNT(*) AS CNT
  FROM DB2INST1.EMPLOYEE
  GROUP BY EMPNO
  HAVING COUNT(*) > 1;

INSERT ... SELECTMERGE で大量に入れて弾かれた場合は、投入元データの中の重複を先に確認します。

-- 投入元(ステージング表)側の重複
SELECT EMPNO, COUNT(*) AS CNT
  FROM STG.EMPLOYEE_IN
  GROUP BY EMPNO
  HAVING COUNT(*) > 1;

-- 投入先に既に存在するキー(新規のつもりが既存)
SELECT s.EMPNO
  FROM STG.EMPLOYEE_IN s
  WHERE EXISTS (SELECT 1 FROM DB2INST1.EMPLOYEE t WHERE t.EMPNO = s.EMPNO);

重複が「投入元の中」にあるのか「投入先に既存」なのかで、対処(データを直すのか、投入方法を変えるのか)が変わります。

ステップ3:原因別の対処

ケースA:本当に重複データが混ざっている

投入元のファイルやステージング表に重複がある、あるいは同じ業務データが二重に来ている場合です。データ側を正すのが筋で、投入前に重複を排除します。

-- 重複を除いて投入(同一キーは1件に寄せる)
INSERT INTO DB2INST1.EMPLOYEE (EMPNO, FIRSTNME, LASTNAME)
SELECT EMPNO, MAX(FIRSTNME), MAX(LASTNAME)
  FROM STG.EMPLOYEE_IN
  GROUP BY EMPNO;

ケースB:既存キーは更新・新規キーは挿入したい(UPSERT)

「あれば更新、なければ挿入」がやりたくて INSERT が弾かれているなら、Db2 では MERGE を使います。INSERT を素朴にリトライするより、最初から MERGE で書くのが安全です。

MERGE INTO DB2INST1.EMPLOYEE AS t
USING STG.EMPLOYEE_IN AS s
   ON t.EMPNO = s.EMPNO
WHEN MATCHED THEN
     UPDATE SET t.FIRSTNME = s.FIRSTNME, t.LASTNAME = s.LASTNAME
WHEN NOT MATCHED THEN
     INSERT (EMPNO, FIRSTNME, LASTNAME)
     VALUES (s.EMPNO, s.FIRSTNME, s.LASTNAME);

「既存キーはスキップして新規だけ入れたい」なら、WHERE NOT EXISTS で既存を除外してから挿入します。

INSERT INTO DB2INST1.EMPLOYEE (EMPNO, FIRSTNME, LASTNAME)
SELECT s.EMPNO, s.FIRSTNME, s.LASTNAME
  FROM STG.EMPLOYEE_IN s
 WHERE NOT EXISTS
     (SELECT 1 FROM DB2INST1.EMPLOYEE t WHERE t.EMPNO = s.EMPNO);

ケースC:想定していない一意制約に当たっている

UNIQUERULE='U'(主キーではない一意制約・一意索引)で弾かれているなら、その一意キーの存在を見落としている可能性があります。業務上その列は本当に一意であるべきか、制約設計を確認します。制約が過剰(本来重複を許すべき列に一意を張っている)なら、影響を確認したうえで制約を見直します。ただし本番表の制約変更は慎重に行い、なぜ一意にしていたのかを必ず確認してから判断します。

現場で効く注意点

  • SQLSTATE=23505 をアプリで握りつぶさない。 一意制約違反はアプリ側で 23505 を判定して「既存扱いにフォールバック」など明示的に処理すべきです。ログに残さず握りつぶすと、二重投入の温床になります。
  • 索引番号が整数でない場合。 メッセージの識別子が索引名そのもののこともあります。その場合は SYSCAT.INDEXESINDNAME で直接引きます。
  • 外部キー更新の巻き添え。 親表の DELETEON DELETE SET NULL)が従属表の一意索引に触れて SQL0803N になることもあります。メッセージの表名が「操作した表」と違うときはこれを疑います。

SQL0803N が出たときのチェックリスト

  1. メッセージから索引番号表名を読む(SQLSTATEは 23505
  2. SYSCAT.INDEXESIID + TABSCHEMA + TABNAME で引き、索引名と UNIQUERULE(P/U/D)を確定(IIDは表内でのみ一意なので表名必須)
  3. 必要なら SYSCAT.TABCONSTSYSCAT.INDEXCOLUSE で制約名とキー列を確認
  4. GROUP BY キー列 HAVING COUNT(*)>1重複行を特定。投入元/投入先のどちらの重複かも見る
  5. 本当に重複データなら投入前に排除(ケースA)
  6. UPSERTしたいなら MERGE新規だけ入れたいなら WHERE NOT EXISTS(ケースB)
  7. 想定外の一意制約なら制約設計を確認(ケースC、変更は慎重に)
  8. 恒久対策は「投入を冪等に(MERGE)」+「アプリで 23505 を握りつぶさない」

SQL0803N は「重複が入った」という結果だけ見ると原因が広く見えますが、メッセージの索引番号を SYSCAT.INDEXES に通せば、どの一意制約が・どのキー列で弾いたかはその場で確定できます。あとは重複が元データ由来か再実行由来かを切り分ければ、対処は機械的に決まります。

📕 もっと深く:Db2運用を12章で体系化した本を書きました

この記事で扱った SQL0803N の切り分けは、Db2運用のほんの一部です。

Db2はRDBMS市場でシェア数%。情報が少なく、頼れるのは膨大な公式マニュアルだけ——そんな現状を変えたくて、「なぜこの値にするのか」「この障害のとき次に何を見るのか」という、マニュアルに載っていない現場の文脈を1冊にしました。実機で検証した内容と、Db2の設計上そうなる理由をまとめました。

アーキテクチャ/構成パラメーター選定/セキュリティ/バックアップ・リカバリ/HADR/pureScale/日常点検の自動化/メモリ・ロック競合/SQLチューニング/RUNSTATS・REORG/MON_GET監視/現場の難題・解決集まで、全12章+運用シェルスクリプト集です。

https://zenn.dev/firese/books/db2-practical-bible

Db2の運用で踏んだ地雷や、扱ってほしいテーマがあれば、ぜひコメントで教えてください。次の記事のネタにさせてもらいます。

Discussion