INSERTしたい行がすでに存在したらUPDATEしたい――この処理はよく「UPSERT」と呼ばれます。ただし、UPSERTは1つの共通SQLキーワードではなく、DBごとに構文が異なります。さらにMERGEは、sourceとtargetを照合して複数の条件分岐を行えるため、単純なunique conflict処理とは役割が少し違います。
まず比較:MERGEとUPSERT系構文は何が違う?
| 観点 | MERGE | UPSERT系構文 |
|---|---|---|
| 主な目的 | sourceとtargetを照合し、条件ごとにINSERT/UPDATE/DELETE | INSERT時の重複・競合を起点にINSERTまたはUPDATE |
| 判定基準 | JOIN条件とWHEN句 | UNIQUE/PRIMARY KEYなどの競合 |
| PostgreSQL例 | MERGE | INSERT … ON CONFLICT |
| MySQL例 | 製品/version仕様を確認 | INSERT … ON DUPLICATE KEY UPDATE |
| 向く処理 | 同期・一括取込・複数分岐 | 一意キー単位の登録/更新 |
PostgreSQLの公式資料でも、MERGEとINSERT … ON CONFLICTには違いと制約があり、相互に置き換え可能ではないと説明されています。まず「sourceとの照合が必要か」「unique conflictだけ処理したいか」で分けると選びやすくなります。
UPSERTは『INSERTまたはUPDATE』という処理パターン
用語解説:UPSERT
INSERTし、既存行との競合があればUPDATEする処理パターンの通称です。具体的な構文はDB製品ごとに異なります。
たとえばユーザーIDや商品コードのような一意キーで、存在しなければ追加、存在すれば更新したいケースが典型です。アプリ側でSELECT→INSERT/UPDATEと分岐させる代わりに、DB側の競合処理構文を使えます。
PostgreSQL:単純なUPSERTならON CONFLICT
PostgreSQLでは`INSERT … ON CONFLICT`を使い、unique constraintやunique indexの競合時に`DO NOTHING`または`DO UPDATE`を指定できます。
INSERT INTO users (id, name, updated_at)
VALUES (101, 'Sato', CURRENT_TIMESTAMP)
ON CONFLICT (id)
DO UPDATE SET
name = EXCLUDED.name,
updated_at = EXCLUDED.updated_at;
`ON CONFLICT DO UPDATE`は、提案された各行についてINSERTするか、指定した競合対象にぶつかった既存行をUPDATEします。unique conflictが判断軸なら、意図を読み取りやすい選択肢です。
PostgreSQL:sourceとtargetを照合するならMERGE
MERGEは`USING`のdata sourceとtarget tableをJOIN条件で照合し、`WHEN MATCHED`や`WHEN NOT MATCHED`ごとに処理を定義します。PostgreSQL 18ではINSERT、UPDATE、DELETE、DO NOTHINGを条件付きで組み合わせられます。
MERGE INTO products AS target
USING import_products AS src
ON target.id = src.id
WHEN MATCHED THEN
UPDATE SET price = src.price
WHEN NOT MATCHED THEN
INSERT (id, name, price)
VALUES (src.id, src.name, src.price);
CSV取込後のstaging tableと本番tableを同期するように、source集合を基準に複数行を処理したい場合はMERGEの形が自然です。複数のWHEN条件を使う場合は、上から最初にtrueになったactionが実行されます。
MySQL:ON DUPLICATE KEY UPDATEを使う
MySQL 8.4では`INSERT … ON DUPLICATE KEY UPDATE`を使えます。INSERTしようとした行がPRIMARY KEYまたはUNIQUE indexの重複を起こす場合、既存行をUPDATEします。
INSERT INTO users (id, name, updated_at)
VALUES (101, 'Sato', CURRENT_TIMESTAMP) AS new
ON DUPLICATE KEY UPDATE
name = new.name,
updated_at = new.updated_at;
MySQL公式ドキュメントでは、複数のunique indexがあるtableでの利用には注意が必要とされています。また、旧来の`VALUES(column)`参照は非推奨のため、現行8.4ではrow aliasを使う書き方も確認しておきます。
どちらを選ぶ?実務の判断基準
| やりたいこと | 候補 |
|---|---|
| 一意キーが重複したらその行を更新 | PostgreSQL ON CONFLICT / MySQL ON DUPLICATE KEY UPDATE |
| source tableとtarget tableを照合して同期 | MERGE |
| matched時にも条件を分けたい | MERGE |
| 競合時は何もしない | PostgreSQL ON CONFLICT DO NOTHINGなどDB固有構文 |
| 複数DBで同じSQLを使いたい | 各DBの対応構文・versionを先に確認 |
「1文で書けるか」だけで選ぶと、競合条件や同時実行時の挙動を見落とします。unique key、sourceの重複、transaction isolation、対象DBのversionを含めてテストします。
注意点:DB固有の挙動まで同じではない
- UPSERTという名前だけで構文を決めず、対象DBの公式構文を確認する
- どのUNIQUE/PRIMARY KEYが競合判定に使われるか確認する
- MERGEのsourceが同じtarget rowへ複数回対応しないよう確認する
- 複数unique keyがある場合のDB固有挙動を確認する
- 同時実行時の挙動はtransaction/isolationを含めて検証する
構文が似ていても、競合判定・権限・trigger・concurrencyの詳細はDB製品やversionで異なります。migration時はSQLの見た目だけで置き換えないようにします。
まとめ:unique conflictかsource同期かで選ぶ
- UPSERTはINSERTまたはUPDATEの処理パターンで、構文はDBごとに異なる
- PostgreSQLのON CONFLICTはunique conflict処理に向く
- MERGEはsourceとtargetを照合した複数分岐に向く
- MySQLではON DUPLICATE KEY UPDATEが代表的
- MERGEとUPSERT系構文を同一視せず、判定条件から選ぶ
まず対象処理が「一意キー競合の処理」なのか「sourceとtargetの同期」なのかを書き出し、その後で利用DBの現行構文を選んでみてください。