はじめに
この記事では、SQLで条件分岐を書くときに UNION ALL と CASE のどちらを使うべきか、その判断基準を整理します。
どちらも「条件によって出力を変える」用途に見えるため、混同しやすい組み合わせです。しかし目的が根本的に異なります。
- UNION ALL は行を増やす
- CASE は行の中で値を分類する
挙動の違いを確認する
次のテーブルを例に使います。
-- employees テーブル
-- employee_id | team_id | project_id
-- 1 | NULL | NULL -- 両方未所属
-- 2 | NULL | 1 -- チームのみ未所属
-- 3 | 1 | NULL -- 案件のみ未所属
-- 4 | 1 | 1 -- 両方所属済み
UNION ALL
SELECT employee_id, 'PROJECT_UNASSIGNED' AS reason
FROM employees
WHERE project_id IS NULL
UNION ALL
SELECT employee_id, 'TEAM_UNASSIGNED' AS reason
FROM employees
WHERE team_id IS NULL
ORDER BY employee_id, reason;
結果は次のとおりです。
employee_id | reason
------------+--------------------
1 | PROJECT_UNASSIGNED
1 | TEAM_UNASSIGNED
2 | TEAM_UNASSIGNED
3 | PROJECT_UNASSIGNED
employee_id=1 は両方の条件に該当するため、2行で出力されます。
UNION ALL は複数の SELECT 結果を縦に連結します。そのため、両方に該当したデータは2行として出力されます。
UNION(重複排除あり)ではなく UNION ALL を使うのは、該当理由をそのまま残したいためです。今回は reason カラムの値が異なるため UNION でも重複排除は起きませんが、「重複排除しない」意図を明示するために UNION ALL を使います。
CASE
SELECT
employee_id,
CASE
WHEN project_id IS NULL THEN 'PROJECT_UNASSIGNED'
WHEN team_id IS NULL THEN 'TEAM_UNASSIGNED'
END AS reason
FROM employees
WHERE project_id IS NULL OR team_id IS NULL
ORDER BY employee_id;
結果は次のとおりです。
employee_id | reason
------------+--------------------
1 | PROJECT_UNASSIGNED
2 | TEAM_UNASSIGNED
3 | PROJECT_UNASSIGNED
employee_id=1 は両方の条件に該当しますが、先にマッチした PROJECT_UNASSIGNED のみが返ります。この CASE 式では TEAM_UNASSIGNED は返りません。
CASE は1つの式として1つの値を返します。先にマッチした条件のみが採用されるため、1行を2行には増やせません。
「両方の理由を1行に詰め込む」ために文字列連結で対応する方法もありますが、その場合は呼び出し側でのパースが必要になり、集計や条件絞り込みがしにくくなります。複数の理由を別件として扱うなら、最初から UNION ALL で行を分けた方が扱いやすいです。
CASEが適切な例
同じ employees テーブルで、雇用形態に応じて表示ラベルを変えるケースを考えます。
-- employees テーブル(追加カラム)
-- employee_id | employment_type
-- 1 | full_time
-- 2 | part_time
-- 3 | contract
SELECT
employee_id,
CASE employment_type
WHEN 'full_time' THEN '正社員'
WHEN 'part_time' THEN 'パート'
WHEN 'contract' THEN '契約社員'
ELSE '不明'
END AS employment_label
FROM employees
ORDER BY employee_id;
結果は次のとおりです。
employee_id | employment_label
------------+-----------------
1 | 正社員
2 | パート
3 | 契約社員
1行につき1つの値を返すだけで良く、複数行に増やす必要はありません。テーブルも1回しかスキャンしません。
これを UNION ALL で書くと次のようになります。
SELECT employee_id, '正社員' AS employment_label
FROM employees WHERE employment_type = 'full_time'
UNION ALL
SELECT employee_id, 'パート'
FROM employees WHERE employment_type = 'part_time'
UNION ALL
SELECT employee_id, '契約社員'
FROM employees WHERE employment_type = 'contract';
結果は同じですが、テーブルを3回スキャンします。雇用形態の種類が増えるたびにブロックも増えます。この用途では CASE が適切です。
使い分けの判断基準
「両方に該当したときどうするか」が分岐点です。
- 両方の理由を別件として残したい → UNION ALL
- どれか1つに分類すれば十分 → CASE
この判断はSQLの書き方の問題ではなく、仕様の問題です。要件として先に決まっていないと、後から変更したときに呼び出し側まで影響が広がります。
パフォーマンスの観点でも、同じテーブルへの単純な条件分岐は CASE の方が効率的です。UNION ALL は各 SELECT を独立したクエリとして実行するため、同じテーブルを複数回スキャンします。CASE は1回のスキャンで済みます。「複数の理由を別行で出す」用途では UNION ALL が必要になりますが、単に出力値を条件で変えたいだけなら CASE を選ぶ方が自然です。
UNION ALL を使うときの注意点
UNION ALL で複数行が返る設計を選んだ場合、呼び出し側でも「1件につき複数行が返ることがある」を前提にする必要があります。
件数の扱いに注意が必要です。
-- 未所属「件数」になる(社員数ではない)
SELECT COUNT(*) FROM (...) AS sub;
-- 社員のユニーク数を知りたい場合
SELECT COUNT(DISTINCT employee_id) FROM (...) AS sub;
「何人が未所属か」と「何件の未所属理由があるか」は異なります。集計の目的に合わせてどちらを使うか意識します。
まとめ
UNION ALL と CASE の使い分けは、1件に対して結果を何行出したいかで決まります。
- 複数の条件を別行として残したい → UNION ALL
- 1行の中でどれか1つに分類したい → CASE
判断の起点は「両方に該当したときどうするか」という仕様の問いです。これを先に決めておくと、SQLの構造が自然に決まります。