論理削除 — deleted_at で「削除済み」を管理し有効データだけ取得する
論理削除(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(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を末尾にする。
| user_id | name | last_login_at | deleted_at(timestamp) |
|---|---|---|---|
| U01 | Alice | 2024-05-20 | NULL |
| U02 | Bob | 2024-01-10 | NULL |
| U03 | Carol | NULL | NULL |
| U04 | Dave | 2024-02-28 | NULL |
| U05 | Eve | 2024-05-01 | 2024-04-01 |
期待する出力①:
| user_id | name | last_login_at | deleted_at |
|---|---|---|---|
| U01 | Alice | 2024-05-20 | NULL |
| U02 | Bob | 2024-01-10 | 2024-06-01 06:30:00 |
| U03 | Carol | NULL | NULL |
| U04 | Dave | 2024-02-28 | 2024-06-01 06:30:00 |
| U05 | Eve | 2024-05-01 | 2024-04-01 00:00:00 |
期待する出力②:
| user_id | name | last_login_at |
|---|---|---|
| U01 | Alice | 2024-05-20 |
| U03 | Carol | NULL |
LAG で差分検知 — 前回バッチ実行との値比較で変化のある行だけ抽出する
LAG(列, N) は「同一パーティション内で N行前の値」を返すウィンドウ関数です。「前回のバッチ実行時の値」と「今回の値」を比較して変化のある行だけ抽出する差分バッチに最適です。
LAG(column, 1) OVER ( PARTITION BY group_key -- グループ単位で前後を比較 ORDER BY time_col -- 時系列で並べた「1つ前の行」の値を返す ) -- 最初の行はNULL(前の行がないため)
差分バッチのパターン:全件取得ではなく「変化した行だけ処理」することで外部API呼び出し回数を削減し、コストとレート制限の問題を回避します。
LAG は前の行、LEAD は次の行の値を返します。差分検知には前回値と現在値を比べるため LAG を使います。stock_snapshots テーブルには商品の在庫スナップショットが日次バッチで記録されています。前日と比較して quantity が変化した商品だけを抽出し、変化量(diff)も出力してください。
| snapshot_id | product_id | snapshot_date | quantity |
|---|---|---|---|
| 1 | P01 | 2024-06-01 | 100 |
| 2 | P01 | 2024-06-02 | 85 |
| 3 | P01 | 2024-06-03 | 85 |
| 4 | P02 | 2024-06-01 | 200 |
| 5 | P02 | 2024-06-02 | 200 |
| 6 | P02 | 2024-06-03 | 215 |
| product_id | snapshot_date | quantity | prev_quantity | diff |
|---|---|---|---|---|
| P01 | 2024-06-02 | 85 | 100 | -15 |
| P02 | 2024-06-03 | 215 | 200 | +15 |
カーソルページネーション — LIMIT/OFFSET の問題を解決する次ページ取得パターン
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; -- 常にインデックスを使って高速
orders テーブルから 1ページ5件ずつ、カーソル(最後のorder_id)を使って次のページを取得するクエリを書いてください。
条件:status = 'pending'、order_id の昇順、前回の最後の order_id は 1003(パラメータ :last_id)。次ページ用のカーソル(最後の order_id)も出力してください。
| order_id | customer_id | amount | status |
|---|---|---|---|
| 1001 | C01 | 5000 | pending |
| 1002 | C02 | 3000 | completed |
| 1003 | C03 | 7000 | pending |
| 1004 | C04 | 2500 | pending |
| 1005 | C05 | 8000 | pending |
| 1006 | C06 | 4500 | pending |
| 1007 | C01 | 6000 | cancelled |
| 1008 | C02 | 9000 | pending |
| order_id | customer_id | amount | status | next_cursor |
|---|---|---|---|---|
| 1004 | C04 | 2500 | pending | 1008 |
| 1005 | C05 | 8000 | pending | 1008 |
| 1006 | C06 | 4500 | pending | 1008 |
| 1008 | C02 | 9000 | pending | 1008 |
差分更新バッチ — 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 を必ず持たせる習慣が実務では重要です。
batch_jobs などの管理テーブルに保存します。バッチ完了後に「今回の開始時刻」を上書きすることで、次回実行時の基準になります。orders テーブルと batch_checkpoints テーブルを使って、以下を実装してください。
① batch_checkpoints から前回バッチの実行時刻を取得する。
② 前回実行以降に updated_at が更新された orders を抽出し、customer_id ごとの 注文件数・合計金額・最終更新時刻を集計する。
③ 今回のバッチ開始時刻(NOW())で batch_checkpoints を更新する。
| order_id | customer_id | amount | status | updated_at |
|---|---|---|---|---|
| 1 | C01 | 5000 | completed | 2024-06-01 01:00 |
| 2 | C02 | 3000 | pending | 2024-06-01 02:00 |
| 3 | C01 | 7000 | completed | 2024-06-01 03:00 |
| 4 | C03 | 2500 | cancelled | 2024-05-31 12:00 |
| 5 | C02 | 8000 | completed | 2024-06-01 04:00 |
| job_name | last_run_at |
|---|---|
| order_sync | 2024-06-01 00:00:00 |
| customer_id | order_count | total_amount | last_updated_at |
|---|---|---|---|
| C01 | 2 | 12000 | 2024-06-01 03:00 |
| C02 | 2 | 11000 | 2024-06-01 04:00 |
バッチジョブ監視 — 実行時間・ストール・連続失敗をSQLで一元監視する
バッチジョブの実行履歴テーブルに対して、実行時間・ストール(停止)・連続失敗を1クエリで監視するパターンです。APIのヘルスチェックエンドポイントや監視ダッシュボードで使われます。
-- 実行時間の計算: タイムスタンプ差を秒数に変換 EXTRACT(EPOCH FROM (ended_at - started_at)) AS duration_sec -- EXTRACT(EPOCH FROM interval): インターバルを秒数(実数)に変換する -- 連続失敗カウント: 直近N件中の失敗件数 COUNT(*) FILTER (WHERE status = 'failed') -- 対象行のみカウント
(ended_at - started_at) の結果が INTERVAL 型になるため、これを秒数に変換することで実行時間が計算できます。job_runs テーブルを使って、各ジョブの以下のサマリを1クエリで出力してください。
- 直近5回の実行における 成功数・失敗数・平均実行時間(秒)
- 現在 running 状態で 30分以上経過しているジョブ(ストール検知)
- 直近5回で3回以上失敗しているジョブ名(アラート対象)
| run_id | job_name | status | started_at | ended_at |
|---|---|---|---|---|
| 1 | order_sync | success | 2024-06-01 02:00 | 2024-06-01 02:05 |
| 2 | order_sync | failed | 2024-06-01 03:00 | 2024-06-01 03:02 |
| 3 | email_batch | success | 2024-06-01 02:00 | 2024-06-01 02:30 |
| 4 | order_sync | failed | 2024-06-01 04:00 | 2024-06-01 04:01 |
| 5 | email_batch | failed | 2024-06-01 03:00 | 2024-06-01 03:05 |
| 6 | order_sync | running | 2024-06-01 05:00 | NULL |
| 7 | email_batch | failed | 2024-06-01 04:00 | 2024-06-01 04:08 |
| 8 | order_sync | failed | 2024-06-01 06:00 | 2024-06-01 06:03 |
| 9 | email_batch | failed | 2024-06-01 05:00 | 2024-06-01 05:06 |
| job_name | success_count | fail_count | avg_sec | is_stalled | alert |
|---|---|---|---|---|---|
| email_batch | 1 | 3 | 735.0 | false | ! ALERT |
| order_sync | 1 | 3 | 165.0 | true | ! ALERT |