- SQL
SQL CASE式の使い方|用途別コピペ集とDB比較・実行結果つき
CASE式とは、SQLの中で「条件に応じて返す値を切り替える」ための式です。 if文やswitch文にあたり、SELECT・WHERE・ORDER BY・GROUP BY・UPDATEのSET・集計関数の中など、値を書ける場所ならどこでも使えます。
この記事では、単純CASEと検索CASEの違いと、区分の変換、クロス集計、任意の並び順、一括更新、0除算の回避など用途別のコピペSQLをまとめました。PostgreSQL 16.13とMySQL 8.0.46で実行した結果とエラー文をそのまま掲載しています(OracleとSQL Serverはドキュメント準拠)。ELSEの省略で値がNULLになるUPDATEや、COUNTとCASEで件数が合わない例も実際に確認します。
CASE式の早見表
書き方と用途の早見表
| やりたいこと | 書き方の例 |
|---|---|
| 値を別の値に置き換える(単純CASE) | CASE status WHEN 'todo' THEN '未着手' … END |
| 範囲や複数条件で分ける(検索CASE) | CASE WHEN score >= 90 THEN 'A' WHEN … END |
| NULLを判定する | CASE WHEN col IS NULL THEN '未設定' … END |
| 縦のデータを横に並べる | SUM(CASE WHEN 条件 THEN 1 ELSE 0 END) |
| 任意の順番で並べる | ORDER BY CASE priority WHEN 'high' THEN 1 … END |
| 条件別に一括更新 | UPDATE t SET col = CASE … ELSE col END WHERE … |
| 0除算を避ける | a / NULLIF(b, 0) |
CASEと似た関数のDB比較
| 関数 | 意味 | MySQL 8.0(実行確認済み) | PostgreSQL 16(実行確認済み) | Oracle(ドキュメント準拠) | SQL Server(ドキュメント準拠) |
|---|---|---|---|---|---|
CASE |
標準SQLの条件分岐 | ○ | ○ | ○ | ○ |
IF(条件, 真, 偽) |
2択の条件分岐 | ○ | ×(CASEを使う) | × | × |
IIF(条件, 真, 偽) |
2択の条件分岐 | × | × | × | ○(2012以降) |
DECODE(列, 値1, 結果1, …, 既定) |
値の置き換え | ×(別機能の暗号関数は8.0で削除) | × | ○ | × |
COALESCE(a, b, …) |
最初のNULLでない値 | ○ | ○ | ○ | ○ |
NULLIF(a, b) |
a = bならNULL | ○ | ○ | ○ | ○ |
IFNULL / NVL / ISNULL |
NULLなら置き換え | IFNULL |
×(COALESCE) |
NVL |
ISNULL |
CASE・COALESCE・NULLIFなら、どのDBでもそのまま動きます。 IF・IIF・DECODEは短く書けますが、DBごとの書き換えが必要です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL CASE式の使い方|用途別コピペ集とDB比較・実行結果つき」
サンプルデータ(コピペ用)
優先度や点数がNULLの行、分母が0の行を含むデータです。両DBでそのまま動きます。
CREATE TABLE tasks (
id INT PRIMARY KEY, title VARCHAR(20), status VARCHAR(10),
priority VARCHAR(10), score INT, done_cnt INT, total_cnt INT
);
INSERT INTO tasks VALUES
(1,'設計レビュー','doing','high',92,3,4),
(2,'画面実装','todo','mid',75,0,5),
(3,'API実装','done','high',60,6,6),
(4,'テスト作成','todo',NULL,45,0,0),
(5,'資料更新','doing','low',NULL,1,2);
実行環境は PostgreSQL 16.13とMySQL 8.0.46 です。出力は断りがない限りPostgreSQL(psql)の表示で、MySQLでも同じ値でした。
単純CASEと検索CASEの違い
CASE式には、1つの値と候補を=で比べる「単純CASE」と、条件式を自由に書ける「検索CASE」の2種類があります。
| 単純CASE | 検索CASE | |
|---|---|---|
| 形 | CASE 式 WHEN 値 THEN 結果 … END |
CASE WHEN 条件 THEN 結果 … END |
| 比較 | =のみ |
>=、LIKE、IN、IS NULL、AND/ORなど何でも |
| NULLの判定 | できない | できる |
| 向いている用途 | コード値の置き換え | 範囲、複数列、NULLを含む条件 |
単純CASE:コード値を表示名に置き換える
SELECT id, status,
CASE status
WHEN 'todo' THEN '未着手'
WHEN 'doing' THEN '作業中'
WHEN 'done' THEN '完了'
ELSE 'その他'
END AS status_ja
FROM tasks ORDER BY id;
結果は行1・5が「作業中」、行2・4が「未着手」、行3が「完了」でした。
検索CASE:範囲で分ける
SELECT id, score,
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 70 THEN 'B'
WHEN score >= 50 THEN 'C'
ELSE 'D'
END AS rank
FROM tasks ORDER BY id;
id | score | rank
----+-------+------
1 | 92 | A
2 | 75 | B
3 | 60 | C
4 | 45 | D
5 | | D
scoreがNULLの行5は、どのWHENも真にならないためELSEの「D」になります。 別扱いにするなら先頭にWHEN score IS NULL THEN '未採点'を置きます。
評価のルール:上から順に、最初に真になったものだけ
直す前:条件の順番が逆
CASEは上のWHENから順に調べ、最初に真になったTHENの値を返して終わります。 範囲の条件を小さい順に書くと、すべて最初の条件に当てはまってしまいます。
SELECT id, score,
CASE WHEN score >= 50 THEN 'C'
WHEN score >= 70 THEN 'B'
WHEN score >= 90 THEN 'A'
ELSE 'D' END AS rank_wrong
FROM tasks ORDER BY id;
id | score | rank_wrong
----+-------+------------
1 | 92 | C
2 | 75 | C
3 | 60 | C
4 | 45 | D
5 | | D
直した後は、前の「検索CASE」の例のように、厳しい条件(大きい値)から順に書きます。
ELSEを省略するとNULLになる
どのWHENにも当てはまらず、ELSEもないときの結果はNULLです。 CASE WHEN score >= 70 THEN '合格' ENDとすると、行1・2は「合格」、行3〜5はNULLになりました。NULLにしたいとき以外はELSEを書きます。
単純CASEはNULLに一致しない
直す前:WHEN NULLと書いても一致しない
SELECT id, priority,
CASE priority WHEN 'high' THEN '高' WHEN NULL THEN '未設定' ELSE 'その他' END AS p1
FROM tasks ORDER BY id;
id | priority | p1
----+----------+--------
1 | high | 高
2 | mid | その他
3 | high | 高
4 | | その他
5 | low | その他
単純CASEは内部でpriority = NULLと比べ、NULLとの=は真にならないため、行4は「その他」になります。 MySQL 8.0でも同じでした。
直した後:検索CASEでIS NULLを使う
SELECT id, priority,
CASE WHEN priority IS NULL THEN '未設定'
WHEN priority = 'high' THEN '高'
ELSE 'その他' END AS p2
FROM tasks ORDER BY id;
行4だけが未設定になりました。NULLを置き換えるだけならCOALESCE(priority, '未設定')が短く書けます。
用途別コピペ集
クロス集計(縦のデータを横に並べる)
SUM(CASE WHEN 条件 THEN 1 ELSE 0 END)を列の数だけ並べると、行の値を列に展開できます。 ステータス別に、優先度ごとの件数を横に並べます。
SELECT status,
COUNT(*) AS cnt,
SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) AS high,
SUM(CASE WHEN priority = 'mid' THEN 1 ELSE 0 END) AS mid,
SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) AS low
FROM tasks
GROUP BY status
ORDER BY CASE status WHEN 'todo' THEN 1 WHEN 'doing' THEN 2 ELSE 3 END;
status | cnt | high | mid | low
--------+-----+------+-----+-----
todo | 2 | 0 | 1 | 0
doing | 2 | 1 | 0 | 1
done | 1 | 1 | 0 | 0
PostgreSQLはCOUNT(*) FILTER (WHERE …)とも書けます(詳しくは「SQL GROUP BY HAVING 使い方」の記事)。
ORDER BYで任意の並び順にする
アルファベット順ではなく決まった順番で並べたいときは、ORDER BYにCASEを書きます。
SELECT id, title, priority
FROM tasks
ORDER BY CASE priority WHEN 'high' THEN 1 WHEN 'mid' THEN 2 WHEN 'low' THEN 3 ELSE 4 END, id;
id | title | priority
----+--------------+----------
1 | 設計レビュー | high
3 | API実装 | high
2 | 画面実装 | mid
5 | 資料更新 | low
4 | テスト作成 |
MySQLにはORDER BY FIELD(priority, 'high', 'mid', 'low')という専用関数もありますが、一覧にない値(NULL)は0として先頭に来ます。CASEならNULLの位置もELSEで決められます。
UPDATEで条件別に一括更新する
1回のUPDATEで、カテゴリごとに違う更新をかけられます。 ここではELSEの省略が大きな差になります。
CREATE TABLE items (id INT PRIMARY KEY, name VARCHAR(20), category VARCHAR(10), price INT);
INSERT INTO items VALUES (1,'ノート','文具',200),(2,'ボールペン','文具',150),
(3,'マウス','PC',2000),(4,'ケーブル','PC',800),(5,'付箋','消耗品',300);
直す前(ELSEなし・WHEREなし):
UPDATE items
SET price = CASE category WHEN '文具' THEN price * 1.1 WHEN 'PC' THEN price * 0.9 END;
UPDATE 5
id | name | category | price
----+------------+----------+-------
1 | ノート | 文具 | 220
2 | ボールペン | 文具 | 165
3 | マウス | PC | 1800
4 | ケーブル | PC | 720
5 | 付箋 | 消耗品 |
「消耗品」はどのWHENにも当たらずCASEがNULLを返したため、付箋の価格がNULLで上書きされました。 MySQL 8.0でも同じくNULLになりました。
直した後(ELSE priceとWHEREを両方書く):
UPDATE items
SET price = CASE category WHEN '文具' THEN price * 1.1 WHEN 'PC' THEN price * 0.9 ELSE price END
WHERE category IN ('文具', 'PC');
UPDATE 4
id | name | category | price
----+------------+----------+-------
1 | ノート | 文具 | 220
2 | ボールペン | 文具 | 165
3 | マウス | PC | 1800
4 | ケーブル | PC | 720
5 | 付箋 | 消耗品 | 300
文具とPCだけ価格が変わり、付箋は300のまま残りました。
ELSE 元の列で「当てはまらない行は変えない」ことを保証し、WHEREで更新対象を絞ると、意図しない行が変わりません。
IDごとに違う値を入れるSET name = CASE id WHEN 1 THEN 'A5ノート' WHEN 3 THEN '無線マウス' END WHERE id IN (1, 3)も、WHEREをWHENと同じ行に絞っているため2行だけが更新されました(UPDATE 2)。別テーブルの値を使う更新は「SQLで別テーブルの値を使ってUPDATEする方法」の記事で扱っています。
WHERE・JOINの条件にCASEを使う
行ごとに条件を切り替えたいときは、WHEREの中にCASEを書けます。 完了タスクは50点以上、それ以外は80点以上を抽出します。
SELECT id, title FROM tasks
WHERE CASE WHEN status = 'done' THEN score >= 50 ELSE score >= 80 END
ORDER BY id;
結果は行1(92点)と行3(完了・60点)の2行で、MySQLでも同じでした。JOINのONにも書けます。ただしWHEREのCASEはインデックスが使われにくいため、(status = 'done' AND score >= 50) OR (status <> 'done' AND score >= 80)と書けるならそちらが最適化されやすくなります。
0除算を避ける
割る数が0の行があると、PostgreSQLはエラー、MySQLはNULLと警告になります。
SELECT id, done_cnt * 100 / total_cnt AS pct FROM tasks ORDER BY id;
PostgreSQL 16はERROR: division by zeroで止まり、MySQL 8.0はエラーにならず行4がNULLになってWarning 1365: Division by 0が出ました。
直した後(CASE版とNULLIF版):
SELECT id, done_cnt, total_cnt,
CASE WHEN total_cnt = 0 THEN NULL
ELSE ROUND(done_cnt * 100.0 / total_cnt, 1) END AS pct,
ROUND(done_cnt * 100.0 / NULLIF(total_cnt, 0), 1) AS pct2
FROM tasks ORDER BY id;
id | done_cnt | total_cnt | pct | pct2
----+----------+-----------+-------+-------
1 | 3 | 4 | 75.0 | 75.0
2 | 0 | 5 | 0.0 | 0.0
3 | 6 | 6 | 100.0 | 100.0
4 | 0 | 0 | |
5 | 1 | 2 | 50.0 | 50.0
NULLIF(total_cnt, 0)は「0ならNULL」を返すので、割り算の結果がNULLになります。 0%と表示したいなら外側をCOALESCE(…, 0)で包みます。100.0と書くのは、PostgreSQLの整数同士の割り算で小数が切り捨てられるのを避けるためです。
CASEの入れ子と複数条件
CASEの中にCASEを書けますが、多くの場合は1段のCASEに直せます。
SELECT id,
CASE WHEN status = 'done' THEN '完了'
ELSE CASE WHEN priority = 'high' THEN '急ぎ' ELSE '通常' END
END AS label,
CASE WHEN status = 'done' THEN '完了'
WHEN priority = 'high' THEN '急ぎ'
ELSE '通常' END AS label2
FROM tasks ORDER BY id;
labelとlabel2は全行で同じ結果(急ぎ・通常・完了・通常・通常)になりました。上から順に評価されるルールを使えば、入れ子にしなくても同じ結果になります。 2段目のWHENは「doneではない」ことが前提になっているためです。
型の不一致エラー(THENとELSEの型をそろえる)
CASEの結果は1つの列になるため、すべてのTHEN・ELSEの型がそろっている必要があります。 PostgreSQLは厳密で、MySQLは文字列にそろえて返します。
SELECT id, CASE WHEN score >= 70 THEN 1 ELSE 'なし' END AS x FROM tasks;
PostgreSQL 16:
ERROR: invalid input syntax for type integer: "なし"
LINE 1: SELECT id, CASE WHEN score >= 70 THEN 1 ELSE 'なし' END AS x...
^
列同士の型が違う場合(THEN score ELSE title)はERROR: CASE types character varying and integer cannot be matchedになりました。
MySQL 8.0では最初のSQLがエラーにならず、1となしが混ざった文字列の列として返りました。
直した後は、数値を文字列に変換して型をそろえます。PostgreSQLはTHEN score::text ELSE 'なし'、MySQLはTHEN CAST(score AS CHAR) ELSE 'なし'で、両DBとも「92、75、なし、なし、なし」になりました。画面表示用の文字列はCASEで作り、数値として使う列は数値のまま残すと、後の計算で困りません。
集計関数と組み合わせたときの落とし穴
COUNTにELSE 0を書くと全件数になる
SELECT COUNT(*) AS all_cnt,
COUNT(CASE WHEN status = 'todo' THEN 1 ELSE 0 END) AS wrong,
COUNT(CASE WHEN status = 'todo' THEN 1 END) AS ok1,
SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END) AS ok2
FROM tasks;
all_cnt | wrong | ok1 | ok2
---------+-------+-----+-----
5 | 5 | 2 | 2
COUNTは「NULLでない値」を数えるため、ELSE 0の0も1件として数えます。 COUNTならELSEを書かずにNULLにする、SUMならELSE 0、と覚えておくと迷いません(MySQL 8.0でも同じ値。MySQLはSUM(status = 'todo')とも書けます)。
1件も当てはまらないとSUMはNULL
SELECT SUM(CASE WHEN status = 'canceled' THEN 1 END) AS no_else,
SUM(CASE WHEN status = 'canceled' THEN 1 ELSE 0 END) AS with_else
FROM tasks;
no_else | with_else
---------+-----------
| 0
件数の集計でSUMを使うときはELSE 0を書くと、0件のときも0が返ります。
PostgreSQLでは、CASEで守ったつもりの計算が先に評価されることがある
CASEは通常、真になったTHENだけを評価しますが、PostgreSQLのマニュアルには例外が2つ書かれています。 実際に再現できました。
-- 定数の式は実行前に計算される
SELECT CASE WHEN id > 0 THEN id ELSE 1/0 END FROM tasks LIMIT 1;
-- 集計関数はCASEの判定より先に計算される
SELECT CASE WHEN MIN(total_cnt) > 0 THEN AVG(done_cnt / total_cnt) END FROM tasks;
PostgreSQL 16(2つとも):
ERROR: division by zero
1つ目は、ELSEに進む行がなくても定数の1/0が計画時に計算されるためです(MySQL 8.0では1が返りました)。2つ目は、MIN(total_cnt) > 0の判定より先に全行のAVG(done_cnt / total_cnt)が計算されるためです。集計の中で0除算を避けたいときは、CASEの外側ではなくAVG(done_cnt / NULLIF(total_cnt, 0))のように集計の内側で守ります。
チェックリスト:CASE式を書いたら確認すること
| 確認すること | 理由 |
|---|---|
| 範囲の条件は厳しい順か | 最初に真になったWHENで終わる |
| ELSEを書いたか | 省略するとNULL。UPDATEではNULLで上書き |
NULL判定はIS NULLか |
単純CASEのWHEN NULLは一致しない |
| THEN・ELSEの型はそろっているか | PGはエラー、MySQLは文字列になる |
COUNTにELSE 0を書いていないか |
0も1件と数えられる |
| UPDATEにWHEREを書いたか | 対象外の行まで更新しない |
| 0除算はNULLIFで内側から防いだか | PGでは先に評価されることがある |
よくある質問
Q. CASE「文」とCASE「式」は違うのですか?
A. SELECTやUPDATEの中で使うのは値を返すCASE式です。MySQLのストアドプロシージャには処理を分岐するCASE文(END CASEで閉じる)が別にあり、どのWHENにも当たらずELSEもないとERROR 1339 (20000): Case not found for CASE statementになりました(MySQL 8.0で確認)。CASE式ならNULLが返るだけです。
Q. IF関数とCASEはどちらを使えばよいですか?
A. MySQL専用ならIF(score >= 70, '合格', '不合格')が短く書けます。他のDBでも動かす可能性があるならCASEを選ぶと書き換えが不要です。
Q. GROUP BYにCASEを書けますか?
A. 書けます。SELECTにCASE WHEN score >= 70 THEN '合格' ELSE '不合格' END AS rと書き、GROUP BY rとすると、両DBで「不合格 3・合格 2」になりました。
まとめ
- CASE式は値を書ける場所ならどこでも使える条件分岐。SELECT・WHERE・ORDER BY・UPDATE・集計の中で使える
- 単純CASEは
=での置き換え、検索CASEは範囲やNULLなど自由な条件 - 上から順に評価され、最初に真になったTHENだけが返る。範囲は厳しい順に書く
- ELSEを省略するとNULL。UPDATEでは
ELSE 元の列とWHEREを必ずセットにする - 単純CASEの
WHEN NULLは一致しない。IS NULLかCOALESCEを使う - THEN・ELSEの型をそろえる。PostgreSQLは型が違うとエラーになる
- 集計ではCOUNTにELSE 0を書かない、SUMにはELSE 0、0除算はNULLIFで内側から防ぐ
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL CASE式の使い方|用途別コピペ集とDB比較・実行結果つき」
株式会社RJCでは、読みやすく安全なSQLの書き方を、レビューを通して仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。