UPSERT — INSERT ... ON CONFLICT DO UPDATE で新規・更新を1クエリに統合する
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 を使うと「競合したら何もしない(スキップ)」になります。べき等性(同じデータを何度実行しても結果が変わらない性質)を保証する際に便利です。
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)
| id | external_id (UNIQUE) | name | price | updated_at |
|---|---|---|---|---|
| 1 | EXT-001 | ワイヤレスマウス | 2800 | 2024-01-10 |
| 2 | EXT-002 | メカニカルキーボード | 8900 | 2024-01-10 |
| id | external_id | name | price | updated_at |
|---|---|---|---|---|
| 1 | EXT-001 | ワイヤレスマウス改 | 3200 | 2024-06-01 |
| 2 | EXT-002 | メカニカルキーボード | 8900 | 2024-06-01 |
| 3 | EXT-003 | USBハブ4ポート | 1980 | 2024-06-01 |
UPDATE FROM — 別テーブルを参照して複数行を一括更新する
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 ... の構文を使います。
price_updates(価格改定マスタ)テーブルの内容を使って、products テーブルの price と updated_at を一括更新してください。price_updates に存在する product_id の行だけ更新対象です。
| product_id | name | price | updated_at |
|---|---|---|---|
| 101 | コーヒー豆A | 1200 | 2024-01-01 |
| 102 | コーヒー豆B | 1500 | 2024-01-01 |
| 103 | 紅茶葉C | 900 | 2024-01-01 |
| 104 | 緑茶D | 800 | 2024-01-01 |
| product_id | new_price | applied_at |
|---|---|---|
| 101 | 1380 | 2024-06-01 |
| 103 | 1050 | 2024-06-01 |
| product_id | name | price | updated_at |
|---|---|---|---|
| 101 | コーヒー豆A | 1380 | 2024-06-01 |
| 102 | コーヒー豆B | 1500 | 2024-01-01 |
| 103 | 紅茶葉C | 1050 | 2024-06-01 |
| 104 | 緑茶D | 800 | 2024-01-01 |
INSERT INTO ... SELECT — 条件に合う行を別テーブルへ一括移送・アーカイブする
INSERT INTO ... SELECT は SELECT の結果をそのまま別テーブルに挿入するパターンです。「古いデータをアーカイブテーブルへ移す」「集計結果をサマリテーブルに書き込む」バッチ処理で多用されます。
INSERT INTO archive_table (col1, col2, ...) SELECT col1, col2, ... -- SELECTの列順と型がINSERT先と一致する必要がある FROM source_table WHERE 条件; -- 移送対象を絞る条件
アーカイブ後に元テーブルから削除する場合は DELETE + INSERT INTO...SELECT をトランザクションでセットにします。これで「移送中の消失」を防げます。
orders テーブルから 2024年1月以前(order_date < '2024-02-01')のcompleted注文 を orders_archive テーブルへ一括コピーしてください。コピー後に archived_at 列には現在時刻を設定します。
| order_id | customer_id | amount | status | order_date |
|---|---|---|---|---|
| 1001 | C01 | 12000 | completed | 2024-01-15 |
| 1002 | C02 | 8500 | cancelled | 2024-01-20 |
| 1003 | C03 | 15000 | completed | 2024-01-28 |
| 1004 | C01 | 9200 | completed | 2024-02-05 |
| 1005 | C04 | 6700 | completed | 2024-02-10 |
| order_id | customer_id | amount | status | order_date | archived_at |
|---|---|---|---|---|---|
| (空) | |||||
| order_id | customer_id | amount | status | order_date | archived_at |
|---|---|---|---|---|---|
| 1001 | C01 | 12000 | completed | 2024-01-15 | 2024-06-01 02:00:00 |
| 1003 | C03 | 15000 | completed | 2024-01-28 | 2024-06-01 02:00:00 |
ROW_NUMBER 重複排除 — 同一キーの最新レコードだけをCTEで抽出する
バッチ取込みや二重送信でテーブルに同一キーの重複が生じることがあります。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位(最新)だけ取得
WITH 名前 AS (SELECT ...) で一時的な名前付き結果セットを定義します。本体クエリから 名前 でその結果を参照できます。サブクエリをネストするより可読性が高く、バッチ処理の定番書き方です。user_events テーブルには同じ user_id と event_type の組み合わせが複数回バッチ取込みされた重複データがあります。user_id + event_type ごとに最新の1件だけを残すクエリを書いてください。
| event_id | user_id | event_type | payload | created_at |
|---|---|---|---|---|
| 1 | U01 | login | ip:1.2.3.4 | 2024-06-01 09:00 |
| 2 | U01 | login | ip:5.6.7.8 | 2024-06-01 12:00 |
| 3 | U01 | purchase | item:A | 2024-06-01 10:00 |
| 4 | U02 | login | ip:9.9.9.9 | 2024-06-01 11:00 |
| 5 | U01 | purchase | item:B | 2024-06-01 15:00 |
| event_id | user_id | event_type | payload | created_at | rn |
|---|---|---|---|---|---|
| 2 | U01 | login | ip:5.6.7.8 | 2024-06-01 12:00 | 1 |
| 5 | U01 | purchase | item:B | 2024-06-01 15:00 | 1 |
| 4 | U02 | login | ip:9.9.9.9 | 2024-06-01 11:00 | 1 |
CASE WHEN 一括ステータス更新 — 条件別に異なる値をSETする1クエリ更新
バッチ処理でよくある「複数の条件ごとに異なる値に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 '適切なデフォルト' を書きましょう。夜間バッチで jobs テーブルのステータスを一括更新します。以下のルールで status を変更してください。
- retry_count が 0 かつ status = 'failed' → 'pending'(再キュー)
- retry_count が 3 以上かつ status = 'failed' → 'abandoned'(諦め)
- started_at が NULL かつ status = 'running' → 'stalled'(ゾンビ検知)
- 上記以外は変更しない
| job_id | status | retry_count | started_at |
|---|---|---|---|
| J01 | failed | 0 | 2024-06-01 |
| J02 | failed | 3 | 2024-06-01 |
| J03 | running | 0 | NULL |
| J04 | completed | 0 | 2024-06-01 |
| J05 | failed | 1 | 2024-06-01 |
| job_id | status(更新後) | 変化 |
|---|---|---|
| J01 | pending | failed(0) → pending |
| J02 | abandoned | failed(3) → abandoned |
| J03 | stalled | running(NULL) → stalled |
| J04 | completed | 変化なし |
| J05 | failed | 変化なし(retry_count=1) |