Db2の「SQL0803N 重複キー」はメッセージの索引番号から“どの制約か”を機械的に特定する
INSERT や UPDATE を流したら 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手で終わります。
-
索引番号→索引名 …
SYSCAT.INDEXESを引いて、どの一意索引か(主キーか一意制約か)を確定する -
重複行の特定 … その索引のキー列で
GROUP BY ... HAVING COUNT(*) > 1 - 原因に応じた対処 … 本当に重複データなのか、リトライの二重投入なのか、制約設計の問題なのかを切り分ける
順に見ていきます。
ステップ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';
TYPE は P=主キー / 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 ... SELECT や MERGE で大量に入れて弾かれた場合は、投入元データの中の重複を先に確認します。
-- 投入元(ステージング表)側の重複
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.INDEXESをINDNAMEで直接引きます。 -
外部キー更新の巻き添え。 親表の
DELETE(ON DELETE SET NULL)が従属表の一意索引に触れてSQL0803Nになることもあります。メッセージの表名が「操作した表」と違うときはこれを疑います。
SQL0803N が出たときのチェックリスト
- メッセージから索引番号と表名を読む(SQLSTATEは
23505) -
SYSCAT.INDEXESをIID+TABSCHEMA+TABNAMEで引き、索引名とUNIQUERULE(P/U/D)を確定(IIDは表内でのみ一意なので表名必須) - 必要なら
SYSCAT.TABCONST/SYSCAT.INDEXCOLUSEで制約名とキー列を確認 -
GROUP BY キー列 HAVING COUNT(*)>1で重複行を特定。投入元/投入先のどちらの重複かも見る - 本当に重複データなら投入前に排除(ケースA)
-
UPSERTしたいなら
MERGE、新規だけ入れたいならWHERE NOT EXISTS(ケースB) - 想定外の一意制約なら制約設計を確認(ケースC、変更は慎重に)
- 恒久対策は「投入を冪等に(
MERGE)」+「アプリで23505を握りつぶさない」
SQL0803N は「重複が入った」という結果だけ見ると原因が広く見えますが、メッセージの索引番号を SYSCAT.INDEXES に通せば、どの一意制約が・どのキー列で弾いたかはその場で確定できます。あとは重複が元データ由来か再実行由来かを切り分ければ、対処は機械的に決まります。
📕 もっと深く:Db2運用を12章で体系化した本を書きました
この記事で扱った SQL0803N の切り分けは、Db2運用のほんの一部です。
Db2はRDBMS市場でシェア数%。情報が少なく、頼れるのは膨大な公式マニュアルだけ——そんな現状を変えたくて、「なぜこの値にするのか」「この障害のとき次に何を見るのか」という、マニュアルに載っていない現場の文脈を1冊にしました。実機で検証した内容と、Db2の設計上そうなる理由をまとめました。
アーキテクチャ/構成パラメーター選定/セキュリティ/バックアップ・リカバリ/HADR/pureScale/日常点検の自動化/メモリ・ロック競合/SQLチューニング/RUNSTATS・REORG/MON_GET監視/現場の難題・解決集まで、全12章+運用シェルスクリプト集です。
Db2の運用で踏んだ地雷や、扱ってほしいテーマがあれば、ぜひコメントで教えてください。次の記事のネタにさせてもらいます。
Discussion