[MySQL]並び順(ソート)でNULLを後ろして昇順で並び替える方法
![[MySQL]並び順(ソート)でNULLを後ろして昇順で並び替える方法](/upload-images/images/09/21ef03ec87cfc5cf44517ad6ab646e4da05214cd.png) 
 MySQLでレコードを昇順に並び替えようとするとnullのレコードが存在すると先頭に来てしまいます。
実装的にこれで問題ないこともありますが、昇順で且つnullが後ろに来てほしい場合もあります。
mysql> SELECT
    ->     *
    -> FROM
    ->     products
    -> ORDER BY expiration_at ASC;
+----+---------------------+
| id | expiration_at       |
+----+---------------------+
|  6 | NULL                |
|  8 | 2021-02-03 06:53:28 |
|  3 | 2021-02-03 11:31:17 |
|  2 | 2021-02-05 11:57:46 |
|  9 | 2021-02-06 07:27:59 |
|  1 | 2021-02-09 00:39:14 |
|  7 | 2021-02-09 16:59:48 |
|  4 | 2021-02-09 21:43:08 |
| 10 | 2021-02-10 16:39:31 |
+----+---------------------+
9 rows in set (0.00 sec)例えば期限付きポイントを管理したい場合、よくある仕様は下記が多いと思います。
- ポイントの種類は有効期限がある有効期限ポイント、有効期限が存在しない永久ポイントの2種類を設ける。
- 有効期限が切れたポイントは失効扱うとする。
- ポイント有効期限が近いものから消費し、永久ポイントは最後に消費する。
このような条件を達成するには、nullのレコードは後に来てほしいものです。
ではどのようにするのか、下記のテーブル構成で紹介したいと思います。
CREATE TABLE `products` (
  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `expiration_at` datetime DEFAULT NULL COMMENT '有効期限',
  PRIMARY KEY (`id`)
) ;昇順でnullを後ろに持ってくる
やり方は2種類ありますのでそれぞれ紹介します。
IS NULL ASCを指定する
Selectする際のOrder By句に「IS NULL ASC」を付けてやる方法になります。
mysql> SELECT
    ->     *
    -> FROM
    ->     products
    -> ORDER BY expiration_at IS NULL ASC , expiration_at ASC;
+----+---------------------+
| id | expiration_at       |
+----+---------------------+
|  8 | 2021-02-03 06:53:28 |
|  3 | 2021-02-03 11:31:17 |
|  2 | 2021-02-05 11:57:46 |
|  9 | 2021-02-06 07:27:59 |
|  1 | 2021-02-09 00:39:14 |
|  7 | 2021-02-09 16:59:48 |
|  4 | 2021-02-09 21:43:08 |
| 10 | 2021-02-10 16:39:31 |
|  6 | NULL                |
+----+---------------------+結果を見ると日付が昇順で最後にnullが来ているのが分かると思います。
ASCの部分をDESCにすると降順にできますので自由に操作することができます。
ただ一つ問題があります。それはINDEXを貼っても遅いです。INDEXの詳細は後述に書いています。
MAX値「9999-12-31 23:59:59」を使用する
もう一個のやり方が、Datetime型のMAX値である「9999-12-31 23:59:59」を使用してnull扱いとする方法です。
こちらのやり方は通常のORDER BY するだけで簡単に並び替えができます。
mysql> SELECT
    ->     *
    -> FROM
    ->     products
    -> ORDER BY expiration_at ASC;
+----+---------------------+
| id | expiration_at       |
+----+---------------------+
|  8 | 2021-02-03 06:53:28 |
|  3 | 2021-02-03 11:31:17 |
|  2 | 2021-02-05 11:57:46 |
|  9 | 2021-02-06 07:27:59 |
|  1 | 2021-02-09 00:39:14 |
|  7 | 2021-02-09 16:59:48 |
|  4 | 2021-02-09 21:43:08 |
| 10 | 2021-02-10 16:39:31 |
|  6 | 9999-12-31 23:59:59 |
+----+---------------------+MAX値を入れるのが気にいらないという方もいるかもしれませんが、シンプルでINDEXをしっかり効いてくれるのでおすすめです。
どちらのSQLが早いか試してみる
先ほど紹介した二つのやり方の取得時間を調べてみました。
INDEXが効いているかどうかチェックするにはレコードを増やす必要があります。
120万件弱のデータを投入しました。
mysql> SELECT
    ->     COUNT(*)
    -> FROM
    ->     products;
+----------+
| COUNT(*) |
+----------+
|  1179648 |
+----------+次ににINDEXを作成します。
ALTER TABLE `products` 
ADD INDEX `idx_expiration_at` (`expiration_at`) ;IS NULL ASCを使用した場合の取得時間
IS NULL ASCを使用した取得時間を調べてみます。
mysql>  SELECT
    ->     *
    -> FROM
    ->     products
    -> ORDER BY expiration_at IS NULL ASC , expiration_at ASC
    -> LIMIT 10;
+---------+---------------------+
| id      | expiration_at       |
+---------+---------------------+
|  942385 | 2021-02-01 00:00:00 |
| 1327627 | 2021-02-01 00:00:00 |
| 1349719 | 2021-02-01 00:00:00 |
|  465886 | 2021-02-01 00:00:01 |
|  468138 | 2021-02-01 00:00:01 |
|  558431 | 2021-02-01 00:00:01 |
|  701103 | 2021-02-01 00:00:01 |
| 1177319 | 2021-02-01 00:00:01 |
|  163888 | 2021-02-01 00:00:02 |
|  882634 | 2021-02-01 00:00:04 |
+---------+---------------------+
10 rows in set (1.51 sec)
~2回目~
10 rows in set (1.53 sec)
~3回目~
10 rows in set (1.53 sec)結果平均1.5秒ほどかかりました。
実行計画を確認するとINDEXは効いていますが「Using filesort」が発生していますね。遅い原因は「Using filesort」になります。
mysql> EXPLAIN SELECT
    ->     *
    -> FROM
    ->     products
    -> ORDER BY expiration_at IS NULL ASC , expiration_at ASC
    -> LIMIT 10;
+----+-------------+----------+------------+-------+---------------+-------------------+---------+------+---------+----------+-----------------------------+
| id | select_type | table    | partitions | type  | possible_keys | key               | key_len | ref  | rows    | filtered | Extra                       |
+----+-------------+----------+------------+-------+---------------+-------------------+---------+------+---------+----------+-----------------------------+
|  1 | SIMPLE      | products | NULL       | index | NULL          | idx_expiration_at | 6       | NULL | 1177930 |   100.00 | Using index; Using filesort |
+----+-------------+----------+------------+-------+---------------+-------------------+---------+------+---------+----------+-----------------------------+IS NULL ASCを使ったやり方は、レコード数が少なければ問題にならないが、件数が多くなるとパフォーマンスに影響がでてきます。
MAX値「9999-12-31 23:59:59」を使用した取得時間
つぎに2つ目に紹介したMAX値「9999-12-31 23:59:59」を使用した方法で試します。
mysql> SELECT
    ->     *
    -> FROM
    ->     products
    -> ORDER BY expiration_at ASC
    -> LIMIT 10;
+---------+---------------------+
| id      | expiration_at       |
+---------+---------------------+
|  942385 | 2021-02-01 00:00:00 |
| 1327627 | 2021-02-01 00:00:00 |
| 1349719 | 2021-02-01 00:00:00 |
|  465886 | 2021-02-01 00:00:01 |
|  468138 | 2021-02-01 00:00:01 |
|  558431 | 2021-02-01 00:00:01 |
|  701103 | 2021-02-01 00:00:01 |
| 1177319 | 2021-02-01 00:00:01 |
|  163888 | 2021-02-01 00:00:02 |
|  882634 | 2021-02-01 00:00:04 |
+---------+---------------------+
10 rows in set (0.00 sec)
~2回目~
10 rows in set (0.00 sec)
~3回目~
10 rows in set (0.00 sec)結果、0秒で返ってきました。
実行計画を確認するとしっかりINDEXが効いて、1つ目で発生していた「Using filesort」も発生していませんね。
mysql> EXPLAIN SELECT
    ->     *
    -> FROM
    ->     products
    -> ORDER BY expiration_at ASC
    -> LIMIT 10;
+----+-------------+----------+------------+-------+---------------+-------------------+---------+------+------+----------+-------------+
| id | select_type | table    | partitions | type  | possible_keys | key               | key_len | ref  | rows | filtered | Extra       |
+----+-------------+----------+------------+-------+---------------+-------------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | products | NULL       | index | NULL          | idx_expiration_at | 6       | NULL |   10 |   100.00 | Using index |
+----+-------------+----------+------------+-------+---------------+-------------------+---------+------+------+----------+-------------+100万件のデータでも0秒で返ってくるので、データ量が多いと想定される場合はMAX値を使ったやり方がおすすめです。
最後に
今回NULLを後ろして昇順で並び替える方法を紹介しました。
私が設計する場合は、MAX値「9999-12-31 23:59:59」を使ったやり方をいつも採用しています。 理由はとにかく早いからです。
ぜひ参考にしてみてください。

![[MySQL]テーブルからの奇数行または偶数行を分けて取得する方法](/upload-images/images/09/21ef03ec87cfc5cf44517ad6ab646e4da05214cd-thumbnail.png)
コメント
コメントを残す