データ登録処理を書いていて、あるいは本番のバッチログを眺めていて、こんなエラーに出くわしたことはないでしょうか。
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 "APP.ORDERS" from having
duplicate values for the index key. SQLSTATE=23505
SQL0803N ――いわゆる重複キー違反です。「同じキーの値を持つ行を2つ入れようとした(あるいは更新でそうなった)」というエラーで、意味自体はシンプルです。ところが実際に現場で困るのは、「で、どのキーで弾かれたの?」がメッセージからすぐには読み取れない点にあります。
メッセージが教えてくれるのは identified by "1" ―― index-id という数字だけ。テーブルに主キーとユニークインデックスが複数あると、「どの制約に引っかかったのか」がこの数字だけでは分かりません。この記事では、SQL0803N が出たときに①どの制約で弾かれたかを特定し、②重複しているデータを洗い出し、③二度と起こさない設計に落とすまでを、現場の手順に沿って解説します。
- INSERT/MERGEバッチが
SQL0803Nでコケて、原因の制約を探している人 - 「重複してるはずないのに」と思いながら重複データを探している人
- リトライ処理を入れたら二重登録が起きるようになった、という人
対象は Db2 LUW(Linux/UNIX/Windows)です。コマンドは Db2 11.x / 12.x で確認しています。
まず結論:SQL0803N は「どの制約か」を特定してから動く
SQL0803N の犯人になりうる制約は、次の3種類です。まずこの区別を押さえておくと、後の対処がぶれません。
| 制約の種類 | 正体 | 特徴 |
|---|---|---|
| 主キー(PRIMARY KEY) | 一意インデックス+NOT NULL | テーブルに1つだけ。行を一意に識別する |
| ユニーク制約(UNIQUE) | 一意インデックス | 複数持てる。NULLは基本1行だけ許容 |
| 一意インデックス(UNIQUE INDEX) | CREATE UNIQUE INDEX で作った索引 | 制約としてカタログに出ないこともあり見落としやすい |
この3つはどれも「重複を許さない」点は同じですが、どれで弾かれたかによって、直すべき対象(アプリのロジックか、投入データか、そもそもの設計か)が変わります。まずは犯人の制約を確定させましょう。
ステップ①:index-id から「弾いた制約」を特定する
メッセージの identified by "1" の数字は、Db2内部でインデックスに振られた IID(index identifier)です。テーブル名(例では APP.ORDERS)と合わせて、SYSCAT.INDEXES を引けば正体が分かります。
# index-id(IID)とテーブル名から、弾いたインデックス/制約を特定する
db2 "SELECT INDSCHEMA, INDNAME, UNIQUERULE, COLNAMES
FROM SYSCAT.INDEXES
WHERE TABSCHEMA = 'APP' AND TABNAME = 'ORDERS' AND IID = 1"
UNIQUERULE の値で、制約の種類まで判別できます。
| UNIQUERULE | 意味 |
|---|---|
| P | 主キー(PRIMARY KEY) |
| U | 一意(UNIQUE 制約 または UNIQUE INDEX) |
| D | 重複を許可(=これは犯人ではない) |
COLNAMES に、そのインデックスを構成する列が +ORDER_ID+CUST_ID のような形で並びます。ここに出た列の組み合わせが、重複してはいけないキーです。犯人が確定しました。次は、実際にどの値が重複しているかを探します。
ステップ②:重複しているデータを洗い出す
INSERTで弾かれた場合、投入しようとした値が既存行とぶつかっています。UPDATEやMERGEで弾かれた場合は、更新後の値が既存行とぶつかります。まずはテーブル内に既に重複の芽がないかを、キー列で GROUP BY ... HAVING COUNT(*) > 1 して確認します。
# キー列(特定した COLNAMES の組み合わせ)で重複を探す
db2 "SELECT ORDER_ID, CUST_ID, COUNT(*) AS CNT
FROM APP.ORDERS
GROUP BY ORDER_ID, CUST_ID
HAVING COUNT(*) > 1
ORDER BY CNT DESC
FETCH FIRST 20 ROWS ONLY"
ここで行が返るなら、テーブル内に既に重複がある(=過去にユニークでない状態で入った、または制約を後付けしようとしている)ケースです。行が返らないなら、今まさに投入しようとしている外部データ側に重複があるということになります。投入元がファイルやステージング表なら、そちらを同じキーで GROUP BY して確認します。
大量投入で SQL0803N が出た場合、どの行で弾かれたかを掴むのが先決です。IMPORT なら MESSAGES 句で例外行をファイルに落とし、LOAD なら例外表(FOR EXCEPTION)を指定しておくと、弾かれた行だけを後から回収できます。「全部やり直し」ではなく「弾かれた行だけ対処」に持ち込むのが、大量データでの鉄則です。
db2 "IMPORT FROM data.del OF DEL
MESSAGES /tmp/import_msg.txt
INSERT INTO APP.ORDERS"
ステップ③:よくある原因パターンと恒久対策
犯人の制約と重複データが分かったら、あとは「なぜ重複が生まれたか」を潰します。現場で遭遇するのは、だいたい次の4パターンです。
パターン1:タイムアウト後のリトライによる二重登録
いちばん厄介で、いちばん多いのがこれです。アプリがINSERTを投げ、実際にはコミットされたのに、応答が返る前にタイムアウトした。アプリは「失敗した」と判断してリトライし、2回目のINSERTが SQL0803N になる――という流れです。「ログ上は失敗しているのに、データは入っている」という一見矛盾した状況が起きます。
対策は、リトライを冪等(べきとう)にすることです。単純な INSERT ではなく MERGE(存在すれば何もしない/更新する)を使えば、2回実行しても結果は同じになります。
# INSERT の代わりに MERGE で「無ければ入れる」を冪等にする
db2 "MERGE INTO APP.ORDERS AS T
USING (VALUES (1001, 5, 'NEW')) AS S(ORDER_ID, CUST_ID, STATUS)
ON T.ORDER_ID = S.ORDER_ID AND T.CUST_ID = S.CUST_ID
WHEN NOT MATCHED THEN
INSERT (ORDER_ID, CUST_ID, STATUS)
VALUES (S.ORDER_ID, S.CUST_ID, S.STATUS)"
パターン2:IDENTITY/シーケンスの採番ずれ
主キーを GENERATED ... AS IDENTITY やシーケンスで採番している場合、データ移行やリストア後に採番の現在値がデータとずれていると、次の採番が既存値とぶつかって SQL0803N を連発します。移行直後にこの症状が出たら、まず採番の現在値を疑います。
# IDENTITY列の採番を、既存データの最大値+1に合わせ直す
db2 "ALTER TABLE APP.ORDERS ALTER COLUMN ORDER_ID
RESTART WITH 1002"
# シーケンスの場合
db2 "ALTER SEQUENCE APP.ORDER_SEQ RESTART WITH 1002"
パターン3:upsertのつもりが素のINSERTだった
「あれば更新、なければ挿入」をしたいのに、アプリが素の INSERT を投げているパターン。存在チェックのSELECTとINSERTの間に別処理が割り込む(レースコンディション)と、チェックをすり抜けて SQL0803N になります。これもSELECT+INSERTではなく MERGE 一発にするのが正解です。アプリ側でロックして直列化するより、DBの原子的な操作に任せるほうが堅牢です。
パターン4:制約の後付けで既存の重複が露見した
運用途中で「この列はユニークにしたい」と UNIQUE 制約や一意インデックスを後から張ろうとして SQL0803N が出るケース。これは既に重複データが存在するという意味なので、ステップ②で重複を洗い出し、業務ルールに沿ってどちらを残すかを決めてから制約を張ります。参照制約(外部キー)が絡む重複の削除では、消す順序を誤ると別のエラーを招くので注意が必要です(関連:TRUNCATE 時の参照制約に注意)。
SQL0803N が出たときのチェックリスト
- メッセージから index-id(IID)とテーブル名を控える
SYSCAT.INDEXESをIIDで引き、弾いた制約と対象列(COLNAMES)を特定する- そのキー列で
GROUP BY ... HAVING COUNT(*) > 1し、重複がテーブル内にあるか/投入データ側にあるかを切り分ける - 大量投入なら
MESSAGES/例外表で弾かれた行だけを回収する - リトライ/upsert起因なら
INSERTをMERGEに置き換えて冪等化する - 移行直後なら IDENTITY/シーケンスの採番現在値を疑い、
RESTART WITHで合わせる
この記事のような現場のTIPSを、Db2運用の全体像として12章に体系化しました。公式マニュアルには載らない「あのとき知っていれば」を減らすための1冊です。
現場で引ける Db2 の教科書
現場で引ける!Db2実践エンジニア・バイブル
Db2はRDBMS市場でシェア数%。情報が少なく、頼れるのは膨大な公式マニュアルだけ――
そんな現状を変えたくて、「なぜこの値にするのか」「この障害のとき次に何を見るのか」という、マニュアルに載っていない現場の文脈を1冊にまとめました。書いてあることはすべて実際の現場で経験したことです。
アーキテクチャ/構成パラメーター選定/セキュリティ/バックアップ・リカバリ/HADR/pureScale/日常点検の自動化/メモリ・ロック競合/SQLチューニング/RUNSTATS・REORG/MON_GET監視/現場の難題集 ―― 全12章+運用シェルスクリプト集。
Db2の運用で踏んだ地雷や、この記事で扱ってほしいテーマがあれば、ぜひコメントで教えてください。次の記事のネタにさせてもらいます。