MySQL 9 min read

MySQLのBETWEEN ― 境界値は含まれる、日時の範囲指定で月末が漏れる理由

BETWEEN a AND b は >= a AND <= b と同じ意味です。この記事の出力は MySQL 8.4.6 で採りました。

まず見る(結論)

  • BETWEEN 100 AND 500 は 100 と 500 ちょうどの行も含む
  • 下限と上限を逆に書くと、エラーにならずに0件になる
  • DATETIME の列を BETWEEN '2026-10-01' AND '2026-10-31' で絞ると、10月31日 0時0分より後の行が入らない
  • 日時の範囲は >= '2026-10-01' AND < '2026-11-01' と書く

例の表 orders です。

+----+-------------+--------+---------------------+
| id | customer_id | amount | ordered_at          |
+----+-------------+--------+---------------------+
|  1 |           1 | 100.00 | 2026-10-01 00:00:00 |
|  2 |           2 | 250.00 | 2026-10-15 12:00:00 |
|  3 |           3 | 500.00 | 2026-10-31 13:00:00 |
|  4 |           4 |  50.00 | 2026-09-30 23:59:59 |
|  5 |           5 | 600.00 | 2026-11-01 00:00:00 |
+----+-------------+--------+---------------------+

境界値は含まれる

mysql> SELECT * FROM orders WHERE amount BETWEEN 100 AND 500;
+----+-------------+--------+---------------------+
| id | customer_id | amount | ordered_at          |
+----+-------------+--------+---------------------+
|  1 |           1 | 100.00 | 2026-10-01 00:00:00 |
|  2 |           2 | 250.00 | 2026-10-15 12:00:00 |
|  3 |           3 | 500.00 | 2026-10-31 13:00:00 |
+----+-------------+--------+---------------------+

範囲の外を取る NOT BETWEEN 100 AND 500 では、残りの 50.00 と 600.00 の2行が返りました。

下限と上限を逆にすると0件

BETWEEN 500 AND 100 は >= 500 AND <= 100 なので、どの行も満たしません。エラーにも警告にもならず、0件が返ります。画面の入力値から範囲を組み立てる処理では、大小が逆になっていないかを確かめます。

日時の範囲で最終日が漏れる

10月の注文を取るつもりで書いた SQL です。

mysql> SELECT id, amount, ordered_at FROM orders
    ->  WHERE ordered_at BETWEEN '2026-10-01' AND '2026-10-31';
+----+--------+---------------------+
| id | amount | ordered_at          |
+----+--------+---------------------+
|  1 | 100.00 | 2026-10-01 00:00:00 |
|  2 | 250.00 | 2026-10-15 12:00:00 |
+----+--------+---------------------+

10月31日 13:00 の id 3 が入っていません。DATETIME と比べるとき、'2026-10-31' は 2026-10-31 00:00:00 として扱われるためです。終わりを「翌月1日より前」にすると、31日の行も入ります。

mysql> SELECT id, amount, ordered_at FROM orders
    ->  WHERE ordered_at >= '2026-10-01' AND ordered_at < '2026-11-01';
+----+--------+---------------------+
| id | amount | ordered_at          |
+----+--------+---------------------+
|  1 | 100.00 | 2026-10-01 00:00:00 |
|  2 | 250.00 | 2026-10-15 12:00:00 |
|  3 | 500.00 | 2026-10-31 13:00:00 |
+----+--------+---------------------+

BETWEEN '2026-10-01' AND '2026-10-31 23:59:59' と書く方法もありますが、列が秒未満を持つ型(DATETIME(3) など)だと 23:59:59.5 のような値が漏れます。< で翌日を指定する書き方なら型に関係なく漏れません。

文字列の BETWEEN

文字列は辞書順で比べます。名前が a、b、c、cat、d の5人に BETWEEN 'a' AND 'c' を使うと、a、b、c の3人が返り、cat は入りませんでした。'cat' は 'c' より後ろに並ぶためです。「c で始まる名前まで」を取りたいなら、>= 'a' AND < 'd' と書きます。

索引は使われる

ordered_at に索引を作ってから、日時を BETWEEN で絞る検索を EXPLAIN すると type が range になり、索引で範囲を読んでいました。

+----+-------------+--------+-------+-----------------------+-----------------------+---------+------+------+----------+-----------------------+
| id | select_type | table  | type  | possible_keys         | key                   | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+--------+-------+-----------------------+-----------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | orders | range | idx_orders_ordered_at | idx_orders_ordered_at | 6       | NULL |    2 |   100.00 | Using index condition |
+----+-------------+--------+-------+-----------------------+-----------------------+---------+------+------+----------+-----------------------+

(partitions 列を省いています) DATE(ordered_at) BETWEEN ... のように列を関数で包むと、索引の値と直接比べられなくなるので避けます。