初心者向け:MySQLで期間を指定する強力な味方「BETWEEN」と日付の扱いを徹底解説!
MySQLを使ってデータベースからデータを取得する際、特定の期間内に発生した注文、イベント、ログイン履歴などを抽出したいというニーズは非常に多いです。例えば、「先週の売上データを見たい」「今年の初めから今日までの顧客の行動を追跡したい」といった具合です。
このような「期間指定」によるデータ抽出に便利なのが、MySQLのWHERE
句と組み合わせて使用するBETWEEN
句です。特に日付や時間のデータを扱う際には、このBETWEEN
句が直感的で分かりやすい指定方法を提供してくれます。
しかし、日付や時間データは数値や文字列とは少し異なる特性を持っているため、BETWEEN
句を使う際にいくつかの注意点があります。特に、指定した期間の「終わり」をどのように扱うかが、意図した通りの結果を得るための鍵となります。
この記事は、MySQLを使い始めたばかりの初心者の方を対象に、BETWEEN
句を使って日付データを指定する方法を、基本的なことから応用、さらには知っておくべき落とし穴やパフォーマンスの考慮事項まで、約5000語というボリュームで徹底的に解説します。
この記事を読み終える頃には、あなたはMySQLで日付期間を指定することに自信を持ち、より正確かつ効率的にデータを抽出できるようになっているでしょう。
さあ、MySQLの日付期間指定の世界へ一緒に踏み出しましょう!
1. なぜ期間指定が必要なのか? データベースと日付データの重要性
私たちが日々扱う情報の多くは、時間や日付と密接に関係しています。Webサイトへのアクセス履歴、商品の購入記録、センサーから送られてくる測定値、システムのログなど、ほとんどすべてのデータには「いつ」発生したかという情報が付随しています。
データベースは、これらの膨大なデータを効率的に整理し、保存し、そして必要な時に取り出すための強力なツールです。MySQLはその中でも最も広く使われているデータベースシステムの一つです。
データベースに蓄積されたデータを分析する際、特定の「期間」に絞り込んで集計したり、詳細を確認したりすることは非常に一般的です。例えば、
- あるキャンペーン期間中の売上だけを知りたい。
- 過去1ヶ月間のシステムエラーの発生状況を把握したい。
- 特定のユーザーが初めてサービスを利用した日から今日までの行動履歴を見たい。
- 週末に発生した注文だけを抽出したい。
といった分析は、すべて期間を指定することから始まります。
MySQLでは、このようなデータの絞り込みを行うためにSELECT
文のWHERE
句を使用します。そして、日付や時間に関わる条件を指定する際に、BETWEEN
句が便利な選択肢となるのです。
2. MySQLにおける日付・時間データ型の基礎を知ろう
BETWEEN
句を使って日付期間を指定する前に、MySQLがどのような日付・時間データ型を持っているのかを理解しておくことが重要です。適切なデータ型を選ぶことは、データの正確な格納はもちろん、後の期間指定や検索の効率にも影響します。
MySQLには主に以下の日付・時間データ型があります。
DATE
: 日付のみを格納します。YYYY-MM-DD
形式です。時間情報は持ちません。- 例:
'2023-10-27'
- 格納範囲:
'1000-01-01'
から'9999-12-31'
- 例:
TIME
: 時間のみを格納します。HH:MM:SS
形式や、より長い時間、マイクロ秒を含む形式も扱えます。日付情報は持ちません。- 例:
'10:30:00'
,'23:59:59.999999'
- 格納範囲:
'-838:59:59'
から'838:59:59'
- 例:
DATETIME
: 日付と時間(秒まで)を格納します。YYYY-MM-DD HH:MM:SS
形式です。- 例:
'2023-10-27 10:30:00'
- 格納範囲:
'1000-01-01 00:00:00'
から'9999-12-31 23:59:59'
- MySQL 5.6.4 以降では、マイクロ秒の精度を指定することも可能です(例:
DATETIME(3)
でミリ秒精度)。
- 例:
TIMESTAMP
: 日付と時間(秒まで)を格納します。DATETIME
と似ていますが、いくつかの重要な違いがあります。- 例:
'2023-10-27 10:30:00'
- 格納範囲:
'1970-01-01 00:00:01'
UTC から'2038-01-19 03:14:07'
UTC(一般的なシステムでの範囲、バージョンによって異なる場合があります) - 大きな違いとして、
TIMESTAMP
は通常、タイムゾーンの影響を受けます。値を格納する際は現在のタイムゾーンの時刻がUTC(協定世界時)に変換されて格納され、取り出す際は現在のタイムゾーンに合わせて表示されます。また、NULL
を許可しない設定や、行が更新された際に自動的に現在時刻が格納される設定がデフォルトになることがあります(これもバージョンや設定によります)。 - MySQL 5.6.4 以降では、マイクロ秒の精度を指定することも可能です(例:
TIMESTAMP(3)
でミリ秒精度)。
- 例:
どのデータ型を選択するかは、格納したい情報の種類によります。
- 誕生日など、日付だけが必要なら
DATE
。 - イベントの開始時刻など、日付と時刻が必要なら
DATETIME
。 - データの作成日時や更新日時など、タイムゾーンの影響を受けつつ、自動更新などの特性も利用したい場合は
TIMESTAMP
。 - 時間だけを記録したい場合は
TIME
(これは期間指定で使われることは比較的少ないかもしれません)。
例:サンプルテーブルの作成
これから解説で使用するサンプルテーブルを作成してみましょう。例えば、オンラインストアの注文履歴を記録するテーブルです。
“`sql
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT,
order_date DATETIME, — 注文が発生した日時
total_amount DECIMAL(10, 2),
— その他のカラム…
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, — レコード作成日時 (タイムゾーン影響あり)
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP — レコード更新日時 (自動更新、タイムゾーン影響あり)
);
— サンプルデータをいくつか挿入
INSERT INTO orders (customer_id, order_date, total_amount) VALUES
(101, ‘2023-10-20 10:00:00’, 5500.00),
(102, ‘2023-10-25 14:30:00’, 8200.00),
(101, ‘2023-10-26 09:15:00’, 3100.00),
(103, ‘2023-10-27 11:00:00’, 12000.00),
(102, ‘2023-10-27 16:45:00’, 6700.00),
(101, ‘2023-10-28 08:00:00’, 4900.00),
(104, ‘2023-11-01 20:00:00’, 1500.00),
(103, ‘2023-11-02 09:30:00’, 9800.00),
(101, ‘2023-11-02 15:00:00’, 2500.00),
(105, ‘2023-11-03 10:00:00’, 7600.00);
— 別のデータ型を持つテーブル例
CREATE TABLE events (
event_id INT PRIMARY KEY AUTO_INCREMENT,
event_name VARCHAR(255),
event_date DATE, — イベント開催日 (日付のみ)
start_time TIME — イベント開始時刻 (時間のみ)
);
INSERT INTO events (event_name, event_date, start_time) VALUES
(‘Meeting’, ‘2023-10-30′, ’14:00:00’),
(‘Workshop’, ‘2023-11-05′, ’10:00:00’),
(‘Seminar’, ‘2023-11-05′, ’15:30:00’);
“`
これらのテーブルとデータを使って、BETWEEN
句による期間指定の方法を見ていきましょう。
3. WHERE
句の基本と日付・時間データの比較
期間を指定するということは、特定の条件を満たす行だけを抽出するということです。これはSQLのSELECT
文におけるWHERE
句の役割そのものです。
WHERE
句は、条件式を使って取得する行をフィルタリングします。条件式には、カラム名、比較演算子(=
, !=
, <
, >
, <=
, >=
)、論理演算子(AND
, OR
, NOT
)、そして後に説明するBETWEEN
やLIKE
, IN
, IS NULL
などが使用されます。
例えば、orders
テーブルから合計金額が10000以上の注文を取得するには、以下のようにします。
sql
SELECT *
FROM orders
WHERE total_amount >= 10000;
日付や時間データに対しても、これらの比較演算子を使って条件を指定できます。MySQLでは、日付や時間データは新しくなるほど「大きい」と判断されます。
'2023-10-27'
は'2023-10-26'
より大きい。'2023-10-27 14:30:00'
は'2023-10-27 10:00:00'
より大きい。'2023-11-01'
は'2023-10-31'
より大きい。
したがって、例えば10月27日以降の注文を取得するには、order_date
カラムに対して>=
演算子を使います。
sql
SELECT *
FROM orders
WHERE order_date >= '2023-10-27 00:00:00';
あるいは、10月27日以前の注文を取得するには、<=
演算子を使います。
sql
SELECT *
FROM orders
WHERE order_date <= '2023-10-27 23:59:59'; -- この書き方には少し注意が必要ですが、ここでは一旦このように書きます
このように、基本的な比較演算子とAND
やOR
を組み合わせることで、期間を指定することも可能です。例えば、10月26日から10月27日までの注文を取得するには、以下のように書けます。
sql
SELECT *
FROM orders
WHERE order_date >= '2023-10-26 00:00:00'
AND order_date <= '2023-10-27 23:59:59'; -- この書き方も後で詳しく解説します
この「〇〇以上 かつ △△以下」という条件を、よりシンプルに記述するためのものがBETWEEN
句です。
4. BETWEEN
句とは? 期間指定の基本
BETWEEN
句は、ある値が指定した範囲(下限値から上限値まで)に含まれるかどうかを判定するためのものです。基本的な構文は以下のようになります。
sql
expression BETWEEN lower_value AND upper_value
これは、以下の条件式と同じ意味になります。
sql
expression >= lower_value AND expression <= upper_value
つまり、BETWEEN
句は「指定した値が、下限値以上かつ上限値以下である」という条件をシンプルに書くための「糖衣構文(Syntactic Sugar)」のようなものです。
BETWEEN
句は、数値だけでなく、文字列や日付・時間データに対しても使用できます。
数値での例:
“`sql
— 価格が1000円から5000円の間の商品を検索
SELECT *
FROM products
WHERE price BETWEEN 1000 AND 5000;
— これは以下と同等
SELECT *
FROM products
WHERE price >= 1000 AND price <= 5000;
“`
文字列での例:
sql
-- 'C'から'F'で始まる名前の顧客を検索 (アルファベット順)
SELECT *
FROM customers
WHERE customer_name BETWEEN 'C' AND 'G'; -- 文字列の場合、'G'は含まれないことに注意 ('G'より小さい、つまり'F'まで)
-- これは以下と同等
SELECT *
FROM customers
WHERE customer_name >= 'C' AND customer_name <= 'G'; -- 文字列の比較ルールによる
文字列の比較では、BETWEEN 'C' AND 'G'
は ‘C…’ から ‘Fzzzz…’ のような範囲を意味します。'G'
自体は含まれません。これは日付でも同様の考え方が適用されます。
そして、本題である日付・時間データに対しても、BETWEEN
句は非常に有効です。
5. MySQLにおける日付・時間データに対するBETWEEN
の使い方
日付・時間データに対するBETWEEN
句の使い方は、数値や文字列と同じです。
sql
date_column BETWEEN start_date AND end_date
これは、以下の条件式と同等です。
sql
date_column >= start_date AND date_column <= end_date
start_date
とend_date
には、比較したい日付や時間のリテラル値(例: '2023-10-27'
, '2023-11-01 23:59:59'
)や、日付・時間型のカラム、あるいは日付・時間型の値を返す関数を指定します。
例1:特定の1日を指定する (DATE
型の場合)
events
テーブルから2023年11月5日のイベントを取得したいとします。event_date
カラムはDATE
型なので時間情報は含まれません。
sql
SELECT *
FROM events
WHERE event_date BETWEEN '2023-11-05' AND '2023-11-05';
結果:
+----------+------------+------------+------------+
| event_id | event_name | event_date | start_time |
+----------+------------+------------+------------+
| 2 | Workshop | 2023-11-05 | 10:00:00 |
| 3 | Seminar | 2023-11-05 | 15:30:00 |
+----------+------------+------------+------------+
この場合、BETWEEN '2023-11-05' AND '2023-11-05'
は event_date >= '2023-11-05' AND event_date <= '2023-11-05'
と同等であり、これは結局 event_date = '2023-11-05'
と同じ意味になります。DATE
型の場合は時間情報がないため、このようにシンプルに指定できます。
例2:特定の期間(複数日)を指定する (DATE
型の場合)
events
テーブルから2023年10月30日から2023年11月5日までのイベントを取得したい場合。
sql
SELECT *
FROM events
WHERE event_date BETWEEN '2023-10-30' AND '2023-11-05';
結果:
+----------+------------+------------+------------+
| event_id | event_name | event_date | start_time |
+----------+------------+------------+------------+
| 1 | Meeting | 2023-10-30 | 14:00:00 |
| 2 | Workshop | 2023-11-05 | 10:00:00 |
| 3 | Seminar | 2023-11-05 | 15:30:00 |
+----------+------------+------------+------------+
これもevent_date >= '2023-10-30' AND event_date <= '2023-11-05'
と同等であり、DATE
型なので直感的に分かりやすいです。
6. DATETIME
やTIMESTAMP
でのBETWEEN
– 終端の扱いに潜む落とし穴
ここからが、日付期間指定、特にDATETIME
やTIMESTAMP
型を扱う上での重要なポイントです。これらの型は日付だけでなく時間情報も持っているため、BETWEEN
句の終端値の扱いに注意が必要です。
BETWEEN lower_value AND upper_value
は expression >= lower_value AND expression <= upper_value
を意味することを思い出してください。
例として、orders
テーブルから2023年10月26日から2023年10月27日までの注文を取得したいとします。order_date
はDATETIME
型です。
もし安易に以下のように指定すると、意図した結果が得られない可能性があります。
sql
-- 意図通りにならない可能性のある例
SELECT *
FROM orders
WHERE order_date BETWEEN '2023-10-26' AND '2023-10-27';
このクエリは、内部的には以下のように解釈されます。
sql
SELECT *
FROM orders
WHERE order_date >= '2023-10-26 00:00:00'
AND order_date <= '2023-10-27 00:00:00'; -- 暗黙的に時刻が '00:00:00' とみなされる
この条件では、order_date
が2023年10月26日の0時0分0秒から、2023年10月27日の0時0分0秒までの範囲に含まれるデータが抽出されます。
サンプルデータを見てみましょう。
'2023-10-26 09:15:00'
は範囲に含まれます。'2023-10-27 11:00:00'
は 含まれません! なぜなら、2023年10月27日の0時0分0秒より後の時刻だからです。
したがって、このクエリの実行結果は以下のようになります。
sql
+----------+-------------+---------------------+--------------+---------------------+---------------------+
| order_id | customer_id | order_date | total_amount | created_at | updated_at |
+----------+-------------+---------------------+--------------+---------------------+---------------------+
| 3 | 101 | 2023-10-26 09:15:00 | 3100.00 | 2023-11-06 10:00:00 | 2023-11-06 10:00:00 |
+----------+-------------+---------------------+--------------+---------------------+---------------------+
2023年10月27日の注文データが抽出されません。これがDATETIME
やTIMESTAMP
でBETWEEN
を使う際の最も一般的な落とし穴です。終端の日付の「その日全体」を含めたい場合、単純に日付だけを指定すると、終端日の0時0分0秒までしか含まれないのです。
終端の日付全体を含めるための対策
では、2023年10月26日から2023年10月27日までの両日全ての時間帯の注文を取得するにはどうすれば良いでしょうか? いくつかの方法があります。
方法1:終端の日付の「次の日」の開始時刻を上限とする(推奨)
これは最も一般的で推奨される方法です。終端の日付(ここでは10月27日)の次の日(10月28日)の0時0分0秒を上限とします。ただし、条件式には<=
ではなく<
を使います。
sql
SELECT *
FROM orders
WHERE order_date >= '2023-10-26 00:00:00'
AND order_date < '2023-10-28 00:00:00'; -- 10月28日の0時0分0秒より小さい
このように記述することで、2023年10月26日の0時0分0秒から、2023年10月28日の0時0分0秒の直前(つまり2023年10月27日 23:59:59.999…)までの範囲に含まれる全てのデータが取得できます。
この方法は、マイクロ秒以下の精度を持つデータ型に対しても正確に機能するため、最も安全で推奨されます。
この方法をBETWEEN
句で実現することはできません。BETWEEN
は常に>= AND <=
として解釈されるため、BETWEEN '2023-10-26' AND '2023-10-27'
は '2023-10-26 00:00:00'
から '2023-10-27 00:00:00'
の範囲となり、 '2023-10-28'
を上限に指定すると '2023-10-26 00:00:00'
から '2023-10-28 00:00:00'
の範囲になってしまい、これも意図と少し異なります(10月28日の0時0分0秒自体が含まれてしまう)。
したがって、DATETIME
やTIMESTAMP
カラムで特定の日付を含む期間全体を指定する場合、BETWEEN
を使うよりも、上記のように>= 開始日時の0時0分0秒 AND < 次の日付の0時0分0秒
という形式で比較演算子を使う方が、より正確で安全です。
それでもBETWEEN
を使いたい場合は、上限時刻を明示的に指定する必要があります。
方法2:終端の日付の末尾の時刻を明示的に指定する
終端の日付(10月27日)の可能な限り遅い時刻を上限値としてBETWEEN
句に指定します。
sql
-- 終端時刻を '23:59:59' と指定する例
SELECT *
FROM orders
WHERE order_date BETWEEN '2023-10-26 00:00:00' AND '2023-10-27 23:59:59';
または
sql
-- 暗黙的に時刻が '00:00:00' とみなされることを利用し、開始時刻だけを明示
SELECT *
FROM orders
WHERE order_date BETWEEN '2023-10-26 00:00:00' AND '2023-10-27'; -- これは危険
上記の例は、終端を '2023-10-27'
と指定した場合、MySQLは内部的に '2023-10-27 00:00:00'
として扱うため、終端の日付全体を含めるという目的には合致しません。
したがって、終端の日付全体を含めるためには、明示的にその日の最後の時刻を指定する必要があります。通常は '23:59:59'
とします。
sql
SELECT *
FROM orders
WHERE order_date BETWEEN '2023-10-26 00:00:00' AND '2023-10-27 23:59:59';
このクエリは、2023年10月26日の0時0分0秒から、2023年10月27日の23時59分59秒までの範囲に含まれるデータを取得します。
ただし、この方法には注意点があります。
DATETIME
やTIMESTAMP
型が秒よりも細かい精度(ミリ秒、マイクロ秒など)を持っている場合、この指定方法ではその日の最後のマイクロ秒が含まれません(例: '23:59:59.999'
など)。もしデータベースにマイクロ秒単位のデータが格納されている可能性があるなら、この方法は不完全です。例えば、'2023-10-27 23:59:59.500'
のようなデータは抽出されますが、'2023-10-27 23:59:59.999'
のようなデータは抽出されない可能性があります(データベースの設定やバージョンによる振る舞いの違いにも依存します)。
したがって、秒よりも細かい精度を考慮する必要がある場合は、方法1(>= 開始日 < 次の日の開始日
)を強く推奨します。
マイクロ秒まで考慮してBETWEEN
を使うなら、上限時刻を '23:59:59.999999'
のように指定することも可能ですが、これはデータ型の精度に依存し、煩雑になりがちです。
sql
-- マイクロ秒まで考慮する場合 (データ型が対応していれば)
SELECT *
FROM orders
WHERE order_date BETWEEN '2023-10-26 00:00:00.000000' AND '2023-10-27 23:59:59.999999';
この指定は、カラムの精度に合わせて調整する必要があります(例: DATETIME(3)
なら.999
まで)。
結論として、DATETIME
やTIMESTAMP
型で「特定の日付範囲のデータ全体」を取得したい場合は、BETWEEN
を使うよりも、
WHERE date_column >= '開始日 00:00:00' AND date_column < '次の日 00:00:00'
という形式を使う方が、より正確で安全、かつ推奨される方法です。
もしどうしてもBETWEEN
を使いたい場合、かつデータ型が秒以下の精度を持つ場合は、終端の時刻指定に細心の注意を払うか、終端日付の次の日の開始時刻の直前までを指定する方法(例: BETWEEN '開始日 00:00:00' AND '次の日 - 1マイクロ秒'
のような表現は直接はできませんが、考え方として)を検討することになりますが、これはSQLが複雑になり現実的ではありません。やはり、>= 開始日 AND < 次の日
の形式がシンプルかつ正確です。
方法3:日付部分だけを抽出して比較する(注意が必要)
カラムの時刻情報を無視して、日付部分だけで比較するという方法も考えられます。MySQLのDATE()
関数を使うと、DATETIME
やTIMESTAMP
型の値から日付部分だけを取り出すことができます。
sql
-- DATE() 関数を使う例 (インデックスに注意)
SELECT *
FROM orders
WHERE DATE(order_date) BETWEEN '2023-10-26' AND '2023-10-27';
このクエリは、order_date
カラムの値から日付部分だけを取り出し、その日付が '2023-10-26'
から '2023-10-27'
の間に含まれるかどうかを判定します。この場合、終端の10月27日の日付全体が正しく含まれます。
ただし、この方法には重大な注意点があります。
WHERE
句の条件式でカラムに対して関数(この場合はDATE()
)を使用すると、MySQLは通常、そのカラムに作成されているインデックスを利用できなくなります。インデックスは大量のデータから高速に目的のデータを検索するための仕組みです。インデックスが使えない場合、MySQLはテーブルの全ての行を順番に調べて条件を満たすかどうかを判定することになります。これを「フルテーブルスキャン」と呼びます。
テーブルの行数が少ないうちは問題になりませんが、数万行、数十万行、数百万行と増えていくと、フルテーブルスキャンは非常に遅くなり、クエリのパフォーマンスが著しく低下します。
したがって、パフォーマンスが重要なシステムでは、WHERE
句のカラムにDATE()
のような関数を適用することは避けるべきです。
もしどうしても日付部分だけで比較したい場合は、カラムのデータ型をDATE
型に設計するか、または仮想カラムや関数ベースインデックスなどの高度な機能(MySQL 8.0以降などで利用可能)を検討する必要がありますが、これは初心者向けの範囲を超えるため、ここでは「原則としてWHERE
句のカラムに関数を適用しない」という点だけを覚えておいてください。
終端の扱いのまとめ (DATETIME
/TIMESTAMP
の場合)
BETWEEN '開始日' AND '終端日'
(時刻指定なし) -> 危険! 終端日の0時0分0秒までしか含まれない。BETWEEN '開始日 00:00:00' AND '終端日 23:59:59'
-> 終端日のほとんどが含まれるが、秒以下の精度に注意が必要。BETWEEN '開始日 00:00:00.000000' AND '終端日 23:59:59.999999'
-> 終端日の秒以下の精度まで考慮する場合。データ型に合わせる必要がある。煩雑。date_column >= '開始日 00:00:00' AND date_column < '次の日 00:00:00'
-> 最も推奨! 正確で安全、かつインデックスが効きやすい。
初心者の方は、特にDATETIME
やTIMESTAMP
を扱う際は、BETWEEN
よりも カラム >= 開始時刻 AND カラム < 次の日の開始時刻
の形式を優先して使うことをお勧めします。これは期間指定の鉄板パターンとして覚えておくと良いでしょう。
それでも、特定の時刻範囲を含めてBETWEEN
を使うのが直感的だと感じる場面もあるでしょう。例えば、特定の日の午前中(0時から12時まで)のデータを探す場合などです。
sql
-- 2023年10月27日の午前中 (0時から12時まで) の注文
SELECT *
FROM orders
WHERE order_date BETWEEN '2023-10-27 00:00:00' AND '2023-10-27 12:00:00';
この場合は、開始時刻と終了時刻を両方とも明示的に指定することで、意図した通りの範囲(2023年10月27日 0時0分0秒 から 2023年10月27日 12時0分0秒 まで)を指定できます。このように、時刻情報が重要な要素となる期間指定であれば、BETWEEN
と時刻の明示的な指定は非常に有効です。
7. さまざまな期間指定の応用例
これまでに学んだことを活かして、様々な期間を指定してみましょう。MySQLには現在の日付や時刻を取得したり、日付を計算したりするための便利な関数が多数用意されています。これらを組み合わせることで、動的な期間指定が可能になります。
代表的なMySQLの日付・時間関数:
CURDATE()
: 現在の日付 (YYYY-MM-DD
形式)NOW()
: 現在の日付と時刻 (YYYY-MM-DD HH:MM:SS
形式、またはマイクロ秒含む)CURTIME()
: 現在の時刻 (HH:MM:SS
形式、またはマイクロ秒含む)DATE(datetime_expr)
: 日付/日時式から日付部分を抽出TIME(datetime_expr)
: 日付/日時式から時間部分を抽出YEAR(date)
,MONTH(date)
,DAY(date)
: 年、月、日を抽出HOUR(time)
,MINUTE(time)
,SECOND(time)
: 時、分、秒を抽出DATE_ADD(date, INTERVAL value unit)
: 日付/日時に時間間隔を加算DATE_SUB(date, INTERVAL value unit)
: 日付/日時から時間間隔を減算LAST_DAY(date)
: 指定した日付を含む月の最終日DAYOFWEEK(date)
: 曜日の番号 (1=日曜日, 2=月曜日, …)WEEKDAY(date)
: 曜日の番号 (0=月曜日, 1=火曜日, …)DATE_FORMAT(date, format)
: 日付を指定した形式の文字列に変換(WHERE句での利用は原則避ける!)
これらの関数とBETWEEN
または比較演算子を組み合わせて、具体的な期間を指定してみましょう。
例1:今日のデータ
orders
テーブルから今日の注文を取得します。今日の開始時刻から次の日の開始時刻の前までを指定します。
sql
SELECT *
FROM orders
WHERE order_date >= CURDATE() -- CURDATE() は今日の 'YYYY-MM-DD' を返す。比較では 'YYYY-MM-DD 00:00:00' とみなされる
AND order_date < DATE_ADD(CURDATE(), INTERVAL 1 DAY); -- 今日の日付に1日を加えて、明日の日付を取得
この方法は、DATETIME
やTIMESTAMP
の時刻部分を正確に扱いつつ、インデックスも利用できるため推奨です。
もしorder_date
がDATE
型ならシンプルに以下のように書けます。
sql
SELECT *
FROM events
WHERE event_date = CURDATE(); -- DATE型なので直接比較できる
例2:昨日のデータ
昨日の注文を取得します。
sql
SELECT *
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) -- 今日の日付から1日減算して昨日の日付を取得
AND order_date < CURDATE(); -- 今日の日付の0時0分0秒の前まで
例3:過去7日間のデータ(今日を含む)
過去7日間の注文を取得します。
sql
SELECT *
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) -- 今日を含む7日前の日付 ('今日' - 6日)
AND order_date < DATE_ADD(CURDATE(), INTERVAL 1 DAY); -- 今日の日付の次の日
または、BETWEEN
を使って開始日と終端日を明確にする(時刻に注意しながら)。
“`sql
— 時刻に注意が必要なBETWEENの例 (DATETIME/TIMESTAMPの場合)
SELECT *
FROM orders
WHERE order_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 6 DAY) AND DATE_ADD(CURDATE(), INTERVAL 1 DAY) – INTERVAL 1 SECOND; — 次の日の0時0分0秒から1秒引く (マイクロ秒精度だと不十分)
— もしくは、終端日の最後の時刻を明示 (やはり秒以下に注意)
SELECT *
FROM orders
WHERE order_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 6 DAY) AND CONCAT(CURDATE(), ‘ 23:59:59’); — CONCATで文字列として結合して時刻を付与
``
>= 開始日 AND < 次の日の開始日` のパターンが一番安全で推奨です。
やはり、
sql
-- 推奨パターンで過去7日間 (今日を含む)
SELECT *
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
AND order_date < DATE_ADD(CURDATE(), INTERVAL 1 DAY);
例4:今月のデータ
今月1日から今日までの注文を取得します。月の初日を計算し、上限を今日または次の日に設定します。
月の初日は、任意の日付のYYYY-MM-01
とすることで取得できます。例えば、DATE_SUB(CURDATE(), INTERVAL DAY(CURDATE()) - 1 DAY)
または DATE_FORMAT(CURDATE(), '%Y-%m-01')
のように計算できます。
“`sql
— 今月の初日を計算
SET @first_day_of_month = DATE_SUB(CURDATE(), INTERVAL DAY(CURDATE()) – 1 DAY);
SELECT *
FROM orders
WHERE order_date >= @first_day_of_month
AND order_date < DATE_ADD(CURDATE(), INTERVAL 1 DAY); — 今日の日付の次の日
“`
この範囲は「今月の初日から今日の終わりまで」になります。
もし「今月全体」のデータを取得したい場合は、今月の初日から次の月の初日の前までを指定します。
“`sql
— 今月全体のデータを取得
SET @first_day_of_month = DATE_SUB(CURDATE(), INTERVAL DAY(CURDATE()) – 1 DAY);
SET @first_day_of_next_month = DATE_ADD(@first_day_of_month, INTERVAL 1 MONTH);
SELECT *
FROM orders
WHERE order_date >= @first_day_of_month
AND order_date < @first_day_of_next_month;
``
BETWEENで今月全体を指定したい場合は、終端を今月の最終日の23:59:59などに設定します。
LAST_DAY()`関数が便利です。
sql
-- BETWEENで今月全体 (DATETIME/TIMESTAMPの場合、時刻に注意)
SELECT *
FROM orders
WHERE order_date BETWEEN DATE_SUB(CURDATE(), INTERVAL DAY(CURDATE()) - 1 DAY)
AND CONCAT(LAST_DAY(CURDATE()), ' 23:59:59'); -- やはり秒以下の精度に注意が必要
例5:特定の月のデータ
例えば、2023年10月のデータ全体を取得したい場合。開始日は2023-10-01、終了日は2023-10-31になります。
“`sql
— 推奨パターンで2023年10月全体
SELECT *
FROM orders
WHERE order_date >= ‘2023-10-01 00:00:00’
AND order_date < ‘2023-11-01 00:00:00’; — 11月1日の0時0分0秒の前まで
— BETWEENで2023年10月全体 (DATETIME/TIMESTAMPの場合、時刻に注意)
SELECT *
FROM orders
WHERE order_date BETWEEN ‘2023-10-01 00:00:00’ AND ‘2023-10-31 23:59:59’;
“`
例6:特定の年のデータ
例えば、2023年全体データ。開始日は2023-01-01、終了日は2023-12-31です。
“`sql
— 推奨パターンで2023年全体
SELECT *
FROM orders
WHERE order_date >= ‘2023-01-01 00:00:00’
AND order_date < ‘2024-01-01 00:00:00’; — 2024年1月1日の0時0分0秒の前まで
— BETWEENで2023年全体 (DATETIME/TIMESTAMPの場合、時刻に注意)
SELECT *
FROM orders
WHERE order_date BETWEEN ‘2023-01-01 00:00:00’ AND ‘2023-12-31 23:59:59’;
“`
このように、開始と終了の日時を明確に指定し、特にDATETIME
/TIMESTAMP
の場合は終端の扱いに注意することで、様々な期間を正確に指定できます。そして、現在の日付を基準にした動的な期間(今日、昨日、今月など)は、CURDATE()
, NOW()
, DATE_SUB()
, DATE_ADD()
などの関数と組み合わせることで実現できます。
補足:BETWEEN
が特に活きる場面
BETWEEN
句は、開始と終了の時点が明確に決まっている場合に、>= AND <=
よりも簡潔に記述できるという利便性があります。
例えば、「今日の午前10時から午後3時までの間の注文」のように、日付だけでなく時刻も含む厳密な範囲を指定したい場合、BETWEEN
を使うと直感的に記述できます。
sql
-- 2023年10月27日の10:00:00から15:00:00までの注文
SELECT *
FROM orders
WHERE order_date BETWEEN '2023-10-27 10:00:00' AND '2023-10-27 15:00:00';
このクエリは、order_date >= '2023-10-27 10:00:00' AND order_date <= '2023-10-27 15:00:00'
と同等です。この場合、終端の15:00:00も範囲に含まれます。このように、開始と終了の「時点」が明確で、その両端を含む範囲を指定したい場合には、BETWEEN
が非常に分かりやすいです。
一方で、「終端日の終わりまで」のように、「期間」として指定したい場合には、終端時刻の精度問題から >= 開始 AND < 次の日の開始
の形式が推奨される、という使い分けを意識すると良いでしょう。
8. BETWEEN
と他の条件の組み合わせ、NOT BETWEEN
BETWEEN
句は、もちろん他の条件と組み合わせて使用できます。例えば、「2023年10月中の注文で、合計金額が5000円以上のもの」といった条件です。
sql
-- 2023年10月中の注文で、合計金額が5000円以上のもの
SELECT *
FROM orders
WHERE order_date >= '2023-10-01 00:00:00' AND order_date < '2023-11-01 00:00:00'
AND total_amount >= 5000;
このように、WHERE
句内で複数の条件をAND
やOR
で結合できます。BETWEEN
句全体も一つの条件式として扱われます。
NOT BETWEEN
指定した期間に含まれないデータを抽出したい場合は、NOT BETWEEN
を使用します。
sql
expression NOT BETWEEN lower_value AND upper_value
これは、以下の条件式と同等です。
sql
expression < lower_value OR expression > upper_value
例:2023年10月以外の注文を取得
2023年10月以外の注文を取得するには、NOT BETWEEN
を使用します。
sql
-- 2023年10月以外の注文
SELECT *
FROM orders
WHERE order_date NOT BETWEEN '2023-10-01 00:00:00' AND '2023-10-31 23:59:59'; -- 時刻に注意
これも内部的には order_date < '2023-10-01 00:00:00' OR order_date > '2023-10-31 23:59:59'
として解釈されます。やはりDATETIME
/TIMESTAMP
の場合は終端の時刻に注意が必要です。
より正確な表現としては、BETWEEN
を使わず、NOT (expression >= low AND expression <= high)
とするか、あるいはその同等な expression < low OR expression > high
の形を使用するのが良いでしょう。
sql
-- 推奨パターンで2023年10月以外の注文
SELECT *
FROM orders
WHERE order_date < '2023-10-01 00:00:00' -- 2023年10月1日より前の全て
OR order_date >= '2023-11-01 00:00:00'; -- 2023年11月1日以降の全て
この形は、2023年10月1日の0時0分0秒から2023年11月1日の0時0分0秒の直前まで、という期間の外側を指定しています。
このように、NOT BETWEEN
も便利ですが、BETWEEN
と同様にDATETIME
/TIMESTAMP
の終端の扱いに注意が必要です。正確性を期すなら、比較演算子とOR
を使った形式を検討しましょう。
9. パフォーマンスに関する重要な考慮事項:インデックスと関数の利用
データベースの操作において、データの抽出速度(クエリのパフォーマンス)は非常に重要です。特に大量のデータを扱う場合、期間指定クエリのパフォーマンスがシステム全体の応答時間に大きな影響を与えることがあります。
期間指定クエリのパフォーマンスを考える上で、最も重要な要素の一つが「インデックス」です。
インデックスの重要性
インデックスは、データベーステーブルのカラムに作成する、検索を高速化するための特殊なデータ構造です。本の索引のようなものだと考えてください。特定の値を検索する際に、テーブルの全ての行を順番に調べる代わりに、インデックスを使うことで目的の行に素早くたどり着けます。
日付や時間カラムに対して頻繁に期間指定を行う場合は、そのカラムにインデックスを作成することを強く推奨します。
orders
テーブルのorder_date
カラムにインデックスを作成する例:
sql
CREATE INDEX idx_order_date ON orders (order_date);
インデックスを作成することで、WHERE order_date BETWEEN ... AND ...
や WHERE order_date >= ... AND order_date < ...
といったクエリの実行速度が劇的に向上する可能性があります。
WHERE
句での関数利用とインデックスの問題
前述した「方法3:日付部分だけを抽出して比較する」のところで触れましたが、WHERE
句の条件式で、検索対象のカラムに対して関数を適用すると、多くの場合、インデックスが利用されなくなります。
例えば、以下のクエリです。
sql
SELECT *
FROM orders
WHERE DATE(order_date) BETWEEN '2023-10-26' AND '2023-10-27'; -- DATE() 関数を使用
このクエリでは、MySQLはorder_date
カラムの各行に対してまずDATE()
関数を実行し、その結果(日付部分)を取り出します。そして、取り出した日付部分に対してBETWEEN
条件を適用します。
このプロセスでは、order_date
カラム自体の値ではなく、関数を適用した後の「加工された値」で比較が行われます。MySQLの通常のインデックスは、カラムの「元の値」に基づいて構築されているため、加工された値での検索には利用できません。結果としてフルテーブルスキャンが発生し、パフォーマンスが低下します。
これを「インデックス不使用問題」と呼びます。
したがって、特別な理由がない限り、WHERE
句の検索対象カラムに対して関数を適用することは避けるべきです。期間指定においては、カラムに対して関数を適用する代わりに、比較対象となるリテラル値や変数、あるいは計算結果の方を、カラムのデータ型に合わせて適切に準備するのが正しいアプローチです。
良い例:
WHERE order_date >= '2023-10-01' AND order_date < '2023-11-01'
ここではorder_date
カラム自体には関数を適用していません。比較対象である'2023-10-01'
や'2023-11-01'
はリテラル値であり、MySQLはこれらの値をorder_date
カラムの値と比較するためにインデックスを利用できます。
悪い例(パフォーマンス低下の可能性が高い):
WHERE DATE(order_date) >= '2023-10-01' AND DATE(order_date) <= '2023-10-31'
パフォーマンスを確認する EXPLAIN
自分の書いたクエリがどのように実行されるのか、特にインデックスが使われているかどうかを確認したい場合は、クエリの先頭にEXPLAIN
を付けて実行します。
sql
EXPLAIN SELECT *
FROM orders
WHERE order_date >= '2023-10-01'
AND order_date < '2023-11-01';
実行結果に表示される情報(特にtype
, possible_keys
, key
, rows
, Extra
などのカラム)を見ることで、クエリの実行計画を分析できます。
type
がref
やrange
、eq_ref
などになっている場合は、インデックスが効果的に使われている可能性が高いです。type
がALL
になっている場合は、フルテーブルスキャンが行われている可能性が高いです。key
カラムには、実際に使用されたインデックス名が表示されます。Extra
カラムにUsing where
やUsing index
などが表示されることも、パフォーマンス分析の手がかりになります。
初心者の方がEXPLAIN
の結果を完全に理解するのは難しいかもしれませんが、まずはtype
がALL
になっていないか、そしてkey
に使用したいインデックス名が表示されているかを確認するだけでも、大きな違いを生むクエリの「遅さ」の原因を発見するのに役立ちます。
期間指定クエリを作成する際は、対象となる日付・時間カラムにインデックスが作成されているかを確認し、WHERE
句ではカラムに関数を適用しない書き方を心がけるようにしましょう。特にDATETIME
やTIMESTAMP
で「〇日〜〇日」という期間全体を指定したい場合は、>= 開始日の0時0分0秒 AND < 次の日の0時0分0秒
の形式が、正確性とパフォーマンスの両面から最も推奨される理由がここにあります。
10. よくある落とし穴とトラブルシューティング
これまでに説明した終端の扱いやインデックスの問題以外にも、日付・時間データを扱う上で遭遇しやすい落とし穴があります。
- 日付・時間フォーマットの間違い: MySQLは通常
'YYYY-MM-DD HH:MM:SS'
や'YYYY-MM-DD'
の形式を認識します。異なる形式の文字列を直接比較したり、STR_TO_DATE()
などの変換関数を使わずに挿入・更新しようとするとエラーになったり、意図しない結果になったりします。常にMySQLが期待する形式で日付・時間値を扱うようにしましょう。 - 文字列としての比較と日付・時間としての比較: 日付や時間カラムが誤って文字列型(
VARCHAR
など)で定義されている場合、文字列として比較が行われます。文字列比較はアルファベット順で行われるため、日付の順番とは異なる結果になることがあります(例:'2023-10-01'
は'2023-09-30'
より大きいですが、文字列としては'2023-09-30'
の方が'2023-10-01'
より小さいと判定される可能性があります。特に月や日が1桁の場合に問題が発生しやすいです)。日付・時間は必ず適切な日付・時間データ型で格納しましょう。 - データ型の不一致: 比較する両辺のデータ型が一致しない場合、MySQLが暗黙的な型変換を行いますが、これがパフォーマンス問題を引き起こしたり、予期しない結果になったりすることがあります。例えば、
VARCHAR
型のカラムを日付と比較する場合などです。可能な限り、同じデータ型の値同士を比較するようにしましょう。文字列形式の日付を扱う必要がある場合は、STR_TO_DATE()
関数を使って日付型に変換してから比較するのが安全ですが、これは前述のインデックス問題を引き起こす可能性があるため、設計段階でカラムを適切な日付型にしておくのが最善です。 - タイムゾーンの問題 (
TIMESTAMP
):TIMESTAMP
型はタイムゾーンの影響を受けます。データベースサーバーの設定、クライアントのタイムゾーン、データが格納された時のタイムゾーンなどが影響し、期待していた時刻と異なる時刻が表示されることがあります。タイムゾーンが重要なシステムでは、この特性を理解し、必要に応じてタイムゾーン設定を調整したり、アプリケーション側で時刻を変換したりする必要があります。DATETIME
型はタイムゾーンの影響を受けないため、タイムゾーンを気にせず固定の時刻を扱いたい場合に適しています。 - 夏時間 (Daylight Saving Time): 夏時間は特定の期間に時刻が1時間進められたり戻されたりするため、
TIMESTAMP
型の扱いにおいて予期しない挙動の原因となることがあります。MySQLはタイムゾーン情報(mysql.time_zone
テーブル群)に基づいて夏時間を扱いますが、これらの情報が最新でない場合や、システムのタイムゾーン設定が適切でない場合に問題が起こり得ます。
トラブルが発生した際は、以下の点を確認してみましょう。
- カラムのデータ型は正しいか? (
DATE
,DATETIME
,TIMESTAMP
など) - 比較に使っている日付・時間リテラルのフォーマットは正しいか? (
'YYYY-MM-DD HH:MM:SS'
など) DATETIME
/TIMESTAMP
の場合、終端の時刻指定は意図通りか? (BETWEEN
を使っている場合は特に注意)WHERE
句で検索対象のカラムに関数を使っていないか? (DATE()
,YEAR()
,DATE_FORMAT()
など) -> もし使っているなら、代替手段を検討する。- 対象カラムにインデックスは作成されているか?
EXPLAIN
を使ってクエリの実行計画を確認し、インデックスが使われているか確認する。TIMESTAMP
の場合、タイムゾーン設定は適切か?
これらの点を一つずつ確認していくことで、問題の原因を特定し、解決に繋げることができるでしょう。
11. まとめと次のステップ
この記事では、MySQLでBETWEEN
句を使って日付期間を指定する方法を、初心者向けに詳細に解説しました。
- MySQLには
DATE
,TIME
,DATETIME
,TIMESTAMP
といった主要な日付・時間データ型があること。 WHERE
句はデータのフィルタリングに使用され、日付・時間データも比較演算子やBETWEEN
句でフィルタリングできること。BETWEEN lower AND upper
は>= lower AND <= upper
と同等であること。DATE
型ではBETWEEN
句による期間指定が直感的で分かりやすいこと。DATETIME
やTIMESTAMP
型でBETWEEN
を使う際、終端の時刻の扱いに注意が必要であること。 特に「〇日〜〇日」のように日付全体の期間を指定する場合、単純に日付だけを終端に指定すると、その日の0時0分0秒までしか含まれないという落とし穴があること。DATETIME
/TIMESTAMP
で日付全体を含む期間を正確に指定するには、BETWEEN
よりも>= 開始日の0時0分0秒 AND < 次の日付の0時0分0秒
という比較演算子を使った形式が推奨されること、そしてそれがパフォーマンス面でも有利であること。- MySQLの日付・時間関連関数(
CURDATE()
,NOW()
,DATE_SUB()
,DATE_ADD()
など)を組み合わせることで、動的な期間指定が可能になること。 - 指定した期間に含まれないデータを抽出するには
NOT BETWEEN
または比較演算子とOR
を組み合わせる方法があること。 - 日付期間指定クエリのパフォーマンスにはインデックスが重要であること、そして
WHERE
句で検索対象カラムに関数を適用するとインデックスが使われなくなる可能性があること。 - 日付フォーマット、データ型、タイムゾーンなど、日付・時間データを扱う上での一般的な落とし穴やトラブルシューティングのヒント。
BETWEEN
句は確かに便利で直感的ですが、特にDATETIME
やTIMESTAMP
の終端の挙動を理解し、必要に応じて比較演算子による表現を使い分けることが、正確なデータ抽出の鍵となります。そして、大量データを扱う上では、インデックスの利用とWHERE
句での関数利用を避けることがパフォーマンスの生命線です。
この記事で得た知識は、MySQLを使ったデータ分析やアプリケーション開発において、期間を指定してデータを操作する場面で必ず役立つでしょう。
次のステップとして、以下の点に挑戦してみることをお勧めします。
- 実際に自分のデータベースやサンプルデータを使って、様々な期間指定クエリ(今日、昨日、今週、今月など)を書いて実行してみる。
EXPLAIN
コマンドを使って、自分のクエリがインデックスを使っているかどうかを確認してみる。意図通りになっていない場合は、クエリを修正してインデックスが使われるように工夫してみる。- 日付・時間に関する他のMySQL関数(
WEEK()
,QUARTER()
,DATEDIFF()
,TIMEDIFF()
など)について調べて、さらに複雑な期間指定や日付計算に挑戦してみる。 - 日付関連のデータを扱う設計をする際に、適切なデータ型(
DATE
,DATETIME
,TIMESTAMP
)を選択することを意識してみる。
MySQLの学習は奥深く、日付・時間データの扱いはその重要な一部です。この記事が、あなたのMySQL学習の強力な一助となれば幸いです。
Happy querying!