SQL バッチ処理 — UPSERT・一括更新・重複排除の応用

応用バッチ処理UPSERT・一括更新アーカイブ重複排除WebApp APIPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

UPSERT — INSERT ... ON CONFLICT DO UPDATE で新規・更新を1クエリに統合する

INSERTON CONFLICTUPSERTAPIバッチ同期
前提知識

UPSERT(Upsert = Update + Insert)は「レコードが存在すれば UPDATE、なければ INSERT」を1クエリで行うパターンです。APIバッチで外部サービスからデータを同期する処理の最頻出パターンです。

INSERT INTO target_table (col1, col2, ...)
VALUES (...)
ON CONFLICT (unique_col)          -- 競合判定に使うユニークキー列を指定
DO UPDATE SET
  col2 = EXCLUDED.col2;          -- EXCLUDED = INSERTしようとした値(競合した新データ)

DO NOTHING を使うと「競合したら何もしない(スキップ)」になります。べき等性(同じデータを何度実行しても結果が変わらない性質)を保証する際に便利です。

EXCLUDED とは?
ON CONFLICT DO UPDATE 内で使える特別なテーブル参照です。「INSERTしようとしたが競合した行の値」を指します。EXCLUDED.col で新しい値を、target_table.col で既存の値を参照できます。
問題

外部ECサービスから商品マスタを毎日バッチ同期します。products テーブルに 新商品はINSERT、既存商品は name/price/updated_at を UPDATE してください。

同期データ(VALUES に直書きする):(id: 1, 外部ID: EXT-001, name: ワイヤレスマウス改, price: 3200), (2, EXT-002, メカニカルキーボード, 8900), (3, EXT-003, USBハブ4ポート, 1980)

使用テーブル
▸ products(同期前)
idexternal_id (UNIQUE)namepriceupdated_at
1EXT-001ワイヤレスマウス28002024-01-10
2EXT-002メカニカルキーボード89002024-01-10
期待出力
idexternal_idnamepriceupdated_at
1EXT-001ワイヤレスマウス改32002024-06-01
2EXT-002メカニカルキーボード89002024-06-01
3EXT-003USBハブ4ポート19802024-06-01
QUESTION 2

UPDATE FROM — 別テーブルを参照して複数行を一括更新する

UPDATEFROMJOIN UPDATEバッチ価格改定
前提知識

UPDATE ... FROM は、別テーブルの値を参照しながら対象テーブルを一括更新するパターンです。「価格改定マスタに基づいて商品テーブルを更新する」「承認済みリストと突合してステータスを変更する」など、バッチ処理の核心です。

UPDATE target                        -- 更新対象テーブル
SET    col = src.col                -- 参照元テーブルの値をセット
FROM   source src                   -- 参照元テーブル(JOIN的に結合)
WHERE  target.key = src.key;       -- 結合条件(これがないと全件更新になる!)

PostgreSQL では UPDATE ... FROM、MySQL では UPDATE target JOIN source ON ... の構文を使います。

WHERE 結合条件を忘れると全行更新:FROM を書いただけで WHERE の結合条件を省くと、対象テーブルの全行が更新されます。必ず結合キーのWHERE条件を付けること。
問題

price_updates(価格改定マスタ)テーブルの内容を使って、products テーブルの price と updated_at を一括更新してください。price_updates に存在する product_id の行だけ更新対象です。

使用テーブル
▸ products(更新前)
product_idnamepriceupdated_at
101コーヒー豆A12002024-01-01
102コーヒー豆B15002024-01-01
103紅茶葉C9002024-01-01
104緑茶D8002024-01-01
▸ price_updates(価格改定マスタ)
product_idnew_priceapplied_at
10113802024-06-01
10310502024-06-01
期待出力
product_idnamepriceupdated_at
101コーヒー豆A13802024-06-01
102コーヒー豆B15002024-01-01
103紅茶葉C10502024-06-01
104緑茶D8002024-01-01
QUESTION 3

INSERT INTO ... SELECT — 条件に合う行を別テーブルへ一括移送・アーカイブする

INSERT INTOSELECTアーカイブバッチ移送
前提知識

INSERT INTO ... SELECT は SELECT の結果をそのまま別テーブルに挿入するパターンです。「古いデータをアーカイブテーブルへ移す」「集計結果をサマリテーブルに書き込む」バッチ処理で多用されます。

INSERT INTO archive_table (col1, col2, ...)
SELECT       col1, col2, ...          -- SELECTの列順と型がINSERT先と一致する必要がある
FROM         source_table
WHERE        条件;                    -- 移送対象を絞る条件

アーカイブ後に元テーブルから削除する場合は DELETE + INSERT INTO...SELECT をトランザクションでセットにします。これで「移送中の消失」を防げます。

VALUES は不要:INSERT INTO ... SELECT では VALUES 句を書きません。SELECT の結果がそのまま挿入される行データになります。
問題

orders テーブルから 2024年1月以前(order_date < '2024-02-01')のcompleted注文orders_archive テーブルへ一括コピーしてください。コピー後に archived_at 列には現在時刻を設定します。

使用テーブル
▸ orders
order_idcustomer_idamountstatusorder_date
1001C0112000completed2024-01-15
1002C028500cancelled2024-01-20
1003C0315000completed2024-01-28
1004C019200completed2024-02-05
1005C046700completed2024-02-10
▸ orders_archive(挿入先・初期空)
order_idcustomer_idamountstatusorder_datearchived_at
(空)
期待出力
order_idcustomer_idamountstatusorder_datearchived_at
1001C0112000completed2024-01-152024-06-01 02:00:00
1003C0315000completed2024-01-282024-06-01 02:00:00
QUESTION 4

ROW_NUMBER 重複排除 — 同一キーの最新レコードだけをCTEで抽出する

ROW_NUMBERCTE重複排除データクレンジング
前提知識

バッチ取込みや二重送信でテーブルに同一キーの重複が生じることがあります。ROW_NUMBER() を CTE と組み合わせることで「同一キーの中で最新の1件だけを残す」クレンジング処理が書けます。

WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY dup_key           -- 重複を判定するキー列
      ORDER BY     created_at DESC  -- 最新順に番号を振る(1が最新)
    ) AS rn
  FROM target_table
)
SELECT * FROM ranked WHERE rn = 1;  -- 各グループの1位(最新)だけ取得
CTEとは?(Common Table Expression)
WITH 名前 AS (SELECT ...) で一時的な名前付き結果セットを定義します。本体クエリから 名前 でその結果を参照できます。サブクエリをネストするより可読性が高く、バッチ処理の定番書き方です。
問題

user_events テーブルには同じ user_id と event_type の組み合わせが複数回バッチ取込みされた重複データがあります。user_id + event_type ごとに最新の1件だけを残すクエリを書いてください。

使用テーブル
▸ user_events(重複あり)
event_iduser_idevent_typepayloadcreated_at
1U01loginip:1.2.3.42024-06-01 09:00
2U01loginip:5.6.7.82024-06-01 12:00
3U01purchaseitem:A2024-06-01 10:00
4U02loginip:9.9.9.92024-06-01 11:00
5U01purchaseitem:B2024-06-01 15:00
期待出力
event_iduser_idevent_typepayloadcreated_atrn
2U01loginip:5.6.7.82024-06-01 12:001
5U01purchaseitem:B2024-06-01 15:001
4U02loginip:9.9.9.92024-06-01 11:001
QUESTION 5

CASE WHEN 一括ステータス更新 — 条件別に異なる値をSETする1クエリ更新

UPDATECASE WHENステータス遷移バッチ状態管理
前提知識

バッチ処理でよくある「複数の条件ごとに異なる値にUPDATEしたい」場面では、SET 句に CASE WHEN を使うことで1クエリにまとめられます。

UPDATE orders
SET status =
  CASE
    WHEN 条件A THEN 'value_a'   -- 条件Aに合う行はvalue_aに更新
    WHEN 条件B THEN 'value_b'   -- 条件Bに合う行はvalue_bに更新
    ELSE              status       -- どの条件にも合わない行は現状維持
  END
WHERE 絞り込み条件;               -- UPDATE対象を限定(重要)
ELSE の重要性:ELSE を省略すると条件に合わない行の値が NULL になってしまいます。「変更しない」意図なら必ず ELSE 列名(現状維持)か ELSE '適切なデフォルト' を書きましょう。
問題

夜間バッチで jobs テーブルのステータスを一括更新します。以下のルールで status を変更してください。

  • retry_count が 0 かつ status = 'failed' → 'pending'(再キュー)
  • retry_count が 3 以上かつ status = 'failed' → 'abandoned'(諦め)
  • started_at が NULL かつ status = 'running' → 'stalled'(ゾンビ検知)
  • 上記以外は変更しない
使用テーブル
▸ jobs(更新前)
job_idstatusretry_countstarted_at
J01failed02024-06-01
J02failed32024-06-01
J03running0NULL
J04completed02024-06-01
J05failed12024-06-01
期待出力
job_idstatus(更新後)変化
J01pendingfailed(0) → pending
J02abandonedfailed(3) → abandoned
J03stalledrunning(NULL) → stalled
J04completed変化なし
J05failed変化なし(retry_count=1)