SQL バッチ処理 — 論理削除・ページネーションの応用

応用バッチ処理論理削除ページネーション差分更新・監視WebApp APIPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

論理削除 — deleted_at で「削除済み」を管理し有効データだけ取得する

UPDATEIS NULL論理削除soft delete
前提知識

論理削除(soft delete)は、行を物理的に DELETE せず deleted_at タイムスタンプ列に値をセットして「削除済みとみなす」パターンです。APIやバッチで「削除前の状態を復元できる」「削除履歴を保持できる」要件がある場合に使います。

-- 論理削除: deleted_at に現在時刻をセット
UPDATE users
SET    deleted_at = TIMESTAMP '2024-06-01 06:30'
WHERE  user_id = 42;

-- 有効なレコードだけ取得: deleted_atがNULLのもの
SELECT * FROM users
WHERE  deleted_at IS NULL;
COALESCE(値, デフォルト):最初に NULL でない値を返す関数です。COALESCE(deleted_at, '9999-12-31') なら deleted_at が NULL のとき遠未来の日付として扱えます。論理削除フラグの代わりに日付ソートで使います。
問題

以下の要件を1クエリずつ実装してください。

users テーブルで last_login_at が90日以上前のユーザーを論理削除する(deleted_at = TIMESTAMP '2024-06-01 06:30')。

② 論理削除されていない有効ユーザーだけを取得し、last_login_at が新しい順に並べる。COALESCEでNULLを末尾にする。

使用テーブル
▸ users(基準日: 2024-06-01)
user_idnamelast_login_atdeleted_at(timestamp)
U01Alice2024-05-20NULL
U02Bob2024-01-10NULL
U03CarolNULLNULL
U04Dave2024-02-28NULL
U05Eve2024-05-012024-04-01
期待出力

期待する出力①:

user_idnamelast_login_atdeleted_at
U01Alice2024-05-20NULL
U02Bob2024-01-102024-06-01 06:30:00
U03CarolNULLNULL
U04Dave2024-02-282024-06-01 06:30:00
U05Eve2024-05-012024-04-01 00:00:00

期待する出力②:

user_idnamelast_login_at
U01Alice2024-05-20
U03CarolNULL
QUESTION 7

LAG で差分検知 — 前回バッチ実行との値比較で変化のある行だけ抽出する

LAG差分検知差分バッチウィンドウ関数
前提知識

LAG(列, N) は「同一パーティション内で N行前の値」を返すウィンドウ関数です。「前回のバッチ実行時の値」と「今回の値」を比較して変化のある行だけ抽出する差分バッチに最適です。

LAG(column, 1) OVER (
  PARTITION BY group_key        -- グループ単位で前後を比較
  ORDER BY     time_col         -- 時系列で並べた「1つ前の行」の値を返す
)                                -- 最初の行はNULL(前の行がないため)

差分バッチのパターン:全件取得ではなく「変化した行だけ処理」することで外部API呼び出し回数を削減し、コストとレート制限の問題を回避します。

LEAD との違い:LAG は前の行、LEAD は次の行の値を返します。差分検知には前回値と現在値を比べるため LAG を使います。
問題

stock_snapshots テーブルには商品の在庫スナップショットが日次バッチで記録されています。前日と比較して quantity が変化した商品だけを抽出し、変化量(diff)も出力してください。

使用テーブル
▸ stock_snapshots
snapshot_idproduct_idsnapshot_datequantity
1P012024-06-01100
2P012024-06-0285
3P012024-06-0385
4P022024-06-01200
5P022024-06-02200
6P022024-06-03215
期待出力
product_idsnapshot_datequantityprev_quantitydiff
P012024-06-0285100-15
P022024-06-03215200+15
QUESTION 8

カーソルページネーション — LIMIT/OFFSET の問題を解決する次ページ取得パターン

WHERELIMITページネーションAPIレスポンス
前提知識

APIで大量データを返す際、LIMIT/OFFSET によるページネーションは「OFFSETが大きくなるほどスキャン行数が増え遅くなる」問題があります。カーソルベースページネーションは、最後に取得したレコードのID(カーソル)を次のリクエストで渡す手法で、常にインデックスを活用できます。

-- OFFSET方式(問題あり): 100万件目のページはオフセット分全スキャン
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 990000; -- 遅い

-- カーソル方式(推奨): 前回の最後のidより大きい行をLIMITで取るだけ
SELECT * FROM orders
WHERE  id > :last_seen_id           -- カーソル: 前回最後に取得したID
ORDER BY id
LIMIT  10;                          -- 常にインデックスを使って高速
カーソルページネーションが向く場面:「次のページ」を順に取得する無限スクロール・バッチ処理のチャンク分割。OFFSET が向く場面:ページ番号を任意にジャンプできる必要がある場合(管理画面など)。
問題

orders テーブルから 1ページ5件ずつ、カーソル(最後のorder_id)を使って次のページを取得するクエリを書いてください。

条件:status = 'pending'、order_id の昇順、前回の最後の order_id は 1003(パラメータ :last_id)。次ページ用のカーソル(最後の order_id)も出力してください。

使用テーブル
▸ orders
order_idcustomer_idamountstatus
1001C015000pending
1002C023000completed
1003C037000pending
1004C042500pending
1005C058000pending
1006C064500pending
1007C016000cancelled
1008C029000pending
期待出力
order_idcustomer_idamountstatusnext_cursor
1004C042500pending1008
1005C058000pending1008
1006C064500pending1008
1008C029000pending1008
QUESTION 9

差分更新バッチ — updated_at で前回実行以降の変更行だけを効率処理する

WHERE差分更新増分バッチupdated_at
前提知識

毎回全件取得して処理するのではなく、updated_at >= 前回バッチ実行時刻で絞ることで変更されたレコードだけを処理する「増分(差分)バッチ」パターンです。APIのデータ同期・集計テーブルの更新・通知処理など実務で最頻出の設計です。

-- 前回バッチ実行時刻を :last_run_at として渡す
SELECT *
FROM   orders
WHERE  updated_at >= :last_run_at   -- 前回実行以降に変更されたものだけ
  AND  updated_at <  TIMESTAMP '2024-06-01 06:30';        -- 現在時刻未満(実行中の変更を含めない)

updated_at にインデックスを張ることで WHERE 絞り込みが高速になります。テーブル設計段階から updated_at を必ず持たせる習慣が実務では重要です。

バッチ実行時刻の管理:前回実行時刻(last_run_at)は batch_jobs などの管理テーブルに保存します。バッチ完了後に「今回の開始時刻」を上書きすることで、次回実行時の基準になります。
問題

orders テーブルと batch_checkpoints テーブルを使って、以下を実装してください。

batch_checkpoints から前回バッチの実行時刻を取得する。

② 前回実行以降に updated_at が更新された orders を抽出し、customer_id ごとの 注文件数・合計金額・最終更新時刻を集計する。

③ 今回のバッチ開始時刻(NOW())で batch_checkpoints を更新する。

使用テーブル
▸ orders
order_idcustomer_idamountstatusupdated_at
1C015000completed2024-06-01 01:00
2C023000pending2024-06-01 02:00
3C017000completed2024-06-01 03:00
4C032500cancelled2024-05-31 12:00
5C028000completed2024-06-01 04:00
▸ batch_checkpoints
job_namelast_run_at
order_sync2024-06-01 00:00:00
期待出力
customer_idorder_counttotal_amountlast_updated_at
C012120002024-06-01 03:00
C022110002024-06-01 04:00
QUESTION 10

バッチジョブ監視 — 実行時間・ストール・連続失敗をSQLで一元監視する

EXTRACTCOUNTジョブ監視バッチ運用
前提知識

バッチジョブの実行履歴テーブルに対して、実行時間・ストール(停止)・連続失敗を1クエリで監視するパターンです。APIのヘルスチェックエンドポイントや監視ダッシュボードで使われます。

-- 実行時間の計算: タイムスタンプ差を秒数に変換
EXTRACT(EPOCH FROM (ended_at - started_at)) AS duration_sec
-- EXTRACT(EPOCH FROM interval): インターバルを秒数(実数)に変換する

-- 連続失敗カウント: 直近N件中の失敗件数
COUNT(*) FILTER (WHERE status = 'failed')  -- 対象行のみカウント
EXTRACT(EPOCH FROM interval):2つのタイムスタンプの差(INTERVAL型)を秒数の実数に変換します。(ended_at - started_at) の結果が INTERVAL 型になるため、これを秒数に変換することで実行時間が計算できます。
問題

job_runs テーブルを使って、各ジョブの以下のサマリを1クエリで出力してください。

  • 直近5回の実行における 成功数・失敗数・平均実行時間(秒)
  • 現在 running 状態で 30分以上経過しているジョブ(ストール検知)
  • 直近5回で3回以上失敗しているジョブ名(アラート対象)
使用テーブル
▸ job_runs
run_idjob_namestatusstarted_atended_at
1order_syncsuccess2024-06-01 02:002024-06-01 02:05
2order_syncfailed2024-06-01 03:002024-06-01 03:02
3email_batchsuccess2024-06-01 02:002024-06-01 02:30
4order_syncfailed2024-06-01 04:002024-06-01 04:01
5email_batchfailed2024-06-01 03:002024-06-01 03:05
6order_syncrunning2024-06-01 05:00NULL
7email_batchfailed2024-06-01 04:002024-06-01 04:08
8order_syncfailed2024-06-01 06:002024-06-01 06:03
9email_batchfailed2024-06-01 05:002024-06-01 05:06
期待出力
job_namesuccess_countfail_countavg_secis_stalledalert
email_batch13735.0false! ALERT
order_sync13165.0true! ALERT