MySQLのIFは「条件が真ならA、そうでなければB」を返す関数、IFNULLは「値がNULLなら代わりの値」を返す関数です。 よく似た NULLIF・COALESCE・CASE と合わせて5つを押さえると、SELECT の中の条件分岐はほぼすべて書けるようになります。

この記事では、5つの違いを早見表にまとめ、同じテーブルに対して5つを実行した結果を並べました。条件付き集計、ゼロ除算の回避、空文字と NULL の扱い、並べ替え、UPDATE でのコピペ用 SQL も、すべて MySQL 8.0.46 で実際に実行した出力つきで載せています。

IF・IFNULL・NULLIF・COALESCE・CASEの早見表

関数・式 引数の数 返す値 標準SQL 主な用途
IF(条件, A, B) 3 条件が真なら A、偽または NULL なら B MySQL 独自 2択の分岐、条件付き集計
IFNULL(X, Y) 2 X が NULL でなければ X、NULL なら Y MySQL 独自 NULL を既定値に置き換える
NULLIF(X, Y) 2 X = Y なら NULL、違えば X 標準 0 や空文字を NULL に変える、ゼロ除算の回避
COALESCE(X, Y, ...) 1以上 左から見て最初の NULL でない値。すべて NULL なら NULL 標準 複数の候補から最初の値を選ぶ
CASE WHEN 条件 THEN A ... ELSE Z END 条件をいくつでも 最初に真になった WHEN の値。どれも真でなく ELSE も無ければ NULL 標準 3択以上の分岐、範囲での区分け

「標準SQL」の列は一般に知られた仕様に基づく記載です。後半で PostgreSQL 16 でも CASE・NULLIF・COALESCE が動き、IF・IFNULL は関数が無いことを実行して確かめています。

迷ったときの選び方

やりたいこと 選ぶもの 例
NULL を 0 や文字に置き換えたい IFNULL(候補が3つ以上なら COALESCE) IFNULL(points, 0)
2択で分けたい IF IF(points >= 1000, '上位', '通常')
3択以上で分けたい CASE CASE WHEN ... THEN ... END
0 で割るのを避けたい NULLIF a / NULLIF(b, 0)
空文字も NULL と同じに扱いたい NULLIF と IFNULL の組み合わせ IFNULL(NULLIF(col, ''), '(未設定)')
他のデータベースでも同じ SQL を使いたい CASE / COALESCE / NULLIF IF と IFNULL は書き換えが必要

同じテーブルで5つを実行して比べる

検証用のテーブルです。NULL、0、空文字、空白だけの文字列をわざと混ぜています。

CREATE TABLE members (
  id INT PRIMARY KEY, name VARCHAR(20), nickname VARCHAR(20),
  points INT, orders INT, rank_code CHAR(1), memo VARCHAR(20)
);
INSERT INTO members VALUES
 (1,'佐藤','さとちゃん',1200,4,'A','VIP'),
 (2,'鈴木',NULL,300,0,'B',''),
 (3,'高橋','',NULL,2,'C',NULL),
 (4,'田中','たなか',0,0,NULL,'  '),
 (5,'伊藤',NULL,NULL,NULL,'A','新規');
SELECT id, name,
  IF(points >= 1000, '上位', '通常')        AS if_r,
  IFNULL(points, 0)                         AS ifnull_r,
  NULLIF(points, 0)                         AS nullif_r,
  COALESCE(nickname, memo, name)            AS coalesce_r,
  CASE WHEN points >= 1000 THEN '上位'
       WHEN points >= 1    THEN '通常'
       WHEN points = 0     THEN 'ゼロ'
       ELSE '未設定' END                    AS case_r
FROM members ORDER BY id;

実行結果(MySQL 8.0.46):

+----+--------+--------+----------+----------+-----------------+-----------+
| id | name   | if_r   | ifnull_r | nullif_r | coalesce_r      | case_r    |
+----+--------+--------+----------+----------+-----------------+-----------+
|  1 | 佐藤   | 上位   |     1200 |     1200 | さとちゃん      | 上位      |
|  2 | 鈴木   | 通常   |      300 |      300 |                 | 通常      |
|  3 | 高橋   | 通常   |        0 |     NULL |                 | 未設定    |
|  4 | 田中   | 通常   |        0 |     NULL | たなか          | ゼロ      |
|  5 | 伊藤   | 通常   |        0 |     NULL | 新規            | 未設定    |
+----+--------+--------+----------+----------+-----------------+-----------+

この1つの結果に、5つの性質がすべて表れています。

  • IF は、条件が NULL のときも「偽」の側を返します。 points が NULL の id=3、5 は NULL >= 1000 が NULL になり、「通常」になりました。未設定を区別したいなら CASE か、IF を入れ子にします。
  • IFNULL は NULL だけを置き換えます。 id=3、5 が 0 になりました。もともと 0 の id=4 と見分けが付かなくなる点は意識しておきます。
  • NULLIF は逆向きで、指定した値(ここでは 0)を NULL に変えます。 id=4 の 0 が NULL になりました。
  • COALESCE は左から順に、最初の NULL でない値を返します。 id=5 は nickname が NULL なので memo の「新規」になりました。id=2 は memo が空文字、id=3 は nickname が空文字で、空文字は NULL ではないため、そのまま選ばれています。(対処は後の「空文字と NULL をまとめて扱う」で説明します。)
  • CASE は上から順に評価し、最初に真になったところで決まります。 NULL はどの比較も真にならないので ELSE の「未設定」になりました。

IFの使い方(2択の分岐)

条件の真偽の決まり方

SELECT IF(NULL,'真','偽')  AS a,
       IF(0,'真','偽')     AS b,
       IF('abc','真','偽') AS c,
       IF(2,'真','偽')     AS d,
       IF(NULL = NULL,'等しい','等しくない/不明') AS e;
+-----+-----+-----+-----+------------------------+
| a   | b   | c   | d   | e                      |
+-----+-----+-----+-----+------------------------+
| 偽  | 偽  | 偽  | 真  | 等しくない/不明        |
+-----+-----+-----+-----+------------------------+
条件の値 IF の結果 補足
0 以外の数値 真の側 2 でも −1 でも真
0 偽の側
NULL 偽の側 NULL = NULL も NULL になるので偽の側
数字で始まらない文字列('abc') 偽の側 数値 0 として評価され、警告 1292(Truncated incorrect DOUBLE value: 'abc')が出る

NULL かどうかの判定には = NULL ではなく IS NULL を使います。 points = NULL は常に NULL になり、points <=> NULL(NULL 安全な等価演算子)は 1 か 0 を返すことも確認しました。

NULLを区別したいときは入れ子にするかCASEにする

SELECT id, points,
       IF(points > 500, '多い', '少ない') AS simple,
       IF(points IS NULL, '未設定', IF(points > 500, '多い', '少ない')) AS nested
FROM members;
+----+--------+-----------+-----------+
| id | points | simple    | nested    |
+----+--------+-----------+-----------+
|  1 |   1200 | 多い      | 多い      |
|  2 |    300 | 少ない    | 少ない    |
|  3 |   NULL | 少ない    | 未設定    |
|  4 |      0 | 少ない    | 少ない    |
|  5 |   NULL | 少ない    | 未設定    |
+----+--------+-----------+-----------+

入れ子が2段以上になったら、CASE に書き換えると読みやすくなります。

IF関数とIF文は別のもの

SELECT の中で使う IF(条件, A, B) は関数です。ストアドプログラムの中で使う IF ... THEN ... END IF は制御文で、書き方が違います。

DELIMITER //
CREATE FUNCTION grade(p INT) RETURNS VARCHAR(10) DETERMINISTIC
BEGIN
  IF p IS NULL THEN RETURN '未設定';
  ELSEIF p >= 1000 THEN RETURN '上位';
  ELSE RETURN '通常';
  END IF;
END//
DELIMITER ;

SELECT id, grade(points) FROM members;
-- 1:上位 2:通常 3:未設定 4:通常 5:未設定(実行して確認)

IFNULLとCOALESCEの使い方(NULLを置き換える)

SELECT COALESCE(NULL, NULL)   AS a,
       COALESCE(NULL, 0, 5)   AS b,
       IFNULL(NULL, NULL)     AS c;
-- a: NULL / b: 0 / c: NULL
  • IFNULL は引数が必ず2つです。3つ渡すと ERROR 1582 (42000): Incorrect parameter count in the call to native function 'IFNULL' になりました。
  • COALESCE は引数をいくつでも並べられます。 COALESCE(NULL, 0, 5) が 0 を返すとおり、0 は NULL ではないので選ばれます。
  • 2つの引数で使うなら、IFNULL(X, Y) と COALESCE(X, Y) は同じ結果になります。
比較 IFNULL COALESCE
引数 2つ固定 1つ以上
標準SQL MySQL 独自 標準
向いている場面 MySQL だけで使う短い SQL 候補が3つ以上、他の製品でも使う SQL

集計結果が NULL になるのを 0 にする

対象の行が0件のとき、SUM は 0 ではなく NULL を返します。 画面や帳票に出す値は IFNULL で包んでおくと安心です。

SELECT (SELECT SUM(points) FROM members WHERE id = 99)            AS s_none,
       IFNULL((SELECT SUM(points) FROM members WHERE id = 99), 0) AS s_none0;
-- s_none: NULL / s_none0: 0

平均を取るときは「NULLを0にするか」で結果が変わる

SELECT IFNULL(rank_code,'(なし)') AS rank_code, COUNT(*) AS n,
       SUM(IFNULL(points,0)) AS pts,
       AVG(points)           AS avg_skip_null,
       AVG(IFNULL(points,0)) AS avg_zero
FROM members GROUP BY rank_code ORDER BY 1;
+-----------+---+------+---------------+----------+
| rank_code | n | pts  | avg_skip_null | avg_zero |
+-----------+---+------+---------------+----------+
| (なし)    | 1 |    0 |        0.0000 |   0.0000 |
| A         | 2 | 1200 |     1200.0000 | 600.0000 |
| B         | 1 |  300 |      300.0000 | 300.0000 |
| C         | 1 |    0 |          NULL |   0.0000 |
+-----------+---+------+---------------+----------+

ランク A は「1200 と NULL」の2人です。AVG(points) は NULL を除いて 1200、AVG(IFNULL(points,0)) は NULL を 0 として 600 になります。「未入力は対象外」なのか「未入力は 0 点」なのかを決めてから選びます。

NULLIFの使い方(ゼロ除算の回避)

SELECT NULLIF(NULL, 1)   AS d,   -- NULL
       NULLIF(1, NULL)   AS e,   -- 1
       NULLIF('a', 'A')  AS f,   -- NULL(既定の照合順序では大文字小文字を区別しない)
       NULLIF('a ', 'a') AS g;   -- 'a '(今回の環境では末尾の空白は区別された)

NULLIF の比較は = と同じ規則です。文字列では、列や接続の照合順序によって大文字小文字の扱いが変わります。

0で割る計算を安全にする

SELECT id, points, orders,
       points / orders                                AS raw_div,
       points / NULLIF(orders, 0)                     AS safe_div,
       IFNULL(ROUND(points / NULLIF(orders, 0), 1), 0) AS with_default
FROM members;
+----+--------+--------+----------+----------+--------------+
| id | points | orders | raw_div  | safe_div | with_default |
+----+--------+--------+----------+----------+--------------+
|  1 |   1200 |      4 | 300.0000 | 300.0000 |        300.0 |
|  2 |    300 |      0 |     NULL |     NULL |          0.0 |
|  3 |   NULL |      2 |     NULL |     NULL |          0.0 |
|  4 |      0 |      0 |     NULL |     NULL |          0.0 |
|  5 |   NULL |   NULL |     NULL |     NULL |          0.0 |
+----+--------+--------+----------+----------+--------------+

SELECT では、どちらの書き方も結果は NULL です。違いは警告とエラーに出ます。points / orders のほうは 0 で割った2行について警告が出ました。

+---------+------+---------------+
| Level   | Code | Message       |
+---------+------+---------------+
| Warning | 1365 | Division by 0 |
| Warning | 1365 | Division by 0 |
+---------+------+---------------+

さらに、既定の sql_mode(STRICT_TRANS_TABLES と ERROR_FOR_DIVISION_BY_ZERO を含む)では、INSERT や UPDATE の中で 0 で割るとエラーで止まります。

CREATE TABLE d (x INT);
INSERT INTO d SELECT 10 / 0;
-- ERROR 1365 (22012): Division by 0

INSERT INTO d SELECT 10 / NULLIF(0, 0);
-- 成功。x に NULL が入る

割る数を NULLIF(列, 0) で包んでおくと、SELECT では警告が出ず、INSERT・UPDATE でも止まりません。 結果を 0 と表示したい場合は、外側を IFNULL で包みます(上の with_default 列)。

CASEの使い方(3択以上の分岐)

CASE には2つの書き方があります。

書き方 形 向いている場面
単純 CASE CASE 列 WHEN 値1 THEN ... WHEN 値2 THEN ... END 1つの列を、決まった値と比べる
検索 CASE CASE WHEN 条件1 THEN ... WHEN 条件2 THEN ... END 範囲、複数の列、IS NULL などの条件
SELECT id, rank_code,
  CASE rank_code WHEN 'A' THEN 'ゴールド' WHEN 'B' THEN 'シルバー' END              AS no_else,
  CASE rank_code WHEN 'A' THEN 'ゴールド' WHEN NULL THEN 'なし' ELSE 'その他' END   AS when_null,
  CASE WHEN rank_code IS NULL THEN 'なし' ELSE rank_code END                        AS searched
FROM members;
+----+-----------+--------------+--------------+----------+
| id | rank_code | no_else      | when_null    | searched |
+----+-----------+--------------+--------------+----------+
|  1 | A         | ゴールド     | ゴールド     | A        |
|  2 | B         | シルバー     | その他       | B        |
|  3 | C         | NULL         | その他       | C        |
|  4 | NULL      | NULL         | その他       | なし     |
|  5 | A         | ゴールド     | ゴールド     | A        |
+----+-----------+--------------+--------------+----------+
  • ELSE を書かないと、どの WHEN にも当てはまらない行は NULL になります(no_else の id=3、4)。
  • 単純 CASE の WHEN NULL は一致しません(when_null の id=4 が「なし」ではなく「その他」)。内部では rank_code = NULL と比べているためです。NULL を判定するときは、検索 CASE で WHEN rank_code IS NULL と書きます。

条件付き集計のコピペ集(SUM(IF)とCOUNT(CASE))

WHERE で絞らずに、1回の SELECT で複数の条件の件数や合計を出す書き方です。

SELECT COUNT(*)                                             AS total,
       SUM(IF(rank_code = 'A', 1, 0))                       AS sum_if,
       COUNT(IF(rank_code = 'A', 1, NULL))                  AS count_if,
       COUNT(CASE WHEN rank_code = 'A' THEN 1 END)          AS count_case,
       SUM(rank_code = 'A')                                 AS sum_bool,
       SUM(CASE WHEN points >= 1000 THEN points ELSE 0 END) AS pts_hi,
       COUNT(IF(rank_code = 'A', 1, 0))                     AS count_if_zero
FROM members;
+-------+--------+----------+------------+----------+--------+---------------+
| total | sum_if | count_if | count_case | sum_bool | pts_hi | count_if_zero |
+-------+--------+----------+------------+----------+--------+---------------+
|     5 |      2 |        2 |          2 |        2 |   1200 |             5 |
+-------+--------+----------+------------+----------+--------+---------------+
書き方 結果 ポイント
SUM(IF(条件, 1, 0)) 2 条件に合う行を 1 として合計する
COUNT(IF(条件, 1, NULL)) 2 COUNT は NULL を数えないので、合わない行を NULL にする
COUNT(CASE WHEN 条件 THEN 1 END) 2 ELSE を省くと NULL になる性質を使う。他の製品でも使える書き方
SUM(条件) 2 MySQL では比較の結果が 1 / 0 になる。短いが MySQL 向けの書き方
COUNT(IF(条件, 1, 0)) 5 0 は NULL ではないので全行が数えられる。意図と違う結果になる例

COUNT と組み合わせるときは「合わない行を NULL にする」、SUM と組み合わせるときは「合わない行を 0 にする」 と覚えると迷いません。

条件ごとの最大・最小・割合

SELECT MAX(CASE WHEN rank_code = 'A' THEN points END)      AS max_a,     -- 1200
       MIN(IF(rank_code = 'B', points, NULL))              AS min_b,     -- 300
       COUNT(DISTINCT IF(points > 0, rank_code, NULL))     AS kinds,     -- 2
       ROUND(100 * SUM(points IS NULL) / COUNT(*), 1)      AS null_pct   -- 40.0
FROM members;

グループごとに出したいときは、同じ式に GROUP BY を付けます。

SELECT rank_code, COUNT(*) AS n, SUM(orders > 0) AS has_order
FROM members GROUP BY rank_code;
-- A: n=2, has_order=1 / B: 1, 0 / C: 1, 1 / NULL: 1, 0(実行して確認)

空文字とNULLをまとめて扱う

「未入力」が NULL と空文字の両方で入っているテーブルでは、NULLIF で空文字を NULL にそろえてから IFNULL で置き換える書き方が便利です。

SELECT id, nickname,
       IFNULL(nickname, '(未設定)')                         AS only_null,
       IFNULL(NULLIF(nickname, ''), '(未設定)')             AS null_and_empty,
       COALESCE(NULLIF(TRIM(memo), ''), '(メモなし)')       AS memo_trim,
       IF(nickname IS NULL OR nickname = '', '(未設定)', nickname) AS by_if
FROM members;
+----+-----------------+-----------------+-----------------+----------------+-----------------+
| id | nickname        | only_null       | null_and_empty  | memo_trim      | by_if           |
+----+-----------------+-----------------+-----------------+----------------+-----------------+
|  1 | さとちゃん      | さとちゃん      | さとちゃん      | VIP            | さとちゃん      |
|  2 | NULL            | (未設定)        | (未設定)        | (メモなし)     | (未設定)        |
|  3 |                 |                 | (未設定)        | (メモなし)     | (未設定)        |
|  4 | たなか          | たなか          | たなか          | (メモなし)     | たなか          |
|  5 | NULL            | (未設定)        | (未設定)        | 新規           | (未設定)        |
+----+-----------------+-----------------+-----------------+----------------+-----------------+
  • only_null(IFNULL だけ)では、id=3 の空文字がそのまま残ります。
  • IFNULL(NULLIF(列, ''), 既定値) で、NULL と空文字の両方を置き換えられます。
  • 空白だけの文字列(id=4 の memo)も対象にするなら、TRIM を挟みます。

WHERE 句で「未入力の行」を探すときは、次の3つが同じ件数(3件)を返しました。

SELECT COUNT(*) FROM members WHERE nickname IS NULL OR nickname = '';
SELECT COUNT(*) FROM members WHERE NULLIF(nickname, '') IS NULL;
SELECT COUNT(*) FROM members WHERE IFNULL(nickname, '') = '';

ORDER BYで並べ替えに使う

NULLを最後に回す

MySQL の昇順では NULL が先頭に来ます。

SELECT id, points FROM members ORDER BY points;
-- 並び: 3(NULL), 5(NULL), 4(0), 2(300), 1(1200)

SELECT id, points FROM members ORDER BY points IS NULL, points;
-- 並び: 4(0), 2(300), 1(1200), 3(NULL), 5(NULL)

points IS NULL は NULL の行で 1、それ以外で 0 になるので、先にこの式で並べると NULL が後ろに回ります。降順(ORDER BY points DESC)では、もともと NULL が最後になりました。

決まった順番で並べる

SELECT id, rank_code FROM members
ORDER BY CASE rank_code WHEN 'B' THEN 1 WHEN 'A' THEN 2 ELSE 9 END, id;
+----+-----------+
| id | rank_code |
+----+-----------+
|  2 | B         |
|  1 | A         |
|  5 | A         |
|  3 | C         |
|  4 | NULL      |
+----+-----------+

MySQL には FIELD(rank_code, 'B', 'A') という関数もありますが、リストに無い値と NULL は 0 を返すため先頭に並びます(今回の結果は C、NULL、B、A、A の順)。リストに無い値を後ろに回したいときは、ELSE を書ける CASE のほうが扱いやすいです。

特定の行を先頭に固定する

SELECT id, name FROM members ORDER BY IF(id = 3, 0, 1), id;
-- 並び: 3, 1, 2, 4, 5

UPDATEで条件付き更新をする

行ごとに違う計算をする

UPDATE members SET points = IF(points IS NULL, 0, points + 100);
-- 5行が更新され、1200→1300、300→400、NULL→0、0→100、NULL→0

複数の列を一度に区分けする

UPDATE members
SET rank_code = CASE WHEN points >= 1000 THEN 'A'
                     WHEN points >= 300  THEN 'B'
                     ELSE 'C' END,
    memo = IFNULL(NULLIF(TRIM(memo), ''), '(なし)');
+----+--------+-----------+----------+
| id | points | rank_code | memo     |
+----+--------+-----------+----------+
|  1 |   1300 | A         | VIP      |
|  2 |    400 | B         | (なし)   |
|  3 |      0 | C         | (なし)   |
|  4 |    100 | C         | (なし)   |
|  5 |      0 | C         | 新規     |
+----+--------+-----------+----------+

(上の2つは、members をコピーしたテーブルに対して続けて実行した結果です。)

idごとに違う値を入れるときはELSEかWHEREを付ける

UPDATE members SET points = CASE id WHEN 1 THEN 10 WHEN 2 THEN 20 END;
+----+--------+
| id | points |
+----+--------+
|  1 |     10 |
|  2 |     20 |
|  3 |   NULL |
|  4 |   NULL |
|  5 |   NULL |
+----+--------+

ELSE の無い CASE は、当てはまらない行に NULL を返すので、id=3〜5 の points が NULL で上書きされました。 対象外の行を変えないためには、次のどちらか(両方でも可)を付けます。

UPDATE members
SET points = CASE id WHEN 1 THEN 11 WHEN 2 THEN 22 ELSE points END
WHERE id IN (1, 2);

戻り値の型が変わる例

IF や IFNULL の2つの値に、数値と文字列のように違う型を混ぜると、結果の型は文字列側にそろいます。 CREATE TABLE ... AS SELECT で作った列の型を見ると分かります。

CREATE TABLE ty AS SELECT
  IF(1, 1, 'a')            AS if_int_str,
  IF(1, 1, 2.5)            AS if_int_dec,
  IF(1, 1, 2e0)            AS if_int_dbl,
  IFNULL(1, 'x')           AS ifnull_int_str,
  IFNULL(NULL, 1)          AS ifnull_null_int,
  NULLIF(1, 2)             AS nullif_int,
  COALESCE(NULL, 1, 'x')   AS coal_mix,
  CASE WHEN 1 THEN 1 ELSE 'x' END AS case_mix,
  IF(1, NOW(), 'x')        AS if_dt_str,
  IF(1, CURDATE(), NOW())  AS if_date_dt,
  IFNULL(points, 'なし')   AS ifnull_col
FROM members LIMIT 1;
SHOW COLUMNS FROM ty;
式 できた列の型 読み取れること
IF(1, 1, 'a') varchar(2) 数値と文字列 → 文字列
IF(1, 1, 2.5) decimal(2,1) 整数と小数 → DECIMAL
IF(1, 1, 2e0) double 整数と浮動小数点 → DOUBLE
IFNULL(1, 'x') varchar(2) 数値と文字列 → 文字列
IFNULL(NULL, 1) bigint NULL でない側の型
NULLIF(1, 2) int 第1引数の型
COALESCE(NULL, 1, 'x') varchar(2) 数値と文字列 → 文字列
CASE WHEN 1 THEN 1 ELSE 'x' END varchar(2) 数値と文字列 → 文字列
IF(1, NOW(), 'x') varchar(19) 日時と文字列 → 文字列
IF(1, CURDATE(), NOW()) datetime DATE と DATETIME → DATETIME
IFNULL(points, 'なし') varchar(11) INT の列と文字列 → 文字列

型の決まり方は MySQL 8.0 リファレンスマニュアルのフロー制御関数のページでも確認しました(IFNULL は文字列・実数・整数の順に優先、NULLIF は第1引数と同じ型)。

文字列になると並び順と比較が変わる

SELECT id, IFNULL(points, 'なし') AS v FROM members ORDER BY v;
+----+--------+
| id | v      |
+----+--------+
|  4 | 0      |
|  1 | 1200   |
|  2 | 300    |
|  3 | なし   |
|  5 | なし   |
+----+--------+

数値の列に文字の既定値を入れると、結果は文字列になり、並び順が「0、1200、300」という文字の順になります。 同じ理由で、IF(1, 2, '10') > IF(1, 10, '2') は文字列どうしの比較になって 1(真)を返しました(数値なら 2 > 10 は 0)。

直す前 直した後 理由
ORDER BY IFNULL(points, 'なし') 並べ替えは ORDER BY points、表示だけ IFNULL 並べ替えは数値のまま行う
IFNULL(points, '') を計算に使う IFNULL(points, 0) 既定値の型を列に合わせる
小数の桁をそろえたい IFNULL(5, 0.00) は 5.00 になることを利用、または CAST 既定値の桁数が結果に影響する

PostgreSQL・Oracle・SQL Serverでの対応関数一覧

やりたいこと MySQL PostgreSQL Oracle Database SQL Server
NULL を置き換える(2引数) IFNULL(x, y) COALESCE(x, y) NVL(x, y) ISNULL(x, y)
最初の NULL でない値 COALESCE(...) COALESCE(...) COALESCE(...) COALESCE(...)
等しければ NULL NULLIF(x, y) NULLIF(x, y) NULLIF(x, y) NULLIF(x, y)
2択の分岐 IF(条件, a, b) CASE WHEN 条件 THEN a ELSE b END CASE 式(DECODE もある) IIF(条件, a, b) または CASE
多分岐 CASE CASE CASE CASE
条件付きの件数 SUM(条件) / COUNT(CASE ...) COUNT(*) FILTER (WHERE 条件) / COUNT(CASE ...) COUNT(CASE ...) COUNT(CASE ...)
NULL かどうかを 1 / 0 で返す ISNULL(x) x IS NULL x IS NULL を CASE で x IS NULL を CASE で

MySQL と PostgreSQL の列は実行して確認しました。Oracle Database と SQL Server の列は未実行で、一般に知られた仕様に基づく記載です。

確認できたことは次のとおりです。

  • MySQL 8.0.46 で NVL(points, 0) を実行すると ERROR 1305 (42000): FUNCTION t254.NVL does not exist になりました。
  • MySQL の ISNULL は引数が1つの関数です(ISNULL(NULL) は 1、ISNULL(1) は 0)。SQL Server の感覚で ISNULL(points, 0) と書くと ERROR 1582 (42000): Incorrect parameter count in the call to native function 'ISNULL' になりました。
  • PostgreSQL 16.15 では COALESCE(null, 0)、NULLIF(0, 0)、CASE、10 / NULLIF(0, 0)、COUNT(*) FILTER (WHERE ...) が動きました。IFNULL(null, 0) は ERROR: function ifnull(unknown, integer) does not exist、IF(1>0,'a','b') は ERROR: function if(boolean, unknown, unknown) does not exist でした。
  • PostgreSQL は型の混在に厳密で、COALESCE(null, 1, 'x') は ERROR: invalid input syntax for type integer: "x" になりました。MySQL では文字列にそろえて動く式でも、型を合わせてから移す必要があります。

複数のデータベースで使う SQL は、CASE・COALESCE・NULLIF の3つで書いておくと、書き換えが少なくて済みます。

WHERE句で使うときはインデックスに注意する

列を関数で包むと、その列のインデックスによる絞り込みが使われにくくなります。10万行のテーブルで実行計画を比べました。

WHERE 句 EXPLAIN FORMAT=TREE の要点
IFNULL(points, 0) = 5 Index scan on big using points(rows=100338)+ Filter。インデックス全体を読む
points = 5 Covering index lookup on big using points (points=5)(rows=110)
points IS NULL OR points = 0 Covering index lookup on big using points (points=0 or NULL)
関数インデックス ((IFNULL(points,0))) を追加後の IFNULL(points, 0) = 5 Index lookup on big using idx_f(rows=110)

WHERE 句では、IFNULL で包む代わりに 列 IS NULL OR 列 = 値 と書くと、通常のインデックスで絞り込めます。 式のまま検索したい場合は、関数インデックスを作る方法もあります。

ALTER TABLE big ADD INDEX idx_f ((IFNULL(points, 0)));

よくある質問

Q. IFNULL と COALESCE はどちらを使えばよいですか? A. 引数が2つなら結果は同じです。MySQL だけで使う短い SQL なら IFNULL、候補が3つ以上あるときや他のデータベースでも使う SQL なら COALESCE が向いています。

Q. IF と CASE はどう使い分けますか? A. 2択なら IF、3択以上や NULL を区別したいときは CASE が読みやすくなります。IF は条件が NULL のとき偽の側を返す点を覚えておくと、結果を読み違えません。

Q. IFNULL と NULLIF の違いは何ですか? A. 向きが逆です。IFNULL は「NULL を別の値にする」、NULLIF は「指定した値を NULL にする」関数です。IFNULL(NULLIF(col, ''), '既定値') のように組み合わせると、空文字と NULL をまとめて置き換えられます。

Q. COUNT(IF(条件, 1, 0)) の結果が全件数になります。 A. COUNT は NULL 以外をすべて数えるため、0 も数えられます。COUNT(IF(条件, 1, NULL)) か SUM(IF(条件, 1, 0)) に書き換えてください。

まとめ

  • IF は2択、CASE は多分岐、IFNULL と COALESCE は NULL の置き換え、NULLIF は値を NULL に変える。
  • IF は条件が NULL のとき偽の側を返す。NULL を区別するなら IS NULL を先に判定するか CASE を使う。
  • 条件付き集計は SUM(IF(条件,1,0)) か COUNT(CASE WHEN 条件 THEN 1 END)。COUNT に 0 を渡すと全行が数えられる。
  • ゼロ除算は a / NULLIF(b, 0) で避ける。INSERT・UPDATE での ERROR 1365 も防げる。
  • 空文字と NULL は IFNULL(NULLIF(列, ''), 既定値) でまとめて扱える。
  • UPDATE の CASE には ELSE か WHERE を付ける。付けないと対象外の行が NULL になる。
  • 型の違う値を混ぜると文字列になり、並び順が変わる。他の製品でも使うなら CASE・COALESCE・NULLIF で書く。

株式会社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で一緒に開発しながら、 成長を楽しみませんか?