SQL Server UPDATE … FROM SELECT 3パターン — 業務SE が JOIN / CTE / MERGE を本番で使い分ける判断軸

SQL Server UPDATE … FROM SELECT 3パターン — 業務SE が JOIN / CTE / MERGE を本番で使い分ける判断軸

みなさんこんにちは!ヒロポンです!

業務側から無線が飛んでくる。「A テーブルの一部だけ B テーブルの値で UPDATE してくれ」。本番オペレーション中、朝礼まであと 1 時間。

こういう瞬間、ありますよね??

sqlserver update select の正解構文を、引き出しにいくつ持ってるか。それで復旧時間がガッツリ変わる。ほんとに変わるんですよね。

俺もこれ、物流系の基幹システム保守時代に何度もやりました。SELECT で件数確認 → UPDATE で一括修正。これを 1 ステートメントで安全に流す書き方が 3 つある。JOIN 型・CTE 型・MERGE 型。同じ「SQL Server で UPDATE と SELECT を組み合わせる」でも、5 観点で選び分けないと本番で踏みます。

ちなみに sqlserver update select で検索してくる人の 8 割は、結合元に重複キーがある時の non-deterministic 挙動を知らないまま JOIN UPDATE を書いてる。これ、ご存知でしたか?? 後段で実機再現します。

目次

忙しい人向けに最初にまとめ

  • JOIN 型: 結合元の key が一意なら最短記述。重複ありで踏む — UPDATE t1 SET col = t2.col FROM t1 INNER JOIN t2 ON ...
  • CTE 型: 集約してから反映する時の本命。可読性も高い — WITH src AS (SELECT key, MAX(val) val FROM t2 GROUP BY key) UPDATE t1 SET col = src.val FROM t1 JOIN src ON ...
  • MERGE 型: Upsert (INSERT or UPDATE) の決定打。WHEN NOT MATCHED BY SOURCE を書いた瞬間に全行 DELETE 事故の入り口
  • 対処目安 5 分: 結合元の重複チェック → 該当パターン選択 → BEGIN TRAN で先に流す → @@ROWCOUNT 確認 → COMMIT
  • ⚠️ 業務側に電話で頭下げる前に: この記事を 1 回通読しておくと「修正したつもりが別の行を壊した」のリカバリ時間を 1 時間単位で削れます (物流系基幹の本番修正 5 万件規模で実測ベース)

動作確認メモ: ここで紹介する T-SQL は SQL Server 2022 (Docker / Azure SQL Edge 互換) で実機検証済。SQL Server 2016 系 (互換性レベル 130 未満) でも基本構文は同じですが、MERGE の挙動だけはバージョン依存があるので最終的に本番環境で再現確認してください。


3 パターンを1行ずつ — 障害対応の現場で5分で選ぶ

まず俯瞰します。細かい話は後で詰めるとして、現場で「どれを使えばいいか」を 5 分で決められる粒度。

  • JOIN 型 UPDATE: T-SQL 拡張構文。結合元 1:1 で単純コピーする時の最短記述。業務系では一番よく見ます
  • CTE 型 UPDATE: ANSI 準拠寄り。集約 → 反映を 2 段階で分離できるので、1:N 結合や複雑な集計後の UPDATE で本領発揮
  • MERGE 型 UPDATE: 1 文で INSERT/UPDATE/DELETE を分岐できる強力構文。WHEN NOT MATCHED BY SOURCE 句が罠

3 つとも 1 ステートメントで完結するので原子性 (atomicity) は保てます。違いは「結合元の形」「同時に INSERT も必要か」「重複時の挙動を許容できるか」の 3 軸。次の比較表で 5 観点まで広げます。

判断軸 — 5観点 × 3パターンの比較表

「俺の案件、結局どれだ?」を朝礼で説明する弾薬として使ってください。

UPDATE FROM SELECT の3パターン判断軸比較表 (8観点・本命用途と避けるべき状況を最下部に強調)

行順は通奏低音でグルーピングしてます。上 2 行 = 結合形態、3 行目 = INSERT 同時要否、4-5 行目 = 開発/運用ジョブ、6 行目 = 重複耐性。最下 2 行が「本命」と「避けるべき状況」の判断アンカー。ここだけ覚えれば 5 分で選べます。

📚 関連: JOIN UPDATE で実行計画を見て本当にスキャンしてないか確認したい人は SQL Server 実行計画の読み方 — Estimated vs Actual で業務SE が最初に見る5箇所 で 5 箇所の見方を書いてます。

障害対応中の判断フローチャート

比較表だけだと、本番中に頭が回らない瞬間がある。もう一段、分岐に落として 3 ステップで決められるようにします。

UPDATE 3パターン選択フローチャート: 結合元重複・INSERT 同時要否・並列実行リスクの3分岐で JOIN/CTE/MERGE を選ぶ

ポイントは「結合元の重複チェック」を一番上に置いたこと。ここを先に潰さないと、どのパターン選んでも non-deterministic な値が書き込まれるリスクが残ります。

パターン① JOIN 型 UPDATE — 単純 1:1 コピーの最短記述

業務系で一番よく見るやつ。T-SQL の拡張構文で SQL Server 独自 (PostgreSQL も似た構文を持つけど ANSI ではない)。

-- products テーブルの category_name を categories マスタの最新名で更新
UPDATE p
SET p.category_name = c.name
FROM products p
INNER JOIN categories c ON p.category_id = c.id
WHERE p.category_id IS NOT NULL;

こんな感じで、結合元 (categories) の key (id) が一意なら何も悩まず使えます。SELECT で先に件数確認 → 同じ JOIN 構造のまま UPDATE に書き換えるだけ。朝礼まで 10 分の時にマジで重宝するパターン。

で、1 つだけ譲れないプレチェックがあります。

-- 必須プレチェック: 結合元 (categories) に id 重複がないか確認
SELECT id, COUNT(*) AS cnt
FROM categories
GROUP BY id
HAVING COUNT(*) > 1;

1 件でも返ってきたら JOIN 型 UPDATE は 使ってはいけません。後段の罠①で何が起きるか実機で再現します。

パターン② CTE 型 UPDATE — 集約反映と可読性の本命

CTE (Common Table Expression / 共通表式) で「結合元を SELECT で組み立てる」→ 「組み立てた仮想表に対して UPDATE」の 2 段構造に分離するパターン。集約結合や複雑な条件を挟む時の本命です。

-- 各 category_id に複数 categories 行がある場合、name の最新 (MAX(updated_at)) を採用
WITH src AS (
    SELECT
        c.id AS category_id,
        c.name,
        ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY c.updated_at DESC) AS rn
    FROM categories c
)
UPDATE p
SET p.category_name = src.name
FROM products p
INNER JOIN src ON p.category_id = src.category_id
WHERE src.rn = 1;

CTE 側で「どの行を採用するか」を ROW_NUMBER()MAX() で明示するので、結合元に重複があっても結果が決定的になります。JOIN 型で踏む non-deterministic を構文レベルで吸収できる。これが CTE 型の最大の価値。いい感じに「集約してから反映」が分離できるんですよね。

可読性も上がります。「SELECT で何を取るか」と「どこに反映するか」が分離されてるので、後輩レビューの時も説明しやすい。業務 SE の現場で長期保守を意識するなら、1:N 結合は CTE 型を第一選択にしていいです。

パターン③ MERGE 型 UPDATE — Upsert の決定打 (地雷つき)

MERGE は INSERT/UPDATE/DELETE を 1 文で分岐できる強力構文。Upsert (既存なら UPDATE、なければ INSERT) を 1 ステートメントでやりたい時の決定打です。

-- products に対して、categories マスタの全 id を Upsert
-- (既存 category_id があれば name を更新、なければ INSERT)
MERGE INTO products AS tgt
USING (
    SELECT id, name FROM categories WHERE active = 1
) AS src
ON tgt.category_id = src.id
WHEN MATCHED THEN
    UPDATE SET tgt.category_name = src.name
WHEN NOT MATCHED BY TARGET THEN
    INSERT (category_id, category_name) VALUES (src.id, src.name);
-- ★ WHEN NOT MATCHED BY SOURCE は書かない (障害対応中の鉄則)

WHEN MATCHEDWHEN NOT MATCHED BY TARGET の 2 句だけで Upsert は完結します。ここまでなら MERGE は安全。こんな感じで雛形を縛っておくと事故りません!!

問題は WHEN NOT MATCHED BY SOURCE 句。これは「source に存在しない target 全行」を対象にする節で、書いた瞬間に「source 側が想定より少なかった時に target 全行が DELETE/UPDATE される」事故の入り口になります。障害対応中の MERGE はこの 2 句固定で運用するのがほぼ唯一の安全策。後段の罠③で実機再現します。

本番で踏む 3 つのハマりポイント

対処目安サマリ: ①の重複チェック忘れは「30 分」、②の CTE ターゲット指定漏れは「15 分 + 業務側説明 30 分」、③の MERGE 全行 DELETE 誤爆は「半日 + 業務側信頼回復 1-2 週間」が現場感です。

3 つあります。全部「俺もしくは同業から踏んだ報告を聞いた」やつで、全部 4 要素 (状況 → 事象 → コスト → 学び) で骨格を揃えます。

罠① JOIN 型 UPDATE で結合元に重複キーがあると、採用される値が実行計画依存で非決定的

状況: categories マスタに id=10 が 2 行あった (運用ミスで重複登録)。業務側からは「products の category_name を categories で最新化して」と無線。

事象: UPDATE p SET p.category_name = c.name FROM products p JOIN categories c ON p.category_id = c.id を流す。SQL Server オプティマイザが「どの categories 行の name を採用するか」を保証しない。実行計画 (NLJ / Hash / Merge) の選択次第で結果が変わる。同じクエリを 3 回流すと 3 回違う値になる、みたいなことが起こります。

ん?普通に同じ結果返ってくるよね?? と思った人ほど踏みます。

コスト: 技術復旧時間は 30 分 (重複を消して UPDATE 流し直すだけ)。ところが「修正したのに値が違う」と業務側から再電話が来て、信頼回復に 2-3 日かかる。「2026 年版 業務 SE が業務側に頭下げる時間」で見ると技術復旧の 10 倍はコストかかってます。

学び: JOIN UPDATE の前は SELECT key, COUNT(*) FROM 結合元 GROUP BY key HAVING COUNT(*) > 1 を先に流すのを儀式化する。1 件でも返ったら CTE 型に切り替える。これだけです。

📖 『失敗から学ぶRDBの歩き方』(曽根 壮大, p.46-47) でも「JOIN を知らずに不用意に多段 JOIN するアンチパターン」が挙げられていて、JOIN の特性を理解しないまま書くと INDEX も効かなくなると指摘されてます。俺の現場では UPDATE の文脈で同じことが起きてました。結合元の一意性チェックを「UPDATE 前の儀式」として運用ルール化したら、この罠は踏まなくなりました。

罠② CTE 型 UPDATE のターゲット指定漏れで意図しない行を UPDATE

状況: CTE で集約結果を作って UPDATE する時に、JOIN cteON 句条件を書き間違える (WHERE 句に書くべき絞り込みを ON 句に入れる/逆も同様)。

事象: 結合条件が緩いと、「該当しない products 行までマッチして UPDATE が走る」。SELECT で件数確認した時の N 件と、UPDATE 後の @@ROWCOUNT が合わない。気付くと別の行を壊してます。

コスト: 技術復旧は 15 分 (バックアップから戻す or 別 UPDATE で逆修正)。業務側説明に 30 分。とはいえ「件数が合わなかった」という事実が朝礼で共有されるので、次回以降の修正依頼の通り方が変わる (= 都度二重チェック要求)。これが地味に効いてくる。

学び: CTE 型 UPDATE は「SELECT で 1 回流す → 件数一致確認 → 同じ JOIN 構造を UPDATE に変える」を 3 ステップで分割。SELECT と UPDATE で JOIN 構造を一切変えない。これも運用ルール化が早いです。

罠③ MERGE の WHEN NOT MATCHED BY SOURCE で全行 DELETE 誤爆

状況: 「差分更新したい」と思って MERGE 文の雛形をコピペで持ってきた。雛形に WHEN NOT MATCHED BY SOURCE THEN DELETE が入ってる。source 側の SELECT が想定より少ない件数しか返さなかった (WHERE 条件で誤って絞り込みすぎた)。

事象: target 全行のうち「source に存在しない行」が DELETE される。source が空に近ければ target 側がほぼ全行消える。ROW COUNT で何千行 DELETE と出てから初めて気付く。これは KB 3007007 系の SQL Server バグとは別で、純粋に構文の挙動です。

コスト: 技術復旧は半日 (バックアップリストア + 差分巻き戻し)。業務側信頼回復は 1-2 週間 (場合によっては謝罪訪問)。障害対応のテンプレに「BY SOURCE 禁止」が入ってない現場で出やすい事故。

学び: 障害対応中の MERGE は WHEN MATCHEDWHEN NOT MATCHED BY TARGET の 2 句だけ。BY SOURCE が必要なら別 DELETE / UPDATE 文で明示する。雛形管理してる現場は今すぐ MERGE テンプレから BY SOURCE 句を物理削除してください。

並列実行時のロストアップデート — 3 パターン共通の落とし穴

3 パターンどれを選んでも、並列で同じ行を更新しに行く 2 セッションがあると「あとに COMMIT した側で前の更新が消える」ロストアップデートが起こります。障害対応中に「同じ修正 SQL を 2 人の運用担当が同時に叩いた」ケースが典型。

ロストアップデート発生シーケンス: 並列 UPDATE で後発トランザクションが先行更新を上書き

📖 『失敗から学ぶRDBの歩き方』(曽根 壮大, p.201) では「ロストアップデートは、複数のトランザクションで更新が並列に行われた場合、あとに実行されたトランザクションで結果が上書きされる現象」と定義されています。業務 SE 視点で言うと、これが起きやすいのは障害対応中の「全員で SELECT 確認 → 各自 UPDATE」みたいな並列叩きの瞬間。

対策は 2 つ。こんな感じで書きます。

-- (a) 範囲ロックを早めに取る (UPDATE 1 文で完結する場合)
UPDATE T1 WITH (UPDLOCK, HOLDLOCK)
SET status = 'fixed'
FROM T1 INNER JOIN ... ON ...
WHERE ...;

-- (b) 名前付き排他ロックで完全直列化 (修正バッチが複雑な場合)
BEGIN TRAN;
EXEC sp_getapplock @Resource = 'fix_t1_status_batch', @LockMode = 'Exclusive', @LockTimeout = 60000;
-- ここで複数 UPDATE / SELECT 検証を流す
COMMIT;
-- sp_getapplock は COMMIT 時に自動解放

WITH (UPDLOCK, HOLDLOCK) は SQL Server 標準のヒント。sp_getapplock は「修正バッチ名」をキーに排他ロックを取るので、別セッションは同じ名前を取りに来たら待たされます。障害対応中に「並列で叩かれたら困る」修正は sp_getapplock 一択です。

私の現場メモ — 業務側に電話する瞬間が一番きつい

俺は流通系の基幹システム保守時代に、罠③ の MERGE BY SOURCE 誤爆を後輩の現場で目撃したことがあります。

5 万件規模の products テーブル。source の SELECT が WHERE 条件ミスで 200 件しか返さず、BY SOURCE 句の DELETE が 4.8 万件走った。

技術復旧自体は半日で済んだんですよね。

問題はそこから。

業務側に電話で頭下げる瞬間が、いちばんきつかったらしい。「いつまでに正常化しますか」と問われて、「バックアップから戻すので 6 時間」と答えるあの空気。電話の向こうで先方の鉛筆が止まる音まで聞こえてくる感じ。経験ある人は分かると思います。

ただ、ここで救われたんですよね。後輩はその日のうちに MERGE テンプレを直して、BY SOURCE 句を物理削除した雛形をチームに共有しました。「もうこのチームでは同じ事故出さないようにします」と業務側に宣言した瞬間、業務側のリーダーが「次から事前ヒアリング入れますね」と返してくれた。

信頼回復の入り口は技術の速さじゃなくて、「同じ過ちを繰り返さない仕組みに直したか」だった。これは何度か見てきました。

開発機ではこの罠、ほぼ再現しません。source の件数が想定通り返ってくるので。本番だけ踏む。

そういう温度差を「俺は本番経験少ないから…」と自分を責める方向に向けないでほしいんですよね。本番経験の差じゃない。「テンプレ管理してない現場の構造問題」として直すと、次回からチーム全体で踏まなくなります。これは保守要員だからこそ気付ける視点でもある。

まとめ — 3 パターン使い分けの一行

  • JOIN 型 = 結合元 key が一意で 1:1 コピーするだけの時 (最短)
  • CTE 型 = 集約してから反映 / 1:N 結合 / 可読性重視 (本命)
  • MERGE 型 = Upsert (INSERT or UPDATE) が必要な時だけ。BY SOURCE は禁止

これだけ覚えれば本番で踏みません。朝礼前 5 分でいいので、結合元の重複チェックだけは流してください。ここで踏むと業務側信頼回復に 1-2 週間かかります。お気を付けください!!


ここまでで sqlserver update select の 3 パターン構造論は押さえました。もう一段だけ視野を広げて話したいことがあります。

本番障害対応で頭下げる側に回り続けてると、業務 SE は「俺なんてただの保守要員だから…」と自分の価値を下げる方向に思考が流れがちなんですよね。

でも、5 万件規模の UPDATE を 1 文で安全に流せて、並列実行の落とし穴まで意識できる業務 SE って、市場で見るとそんなに多くない。むしろ希少な部類です。朝礼までに復旧させた経験、業務側に頭下げて信頼回復させた経験、それ自体が他のレイヤーのエンジニアには持てない強みになる。そこを言語化する話は別記事で書いてるので、必要な人は読んでみてください (関連記事 5 本目に置いてます)。

よくある質問

Q1. なぜ MERGE じゃなくて JOIN UPDATE を業務系では多用するんですか?

A. シンプル 1:1 更新が圧倒的に多いから。業務系で MERGE が必要になるのは「マスタ全件同期」とか「夜間バッチで取り込み + 反映」みたいなパターンで、オンラインの障害対応 UPDATE では JOIN 型のほうが構文軽くて速い。構文学習コストも JOIN 型が 1/3 程度。

Q2. CTE 型と JOIN 型は速度差ありますか?

A. ほぼないです。SQL Server オプティマイザは CTE を SELECT として展開してから実行計画を作るので、同じ結合構造なら同じ計画が出ます。CTE 内で ROW_NUMBER() 等のウィンドウ関数を使う場合は、そのソートコストが追加で乗るので、1:1 結合で集約不要なら JOIN 型のほうが微妙に軽い。5 万件規模ではほぼ体感差ゼロ。

Q3. WITH (UPDLOCK, HOLDLOCK) と sp_getapplock はどっちを使うべきですか?

A. UPDATE 1 文で完結するなら WITH (UPDLOCK, HOLDLOCK)。「複数 UPDATE + 中間 SELECT 検証」みたいな複合バッチなら sp_getapplock。後者は名前付きで取るので、別バッチ名なら並列実行可能 (= 修正バッチ A と B が独立してれば衝突しない)。業務系の修正バッチを命名規則で運用してるなら sp_getapplock 一択です。

Q4. PostgreSQL でも同じ書き方ができますか? (反論型)

A. JOIN 型は PostgreSQL でも UPDATE t1 SET col = t2.col FROM t2 WHERE t1.id = t2.id の形で動きます (INNER JOIN キーワードは付けない)。CTE 型もほぼ同じ。MERGE は PostgreSQL 15+ から対応で、SQL Server 構文とは細部が違う (OUTPUT 句がない等)。移植性必要なら CTE 型を第一選択にしてください。「全部 SQL Server で動くから他 DBMS でも動くだろう」で書くと、移植段階で構文エラー祭りになります (NULLS LAST / LIMIT も同じ罠)。

Q5. 本番で 1 回も MERGE 使ったことないけど、業務SE としてマズいですか? (反論型)

A. 全然マズくないです。むしろ MERGE は構文重め + 過去バグ歴あり (KB 3007007 系)・WHEN NOT MATCHED BY SOURCE の事故率高 で、「業務系の障害対応では JOIN + CTE で十分回る」派の方が現場では多数派。Upsert が業務要件として出てきた時に初めて MERGE 検討すれば OK。引き出しの 1 つとして「BY SOURCE 句は禁止」だけ覚えておけば、必要な時に安全に使えます。

次に読むべき記事

以上!

この記事の参考文献

失敗から学ぶ RDB の歩き方

  • 著者: 曽根 壮大
  • 出版社: 技術評論社 (2019)
  • 引用ページ: p.46-47 (JOIN を知らずに多段 JOIN するアンチパターン)、p.201 (ロストアップデートの定義)
  • 本の特徴: RDB の設計・運用・性能チューニングで「業務 SE が現場で踏みがちな失敗パターン」を実例ベースで網羅。BTree インデックス / 統計情報 / 実行計画乖離など、SQL Server 案件でも直接効く知識多数。
  • おすすめ対象: 業務 SE / DBA / DB 設計担当・SQL を業務で書くエンジニア全般

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次