DB2 13 min read

Db2「SQL0803N」重複キー違反の原因を特定する ― どの制約で弾かれたかを突き止める手順

データ登録処理を書いていて、あるいは本番のバッチログを眺めていて、こんなエラーに出くわしたことはないでしょうか。

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 して確認します。

IMPORT/LOAD/INGEST でまとめて弾かれたとき

大量投入で 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 が出たときのチェックリスト

  1. メッセージから index-id(IID)とテーブル名を控える
  2. SYSCAT.INDEXESIID で引き、弾いた制約と対象列(COLNAMES)を特定する
  3. そのキー列で GROUP BY ... HAVING COUNT(*) > 1し、重複がテーブル内にあるか/投入データ側にあるかを切り分ける
  4. 大量投入なら MESSAGES/例外表で弾かれた行だけを回収する
  5. リトライ/upsert起因なら INSERTMERGE に置き換えて冪等化する
  6. 移行直後なら IDENTITY/シーケンスの採番現在値を疑い、RESTART WITH で合わせる

この記事のような現場のTIPSを、Db2運用の全体像として12章に体系化しました。公式マニュアルには載らない「あのとき知っていれば」を減らすための1冊です。

現場で引ける Db2 の教科書

現場で引ける!Db2実践エンジニア・バイブル

Db2はRDBMS市場でシェア数%。情報が少なく、頼れるのは膨大な公式マニュアルだけ――
そんな現状を変えたくて、「なぜこの値にするのか」「この障害のとき次に何を見るのか」という、マニュアルに載っていない現場の文脈を1冊にまとめました。書いてあることはすべて実際の現場で経験したことです。

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


Zennで読む(¥2,000)→

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