MySQL で ORDER BY で NULL を下に持ってくるには下記の記事のような SQL で出来ますが、
- MySQL の ORDER BY で NULL を先にもってきて NULL 以外は降順にする « をぶろぐ
- MySQL で NULL を一番最後にして昇順にソートする | Sun Limited Mt.
- MySQLでNULLを下にASCソートする
※ mysql order by null でググって出てきた記事を羅列しただけ
例えば、次のようなそこそこの行数のテーブルで ORDER BY 狙いのインデックスがある場合・・・
テーブル作成
drop table if exists t; create table t ( id int not null primary key auto_increment, val int );
コンソールから適当に行を突っ込む
$ seq 10 | mysql test -e "load data local infile '/dev/stdin' into table t (id) set val = rand()*1000000" $ seq 1000000 | mysql test -e "load data local infile '/dev/stdin' into table t (id) set val = null"
ORDER BY 狙いのインデックス
create index idx on t (val, id);
LIMIT を掛ければ ORDER BY 狙いのインデックスによってたかだか 20 行ちょいしか読まれないので高速なはずです。
select * from t order by val asc, id asc limit 20;
+----+------+ | id | val | +----+------+ | 11 | NULL | | 12 | NULL | | 13 | NULL | | 14 | NULL | | 15 | NULL | | 16 | NULL | | 17 | NULL | | 18 | NULL | | 19 | NULL | | 20 | NULL | | 21 | NULL | | 22 | NULL | | 23 | NULL | | 24 | NULL | | 25 | NULL | | 26 | NULL | | 27 | NULL | | 28 | NULL | | 29 | NULL | | 30 | NULL | +----+------+ 20 rows in set (0.00 sec)
が、NULL を下に持ってこようとすると ORDER BY 狙いのインデックスが有効に使われないので遅くなります。
select * from t order by val is null asc, val asc, id asc limit 20;
+----+--------+ | id | val | +----+--------+ | 6 | 167023 | | 9 | 302768 | | 5 | 387339 | | 3 | 409595 | | 7 | 673096 | | 2 | 708250 | | 1 | 710551 | | 8 | 864411 | | 10 | 920608 | | 4 | 923225 | | 11 | NULL | | 12 | NULL | | 13 | NULL | | 14 | NULL | | 15 | NULL | | 16 | NULL | | 17 | NULL | | 18 | NULL | | 19 | NULL | | 20 | NULL | +----+--------+ 20 rows in set (0.24 sec)
次のように is null と is not null を無理やり2回に分けた方が速いことがあります。
(select * from t where val is null order by id asc limit 20) union all (select * from t where val is not null order by val asc, id asc limit 20) order by val is null asc, val asc, id asc limit 20;
+----+--------+ | id | val | +----+--------+ | 6 | 167023 | | 9 | 302768 | | 5 | 387339 | | 3 | 409595 | | 7 | 673096 | | 2 | 708250 | | 1 | 710551 | | 8 | 864411 | | 10 | 920608 | | 4 | 923225 | | 11 | NULL | | 12 | NULL | | 13 | NULL | | 14 | NULL | | 15 | NULL | | 16 | NULL | | 17 | NULL | | 18 | NULL | | 19 | NULL | | 20 | NULL | +----+--------+ 20 rows in set (0.00 sec)
explain で違いを見てみると・・・
次のクエリはインデックスを 全行走査 して filesort でソートして先頭の 20 行を返します。
explain select * from t order by val is null asc, val asc, id asc limit 20;
+----+-------------+-------+-------+---------------+------+---------+------+--------+-----------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+------+---------+------+--------+-----------------------------+ | 1 | SIMPLE | t | index | NULL | idx | 9 | NULL | 998968 | Using index; Using filesort | +----+-------------+-------+-------+---------------+------+---------+------+--------+-----------------------------+
次のクエリは(そうでは無いように見えるけど多分)次のようになっています。
- idx の
val is null
である範囲を昇順に走査して最初の 20 件 - idx の
val is not null
である範囲を昇順に走査して最初の 20 件(実際には 10 件しかありませんが) - これらの合計の40件でテンポラリテーブルを作ってソートして先頭の 20 件を返す
explain (select * from t where val is null order by id asc limit 20) union all (select * from t where val is not null order by val asc, id asc limit 20) order by val is null asc, val asc, id asc limit 20;
+------+--------------+------------+-------+---------------+------+---------+-------+--------+---------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+--------------+------------+-------+---------------+------+---------+-------+--------+---------------------------------+ | 1 | PRIMARY | t | ref | idx | idx | 5 | const | 499484 | Using where; Using index | | 2 | UNION | t | range | idx | idx | 5 | NULL | 10 | Using where; Using index | | NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | Using temporary; Using filesort | +------+--------------+------------+-------+---------------+------+---------+-------+--------+---------------------------------+
show status
を使えば実際に走査されている行数をざっくり見積もれます。
flush status; select * from t order by val is null asc, val asc, id asc limit 20; show status like 'handler%';
+----------------------------+---------+ | Variable_name | Value | +----------------------------+---------+ | Handler_commit | 1 | | Handler_delete | 0 | | Handler_discover | 0 | | Handler_external_lock | 2 | | Handler_mrr_init | 0 | | Handler_prepare | 0 | | Handler_read_first | 1 | | Handler_read_key | 1 | | Handler_read_last | 0 | | Handler_read_next | 0 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 1000001 | | Handler_rollback | 0 | | Handler_savepoint | 0 | | Handler_savepoint_rollback | 0 | | Handler_update | 0 | | Handler_write | 0 | +----------------------------+---------+
flush status; (select * from t where val is null order by id asc limit 20) union all (select * from t where val is not null order by val asc, id asc limit 20) order by val is null asc, val asc, id asc limit 20; show status like 'handler%';
+----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | Handler_commit | 1 | | Handler_delete | 0 | | Handler_discover | 0 | | Handler_external_lock | 4 | | Handler_mrr_init | 0 | | Handler_prepare | 0 | | Handler_read_first | 0 | | Handler_read_key | 2 | | Handler_read_last | 0 | | Handler_read_next | 29 | | Handler_read_prev | 0 | | Handler_read_rnd | 20 | | Handler_read_rnd_next | 31 | | Handler_rollback | 0 | | Handler_savepoint | 0 | | Handler_savepoint_rollback | 0 | | Handler_update | 0 | | Handler_write | 30 | +----------------------------+-------+
後者の方がはるかに走査行が少なくなります(その代わりテンポラリテーブルを作成するコストがあります)。
無理矢理にでも複数の SELECT に分けたほうが早いのは下記の記事と同じですね。
PostgreSQL なんかだとインデックスの作成時に NULL をどっちに持ってくるかを指定できるようです(NULLS FIRST/NULLS LAST)。