0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UNION ALL と CASE の使い分け

0
Posted at

はじめに

この記事では、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の構造が自然に決まります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?