SQLのMERGEを避けたいときのUPSERT代替策 DB別の選び方と失敗しない実装ポイント

SQLのMERGEを避けたい場面では、代替策を1つに決め打ちしないのが正解です。PostgreSQLやSQLiteなら INSERT ... ON CONFLICT、MySQL/MariaDBなら INSERT ... ON DUPLICATE KEY UPDATE、SQL Serverなら UPDATE と INSERT を分けた実装や INSERT ... WHERE NOT EXISTS のほうが扱いやすいケースが多くあります。SQL Serverの公式ドキュメントでも、単純な同期では分離したDMLのほうが性能・スケーラビリティ面で有利な場合があり、高並行では MERGE が複雑な競合を招くことがあると案内されています。PostgreSQLでも MERGE と ON CONFLICT DO UPDATE は同じ意味ではなく、後者には INSERT か UPDATE のどちらかが行われる保証が明記されています。(Microsoft Learn)

この記事では、SQLのMERGEを避けたいときに選ぶべきUPSERT代替策を、DB製品ごとの差、向いている運用、実装時の注意点まで含めて整理します。構文の一覧ではなく、「自分の環境ならどれを採用するべきか」が判断できる形でまとめます。

目次

SQLのMERGEを避けたいときの結論

  • PostgreSQL / SQLite では、通常のUPSERTは INSERT ... ON CONFLICT が第一候補です。PostgreSQLでは DO UPDATE に conflict target が必要で、SQLite の UPSERT も PostgreSQL 系の構文をベースにしています。(PostgreSQL)
  • MySQL / MariaDB では、INSERT ... ON DUPLICATE KEY UPDATE が基本です。ただし、複数の UNIQUE キーを持つテーブルでは挙動が分かりにくくなるため、設計ごと見直したほうが安全です。(MySQL Developer Zone)
  • SQL Server では、単純なUPSERTなら UPDATE → IF @@ROWCOUNT = 0 INSERT、または INSERT ... WHERE NOT EXISTS を選ぶほうが実務的です。Microsoftの公式ドキュメントでも、単純な同期では分離した INSERT / UPDATE / DELETE が推奨される場面があります。(Microsoft Learn)
  • 大量データの同期 では、staging table に取り込んでから set-based な UPDATE / INSERT / 必要時のみ DELETE を別々に実行する方法が堅実です。行単位のUPSERTを大量に回すより、往復回数とログ書き込みを抑えやすくなります。(Microsoft for Developers)

最初にやるべきはUPSERT文を探すことではなく、競合を判定するキーに PRIMARY KEY か UNIQUE を定義することです。PostgreSQL の ON CONFLICT、SQLite の UPSERT、MySQL/MariaDB の ON DUPLICATE KEY UPDATE は、いずれも一意制約や主キーの衝突を前提に動きます。キーが曖昧なままだと、文法だけ変えても再現性のあるUPSERTにはなりません。(PostgreSQL)

MERGEを避けたい場面を先に整理する

次の条件に当てはまるなら、MERGEを無理に使わないほうが判断しやすくなります。

  • やりたいことが「更新か挿入」だけのとき。SQL Serverの公式ドキュメントでも、単純な同期では分離した INSERT / UPDATE / DELETE のほうが性能・スケーラビリティ面で有利な場合があるとされています。(Microsoft Learn)
  • 同じキーへの同時書き込みが多いとき。SQL Serverでは MERGE のロック挙動が通常の連続DMLと異なり、大規模環境では競合やトラブルシュートが複雑になりやすいと明記されています。(Microsoft Learn)
  • PostgreSQLで「必ず insert か update のどちらかを実行したい」とき。INSERT ... ON CONFLICT DO UPDATE は Read Committed でその保証がありますが、MERGE は同じ保証ではありません。(PostgreSQL)
  • トリガーや監査ロジックが細かいとき。SQL Serverでは MERGE に対する AFTER トリガー内の @@ROWCOUNT が、INSERT・UPDATE・DELETE の合計行数を返すため、個別DML前提の処理が崩れることがあります。(Microsoft Learn)

一方で、MERGEが常に悪いわけではありません。SQL Serverの公式ドキュメントでも、低並行なETL時間帯や、小規模で複雑な条件分岐を1文でまとめたい場面では適した選択肢になり得るとされています。ポイントは「MERGEを避けること」ではなく、MERGEでしか得をしない場面だけで使うことです。(Microsoft Learn)

PostgreSQLとSQLiteのUPSERT代替策

PostgreSQLとSQLiteでは、通常のUPSERTは INSERT ... ON CONFLICT を素直に使うのが最も分かりやすいです。PostgreSQLでは、一意制約または排他制約にぶつかったときに DO NOTHING か DO UPDATE を選べます。さらに PostgreSQL は、Read Committed でも ON CONFLICT DO UPDATE なら各行が「挿入されるか、更新されるか」のどちらかになることを明示しています。SQLite の UPSERT も同じ発想で、重複時に更新または何もしない動作を選べます。(PostgreSQL)

向いている使い方

  • 重複があっても新規だけ入れたい
    DO NOTHING を使う。
  • 重複時に列の一部だけ更新したい
    DO UPDATE SET ... を使う。
  • アプリ側の存在確認を減らしたい
    1文で完結させる。
INSERT INTO users (user_id, email, updated_at)
VALUES (1001, '[email protected]', CURRENT_TIMESTAMP)
ON CONFLICT (user_id)
DO UPDATE SET
  email = EXCLUDED.email,
  updated_at = EXCLUDED.updated_at;

この書き方が向いているのは、競合キーが明確で、更新対象列も限定されているケースです。たとえば「ユーザーIDが同じならメールアドレスだけ更新」「商品コードが同じなら在庫数だけ更新」といった業務では、MERGEより読みやすく、レビューもしやすくなります。PostgreSQLでは DO UPDATE に conflict target が必要で、挿入予定の値は excluded から参照できます。(PostgreSQL)

SQLiteでありがちな失敗

SQLite では INSERT OR REPLACE をUPSERT代わりに使いたくなりますが、本当に更新しているわけではない点に注意が必要です。SQLite の REPLACE 系は、競合した既存行を削除してから処理を続ける動作で、条件によっては delete trigger も発火します。更新のつもりで書いたのに、監査列や外部キー周りで想定外の副作用が出る原因になりやすいです。SQLite で「更新したい」なら、まず ON CONFLICT DO UPDATE を検討するほうが安全です。(SQLite)

MySQL/MariaDBのUPSERT代替策

MySQL と MariaDB では、UPSERTの定番は INSERT ... ON DUPLICATE KEY UPDATE です。PRIMARY KEY または UNIQUE インデックスに重複する値が入ると、新規挿入ではなく既存行の更新が走ります。MariaDB のドキュメントでも、この構文は MySQL/MariaDB 拡張として説明されています。(MySQL Developer Zone)

-- MySQL 8.0.20以降で新規値を参照する書き方
INSERT INTO inventory (sku, qty, updated_at)
VALUES ('A001', 10, NOW()) AS new
ON DUPLICATE KEY UPDATE
  qty = new.qty,
  updated_at = new.updated_at;

実務では、「SKU が同じなら数量だけ更新」「メールアドレスが重複したら最終更新日時を更新」といった用途で非常に使いやすい構文です。複数行INSERTにもそのまま乗せられるため、アプリ側で1行ずつ SELECT → UPDATE/INSERT を回すよりコードが短くなります。MySQL では新規行の値参照に VALUES(col) が長く使われてきましたが、8.0.20 以降は非推奨になっており、公式ドキュメントでも行エイリアスや列エイリアスの利用が推奨されています。(MySQL Developer Zone)

MySQL/MariaDBで注意したい点

  • 複数の UNIQUE キーがあるテーブルでは要注意です。MySQL も MariaDB も、複数の一意キーにまたがる重複では「どの衝突を優先して更新するか」が分かりにくくなり、ドキュメントでも非推奨または注意扱いです。まずはUPSERT対象のキーを1つに絞る設計を優先してください。(MySQL Developer Zone)
  • 更新件数の解釈が素直ではないことがあります。MySQL では、挿入なら影響行数は 1、更新なら 2、値が変わらない更新では 0 になる場合があり、単純な UPDATE と同じ感覚で件数判定すると誤解しやすいです。アプリ側で「1件更新されたはず」と決め打ちしないほうが安全です。(MySQL Developer Zone)
  • REPLACE は別物です。MySQL の REPLACE は、重複時に古い行を削除してから新しい行を挿入する動作です。更新トリガー前提の設計や外部キーを含むテーブルでは、UPSERT代替策として安易に使わないほうがよいです。(MySQL Developer Zone)

SQL ServerのUPSERT代替策

SQL Serverでは、MERGEより分離したDMLを基本に考えるほうが無難です。公式ドキュメントでも、単純な更新・挿入であれば INSERT / UPDATE / DELETE を分けたほうが性能とスケーラビリティの面でよい場合があるとされ、さらに MERGE は高並行環境で複雑な競合を招く可能性があるため、本番投入前の十分な検証が推奨されています。(Microsoft Learn)

更新できたら更新し、なければ挿入する

SQL Serverの公式サンプルにもある定番は、まず UPDATE し、更新件数が 0 なら INSERT する形です。これはMERGEより意図が読みやすく、トラブル時にどこで失敗したかも追いやすい書き方です。(Microsoft Learn)

BEGIN TRAN;

UPDATE dbo.Accounts
SET Name = @Name,
    UpdatedAt = SYSUTCDATETIME()
WHERE AccountId = @AccountId;

IF @@ROWCOUNT = 0
BEGIN
    INSERT INTO dbo.Accounts (AccountId, Name, UpdatedAt)
    VALUES (@AccountId, @Name, SYSUTCDATETIME());
END

COMMIT;

このパターンをそのままコピペして終わりにしないことも重要です。同じ AccountId に対する同時書き込みがあり得るなら、キー列に一意制約を置いたうえで、トランザクション分離やロックヒントを含めて設計する必要があります。SQL Server では UPDLOCK は更新ロックをトランザクション終了まで保持し、HOLDLOCK は SERIALIZABLE 相当として扱われます。つまり、SQL Server でUPSERTを安全に寄せるには、文法よりロック設計が本体です。(Microsoft Learn)

「なければ入れる」だけなら INSERT ... WHERE NOT EXISTS

更新が不要で、重複を避けつつ新規だけ入れたいなら、INSERT ... SELECT ... WHERE NOT EXISTS が使いやすいです。Microsoft の Azure SQL 公式ブログでも、仮想テーブルと NOT EXISTS を組み合わせたパターンが紹介されています。(Microsoft for Developers)

INSERT INTO dbo.Tags (post_id, tag)
SELECT s.post_id, s.tag
FROM (VALUES (@post_id, @tag)) AS s(post_id, tag)
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.Tags AS t WITH (UPDLOCK)
    WHERE t.post_id = s.post_id
      AND t.tag = s.tag
);

この形は、タグ付け・中間テーブル・重複登録防止のような insert-only の要件と相性がよいです。逆に、既存行の複数列を更新する要件まで混ざるなら、最初から UPDATE と INSERT を分けたほうが意図を保ちやすくなります。なお、競合が厳しい環境では UPDLOCK だけで満足せず、対象インデックスや HOLDLOCK を含めて負荷テストで確認してください。(Microsoft for Developers)

変更内容を返したいなら OUTPUT

MERGEを避けると「挿入されたか、更新されたかをどう返すのか」が気になることがあります。SQL Server では OUTPUT 句を使うと、INSERT / UPDATE / DELETE / MERGE の各行について変更結果を返したり、テーブル変数へ流し込んだりできます。別途 SELECT し直さず監査ログやAPIレスポンスへ回せるので、MERGEを使わなくても結果の取り回しは十分に実装可能です。(Microsoft Learn)

大量データで使うUPSERT代替策

数千件、数万件、さらにそれ以上のデータを同期するなら、1行ずつUPSERTする発想を捨てるのが近道です。Microsoft の Azure SQL 公式ブログでも、アプリ層から1行ごとに INSERT/UPDATE を呼び出す方法と、まとめて取り込んで一括で INSERT/UPDATE する方法では、往復回数とログ書き込みの差が大きく、後者のほうが大規模データで圧倒的に有利だと説明されています。(Microsoft for Developers)

-- 1. staging にデータを取り込む

-- 2. 既存行を更新
UPDATE t
SET
  t.name = s.name,
  t.updated_at = s.updated_at
FROM target_table AS t
JOIN staging_table AS s
  ON s.id = t.id;

-- 3. 新規行を挿入
INSERT INTO target_table (id, name, updated_at)
SELECT s.id, s.name, s.updated_at
FROM staging_table AS s
LEFT JOIN target_table AS t
  ON t.id = s.id
WHERE t.id IS NULL;

このやり方の利点は、速さだけではありません。更新件数と挿入件数を分けて監視しやすい、失敗時の切り戻しポイントを分けやすい、DELETE を別判断にできるという運用上の強さがあります。特に「対象に存在しない行を消すかどうか」は業務要件で割れやすいので、UPSERTとDELETEを同じ文に押し込めないほうが保守しやすいです。(Microsoft for Developers)

PostgreSQL なら RETURNING、SQL Server なら OUTPUT を使えば、変更された行や採番されたIDも追加クエリなしで受け取れます。大量同期で「何件更新されたか」「どのIDが入ったか」を追跡したいなら、ここまで含めて設計すると後工程が楽になります。(PostgreSQL)

代替策でも失敗しやすいポイント

一意制約がないままUPSERT文だけ書く

これは最も多い失敗です。UPSERTは「重複をどう扱うか」を決める文であって、「何が重複か」を決めるのはテーブル設計です。PRIMARY KEY や UNIQUE が曖昧なら、アプリが期待する“同じデータ”と、DBが判定する“衝突”が一致しません。(PostgreSQL)

アプリ側で存在確認してから別文で INSERT / UPDATE する

「まず SELECT で存在確認し、なければ INSERT」は見た目は分かりやすいのですが、並行実行が入ると別セッションに割り込まれる余地があります。Azure SQL の公式ブログでも、この2段階アルゴリズム自体に競合リスクがあると説明されています。UPSERTは文法の問題というより、原子的に処理する設計の問題です。(Microsoft for Developers)

REPLACE を「更新の別名」だと思う

MySQL の REPLACE も SQLite の REPLACE 系も、重複時には削除してから挿入する意味合いが強く、真の UPDATE とは違います。監査列、外部キー、削除トリガー、変更件数カウントに影響するため、「上書きしたい」だけで REPLACE を選ばないことが大切です。(MySQL Developer Zone)

SQL Serverでロック設計を後回しにする

SQL Server のUPSERT代替策は、文法を覚えれば終わりではありません。UPDLOCK は更新ロックを保持し、HOLDLOCK は SERIALIZABLE 相当として働くため、同時実行時の正しさはインデックスとロック戦略で決まる面があります。サンプルをそのまま使うのではなく、同じキーへ同時更新をぶつけるテストまで含めて確認してください。(Microsoft Learn)

「MERGEをやめれば移植性が上がる」と思い込む

ここは少し見落とされがちです。SQLite の UPSERT は非標準SQLと明記されており、MariaDB/MySQL の ON DUPLICATE KEY UPDATE も製品拡張です。つまり、MERGEを避けてもUPSERT構文そのものはDB依存です。複数DBをまたぐプロダクトなら、SQLの共通化より「DBごとに分岐する層」をどう切るかまで考えたほうが実装が安定します。(SQLite)

次にやること

SQLのMERGEを避けたいときの実務的な正解は、より小さく、より明確な文に分解することです。PostgreSQLとSQLiteなら INSERT ... ON CONFLICT、MySQL/MariaDBなら INSERT ... ON DUPLICATE KEY UPDATE、SQL Serverなら UPDATE と INSERT を分ける方針から始めると、読みやすさと運用のしやすさを両立しやすくなります。(PostgreSQL)

まずやるべき作業は3つです。競合キーを決めて一意制約を置くこと、要件が「挿入のみ」なのか「更新も必要」なのかを切り分けること、同じキーを同時に書き込む負荷テストを先に作ることです。ここまでできれば、「なんとなくMERGEは不安だから避ける」という状態から卒業して、要件に合ったUPSERT代替策を自信を持って選べるようになります。(PostgreSQL)

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次