はじめに
データベースやデータ基盤を触っていると、しばしば耳にするのが「UPSERT」と「MERGE」というキーワードです。
どちらも“既存のデータを更新するのか、新しく追加するのか”といった文脈で登場しますが、実装中に明確な使い分けが出来なかったので、ここでそれぞれの特徴と違いを整理し、どう使い分ければよいか考えていければと思います。
基本的なUPSERTとMERGE
UPSERT=「そのキーがあれば UPDATE/なければ INSERT」を1文でやる便利ワザ。
UPSERTはSQL標準ではなく、「 UPDATE」と「INSERT」を組み合わせたもの。
DBごとに書き方が違う(PostgreSQL: ON CONFLICT、MySQL: ON DUPLICATE KEY など)。
少量・単発更新に強い。
MERGE=「ソースとターゲットを突き合わせて INSERT/UPDATE/DELETE をまとめて実行」する統合ワザ。
MERGEはANSI SQL標準。ONで一致条件を決め、WHEN MATCHED/NOT MATCHEDで分岐でき、DELETEも書ける。
差分同期・大量更新に強い。
速度の違い
小規模(1〜数百件): UPSERTが軽量で速い。
単に「そのキーある?」のチェックで済むため。
中〜大規模(万件〜):MERGEが有利。
1文で全体を結合し、一括で挿入・更新・削除を裁けるため、ループUPSERTより効率的。
ざっくりな速度の基準
100件:UPSERTがサクッ(MERGEは結合オーバーヘッドが相対的に重い)
1万件:MERGEが明確に有利(UPSERTを1件ずつ回すと遅い)
100万件:MERGE一択(バルク+結合最適化が効く)
具体例
--UPSERT
INSERT INTO users (id, name, updated_at)
VALUES (1, 'Taro', NOW())
ON CONFLICT (id)
DO UPDATE SET
name = EXCLUDED.name,
updated_at = NOW();
ON CONFLICT (id)
が「キー衝突=既存あり」を示してます。
ポイント: 衝突判定に使う一意制約/インデックスは必須。
--UPSERT
INSERT INTO users (id, name, updated_at)
VALUES (1, 'Taro', NOW())
ON DUPLICATE KEY UPDATE
name = VALUES(name),
updated_at = NOW();
ON DUPLICATE KEY UPDATE
が「キー衝突=既存あり」を示してます。
--MERGE
MERGE INTO users AS t
USING staging_users AS s
ON t.id = s.id
WHEN MATCHED THEN
UPDATE SET name = s.name, updated_at = CURRENT_TIMESTAMP()
WHEN NOT MATCHED THEN
INSERT (id, name, updated_at) VALUES (s.id, s.name, CURRENT_TIMESTAMP())
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
→DELETEまで一気に書けるのが強み。
NOT MATCHED BY SOURCE
はソースに無い=削除対象という“同期”の発想。
実務TIPS
キー設計
UPSERT/ON CONFLICT で使う列にユニークインデックス必須。
バッチ戦略:
- 少量はUPSERTをまとめてINSERT(複数VALUES)で往復回数を削減
- 大量は一旦ステージングにロード → MERGEが定石
- ロック/競合:同じキーに同時UPSERTが来るなら、更新列を最小化&衝突ターゲット明確化
- 削除の扱い:物理DELETEが重い/怖い時はis_deletedフラグでソフトデリート→定期VACUUM/クリーンアップ
- 再実行性:MERGEは冪等になりやすい。失敗時の再実行計画を決めておく
- 監査列:updated_at や last_modified_by を両方の分岐(UPDATE/INSERT)で埋めるのを忘れない
“UPSERTのほうが良い”典型
APIから1件ずつ到着するプロフィール更新、設定保存、お気に入り追加など。
フォーム送信に対して即時反映したい。 → MERGEは大げさ&遅延が増えがち。
“MERGEのほうが良い”典型
日次/時間毎の差分ロード(DWH・データレイク)。
外部SaaSのエクスポートを丸ごと同期(新規/変更/廃止を一括反映)。
参照整合性を保ちつつ削除も必須な同期。
よくある勘違い
「UPSERTでもDELETEできる?」→できない(別クエリが必要)。
「MERGEはいつでも速い?」→少量相手ならUPSERTが勝つことは普通にある。
「UPSERTは標準構文?」→DB依存。移植性を気にするならMERGE(対応DB)か抽象化レイヤで吸収。
まとめ
UPSERT=小回りが効く単発更新(少量)
MERGE=差分同期の主役(大量)
普段のアプリ開発ではUPSERT的な構文を覚え、大量データ更新のあるデータ基盤ではMERGEを押さえておく。
使い分けができると、SQLのレベルがグッと上がりそうですね☘️