1.はじめに
「達人に学ぶSQL徹底指南書 第2版」を読み、CASE式に対する認識が大きく変わりました。これまでは、単なる条件分岐の構文として理解していましたが、評価されて値を返す式として理解するようになりました。
この記事のゴールは、CASEを単なる「文」ではなく、評価されてひとつの値を返す「式」として正しく理解することです。
CASEが「式」であることを腹落ちさせれば、これまでアプリケーション側で泥臭く処理していた複雑な集計や、パフォーマンスを劣化させていた冗長なクエリを、SQL単体でシンプルかつ高速に記述できるようになります。この記事では、実務ですぐに使えるテクニックを交えながら、SQLらしい「集合指向」の思考法へ切り替えるための第一歩を解説します。
2. 最大のパラダイムシフト:「文」ではなく「式」である
SQLにおいて CASE を学ぶ際、最初にぶつかる壁であり、最大のブレイクスルーとなるのがCASEは「文」ではなく「式」であるという事実の理解です。
現場ではよく「CASE文」と呼ばれがちですが、SQL標準における正式名称は「CASE式(CASE Expression)」です。この名前の違いは、単なる言葉遊びではなく、SQLという言語の思想を理解する上で決定的な意味を持ちます。
「文」と「式」の決定的な違い
手続き型言語(Java, Python, Cなど)におけるif 〜 elseは「文」です。
文とは、プログラムの実行フローを制御するための「命令」であり、それ自体が値を持つわけではありません。
一方、SQLの CASE は「式」です。
式とは、評価された結果として「ひとつの値」を返すものです。手続き型言語で言えば、三項演算子( 条件 ? 値1 : 値2 )に似ています。
つまり、SQLエンジンにとってCASE WHEN ... THEN ... ENDというまとまりは、評価が終われば1や'A'といった「単なる一つの値(スカラ値)」と同義になります。
なぜこれが重要なのか?
「CASEが式である」という事実の最大のメリットは、「値(定数や列名)が書ける場所なら、SQLの構文上のどこにでも書ける」という点です。
手続き型言語のif文は、SQLでいうSELECTやWHEREの中に直接埋め込むことはできませんが、CASE式であればそれが可能です。これにより、SQLの表現力は爆発的に向上します。
- SELECT句で使う(値の変換)
最も基本的な使い方です。列の値を評価し、別の値に変換して出力します。
SELECT
user_name,
CASE WHEN age >= 20 THEN '成人'
ELSE '未成年' END AS age_category
FROM users;
- GROUP BY句で使う(任意のカテゴリで集約)
テーブルに存在しない独自のカテゴリをその場で作成し、そのカテゴリ単位で集計を行うことができます。
-- 年齢層(20代、30代...)ごとにユーザー数をカウント
SELECT
CASE WHEN age < 20 THEN '20歳未満'
WHEN age BETWEEN 20 AND 29 THEN '20代'
WHEN age BETWEEN 30 AND 39 THEN '30代'
ELSE '40歳以上' END AS age_group,
COUNT(*) AS user_count
FROM users
GROUP BY
CASE WHEN age < 20 THEN '20歳未満'
WHEN age BETWEEN 20 AND 29 THEN '20代'
WHEN age BETWEEN 30 AND 39 THEN '30代'
ELSE '40歳以上' END;
- ORDER BY句で使う(独自のソート順を定義)
昇順・降順だけでなく、「特定のステータスを一番上に持ってくる」といった業務要件によくある特殊なソートも、CASE式で解決できます。
-- statusが 'URGENT'(緊急) のものを最優先で並べ、その後は登録日順
SELECT task_name, status, created_at
FROM tasks
ORDER BY
CASE WHEN status = 'URGENT' THEN 0
ELSE 1 END,
created_at ASC;
- UPDATE文のSET句で使う(条件分岐アップデート)
複数回のUPDATE文を発行しなくても、条件に応じた値の更新を1回のクエリで安全に実行できます。
-- 役職に応じて給与のベースアップ額を変える
UPDATE employees
SET salary = CASE WHEN role = 'Manager' THEN salary * 1.1
WHEN role = 'Member' THEN salary * 1.05
ELSE salary END;
このように、「CASEは式である」と認識を改めるだけで、これまで「SQLだけでは無理だから、アプリケーション側(プログラム側)でループして処理しよう」と諦めていた多くの操作が、SQL単体でシンプルに記述できるようになります。
この章で「CASE式がどこにでも書ける」という強力な前提を共有することで、次の章の「クロス集計」や「UNIONの排除」といった応用テクニックの理解が格段にスムーズになります。
3. 実践テクニック①:条件を分岐させた集計(クロス集計)
CASE式のメリットを最も実感しやすいのが、この「クロス集計(行列変換)」です。
業務システムでは「縦持ち(行持ち)のデータを、横持ち(列持ち)のレポート形式に変換してほしい」という要件が頻繁に発生します。
課題:縦持ち(行持ち)データを横持ち(列持ち)の表に変換したい
例えば、次のような都道府県別の人口推移データ(Populationテーブル)があるとします。
-- 縦持ちの元データ
SELECT pref_name, sex, population FROM Population;
| pref_name | sex | population |
|---|---|---|
| 徳島 | 1 | 60 |
| 徳島 | 2 | 40 |
| 香川 | 1 | 100 |
| 香川 | 2 | 100 |
| 愛媛 | 1 | 100 |
| 愛媛 | 2 | 50 |
| 高知 | 1 | 100 |
| 高知 | 2 | 100 |
(sex 1:男性、2:女性)
このデータを、以下のような「都道府県ごとの男女別人口」という横持ちのクロス集計表として出力したいとします。
| pref_name | count_male (男) | count_female (女) |
|---|---|---|
| 徳島 | 60 | 40 |
| 香川 | 100 | 100 |
| 愛媛 | 100 | 50 |
| 高知 | 100 | 100 |
解決策:集約関数の中にCASE式を組み込む
手続き型言語の思考に引きずられていると、一度全データをSELECTして、アプリケーション側のプログラムでループを回して集計しようと考えがちです。しかし、CASE式を使えばSQLだけで完結します。
ポイントは、SUM などの集約関数の引数としてCASE式を渡すことです。前章で学んだ通り、CASEは「式(評価されて値になるもの)」なので、関数の引数として全く問題なく機能します。
CASE式を使ったクロス集計クエリ
SELECT
pref_name,
-- 男性の人口だけを合計する
SUM(CASE WHEN sex = '1' THEN population ELSE 0 END) AS count_male,
-- 女性の人口だけを合計する
SUM(CASE WHEN sex = '2' THEN population ELSE 0 END) AS count_female
FROM
Population
GROUP BY
pref_name;
どう動いているのか?(SQLエンジンの思考)
このクエリが内部でどう評価されているか、順を追って見てみましょう。
-
GROUP BY pref_name の実行
まず、都道府県ごとにレコードがグループ化されます(例えば「徳島」のグループには、男の行と女の行の2行が含まれます)。 -
SUM 関数内の CASE 式の評価
徳島グループに対して`count_male列を計算します。
1行目(男): sex = '1' が真なので、population(60)を返す。
2行目(女): sex = '1' が偽なので、0 を返す。 -
SUM による集約
評価された値が SUM(60 + 0)となり、結果として「60」が出力されます。
これが、SQLで条件付き集計を行う際の強力なデザインパターンです。
4. 実践テクニック②:悪手「UNION」からの脱却(パフォーマンス改善)
CASE式をマスターすることで得られるもう一つの巨大な恩恵が、SQLのパフォーマンス改善です。特に、手続き型言語に慣れたエンジニアが陥りがちな「無用なUNION」を排除できる点は、実務において非常に重要です。
課題:条件によって処理を分けたい時の「UNION病」
例えば、商品のデータ(Itemsテーブル)があり、キャンペーンのために以下の条件で割引価格を算出したいとします。
- カテゴリが「食品」なら10%オフ
- カテゴリが「家電」なら20%オフ
手続き型言語の「IF条件がAなら処理A、Bなら処理B」という思考を引きずっていると、SQLでも「条件AのSELECT文」と「条件BのSELECT文」を書き、それを UNION(または UNION ALL)でくっつける、という発想になりがちです。
(悪手)UNION ALLを使ったクエリ
-- 食品の割引価格を計算
SELECT item_name, price * 0.9 AS campaign_price
FROM Items
WHERE category = '食品'
UNION ALL
-- 家電の割引価格を計算
SELECT item_name, price * 0.8 AS campaign_price
FROM Items
WHERE category = '家電';
このクエリは結果としては正しい値を返します。
しかし、パフォーマンスの観点からは非常に効率が悪い悪手です。
なぜUNIONで条件分岐をしてはいけないのか?
RDBMSにおいて、最も重い処理になりやすいのがテーブルへのアクセスです。
上記のUNIONを使ったクエリはItemsテーブルに対するスキャンが2回発生します。条件が3つ、4つと増えてUNIONを繋げれば、スキャン回数も3回、4回と倍増していきます。数百万行、数千万行の大規模なテーブルでこれを行うと、致命的なパフォーマンス低下を引き起こします。
解決策:CASE式で「1回のスキャン」にまとめる
ここで「CASEは式である」という原則を思い出してください。条件分岐はSELECT句の中で「値」として処理すれば良いのです。
(最適解)CASE式を使ったクエリ
SELECT
item_name,
CASE category
WHEN '食品' THEN price * 0.9
WHEN '家電' THEN price * 0.8
END AS campaign_price
FROM
Items
WHERE
category IN ('食品', '家電');
この書き方であれば、Items テーブルへのスキャンは1回だけで済みます。
1回レコードを読み込み、そのレコードの category の値に応じて、SELECT句の中のCASE式が動的に計算ルールを切り替えてくれます。I/OコストがUNIONの時の半分になるため、劇的なパフォーマンス改善が見込めます。
SQLの思考法:分割して結合するのではなく、集合を一括処理する
UNIONは本来、「異なるテーブルの集合」や「インデックスの効き方が全く違う複雑な検索結果」を結合するためのものです。「同じテーブルに対して、条件によって出力する値を変えたい」という要件に対してUNIONを使うのは、SQLの設計思想に反しています。
- 手続き型の思考:条件ごとにデータを分割(SELECT)し、後で結合(UNION)する
- SQL(宣言型)の思考:対象となるデータの集合を一度だけ取得(スキャン)し、要素ごとに評価(CASE)する
この思考の切り替えができるようになると、クエリの実行速度は飛躍的に向上します。
5. CASE式を書く際の「3つの鉄則」
CASE式は強力な反面、書き方を誤ると予期せぬバグの温床になります。実務で安全に運用するための3つの鉄則を紹介します。
1.ELSEは省略しない
CASE式において ELSE 句は文法上省略可能ですが、実務においては絶対に省略してはいけません。
ELSE を省略した場合、どの WHEN 条件にも合致しなかったレコードには暗黙的にNULLが返されます。この暗黙のNULLが、意図せぬ集計エラーや、アプリケーション側でのNullPointerExceptionを引き起こす原因になります。
-- 【悪手】ELSEを省略している
CASE WHEN age >= 20 THEN '成人' END
-- 【正解】明示的にELSEを書く(該当なしをNULLとしたい場合でも明記する)
CASE WHEN age >= 20 THEN '成人' ELSE '未成年' END
CASE WHEN age >= 20 THEN '成人' ELSE NULL END
2.ENDを忘れない
初学者が最もよく遭遇する構文エラーがENDの書き忘れです。
手続き型言語のswitchやifのようなブロック終端記号の感覚に慣れていると、最後にENDをつけるのを忘れがちです。
-- 【エラーになる】ENDが足りない
SELECT
CASE WHEN sex = '1' THEN '男'
WHEN sex = '2' THEN '女'
ELSE '不明'
AS sex_name -- ここで構文エラー
FROM users;
CASE式の記述を始める際は、まず CASE と END をセットで書いてから中身の WHEN を埋めていく癖をつけることをおすすめします。
3.「単純CASE式」より「検索CASE式」を使う
CASE式には2つの書き方があります。「単純CASE式」と「検索CASE式」です。
-- 1. 単純CASE式(対象の列をCASEの直後に書く)
CASE sex
WHEN '1' THEN '男'
WHEN '2' THEN '女'
ELSE '不明'
END
-- 2. 検索CASE式(WHENの後に条件式を書く)
CASE
WHEN sex = '1' THEN '男'
WHEN sex = '2' THEN '女'
ELSE '不明'
END
結論から言うと、実務では「検索CASE式」に統一すべきです。
単純CASE式は =(等値比較)しか表現できず、以下のような柔軟な条件指定ができません。
- age >= 20 のような不等号
- name LIKE '%太郎%' のような部分一致
- A = 1 AND B = 2 のような複数列の組み合わせ
- col IS NULL のようなNULL判定(単純CASE式で WHEN NULL と書いても正しく判定されません)
検索CASE式であればこれらすべてに対応可能です。可読性と将来の仕様変更への強さを考慮し、最初から検索CASE式で書くことを習慣づけましょう。
6. おわりに
「達人に学ぶSQL徹底指南書 2版」の第1章で語られているように、CASE式を「式」として理解し活用することは、SQLにおけるパラダイムシフトです。
- クロス集計による柔軟なデータ変換
- UNION排除によるパフォーマンスの劇的な改善
これらを使いこなすことで、アプリケーション側のコードを減らし、データベースエンジンが持つ本来のパワーを引き出すことができます。「手続き型(ループと分岐)」の思考から「宣言型(集合と評価)」の思考へ切り替える第一歩として、CASE式は非常に優れた教材だと思います。
これまでは、CASE式を単なる条件分岐の構文として理解していたので、本書は非常に腑に落ちる内容となりました。本記事が、皆さんのSQL力向上のきっかけになれば幸いです。