SQLのBETWEENとは、「値 BETWEEN 下限 AND 上限」と書いて、下限以上・上限以下の行を取り出す演算子です。 両端の値そのものも結果に含まれます。amount BETWEEN 1000 AND 3000 は amount >= 1000 AND amount <= 3000 と同じ意味です。

書き方は短くて読みやすい一方で、日時(時刻つき)の列に日付だけを指定すると、最終日のデータがほとんど取れないという、気づきにくい動きがあります。この記事では、数値・日付・文字列それぞれの境界値の動き、最終日が抜ける現象の再現と直し方、NOT BETWEENとNULLの関係、下限と上限を逆に書いたときの結果を、MySQL 8.0.46・PostgreSQL 16.15・SQLite 3.45.1 で実際に実行した結果つきで説明します。SQL ServerとOracleの書き方も一覧表に入れました(この2つは今回の環境で動かせなかったため、表の中で「未実行」と明記しています)。

BETWEENの早見表

まず、よく使う書き方と結果を1枚にまとめます。

やりたいこと 書き方 境界の扱い
数値の範囲(以上・以下) amount BETWEEN 1000 AND 3000 1000 と 3000 を含む
範囲の外側 amount NOT BETWEEN 1000 AND 3000 1000 未満、または 3000 より大きい。NULL の行は出ない
日付型(DATE)の範囲 d BETWEEN '2026-09-01' AND '2026-09-30' 9月1日と9月30日を含む
日時型(DATETIME / TIMESTAMP)の1か月分 t >= '2026-09-01' AND t < '2026-10-01' 開始を含み、翌月1日を含まない(おすすめ)
日時型の1日分 t >= '2026-09-30' AND t < '2026-10-01' 9月30日の 0:00:00 から 23:59:59.999… まで
文字列の範囲 name BETWEEN 'B' AND 'D' 'B' 以上 'D' 以下。'Doi' は 'D' より大きいので含まれない
以上・未満(上限を含めない) x >= 下限 AND x < 上限 BETWEEN では書けないので比較演算子を使う

覚えておきたい動きは次の4つです。

動き 結果
両端の値 含まれる(以上・以下)
下限と上限を逆に書く エラーにならず 0 件
列の値が NULL BETWEEN でも NOT BETWEEN でも結果に出ない
日時列に日付だけを指定 上限は「その日の 0:00:00」として扱われ、最終日の日中のデータが出ない

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL BETWEENの使い方|日付の範囲指定と境界値・NOT BETWEEN」

この記事で使うサンプルテーブル

以降の実行結果はすべて、次の orders(注文)テーブルで確認しています。境界の動きが見えるよう、月の変わり目の時刻と NULL をあえて入れてあります。

-- PostgreSQL 16 / SQLite 3.45 用(MySQL 8.0 では TIMESTAMP を DATETIME(3) に置き換える)
CREATE TABLE orders (
  id INT PRIMARY KEY,
  customer VARCHAR(20),
  amount INT,
  ordered_at TIMESTAMP
);

INSERT INTO orders VALUES
(1,'Aoki',500,'2026-08-31 23:59:59'),
(2,'Baba',1000,'2026-09-01 00:00:00'),
(3,'Chiba',1500,'2026-09-15 12:30:00'),
(4,'Doi',2000,'2026-09-30 00:00:00'),
(5,'Endo',2500,'2026-09-30 18:45:00'),
(6,'Fujii',3000,'2026-09-30 23:59:59.500'),
(7,'Goto',NULL,'2026-10-01 00:00:00'),
(8,'Hara',3500,NULL);

MySQL の DATETIME は、そのままだと秒より下を保存しません。id=6 の「.500」(0.5秒)を残すため、MySQL では DATETIME(3)(ミリ秒まで保存)で作成しました。

id customer amount ordered_at この行を入れた理由
1 Aoki 500 2026-08-31 23:59:59 9月の直前
2 Baba 1000 2026-09-01 00:00:00 9月の最初の瞬間・金額の下限
3 Chiba 1500 2026-09-15 12:30:00 範囲の中ほど
4 Doi 2000 2026-09-30 00:00:00 最終日の 0 時ちょうど
5 Endo 2500 2026-09-30 18:45:00 最終日の夕方
6 Fujii 3000 2026-09-30 23:59:59.500 最終日の最後の1秒の途中・金額の上限
7 Goto NULL 2026-10-01 00:00:00 翌月の最初の瞬間・金額が NULL
8 Hara 3500 NULL 日時が NULL

BETWEENの基本の書き方(数値で以上・以下を確認)

構文は次のとおりです。

SELECT 列 FROM テーブル
WHERE 列 BETWEEN 下限 AND 上限;

BETWEEN は両端を含みます。 金額が 1000 以上 3000 以下の注文を取り出してみます。

SELECT id, amount FROM orders
WHERE amount BETWEEN 1000 AND 3000
ORDER BY id;

実行結果(PostgreSQL 16.15。MySQL 8.0.46・SQLite 3.45.1 でも同じ5行):

 id | amount
----+--------
  2 |   1000
  3 |   1500
  4 |   2000
  5 |   2500
  6 |   3000
(5 rows)

下限の 1000(id=2)と上限の 3000(id=6)がどちらも入っています。次の2つは同じ意味です。

-- BETWEEN で書く
WHERE amount BETWEEN 1000 AND 3000

-- 比較演算子で書く(同じ結果)
WHERE amount >= 1000 AND amount <= 3000

「以上・未満」や「より大きい・以下」のように片方の端を含めたくないときは、BETWEEN ではなく比較演算子で書きます。

欲しい範囲 書き方
1000 以上 3000 以下 amount BETWEEN 1000 AND 3000
1000 以上 3000 未満 amount >= 1000 AND amount < 3000
1000 より大きく 3000 以下 amount > 1000 AND amount <= 3000
1000 より大きく 3000 未満 amount > 1000 AND amount < 3000

日付の範囲指定:日時列では最終日が抜ける

ここがこの記事でいちばん伝えたい部分です。「9月分の注文」を取り出すつもりで、次のように書いたとします。

-- 直す前:日時列に日付だけを指定
SELECT id, ordered_at FROM orders
WHERE ordered_at BETWEEN '2026-09-01' AND '2026-09-30'
ORDER BY id;

実行結果(MySQL 8.0.46):

+----+-------------------------+
| id | ordered_at              |
+----+-------------------------+
|  2 | 2026-09-01 00:00:00.000 |
|  3 | 2026-09-15 12:30:00.000 |
|  4 | 2026-09-30 00:00:00.000 |
+----+-------------------------+

9月30日 18:45 の id=5 と、23:59:59.5 の id=6 が出てきません。 PostgreSQL 16.15 と SQLite 3.45.1 でも同じ3行でした。

なぜ最終日が抜けるのか

日時型の列と比べるとき、'2026-09-30' は「2026-09-30 00:00:00」として扱われます。つまり上の条件は、次の意味になっています。

WHERE ordered_at >= '2026-09-01 00:00:00'
  AND ordered_at <= '2026-09-30 00:00:00'

上限が「9月30日の 0 時ちょうど」なので、9月30日のうち 0:00:00 の1点だけが含まれ、それ以降の時刻はすべて範囲の外になります。id=4(0時ちょうど)だけが残ったのはそのためです。

flowchart LR
    A["9/1 00:00:00<br>下限・含む"] --> B["9/15 12:30<br>含む"]
    B --> C["9/30 00:00:00<br>上限・含む"]
    C --> D["9/30 18:45<br>範囲の外"]
    D --> E["9/30 23:59:59.5<br>範囲の外"]

エラーも警告も出ないため、集計の件数が少し足りないことに後から気づく、という形で見つかることが多い動きです。

23:59:59 と書く方法と、その限界

上限を「その日の最後の時刻」にする書き方がよく紹介されています。

SELECT id, ordered_at FROM orders
WHERE ordered_at BETWEEN '2026-09-01 00:00:00' AND '2026-09-30 23:59:59'
ORDER BY id;

実行結果(PostgreSQL 16.15):

 id |     ordered_at
----+---------------------
  2 | 2026-09-01 00:00:00
  3 | 2026-09-15 12:30:00
  4 | 2026-09-30 00:00:00
  5 | 2026-09-30 18:45:00
(4 rows)

id=5 は取れるようになりましたが、23:59:59.500 の id=6 はまだ出ません。 23:59:59 ちょうどより後、0.5 秒ぶん範囲の外にあるからです。MySQL 8.0.46(DATETIME(3))でも同じ4行でした。

秒より細かい値を保存している列では、「.999」「.999999」と桁を増やして合わせる方法もありますが、列の精度(ミリ秒・マイクロ秒など)が変わるたびに書き直しが必要になります。

上限の書き方 秒までの列 ミリ秒・マイクロ秒を持つ列
'2026-09-30' 最終日は 0:00:00 だけ含む 同じ
'2026-09-30 23:59:59' 最終日をすべて含む 23:59:59 より後の端数が抜ける
'2026-09-30 23:59:59.999' 含む マイクロ秒の列では端数が抜ける
< '2026-10-01'(翌日未満) 含む 含む(精度に関係なく使える)

おすすめ:「>= 開始 AND < 翌日」で書く

日時列の範囲は、BETWEEN ではなく「開始以上・翌日(翌月1日)未満」で書くと、列の精度に関係なく正しく取れます。

-- 直した後:開始以上・翌月1日未満
SELECT id, ordered_at FROM orders
WHERE ordered_at >= '2026-09-01'
  AND ordered_at <  '2026-10-01'
ORDER BY id;

実行結果(PostgreSQL 16.15。MySQL 8.0.46・SQLite 3.45.1 でも同じ5行):

 id |      ordered_at
----+-----------------------
  2 | 2026-09-01 00:00:00
  3 | 2026-09-15 12:30:00
  4 | 2026-09-30 00:00:00
  5 | 2026-09-30 18:45:00
  6 | 2026-09-30 23:59:59.5
(5 rows)

9月のデータ5件がすべて取れ、8月31日 23:59:59 の id=1 と 10月1日 0:00:00 の id=7 は含まれていません。この書き方は「半開区間」(片方の端だけを含む区間)と呼ばれ、次の利点があります。

  • 月末が何日かを考えなくてよい(28日・29日・30日・31日のどれでも「翌月1日未満」で済む)
  • 秒の端数の桁数を気にしなくてよい
  • 9月分と10月分を続けて集計しても、同じ行が両方に入ったり、どちらにも入らなかったりしない

1日分・1か月分をコピペで使う形

開始日だけを書き換えれば使える形にしておくと便利です。

-- MySQL 8.0:1日分(9月30日)
SELECT id FROM orders
WHERE ordered_at >= '2026-09-30'
  AND ordered_at <  DATE_ADD('2026-09-30', INTERVAL 1 DAY);

-- MySQL 8.0:1か月分(9月)
SELECT id FROM orders
WHERE ordered_at >= '2026-09-01'
  AND ordered_at <  '2026-09-01' + INTERVAL 1 MONTH;
-- PostgreSQL 16:1日分(9月30日)
SELECT id FROM orders
WHERE ordered_at >= '2026-09-30'
  AND ordered_at <  DATE '2026-09-30' + 1;

-- PostgreSQL 16:1か月分(9月)
SELECT id FROM orders
WHERE ordered_at >= DATE '2026-09-01'
  AND ordered_at <  DATE '2026-09-01' + INTERVAL '1 month';

どちらのDBでも、1日分は id が 4・5・6 の3行、1か月分は 2〜6 の5行になることを確認しました。

列を日付に変換してからBETWEENを使う方法

列のほうを日付型に変換すれば、BETWEEN でも最終日を含められます。

SELECT id, ordered_at FROM orders
WHERE CAST(ordered_at AS DATE) BETWEEN '2026-09-01' AND '2026-09-30'
ORDER BY id;

MySQL 8.0.46・PostgreSQL 16.15 とも、id が 2〜6 の5行になりました。読みやすい書き方ですが、列に関数や変換をかけると、その列の通常のインデックスを範囲検索に使いにくくなります。 行数が多いテーブルでは、列はそのままにして比較する値のほうを工夫する(半開区間で書く)のが向いています。インデックスとの関係は後の章で実行計画を載せます。

DATE型の列ならBETWEENでそのまま書ける

時刻を持たない DATE 型の列では、最終日が抜ける現象は起きません。1日が1つの値なので、両端を含む BETWEEN がそのまま「1日から30日まで」になります。

-- PostgreSQL 16.15 で確認
SELECT DATE '2026-09-10' BETWEEN DATE '2026-09-01' AND DATE '2026-09-10' AS d;
 d
---
 t
(1 row)
列の型 月の範囲のおすすめの書き方
DATE(時刻なし) d BETWEEN '2026-09-01' AND '2026-09-30' でよい
DATETIME / TIMESTAMP(時刻あり) t >= '2026-09-01' AND t < '2026-10-01'
型がわからない・将来変わるかもしれない 半開区間で書いておくと、どちらの型でも同じ結果になる

存在しない日付を書いたときの動き

「月末は31日」と決め打ちして、9月に '2026-09-31' と書いた場合の動きはDBによって違いました。

SELECT id FROM orders
WHERE ordered_at BETWEEN '2026-09-01' AND '2026-09-31';

PostgreSQL 16.15 はエラーで知らせてくれます。

ERROR:  date/time field value out of range: "2026-09-31"

MySQL 8.0.46 はエラーにならず、結果が 0 件になり、警告が記録されました(SHOW WARNINGS で確認)。

Warning | 1292 | Incorrect datetime value: '2026-09-31' for column 'ordered_at' at row 1

「翌月1日未満」で書いておけば、月末の日付を書く必要がそもそもなくなります。

文字列でBETWEENを使う

文字列にも BETWEEN は使えます。比較は辞書順(文字の並び順)で行われます。

SELECT id, customer FROM orders
WHERE customer BETWEEN 'B' AND 'D'
ORDER BY id;

実行結果(PostgreSQL 16.15。MySQL 8.0.46・SQLite 3.45.1 でも同じ2行):

 id | customer
----+----------
  2 | Baba
  3 | Chiba
(2 rows)

'Doi' が入っていない点に注目してください。辞書順では 'D' の後ろに文字が続く 'Doi' のほうが 'D' より大きいため、上限 'D' を超えています。D で始まる名前まで含めたいときは、次のどちらかにします。

-- 上限を実際の値にする(Baba / Chiba / Doi の3行)
WHERE customer BETWEEN 'Baba' AND 'Doi'

-- 「B以上・E未満」の半開区間にする
WHERE customer >= 'B' AND customer < 'E'

日付の「最終日が抜ける」現象と同じ構造です。上限に「短い値」を書くと、それより長い値は範囲の外になると覚えておくと、どちらにも応用できます。

また、大文字と小文字を区別するかどうかは、DBと照合順序(文字の比べ方の設定)によって変わります。MySQL 8.0.46 の既定の照合順序(utf8mb4_0900_ai_ci)では、BETWEEN 'b' AND 'd' と小文字で書いても Baba と Chiba の2行が返りました。

NOT BETWEENで範囲の外を取り出す

NOT BETWEEN は「下限より小さい、または上限より大きい」行を取り出します。

SELECT id, amount FROM orders
WHERE amount NOT BETWEEN 1000 AND 3000
ORDER BY id;

実行結果(MySQL 8.0.46。PostgreSQL 16.15・SQLite 3.45.1 でも同じ2行):

+----+--------+
| id | amount |
+----+--------+
|  1 |    500 |
|  8 |   3500 |
+----+--------+

次の3つの書き方はすべて同じ結果になることを MySQL 8.0.46 で確認しました。

WHERE amount NOT BETWEEN 1000 AND 3000
WHERE NOT (amount BETWEEN 1000 AND 3000)
WHERE amount < 1000 OR amount > 3000

境界の値(1000 と 3000)は BETWEEN 側に含まれるので、NOT BETWEEN 側には含まれません。

NULLがあるとBETWEENとNOT BETWEENのどちらにも出ない

サンプルには金額が NULL の行(id=7)があります。この行は BETWEEN の結果にも、NOT BETWEEN の結果にも出てきません。 件数を数えると、よくわかります。

SELECT COUNT(*) AS total,
       SUM(CASE WHEN amount BETWEEN 1000 AND 3000 THEN 1 ELSE 0 END) AS in_range,
       SUM(CASE WHEN amount NOT BETWEEN 1000 AND 3000 THEN 1 ELSE 0 END) AS out_range
FROM orders;

実行結果(PostgreSQL 16.15。MySQL 8.0.46・SQLite 3.45.1 でも同じ):

 total | in_range | out_range
-------+----------+-----------
     8 |        5 |         2
(1 row)

全体は 8 行なのに、範囲内 5 行と範囲外 2 行を足しても 7 行です。残りの1行が NULL の行です。

NULL は「値がわからない」ことを表すため、NULL と何かを比べた結果は真でも偽でもなく「不明(NULL)」になります。WHERE は結果が真の行だけを通すので、不明の行は NOT を付けても付けなくても通りません。

-- PostgreSQL 16.15
SELECT 5 BETWEEN 1 AND 5 AS a, 1 BETWEEN 1 AND 5 AS b, 6 BETWEEN 1 AND 5 AS c,
       NULL BETWEEN 1 AND 5 AS d, 6 BETWEEN 1 AND NULL AS e, 0 BETWEEN 1 AND NULL AS f;
 a | b | c | d | e | f
---+---+---+---+---+---
 t | t | f |   |   | f
(1 row)
式 結果 理由
5 BETWEEN 1 AND 5 真 上限を含む
1 BETWEEN 1 AND 5 真 下限を含む
6 BETWEEN 1 AND 5 偽 上限より大きい
NULL BETWEEN 1 AND 5 NULL(不明) 値がわからない
6 BETWEEN 1 AND NULL NULL(不明) 下限は満たすが、上限がわからない
0 BETWEEN 1 AND NULL 偽 下限を満たさない時点で偽と決まる

MySQL 8.0.46 でも同じ結果(真は 1、偽は 0、不明は NULL と表示)でした。

NULLの行も範囲外として取り出したいとき

「範囲に入っていないものを全部」取り出したいなら、IS NULL を足します。

SELECT id, amount FROM orders
WHERE amount NOT BETWEEN 1000 AND 3000
   OR amount IS NULL
ORDER BY id;
 id | amount
----+--------
  1 |    500
  7 |
  8 |   3500
(3 rows)

これで 5 行+3 行=8 行となり、全体の件数と合います。

下限や上限にNULLが入るとき

検索画面の入力値をそのまま渡すような場面では、下限や上限が NULL になることがあります。amount BETWEEN 1000 AND NULL の件数は、MySQL・PostgreSQL・SQLite のいずれでも 0 件でした。片方だけ指定された検索にも対応したい場合は、条件を分けて書きます。

-- :min と :max は外から渡す値。NULL のときはその条件を使わない
SELECT id, amount FROM orders
WHERE (:min IS NULL OR amount >= :min)
  AND (:max IS NULL OR amount <= :max);

下限と上限を逆に書くと0件になる

BETWEEN は「小さいほう AND 大きいほう」の順で書きます。逆に書いてもエラーにはならず、0 件が返ります。

SELECT COUNT(*) FROM orders
WHERE amount BETWEEN 3000 AND 1000;

実行結果(MySQL 8.0.46・PostgreSQL 16.15・SQLite 3.45.1 のすべてで同じ):

 count
-------
     0
(1 row)

amount >= 3000 AND amount <= 1000 という、同時には満たせない条件になるためです。日付でも同じで、BETWEEN '2026-09-30' AND '2026-09-01' は 0 件になります。「データがあるはずなのに 0 件」のときは、まず順番を見直すと早く解決します。

PostgreSQL には、順番を自動で入れ替えてくれる BETWEEN SYMMETRIC があります。

-- PostgreSQL 16.15 で確認(5行が返る)
SELECT id, amount FROM orders
WHERE amount BETWEEN SYMMETRIC 3000 AND 1000
ORDER BY id;

開始日と終了日をユーザーが自由に入力する画面などで役立ちます。ほかのDBでは、アプリ側で小さいほうを先に渡すか、LEAST / GREATEST のような関数で並べ替えてから渡します。

DB別の日付リテラルと範囲指定の書き方一覧

日付の値(リテラル)の書き方と、「翌日」「翌月」の求め方はDBごとに少しずつ違います。MySQL・PostgreSQL・SQLite は今回実行して確認した書き方、SQL Server と Oracle は今回の環境で動かせなかったため未実行(一般に知られた仕様に沿った書き方)です。

DB 日付リテラル 日時リテラル 翌日・翌月の求め方 確認
MySQL 8.0 '2026-09-01' または DATE '2026-09-01' '2026-09-01 00:00:00' または TIMESTAMP '2026-09-01 00:00:00' DATE_ADD('2026-09-30', INTERVAL 1 DAY)、'2026-09-01' + INTERVAL 1 MONTH 実行済み(8.0.46)
PostgreSQL 16 DATE '2026-09-01' または '2026-09-01' TIMESTAMP '2026-09-01 00:00:00' DATE '2026-09-30' + 1、DATE '2026-09-01' + INTERVAL '1 month' 実行済み(16.15)
SQLite 3 '2026-09-01'(文字列として保存・比較) '2026-09-01 00:00:00' date('2026-09-30','+1 day')、date('2026-09-01','+1 month') 実行済み(3.45.1)
SQL Server '20260901'(区切りなしの8桁)や CAST('2026-09-01' AS date) '2026-09-01T00:00:00' DATEADD(day, 1, '20260930')、DATEADD(month, 1, '20260901') 未実行
Oracle Database DATE '2026-09-01' または TO_DATE('2026-09-01','YYYY-MM-DD') TIMESTAMP '2026-09-01 00:00:00' DATE '2026-09-30' + 1、ADD_MONTHS(DATE '2026-09-01', 1) 未実行

DBごとに知っておくと役立つ点を補足します。

DB 知っておきたい点
MySQL DATETIME は既定で秒まで。DATETIME(3) や DATETIME(6) にすると秒より下も保存され、23:59:59 指定では端数が抜ける(今回 DATETIME(3) で確認)
PostgreSQL timestamp はマイクロ秒まで保存する。BETWEEN SYMMETRIC が使える。存在しない日付はエラーになる
SQLite 専用の日付型がなく、文字列として比べる。書式(桁数・区切り)をそろえて保存すると、文字列の大小と日時の前後が一致する
SQL Server(未実行) datetime 型は精度が約3ミリ秒単位で、上限を 23:59:59.999 と書くと翌日 0 時に丸められることが知られている。半開区間で書くとこの影響を受けない
Oracle(未実行) DATE 型でも時刻(秒まで)を持つため、DATE 型の列でも最終日が抜ける現象が起こりうる。半開区間で書くのが定番

SQLite で「翌月」を求める関数は、次のように確認しました。

-- SQLite 3.45.1
SELECT date('2026-09-01','+1 month'), date('2026-09-30','+1 day');
2026-10-01|2026-10-01

どのDBでも共通して使えるのは「列 >= 開始 AND 列 < 翌日(翌月1日)」の形です。DBを移るときも、翌日を求める関数の部分だけを置き換えれば済みます。

BETWEENとインデックスの関係

BETWEEN は、列にインデックス(検索を速くするための索引)があれば範囲検索に利用されます。PostgreSQL 16.15 で、約20万行を追加して ordered_at にインデックスを作り、実行計画(DBがどう検索するかの計画)を確認しました。

CREATE INDEX idx_orders_at ON orders(ordered_at);

EXPLAIN SELECT * FROM orders
WHERE ordered_at BETWEEN '2020-02-01' AND '2020-02-02';
 Index Scan using idx_orders_at on orders  (cost=0.42..58.38 rows=1448 width=18)
   Index Cond: ((ordered_at >= '2020-02-01 00:00:00'::timestamp without time zone)
            AND (ordered_at <= '2020-02-02 00:00:00'::timestamp without time zone))

実行計画の中で、BETWEEN が >= と <= の2つの条件に置き換えられているのがわかります(表示は読みやすいよう改行しています)。半開区間で書いた場合も同じく Index Scan になりました。

 Index Scan using idx_orders_at on orders  (cost=0.42..111.04 rows=2831 width=18)
   Index Cond: ((ordered_at >= '2020-02-01 00:00:00'::timestamp without time zone)
            AND (ordered_at < '2020-02-03 00:00:00'::timestamp without time zone))

一方、列を日付に変換してから比べると、テーブル全体を読む計画になりました。

EXPLAIN SELECT * FROM orders
WHERE CAST(ordered_at AS DATE) BETWEEN '2020-02-01' AND '2020-02-02';
 Gather  (cost=1000.00..4725.87 rows=1000 width=18)
   Workers Planned: 1
   ->  Parallel Seq Scan on orders  (cost=0.00..3625.87 rows=588 width=18)
         Filter: (((ordered_at)::date >= '2020-02-01'::date) AND ((ordered_at)::date <= '2020-02-02'::date))
書き方 PostgreSQL 16.15 の実行計画
ordered_at BETWEEN a AND b Index Scan(インデックスで範囲を絞る)
ordered_at >= a AND ordered_at < b Index Scan(同上)
CAST(ordered_at AS DATE) BETWEEN a AND b Seq Scan(全体を読んで絞り込む)

MySQL 8.0.46 でも、8行のサンプルで EXPLAIN を見ると、半開区間は type: range(範囲検索)、DATE(ordered_at) BETWEEN … は type: index(インデックス全体を読む)と表示されました。「列はそのまま、比べる値のほうを整える」と覚えておくと、速さと正確さの両方を保てます。

BETWEENと比較演算子の使い分け

場面 おすすめ 理由
整数の範囲(1〜10、年齢、数量など) BETWEEN 両端を含む意味がそのまま読める
DATE 型の期間 BETWEEN 1日が1つの値なので最終日も含まれる
DATETIME / TIMESTAMP 型の期間 >= と < 最終日の時刻と秒の端数を取りこぼさない
小数の区間を連続して区切る(0以上10未満、10以上20未満…) >= と < 境界の値が両方の区間に入るのを避けられる
文字列の頭文字の範囲 >= と < 上限より長い文字列を取りこぼさない
範囲の外側 NOT BETWEEN(必要なら OR 列 IS NULL) NULL の行を含めるかを明示できる

連続する区間を BETWEEN で区切ると、境界の値が両方に数えられます。たとえば「1000〜2000」と「2000〜3000」をどちらも BETWEEN で書くと、2000 の行は両方に入ります。区間を並べるときは半開区間が向いています。

-- 金額帯ごとの件数(境界が重ならない書き方)
SELECT
  SUM(CASE WHEN amount >= 0    AND amount < 1000 THEN 1 ELSE 0 END) AS under_1000,
  SUM(CASE WHEN amount >= 1000 AND amount < 2000 THEN 1 ELSE 0 END) AS from_1000,
  SUM(CASE WHEN amount >= 2000 AND amount < 3000 THEN 1 ELSE 0 END) AS from_2000,
  SUM(CASE WHEN amount >= 3000                   THEN 1 ELSE 0 END) AS from_3000
FROM orders;

結果が合わないときのチェックリスト

症状 確認すること 直し方
最終日のデータが少ない・出ない 列が日時型で、上限に日付だけを書いていないか 列 < 翌日 に変える
最終日の最後の1秒が出ない 列が秒より下の精度を持っていないか 23:59:59 指定をやめて 列 < 翌日 にする
0 件になる 下限と上限の順番が逆になっていないか 小さいほうを先に書く
MySQL で 0 件+警告 1292 9月31日など存在しない日付を書いていないか 「翌月1日未満」で書く
範囲内と範囲外を足しても全件にならない 列に NULL がないか OR 列 IS NULL を足す、または件数を別に数える
頭文字の範囲で最後の文字のデータが出ない 上限が 'D' のように短くないか 列 < 'E' のように次の文字未満で書く
検索に時間がかかる 列に関数や CAST をかけていないか 列はそのままにして、比べる値を整える
同じ行が2つの区間に数えられる 連続する区間を BETWEEN で書いていないか >= と < で区切る

よくある質問

Q. BETWEENは境界の値を含みますか? A. 含みます。x BETWEEN 1 AND 5 は 1 以上 5 以下で、1 と 5 のどちらも結果に入ります。MySQL 8.0.46・PostgreSQL 16.15・SQLite 3.45.1 で確認しました。片方の端を含めたくないときは >= と < を組み合わせて書きます。

Q. 日付の範囲指定で、最終日のデータが取れないのはなぜですか? A. 列が日時型(時刻つき)の場合、'2026-09-30' は「9月30日 0:00:00」として比べられるためです。0 時ちょうどより後のデータは上限を超えます。列 >= '2026-09-01' AND 列 < '2026-10-01' のように、翌日(翌月1日)未満で書くと最終日のすべての時刻が含まれます。

Q. NOT BETWEENでNULLの行は取れますか? A. 取れません。NULL との比較は「不明」になり、NOT を付けても不明のままなので WHERE を通りません。NULL の行も含めたいときは OR 列 IS NULL を足します。

Q. BETWEENと「>= AND <=」では、どちらが速いですか? A. 同じ意味の条件として扱われます。PostgreSQL 16.15 の実行計画では、BETWEEN が >= と <= の条件に置き換えられてインデックス検索に使われていました。速さよりも「境界を含めたいかどうか」で書き方を選ぶのがおすすめです。

まとめ

  • BETWEEN は両端を含む(以上・以下)。x BETWEEN a AND b は x >= a AND x <= b と同じ
  • 日時列に日付だけを指定すると、上限は 0:00:00 扱いになり最終日が抜ける(3つのDBで再現)
  • 日時の範囲は「>= 開始 AND < 翌日」で書くと、秒の端数や月末の日数に左右されない
  • NOT BETWEEN は NULL の行を返さない。必要なら OR 列 IS NULL を足す
  • 下限と上限を逆に書くとエラーにならず 0 件。PostgreSQL には BETWEEN SYMMETRIC がある
  • 文字列でも、上限より長い値は範囲の外。頭文字の範囲は「次の文字未満」で書く
  • 列に関数をかけず、比べる値のほうを整えると、インデックスを使った範囲検索になりやすい

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL BETWEENの使い方|日付の範囲指定と境界値・NOT BETWEEN」

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