SQL バッチ処理 — UPDATE・論理削除・EXISTSの基礎

基礎SQL基礎文法SELECT / WHERE / JOIN集計・更新CTE・サブクエリAPI・バッチPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

UPDATE + WHERE — バッチでステータスを一括更新する安全パターン

UPDATEWHERESETステータス更新
前提知識

UPDATE は既存の行の列値を書き換えます。WHERE で必ず更新対象を絞り込むことが最重要です。WHERE を省略すると全行が書き換わります。

UPDATE table_name                   -- 更新対象テーブル
SET
  col1 = 'new_value',               -- 更新する列と値(複数列はカンマ区切り)
  col2 = NOW()                       -- 複数列を同時に更新できる
WHERE  id = 1;                       -- 必須!省略すると全行更新(壊滅的バグ)

バッチ処理でよくあるパターンは「一定期間アクセスのないユーザーを inactive に一括変更」「期限切れトークンを無効化」などです。

WHERE なし UPDATE は最大の地雷:UPDATE users SET status = 'inactive' は全ユーザーが inactive になる。本番環境では取り返しがつかない。UPDATE の前に必ず WHERE の条件を確認すること。
問題

subscriptions テーブルで、end_date が今日以前(期限切れ)かつ status が 'active' の契約を、status='expired' に一括更新してください。更新と同時に updated_at を現在時刻に更新し、更新した行の subscription_id と status を RETURNING で返してください。

使用テーブル
▸ subscriptions(現在日: 2024-06-01)
subscription_iduser_idstatusend_dateupdated_at
1U01active2024-05-202024-04-01
2U02active2024-06-302024-05-01
3U03active2024-05-312024-04-15
4U04expired2024-04-012024-04-02
5U05active2024-07-152024-05-10
期待出力
subscription_idstatus
1expired
3expired
模範解答コード
UPDATE subscriptions
SET
  status     = 'expired',
  updated_at = NOW()
WHERE  -- UPDATE は WHERE 必須(全行更新を防ぐ)
  end_date <= DATE '2024-06-01'  -- 問題文の現在日を固定
  AND status = 'active'       -- 有効な行のみ(無駄な更新を防ぐ)
RETURNING subscription_id, status;  -- 更新された行を返す

/*
  実行順序(SQLの論理的な評価順):
  1. FROM subscriptions  → 行を読み込む
  2. WHERE               → 行を絞り込む
  3. SET status='expired'→ 列の値を更新
  4. RETURNING           → 更新後の値を返す
*/
解説(テーブル変化・ポイント)
UPDATE subscriptions SET status = 'expired', updated_at = NOW() WHERE end_date <= CURRENT_DATE AND status = 'active' RETURNING subscription_id, status;
LEGEND
データ取得・読込対象
① FROM
FROM subscriptionssubscriptions テーブル全5行を読み込みます。現在日は 2024-06-01 です。
1 / 4
subscription_iduser_idstatusend_dateupdated_at
1U01active2024-05-202024-04-01
2U02active2024-06-302024-05-01
3U03active2024-05-312024-04-15
4U04expired2024-04-012024-04-02
5U05active2024-07-152024-05-10
全 5行 読込
学習ポイント
超基礎:UPDATE はすでにテーブルに存在する行のデータを書き換えます。WHEREを指定しないと「テーブルの全行」が書き換わってしまうため、UPDATEとWHEREは必ずセットで考える癖をつけましょう。
バッチ期限切れ処理の定番パターン:深夜バッチで WHERE end_date <= CURRENT_DATE AND status='active' を毎晩実行する設計が最頻出。CRON + このUPDATE + RETURNINGで更新件数をログに記録するのが実務標準。
べき等性(Idempotency)の確保:AND status='active' の条件を付けることで「何度実行しても結果が変わらない」べき等なバッチになる。バッチが2重実行されても問題が起きない設計が重要。
CURRENT_DATE と NOW() の違い:CURRENT_DATE は日付のみ(時刻なし)、NOW() はタイムスタンプ(日付+時刻)。日付比較には CURRENT_DATE、更新日時の記録には NOW() を使い分ける。
アンチパターン
WHERE なし UPDATE:UPDATE subscriptions SET status='expired' は全5件が expired になる。本番では絶対に起こしてはならないミス。UPDATE前には必ず SELECT COUNT(*) FROM subscriptions WHERE ... で対象件数を確認する習慣をつける。
SET の列名を間違える:SET stats = 'expired'(status のタイポ)は列が存在しないためエラーになるが、テーブルに同名の別列があった場合は意図しない列を書き換えてしまう。SQL実行前にレビューを必ずすること。
実務コラム:バッチ処理の「冪等性(べきとうせい)」
夜間バッチはネットワークエラーなどで途中で落ち、再実行されることが日常茶飯事です。そのため、「何度実行しても結果が同じになる(=冪等性がある)」設計が不可欠です。本問の AND status = 'active' のように、すでに処理済みのデータは対象外とする絞り込みを入れることで、2重実行されても安全なSQLになります。UPDATEバッチを書く時は「途中で止まって再実行されたらどうなるか?」を常に想像しましょう。
QUESTION 7

DELETE vs 論理削除 — APIで安全にデータを「消す」設計パターン

DELETEUPDATE論理削除データ保全
前提知識

DELETE はテーブルから行を物理的に削除します。一方、論理削除は行を消さず deleted_at 列に削除日時を記録するパターンで、API開発では論理削除が主流です。

-- 物理削除(完全に消える・元に戻せない)
DELETE FROM table_name             -- 対象テーブル
WHERE id = 1;                       -- 必須!省略すると全行削除

-- 論理削除(deleted_at に日時を入れるだけ、行は残る)
UPDATE table_name
SET    deleted_at = NOW()           -- NULL → 現在日時 に変更することで「削除済み」を表現
WHERE  id = 1
  AND  deleted_at IS NULL;          -- まだ削除されていない行のみ(べき等性の確保)
論理削除の利点:①データ復元が可能 ②監査ログとして削除履歴が残る ③外部キー参照が壊れない。取得クエリには WHERE deleted_at IS NULL を常に付ける必要がある。
問題

comments テーブルで、comment_id=3 のコメントを論理削除してください(deleted_at に現在時刻を記録)。また、論理削除済みを除いた有効なコメント一覧を取得するSQLも書いてください。

使用テーブル
▸ comments
comment_idpost_idbodydeleted_at
110素晴らしい記事ですNULL
210参考になりましたNULL
311不適切なコメントNULL
411ありがとうございます2024-05-01
期待出力
comment_idpost_idbodydeleted_at
110素晴らしい記事ですNULL
210参考になりましたNULL
模範解答コード
-- ① 論理削除: comment_id=3 の deleted_at に現在時刻を記録
UPDATE comments
SET    deleted_at = NOW()   -- 削除日時をセット=論理削除(DELETE しない)
WHERE  comment_id = 3
  AND  deleted_at IS NULL;  -- 未削除の行だけ

-- ② 有効コメント一覧取得: deleted_at が NULL の行のみ返す
SELECT
  comment_id,
  post_id,
  body,
  deleted_at
FROM   comments
WHERE  deleted_at IS NULL  -- 未削除(IS NULL で判定)
ORDER BY comment_id ASC;

/*
  実行順序(SQLの論理的な評価順 / ②の一覧取得):
  1. FROM comments             → 行を読み込む
  2. WHERE deleted_at IS NULL  → 行を絞り込む
  3. SELECT                    → 列を評価
  4. ORDER BY comment_id       → 並び替えて出力
*/
解説(テーブル変化・ポイント)
UPDATE comments SET deleted_at = NOW() WHERE comment_id = 3 AND deleted_at IS NULL;
LEGEND
データ取得・読込対象
① FROM
FROM comments — 論理削除前comments テーブル全4行を読み込みます。id:4 はすでに deleted_at に日時が入っており、論理削除済みです。
1 / 3
comment_idbodydeleted_at
1素晴らしい記事ですNULL
2参考になりましたNULL
3不適切なコメントNULL
4ありがとうございます2024-05-01
全 4行 読込
SELECT comment_id, post_id, body, deleted_at FROM comments WHERE deleted_at IS NULL ORDER BY comment_id ASC;
LEGEND
データ取得・読込対象
① FROM
FROM comments — 論理削除後のテーブル論理削除後の comments テーブルです。id:3 の deleted_at に日時が入り、id:4 とともに「削除済み」状態になっています。行は残っていることに注目してください。
1 / 3
comment_idpost_idbodydeleted_at
110素晴らしい記事ですNULL
210参考になりましたNULL
311不適切なコメント2024-06-01
411ありがとうございます2024-05-01
全 4行
学習ポイント
超基礎:DELETE文はデータをディスクから完全に消し去ります。一方、論理削除はUPDATE文で「削除フラグ(削除日時)」を更新するだけで、データ自体は残すという「システム上の運用の工夫」です。
論理削除が実務標準の理由:①削除取り消し(undo)が可能 ②「いつ誰が削除したか」の監査証跡になる ③外部テーブルから参照している場合に参照整合性エラーが発生しない。特にユーザーや注文データは論理削除が原則。
全クエリに deleted_at IS NULL を付ける:論理削除を採用すると全てのSELECT/UPDATE/DELETEに WHERE deleted_at IS NULL が必要になる。ORMのデフォルトスコープやビュー(VIEW)で自動適用するのが実務上のベストプラクティス。
物理削除が適切なケース:GDPRなどの法的な「忘れられる権利」対応、PII(個人情報)の完全消去が必要な場合は物理削除が必須。論理削除ではデータが残り続けるため法的要件を満たせない場合がある。
アンチパターン
WHERE なし DELETE:DELETE FROM comments は全行削除。誤ってWHEREを省略したときの被害を最小限にするため、本番環境ではDELETEを実行する前に必ず SELECT COUNT(*) WHERE ... で件数確認をする。
deleted_at に boolean(is_deleted)を使う:is_deleted = true/false では「いつ削除されたか」の情報が失われる。TIMESTAMPTZ型のdeleted_atにすれば「NULL=有効、日時=削除済み」の両方の情報が1列に収まり、削除日時による絞り込みも可能になる。
実務コラム:論理削除とユニーク制約の衝突
論理削除を採用すると、「同じメールアドレスで再登録したい」というユーザーが現れた際、email 列のユニーク制約(重複禁止)に引っかかるという有名な問題が発生します。実務ではこれを回避するため、「email と deleted_at の複合ユニーク制約」にするか、削除時に email の末尾に _deleted_タイムスタンプ を付与して退避させるなど、スキーマレベルでの運用工夫がセットで必要になります。
QUESTION 8

WITH(CTE) — 複雑なバッチクエリを分解して読みやすく・再利用しやすくする

WITHCTEクエリ分解バッチ処理
前提知識

CTE(Common Table Expression)は WITH 句で一時的な名前付き結果セットを定義し、後続のクエリで何度でも参照できる機能です。複雑なバッチクエリをステップごとに分解して読みやすくします。

WITH
  cte1 AS (                        -- CTE名を定義(テーブルのように後で使える)
    SELECT col1, col2
    FROM   table_a
    WHERE  ...
  ),
  cte2 AS (                        -- 複数のCTEはカンマで繋ぐ(cte1を参照することも可)
    SELECT col1
    FROM   cte1                    -- 前のCTEを参照できる
    WHERE  ...
  )
SELECT * FROM cte2;               -- 最後に本体クエリ(CTEを参照する)
CTEとサブクエリの違い:サブクエリはネストして読みにくくなる。CTEは名前をつけて上から下へ読める。「何をしているか」がステップで明確になり、バッチ処理のSQL設計で多用される。
問題

「過去30日以内に2件以上注文したユーザー」をVIP顧客として取得したいです。orders(注文)と users(ユーザー)テーブルを使い、CTEを2段階に分けて、VIPユーザーの user_id, name, order_count を order_count 降順で返してください。現在日は 2024-06-01 とします。

使用テーブル
▸ orders
order_iduser_idordered_at
1U012024-05-10
2U012024-05-20
3U022024-05-25
4U032024-04-01
5U012024-05-28
6U022024-05-29
7U032024-03-15
▸ users
user_idname
U01田中 太郎
U02佐藤 花子
U03鈴木 一郎
期待出力
user_idnameorder_count
U01田中 太郎3
U02佐藤 花子2
模範解答コード
WITH
  recent_orders AS (               -- CTE①: 過去30日の注文をユーザー別に集計
    SELECT
      user_id,
      COUNT(*) AS order_count
    FROM   orders
    WHERE  ordered_at >= DATE '2024-06-01' - INTERVAL '30 days'  -- 問題文の基準日から直近30日
    GROUP BY user_id
  ),
  vip_users AS (                   -- CTE②: 2件以上のユーザーに絞り込み
    SELECT user_id, order_count
    FROM   recent_orders            -- CTE① を参照
    WHERE  order_count >= 2
  )
SELECT
  v.user_id,
  u.name,
  v.order_count
FROM       vip_users v
INNER JOIN users    u              -- VIP に氏名を結合
  ON  v.user_id = u.user_id
ORDER BY v.order_count DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. CTE① recent_orders       → CTE を定義
  2. CTE② vip_users           → CTE を定義
  3. FROM vip_users v          → 行を読み込む
  4. INNER JOIN users u        → 結合(一致行のみ)
  5. SELECT                    → 列を評価
  6. ORDER BY order_count DESC → 並び替えて出力
*/
解説(テーブル変化・ポイント)
WITH recent_orders AS ( SELECT user_id, COUNT(*) AS order_count FROM orders WHERE ordered_at >= DATE '2024-06-01' - INTERVAL '30 days' GROUP BY user_id ), vip_users AS ( SELECT user_id, order_count FROM recent_orders WHERE order_count >= 2 ) SELECT v.user_id, u.name, v.order_count FROM vip_users v INNER JOIN users u ON v.user_id = u.user_id ORDER BY v.order_count DESC;
LEGEND
データ取得・読込対象
除外・非表示データ
✓ 通過
✗ 除外
① CTE① FROM+WHERE
recent_orders AS (FROM orders WHERE ordered_at >= 30日前)CTE① の内部処理です。orders テーブルから過去30日以内(2024-05-02以降)の注文だけを残します。U03 の注文(04-01, 03-15)はすべて30日超のため除外されます。
1 / 4
order_iduser_idordered_at30日以内?
1U012024-05-10
2U012024-05-20
3U022024-05-25
4U032024-04-01
5U012024-05-28
6U022024-05-29
7U032024-03-15
7行 → 5行(30日以内)
学習ポイント
超基礎:WITH句(CTE)は、クエリの最初に「一時的な名前付きのテーブル」を作って、後からそれを通常のテーブルのように何度も呼び出して使えるようにする機能です。長くて複雑なSQLを整理するために使います。
CTEの命名で意図を伝える:recent_orders(最近の注文)、vip_users(VIPユーザー)のように意味のある名前をつけることで、SQLを読んだ人が「何をしているか」を即座に理解できる。コメントの代わりになる。
CTEは同じ結果を複数回参照できる:同じサブクエリを2回書く必要があるときにCTEで1回定義すれば複数箇所から参照できる。コードの重複排除とメンテナンス性向上に効果的。
バッチSQL設計の定番構成:CTEで①データ抽出・②絞り込み・③集計、の各ステップを分けて定義し、本体クエリで最終形を作る構成がバッチSQL設計の王道パターン。デバッグ時はCTEを1段ずつ単独でSELECTして中間結果を確認できる。
アンチパターン
過度にネストしたサブクエリ:SELECT * FROM (SELECT * FROM (SELECT ...) sub1) sub2 のように3重・4重にネストするとどこで何をしているか全く読めなくなる。2段階以上になったらCTEへリファクタリングすること。
CTEに不必要に全列を含める:SELECT * FROM orders でCTEを作ると後続で不要な列まで抱えることになる。CTEでも必要な列だけ明示的に SELECT すること。パフォーマンスと可読性の両方に影響する。
実務コラム:CTEのパフォーマンスとマテリアライズドビュー
CTEはSQLを劇的に読みやすくしますが、巨大なデータに対して何度も同じCTEを呼び出すと、内部的に毎回再計算が走りパフォーマンスが劣化することがあります(※DBのバージョンによる)。分析系バッチで1クエリが数分かかるような場合は、CTEの中間結果を実テーブルやマテリアライズドビュー(Materialized View)として物理的に保存し、それにインデックスを張るアプローチへの切り替えが検討されます。
QUESTION 9

EXISTS / NOT EXISTS — 関連データの存在チェックを効率的に行う

EXISTSNOT EXISTS存在チェック条件フィルタ
前提知識

EXISTS はサブクエリが1行以上の結果を返すかどうかを TRUE/FALSE で判定します。「関連データが存在する行だけ取得」「まだ処理されていない行を取得」のバッチ処理で頻出です。

SELECT col1
FROM   table_a a
WHERE  EXISTS (                      -- サブクエリが1件以上あれば TRUE
  SELECT 1                          -- EXISTS内はSELECT 1でよい(列は何でもよい)
  FROM   table_b b
  WHERE  b.a_id = a.id              -- 外側のテーブルを参照(相関サブクエリ)
);

WHERE  NOT EXISTS (...)             -- NOT EXISTS: サブクエリが0件のとき TRUE
EXISTS vs IN の違い:WHERE id IN (SELECT id FROM ...) でも似たことができるが、EXISTSはサブクエリが1行見つかった時点でスキャンを中断するため大量データで高速。また IN はサブクエリにNULLが含まれると予期しない動作をする場合がある。
問題

users テーブルから、まだ一度も注文したことがないユーザーorders テーブルに対応レコードなし)を取得してください。user_id と name を返し、user_id の昇順で並べてください。

使用テーブル
▸ users
user_idname
1田中 太郎
2佐藤 花子
3鈴木 一郎
4山田 次郎
5伊藤 三郎
▸ orders
order_iduser_idamount
10115000
10233200
10318800
期待出力
user_idname
2佐藤 花子
4山田 次郎
5伊藤 三郎
模範解答コード
SELECT
  u.user_id,
  u.name
FROM  users u
WHERE NOT EXISTS (              -- 注文が1件も無いユーザーだけ
  SELECT 1                      -- 存在判定なので値は何でもよい
  FROM   orders o
  WHERE  o.user_id = u.user_id  -- 外側と相関(注文の有無を判定)
)
ORDER BY u.user_id ASC;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM users u             → 行を読み込む
  2. NOT EXISTS (サブクエリ)  → サブクエリを先に評価
  3. SELECT                   → 列を評価
  4. ORDER BY u.user_id       → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT u.user_id, u.name FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id ) ORDER BY u.user_id ASC;
LEGEND
データ取得・読込対象
① FROM
FROM users u外側クエリの対象テーブル users 全5行を読み込みます。次のステップで各行に対して NOT EXISTS の相関サブクエリが実行されます。
1 / 3
user_idname
1田中 太郎
2佐藤 花子
3鈴木 一郎
4山田 次郎
5伊藤 三郎
全 5行 読込
学習ポイント
超基礎:サブクエリ(副問合せ)とは、SQL文の中に別のSELECT文を入れ込む書き方です。EXISTSは、そのサブクエリが「1件でもデータを返せばTRUEになる」という判定を行うための命令です。
バッチ処理での活用例:「まだメールを送っていないユーザーを取得」NOT EXISTS (SELECT 1 FROM email_logs WHERE user_id = u.user_id)、「未課金ユーザーへのリマインダー送信」など、処理済み/未処理を管理するバッチに頻出。
SELECT 1 の理由:EXISTS はサブクエリが1行でも返せばTRUEになる。SELECT している列の値は評価されないため、SELECT 1(定数)で書くのが慣例。SELECT * でも動くが意図が伝わりにくい。
LEFT JOIN + IS NULL との使い分け:両者は同じ結果を返すがクエリプランナーによってパフォーマンスが異なる場合がある。PostgreSQLでは両者を同等に最適化することが多い。可読性から NOT EXISTS の方が意図が明確なのでこちらを推奨。
アンチパターン
NOT IN でのNULL問題:WHERE user_id NOT IN (SELECT user_id FROM orders) は orders.user_id にNULLが1件でも含まれると全件 FALSE になり0件が返る。NOT IN はNULLに弱いため NOT EXISTS を使う方が安全。
相関サブクエリで外側テーブルの参照を忘れる:WHERE EXISTS (SELECT 1 FROM orders)WHERE o.user_id = u.user_id なし)はordersに1行でもあれば全ユーザーがTRUEになる。外側テーブルとの結合条件は必須。
実務コラム:アンチジョイン(NOT EXISTS vs LEFT JOIN)
「存在しないデータ」を探す手法(アンチジョイン)として、NOT EXISTS の他に LEFT JOIN + IS NULL を使う方法があります。実行計画(パフォーマンス)はデータベースエンジンによって同等に最適化されることが多いですが、NOT EXISTS の方が「存在チェックをしている」という意図がSQLから明確に読み取れるため、チーム開発での可読性・メンテナンス性の観点から実務で好んで採用されます。
QUESTION 10

CASE WHEN — バッチ集計・APIレスポンスで値を条件分岐して変換する

CASE WHENTHEN/ELSE値変換APIレスポンス
前提知識

CASE WHEN はSQLのif-else構文です。SELECT・WHERE・ORDER BY・GROUP BY など様々な場所で使えます。APIレスポンスでDBの値をラベル変換したり、集計で条件付きカウントを行うバッチ処理に頻出です。

-- 単純CASE(列の値を列挙して分岐)
CASE col1
  WHEN 'val1' THEN '表示A'         -- col1 = 'val1' のとき
  WHEN 'val2' THEN '表示B'         -- col1 = 'val2' のとき
  ELSE 'その他'                    -- どれにも一致しない場合(省略すると NULL)
END                                -- CASE は必ず END で閉じる

-- 検索CASE(任意の条件式で分岐・より汎用的)
CASE
  WHEN col2 > 100 THEN '大'         -- 比較演算子も使える
  WHEN col2 > 50  THEN '中'         -- 上から順に評価し最初に一致した THEN を返す
  ELSE '小'
END
CASE WHEN の評価順:上から順に評価し、最初に TRUE になった WHEN ... THEN を返して終了します。残りの条件は評価されません。
問題

orders テーブルから、各注文の amount を基に購買ランク(10000以上:premium / 5000以上:standard / それ未満:basic)をつけ、さらに status を日本語ラベルに変換して返してください。order_id, amount, rank_label, status_label を ordered_at の降順で返してください。

使用テーブル
▸ orders
order_idamountstatusordered_at
112000completed2024-05-20
24800pending2024-05-18
37500cancelled2024-05-15
4500completed2024-05-10
55000pending2024-05-08
期待出力
order_idamountrank_labelstatus_label
112000premium完了
24800basic処理中
37500standardキャンセル
4500basic完了
55000standard処理中
模範解答コード
SELECT
  order_id,
  amount,
  CASE                              -- 金額で購買ランクを分岐
    WHEN amount >= 10000 THEN 'premium'
    WHEN amount >=  5000 THEN 'standard'  -- 上から順に評価(最初の一致を採用)
    ELSE                       'basic'
  END AS rank_label,
  CASE status                      -- 列の値で分岐(単純 CASE)
    WHEN 'completed' THEN '完了'
    WHEN 'pending'   THEN '処理中'
    WHEN 'cancelled' THEN 'キャンセル'
    ELSE '不明'                      -- 想定外の値への備え
  END AS status_label

FROM   orders
ORDER BY ordered_at DESC;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders              → 行を読み込む
  2. SELECT                   → 列を評価(rank_label, status_label)
  3. ORDER BY ordered_at DESC      → 並び替えて出力
*/
解説(テーブル変化・ポイント)
SELECT order_id, amount, CASE WHEN amount >= 10000 THEN 'premium' WHEN amount >= 5000 THEN 'standard' ELSE 'basic' END AS rank_label, CASE status WHEN 'completed' THEN '完了' WHEN 'pending' THEN '処理中' WHEN 'cancelled' THEN 'キャンセル' ELSE '不明' END AS status_label FROM orders ORDER BY ordered_at DESC;
LEGEND
データ取得・読込対象
① FROM
FROM ordersorders テーブル全5行を読み込みます。CASE WHEN は SELECT 句の中で各行に対して評価されます。
1 / 4
order_idamountstatusordered_at
112000completed2024-05-20
24800pending2024-05-18
37500cancelled2024-05-15
4500completed2024-05-10
55000pending2024-05-08
全 5行 読込
学習ポイント
超基礎:CASE WHEN は、SQLの中で使える「If-Else文(条件分岐)」です。条件に合致したときに返す値を指定でき、データの表示名を変えたり、グループ分けをしたりする際にとても便利です。
APIレスポンスの変換層として活用:DBにはコード値('completed')を保存し、APIレスポンスはCASE WHENでラベル変換して返すパターンが実務標準。アプリ側で変換するより1クエリで完結するため効率的。
条件付き集計への応用:SUM(CASE WHEN status='completed' THEN amount ELSE 0 END) のように集計関数と組み合わせると「特定条件の合計金額だけ集計」ができる。GROUP BY と組み合わせてKPI集計に頻用される。
ELSEは常に書く:ELSEを省略すると一致しない値がNULLになり、APIレスポンスにNULLが混入して予期しないエラーを引き起こす。想定外の値のフォールバックとして必ず ELSE '不明' などを書く習慣が重要。
アンチパターン
WHEN の順序を間違える(検索CASEの落とし穴):WHEN amount >= 5000 THEN 'standard' WHEN amount >= 10000 THEN 'premium' と順番を逆にすると、12000は最初のWHEN(>=5000)で 'standard' に確定してしまい 'premium' に到達しない。上から順に評価される点を必ず意識すること。
END を忘れる:CASE は必ず END で閉じなければならない。CASEのネストや複数CASE使用時に END の数が合わないとエラーになる。書いたCASEと同じ数のENDがあるか確認すること。
実務コラム:ビジネスロジックはアプリ側か?DB側か?
CASE WHEN を使えば複雑な条件分岐をSQL内で完結できますが、多用しすぎると「仕様変更のたびにSQLを修正する」ことになり、バックエンドコード(アプリ側)との二重管理になりがちです。実務でのベストプラクティスは、「データの絞り込み・集約に関する条件はDB(SQL)に任せ、表示用の単純なラベル変換や細かなフォーマット調整はアプリ側(フロントエンドやBFF)に任せる」という役割分担を明確にすることです。