【SQL】時間を無視して日付のみで比較する方法(MySQL/PostgreSQL/Oracle/SQL Server対応)

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)

Q. TRUNC関数で時間を切り捨てるとインデックスが効かなくなりますか?
A. WHERE句でTRUNC(created_at)を使うとインデックスが効かなくなる場合があります。パフォーマンスが重要な場合は日付範囲検索(BETWEEN)を使うか、日付のみの計算列にインデックスを貼ることを検討してください。
Q. MySQLのDATE()関数はパフォーマンスに影響しますか?
A. DATE(created_at)='2024-03-28'のようなWHERE句では、インデックスが使えない場合があります。代わりにcreated_at >= '2024-03-28 00:00:00' AND created_at < '2024-03-29 00:00:00'のような範囲検索を使うとインデックスが有効になります。
Q. タイムゾーンが異なる環境での日付比較はどうすればいいですか?
A. サーバーとDBのタイムゾーンが異なる場合、UTC基準で保存してアプリ側で変換するか、DBのタイムゾーン関数を使って統一することが重要です。MySQL8.0以降はCONVERT_TZ()関数を使えます。
Q. SQL ServerでCASTとCONVERTの違いは何ですか?
A. CASTはANSI SQL標準で移植性が高く、CONVERTはSQL Server固有でスタイル指定ができます。日付変換ではどちらも使えますが、移植性を重視するならCASTを推奨します。
Q. PostgreSQLでDATE型とTIMESTAMP型を混在して比較できますか?
A. はい、PostgreSQLは自動型変換を行いますが、明示的にCAST(timestamp_col AS DATE)や::DATEで変換するほうが意図が明確です。

まとめ

  • DATETIME型カラムで日付のみ比較したい場合、関数で日付を抽出するか、範囲比較を使う
  • MySQL: DATE()PostgreSQL: ::dateSQL Server: CAST(col AS DATE)Oracle: TRUNC()
  • 関数を使うとインデックスが効かなくなるため、大量データでは >= AND < の範囲比較が推奨
  • BETWEEN はミリ秒の取りこぼしリスクがあるため、>= AND < パターンが最も安全
  • パフォーマンスに問題がある場合は EXPLAIN で実行計画を確認し、関数インデックスの作成も検討する