DATETIME型やTIMESTAMP型のカラムを検索するとき、時間部分が邪魔で正しくヒットしないという経験はありませんか?
たとえば 2024-03-28 14:30:00 のようなデータに対して WHERE order_date = '2024-03-28' と書いても、時間部分が一致しないためレコードが返ってこないことがあります。
この記事では、MySQL・PostgreSQL・SQL Server・Oracle の4つのRDBMS別に、時間を無視して日付だけで比較する方法を解説します。さらに、インデックスへの影響やパフォーマンスを考慮したベストプラクティスも紹介します。
サンプルテーブル
この記事では、以下の orders テーブルを使って解説します。
CREATE TABLE orders (
id INT PRIMARY KEY,
order_date DATETIME,
amount DECIMAL(10,2)
);
INSERT INTO orders VALUES
(1, '2024-03-28 09:15:00', 1500),
(2, '2024-03-28 14:30:00', 2300),
(3, '2024-03-29 08:00:00', 800),
(4, '2024-03-29 18:45:00', 4200),
(5, '2024-03-30 12:00:00', 950);
なぜ日付比較で問題が起きるのか?
DATETIME型やTIMESTAMP型は、日付 + 時刻 を保持するデータ型です。
-- order_date = '2024-03-28 14:30:00' のため不一致 SELECT * FROM orders WHERE order_date = '2024-03-28'; -- → 結果: 0件(時間部分が '00:00:00' と比較される)
'2024-03-28' は暗黙的に '2024-03-28 00:00:00' に変換されます。そのため、時刻が 00:00:00 ちょうどのレコード以外はヒットしません。
注意:この問題は MySQL・PostgreSQL・SQL Server・Oracle すべてのRDBMSで発生します。日付型(DATE)ではなく日時型(DATETIME / TIMESTAMP)のカラムを使っている場合は必ず意識しましょう。
【MySQL】DATE() 関数を使う方法
MySQLでは DATE() 関数でDATETIMEから日付部分だけを取り出せます。
DATE() 関数で日付を抽出
SELECT * FROM orders WHERE DATE(order_date) = '2024-03-28';
実行結果
+----+---------------------+---------+ | id | order_date | amount | +----+---------------------+---------+ | 1 | 2024-03-28 09:15:00 | 1500.00 | | 2 | 2024-03-28 14:30:00 | 2300.00 | +----+---------------------+---------+
DATE() は時間部分を切り捨てて '2024-03-28' だけを返します。最もシンプルで直感的な方法です。
DATE_FORMAT() で日付を整形して比較
DATE_FORMAT() を使うと、任意のフォーマットに変換して比較できます。
-- 年月のみで比較(2024年3月のデータ) SELECT * FROM orders WHERE DATE_FORMAT(order_date, '%Y-%m') = '2024-03'; -- 年のみで比較(2024年のデータ) SELECT * FROM orders WHERE DATE_FORMAT(order_date, '%Y') = '2024';
CAST() で DATE型に変換
SELECT * FROM orders WHERE CAST(order_date AS DATE) = '2024-03-28';
【PostgreSQL】::date キャストを使う方法
PostgreSQLでは ::date キャスト演算子で日付部分だけを取り出せます。
::date キャストで日付を抽出
-- ::date キャスト(PostgreSQL独自構文) SELECT * FROM orders WHERE order_date::date = '2024-03-28'; -- CAST構文(標準SQL) SELECT * FROM orders WHERE CAST(order_date AS DATE) = '2024-03-28';
DATE_TRUNC() で切り捨て
DATE_TRUNC() は指定した精度で日時を切り捨てます。月単位・年単位の比較に便利です。
-- 日付単位で切り捨て
SELECT * FROM orders
WHERE DATE_TRUNC('day', order_date) = '2024-03-28';
-- 月単位で切り捨て(2024年3月のデータ)
SELECT * FROM orders
WHERE DATE_TRUNC('month', order_date) = '2024-03-01';
【SQL Server】CAST / CONVERT を使う方法
SQL Serverでは CAST() または CONVERT() でDATE型に変換します。
CAST() で DATE型に変換
SELECT * FROM orders WHERE CAST(order_date AS DATE) = '2024-03-28';
CONVERT() でフォーマット指定
CONVERT() はスタイル番号を指定して変換できます。
-- DATE型に変換して比較 SELECT * FROM orders WHERE CONVERT(DATE, order_date) = '2024-03-28'; -- 文字列に変換して比較(スタイル112: yyyymmdd) SELECT * FROM orders WHERE CONVERT(VARCHAR(8), order_date, 112) = '20240328';
ポイント:SQL Server 2008以降では CAST(col AS DATE) が最もシンプルで推奨されます。古いバージョンでは CONVERT() を使います。
【Oracle】TRUNC() / TO_CHAR() を使う方法
Oracleでは TRUNC() 関数で時刻部分を切り捨てるか、TO_CHAR() で文字列に変換して比較します。
TRUNC() で時刻を切り捨て
-- 時間を切り捨てて日付のみで比較 SELECT * FROM orders WHERE TRUNC(order_date) = DATE '2024-03-28'; -- 月単位で切り捨て(2024年3月のデータ) SELECT * FROM orders WHERE TRUNC(order_date, 'MM') = DATE '2024-03-01';
TO_CHAR() で文字列変換して比較
-- 日付文字列で比較 SELECT * FROM orders WHERE TO_CHAR(order_date, 'YYYY-MM-DD') = '2024-03-28'; -- 年月のみで比較 SELECT * FROM orders WHERE TO_CHAR(order_date, 'YYYY-MM') = '2024-03';
RDBMS別 日付抽出方法の比較
各RDBMSの日付比較方法を一覧で比較します。
| RDBMS | 関数・構文 | 使用例 |
|---|---|---|
| MySQL | DATE() |
DATE(order_date) = '2024-03-28' |
| MySQL | DATE_FORMAT() |
DATE_FORMAT(col, '%Y-%m-%d') |
| PostgreSQL | ::date |
order_date::date = '2024-03-28' |
| PostgreSQL | DATE_TRUNC() |
DATE_TRUNC('day', col) |
| SQL Server | CAST() |
CAST(order_date AS DATE) |
| SQL Server | CONVERT() |
CONVERT(DATE, order_date) |
| Oracle | TRUNC() |
TRUNC(order_date) |
| Oracle | TO_CHAR() |
TO_CHAR(col, 'YYYY-MM-DD') |
日付範囲で比較する方法
特定の日付のデータを取得するには、範囲指定を使う方法もあります。関数を使わないため、インデックスが有効に活用されます。
BETWEEN を使う方法
-- 2024-03-28 の全レコードを取得
SELECT * FROM orders
WHERE order_date BETWEEN '2024-03-28 00:00:00'
AND '2024-03-28 23:59:59';
注意:BETWEEN '2024-03-28' AND '2024-03-28 23:59:59' を使うと、23:59:59.500 のようなミリ秒を含むデータを取りこぼす可能性があります。正確性を求める場合は >= AND < パターンを使いましょう。
>= AND < パターン(推奨)
最も安全で確実な方法です。ミリ秒やマイクロ秒の取りこぼしがありません。
-- 2024-03-28 のデータを正確に取得 SELECT * FROM orders WHERE order_date >= '2024-03-28' AND order_date < '2024-03-29';
実行結果
+----+---------------------+---------+ | id | order_date | amount | +----+---------------------+---------+ | 1 | 2024-03-28 09:15:00 | 1500.00 | | 2 | 2024-03-28 14:30:00 | 2300.00 | +----+---------------------+---------+
このパターンでは '2024-03-28 00:00:00.000000' から '2024-03-28 23:59:59.999999' までのすべての時刻が正確にカバーされます。
BETWEEN vs >= AND < の違い
BETWEENは 両端を含む(>= AND <=)>= AND <は 開始を含み終了を含まない- 日付範囲の場合、
>= AND <のほうがミリ秒の取りこぼしがなく安全 - パフォーマンス面では両者に大きな差はない
インデックスへの影響(重要)
日付比較でもっとも重要なのがインデックスの利用効率です。関数を使う場合と使わない場合で、パフォーマンスに大きな差が出ます。
関数を使うとインデックスが効かない
WHERE句でカラムに関数を適用すると、インデックスが使われなくなります(フルテーブルスキャンが発生)。
-- インデックス作成 CREATE INDEX idx_order_date ON orders(order_date); -- NG: 関数を使うとインデックスが効かない SELECT * FROM orders WHERE DATE(order_date) = '2024-03-28'; -- フルスキャン -- OK: 範囲指定ならインデックスが有効 SELECT * FROM orders WHERE order_date >= '2024-03-28' AND order_date < '2024-03-29'; -- インデックス利用
| 書き方 | インデックス利用 | パフォーマンス |
|---|---|---|
DATE(col) = '...' |
使われない | 遅い(フルスキャン) |
TRUNC(col) = ... |
使われない | 遅い(フルスキャン) |
col::date = '...' |
使われない | 遅い(フルスキャン) |
col >= ... AND col < ... |
使われる | 高速(Range Scan) |
関数インデックスを作成する方法
どうしても関数を使いたい場合は、関数インデックス(式インデックス)を作成することでパフォーマンスを改善できます。
-- MySQL 8.0+ : 式インデックス CREATE INDEX idx_order_date_only ON orders ((DATE(order_date))); -- PostgreSQL : 式インデックス CREATE INDEX idx_order_date_only ON orders ((CAST(order_date AS DATE))); -- Oracle : ファンクションベース・インデックス CREATE INDEX idx_order_date_only ON orders (TRUNC(order_date));
注意:関数インデックスはINSERT/UPDATEのパフォーマンスに影響します。テーブルの更新頻度が高い場合は、範囲比較(>= AND <)を使う方が総合的に有利です。
今日のデータを取得するパターン
業務でよく使う「今日のデータだけ取得したい」というパターンをRDBMS別にまとめます。
関数を使う方法
-- MySQL SELECT * FROM orders WHERE DATE(order_date) = CURDATE(); -- PostgreSQL SELECT * FROM orders WHERE order_date::date = CURRENT_DATE; -- SQL Server SELECT * FROM orders WHERE CAST(order_date AS DATE) = CAST(GETDATE() AS DATE); -- Oracle SELECT * FROM orders WHERE TRUNC(order_date) = TRUNC(SYSDATE);
範囲比較で取得(推奨・高速)
-- MySQL SELECT * FROM orders WHERE order_date >= CURDATE() AND order_date < CURDATE() + INTERVAL 1 DAY; -- PostgreSQL SELECT * FROM orders WHERE order_date >= CURRENT_DATE AND order_date < CURRENT_DATE + 1; -- SQL Server SELECT * FROM orders WHERE order_date >= CAST(GETDATE() AS DATE) AND order_date < DATEADD(DAY, 1, CAST(GETDATE() AS DATE)); -- Oracle SELECT * FROM orders WHERE order_date >= TRUNC(SYSDATE) AND order_date < TRUNC(SYSDATE) + 1;
現在の日時を取得する関数(RDBMS別)
| RDBMS | 現在の日時 | 現在の日付 |
|---|---|---|
| MySQL | NOW() |
CURDATE() |
| PostgreSQL | NOW() |
CURRENT_DATE |
| SQL Server | GETDATE() |
CAST(GETDATE() AS DATE) |
| Oracle | SYSDATE |
TRUNC(SYSDATE) |
よくあるエラーと対処法
エラー1: 日付フォーマットの不一致
-- NG: フォーマットが合っていない SELECT * FROM orders WHERE DATE(order_date) = '03/28/2024'; -- OK: ISO 8601形式(YYYY-MM-DD)を使う SELECT * FROM orders WHERE DATE(order_date) = '2024-03-28';
ポイント:日付リテラルは YYYY-MM-DD(ISO 8601)形式で書くのが最も安全です。どのRDBMSでも正しく解釈されます。
エラー2: タイムゾーンによるズレ
-- TIMESTAMP WITH TIME ZONE の場合 -- UTC で保存された '2024-03-28 23:30:00+00' は -- JST(+9) では '2024-03-29 08:30:00' になる -- MySQL: タイムゾーンを明示的に設定 SET time_zone = '+09:00'; -- PostgreSQL: AT TIME ZONE で変換 SELECT * FROM orders WHERE (order_date AT TIME ZONE 'Asia/Tokyo')::date = '2024-03-28';
注意:TIMESTAMP WITH TIME ZONE型を使用している場合、タイムゾーンの変換で日付が変わることがあります。特にUTCで保存しているシステムでは、日本時間(JST)との9時間のズレに注意しましょう。
エラー3: NULL値の扱い
-- order_date が NULL の行は、どの比較でもヒットしない SELECT * FROM orders WHERE DATE(order_date) = '2024-03-28'; -- → NULLの行は結果に含まれない -- NULLも含めたい場合 SELECT * FROM orders WHERE DATE(order_date) = '2024-03-28' OR order_date IS NULL;
パフォーマンス最適化のまとめ
大量データのテーブルで日付検索を行う場合のパフォーマンス最適化をまとめます。
| 最適化ポイント | 推奨方法 | 理由 |
|---|---|---|
| 日付の一致検索 | >= AND < で範囲指定 |
インデックスが有効に利用される |
| インデックス設計 | 日時カラムにインデックスを作成 | Range Scanで高速検索 |
| 関数の使用 | WHERE句では避ける | 関数適用でインデックス無効化 |
| BETWEEN の使用 | 境界値に注意 | ミリ秒の取りこぼしリスク |
| 関数が必要な場合 | 関数インデックスを作成 | 関数の結果にインデックスが効く |
| 大量データの集計 | パーティションテーブル | 検索対象を物理的に絞り込む |
EXPLAINで実行計画を確認する
パフォーマンスに問題がある場合は、EXPLAIN で実行計画を確認しましょう。
-- 関数を使った場合の実行計画 EXPLAIN SELECT * FROM orders WHERE DATE(order_date) = '2024-03-28'; -- type: ALL(フルテーブルスキャン) -- 範囲比較の場合の実行計画 EXPLAIN SELECT * FROM orders WHERE order_date >= '2024-03-28' AND order_date < '2024-03-29'; -- type: range(インデックスレンジスキャン)
実践的な使用例
月ごとの集計(GROUP BY)
-- MySQL: 月ごとの注文数と売上合計
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;
実行結果
+---------+-------------+--------------+ | month | order_count | total_amount | +---------+-------------+--------------+ | 2024-03 | 5 | 9750.00 | +---------+-------------+--------------+
過去7日間のデータを取得
-- MySQL SELECT * FROM orders WHERE order_date >= CURDATE() - INTERVAL 7 DAY; -- PostgreSQL SELECT * FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '7 days'; -- SQL Server SELECT * FROM orders WHERE order_date >= DATEADD(DAY, -7, CAST(GETDATE() AS DATE)); -- Oracle SELECT * FROM orders WHERE order_date >= TRUNC(SYSDATE) - 7;
特定の日付範囲で日別集計
-- MySQL: 日別の注文件数
SELECT
DATE(order_date) AS order_day,
COUNT(*) AS order_count
FROM orders
WHERE order_date >= '2024-03-28'
AND order_date < '2024-04-01'
GROUP BY DATE(order_date)
ORDER BY order_day;
実行結果
+------------+-------------+ | order_day | order_count | +------------+-------------+ | 2024-03-28 | 2 | | 2024-03-29 | 2 | | 2024-03-30 | 1 | +------------+-------------+
ベストプラクティスまとめ
日付比較のベストプラクティス
- パフォーマンス重視なら
>= AND <の範囲比較を使う - 可読性重視なら
DATE()やTRUNC()を使う(小規模テーブル向き) - 日付リテラルは必ず
YYYY-MM-DD形式で書く BETWEENは両端を含むため、ミリ秒の取りこぼしに注意- 大規模テーブルでは
EXPLAINで実行計画を必ず確認する - タイムゾーンが関わるシステムでは、変換タイミングに注意する
よくある質問(FAQ)
まとめ
- DATETIME型カラムで日付のみ比較したい場合、関数で日付を抽出するか、範囲比較を使う
- MySQL:
DATE()、PostgreSQL:::date、SQL Server:CAST(col AS DATE)、Oracle:TRUNC() - 関数を使うとインデックスが効かなくなるため、大量データでは
>= AND <の範囲比較が推奨 BETWEENはミリ秒の取りこぼしリスクがあるため、>= AND <パターンが最も安全- パフォーマンスに問題がある場合は
EXPLAINで実行計画を確認し、関数インデックスの作成も検討する