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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?