0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SQL】UPSERTとMERGEを徹底解説

Posted at

はじめに

データベースやデータ基盤を触っていると、しばしば耳にするのが「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一択(バルク+結合最適化が効く)

具体例

PostgreSQL
--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) が「キー衝突=既存あり」を示してます。

ポイント: 衝突判定に使う一意制約/インデックスは必須。

MySQL
--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 が「キー衝突=既存あり」を示してます。

BigQuery
--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のレベルがグッと上がりそうですね☘️

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?