「前日のデータだけ取得したい」——日次バッチ/レポート/通知/アーカイブ削除など、SQLで最頻出の要件です。しかし、DATE(order_date) = CURDATE() - INTERVAL 1 DAYのようなカラムに関数を適用する書き方はインデックスを破壊します。100万件のテーブルでフルスキャンが走り、夜間バッチが数時間オーダーで遅延するのが典型的な事故です。
この記事では、MySQL/PostgreSQL/SQL Server/Oracle の4種DBMSで前日のデータをインデックス活用(SARGable)で高速取得する書き方、DATETIME境界の落とし穴(<=と<の違い、23:59:59.999事故)、タイムゾーン対応(UTC保存→JSTで前日計算)、夏時間/閏年への対応、実務でそのまま使える8パターン(売上集計/ログ分析/削除バッチ/通知/アラート/異常検知/パーティション活用/cron連携)を全網羅した決定版ガイドとしてまとめます。
関連:【SQL】日付の比較完全ガイド/【SQL】日付の範囲指定完全ガイド/【SQL】時間を無視して日付のみで比較する方法/【SQL】前月のデータを取得する完全ガイド/【SQL】前年のデータを取得する方法で日付関連の周辺トピックもカバーできます。
この記事で学べること
- MySQL/PostgreSQL/SQL Server/Oracle4種の前日取得SQL完全版
- SARGable書き方(インデックス活用)と関数適用がなぜ遅いのか
- DATE型 vs DATETIME/TIMESTAMP型の扱い分け
>=/<半開区間とBETWEENの使い分け(23:59:59.999事故回避)- UTC保存→JSTで「前日」を計算するタイムゾーン完全対応
- 夏時間/閏年/月またぎ/年またぎでも壊れない書き方
- 実務8パターン:売上集計/ログ集計/削除バッチ/通知/異常検知等
- パーティション+前日抽出で1秒以内の夜間バッチ
- cron/AWS EventBridge/Lambdaでの定時実行パターン
- EXPLAINで“range” or “fullscan”を確認する診断手順
30秒クイックリファレンス:推奨構文4種
まず結論。カラムに関数を適用せず、範囲指定(半開区間)で書くのが全DBMS共通のベストプラクティスです。
黄金律:半開区間 >= 前日00:00 AND < 当日00:00。カラムを素のまま左辺に置く/関数適用しない/開始含む・終了含まない、の3点でインデックスが効き、境界事故も起きません。
なぜ関数適用はダメなのか:SARGableの本質
SARGable(Search ARGument Able)とは「検索引数としてインデックスが使える書き方」のこと。WHERE句の左辺にカラムをそのまま置くとインデックスを利用できますが、関数で加工するとDBはカラム値を全件計算せざるを得ずフルスキャンになります。
-- ❌ 遅い:DATE()がカラムに適用されるため、インデックス無効 SELECT * FROM orders WHERE DATE(order_date) = CURDATE() - INTERVAL 1 DAY; -- ❌ 遅い:Oracleで TRUNC(col) を使うパターン SELECT * FROM orders WHERE TRUNC(order_date) = TRUNC(SYSDATE) - 1; -- ❌ 遅い:CAST で列を変換 SELECT * FROM orders WHERE CAST(order_date AS DATE) = CAST(DATEADD(day,-1,GETDATE()) AS DATE);
-- ⭕ 速い:カラムは素のまま、比較対象を計算 SELECT * FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE(); -- ⭕ 速い:Oracle SELECT * FROM orders WHERE order_date >= TRUNC(SYSDATE) - 1 AND order_date < TRUNC(SYSDATE); -- ⭕ 速い:SQL Server SELECT * FROM orders WHERE order_date >= CAST(DATEADD(day,-1,GETDATE()) AS DATE) AND order_date < CAST(GETDATE() AS DATE);
実例:100万件で体感する差。SARGable版はB-treeインデックスで数ms〜数十msで完了、非SARGable版は全件スキャン+関数計算で数秒〜数十秒。テーブル成長とともに速度差が指数関数的に開きます。本番でバッチが遅い場合は真っ先にここを疑ってください。
DATETIME境界の落とし穴:23:59:59.999事故
昔のレガシーコードや初心者コードで頻出するのがBETWEENでの境界事故。BETWEEN '2026-04-21' AND '2026-04-21 23:59:59'は23:59:59.999のレコードを取り逃がします。
-- ❌ ミリ秒切れで取り逃がす SELECT * FROM events WHERE event_time BETWEEN '2026-04-21 00:00:00' AND '2026-04-21 23:59:59'; -- → 2026-04-21 23:59:59.500 のレコードは取得されるが -- → 2026-04-21 23:59:59.999 のレコードは取得される(境界だが微妙) -- → DBによってはミリ秒まで含めると1秒の端数処理でバグる -- ❌ より頻発するバグ:当日を含めちゃう(00:00:00含む) SELECT * FROM events WHERE event_time BETWEEN '2026-04-20 00:00:00' AND '2026-04-21 00:00:00'; -- → 2026-04-21 00:00:00 の境界レコードも含まれてしまう -- → 前日だけ取得したいのに当日の先頭も混ざる
-- ⭕ 半開区間:開始含む、終了含まない SELECT * FROM events WHERE event_time >= '2026-04-21 00:00:00' AND event_time < '2026-04-22 00:00:00'; -- ⭕ 動的計算版(MySQL) SELECT * FROM events WHERE event_time >= CURDATE() - INTERVAL 1 DAY AND event_time < CURDATE();
半開区間[start, end)の数学的な美しさ
- 連続する期間が重複なく完全につながる(前日 + 当日 = 2日分)
- ミリ秒/ナノ秒/将来のTimestamptz変更でも壊れない
- 分単位データでも秒単位データでも同じ書き方で動く
- プログラミング言語の
range(start, end)と同じ流儀
MySQLで前日のデータを取得する
-- ⭕ SARGable推奨(半開区間) SELECT * FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE(); -- DATE_SUB()派(好みで) SELECT * FROM orders WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND order_date < CURDATE(); -- カラムがDATE型しか入っていないケース(時刻なし) -- これならシンプルに「= 前日」で良い SELECT * FROM daily_reports WHERE report_date = CURDATE() - INTERVAL 1 DAY;
-- 前日の総件数 SELECT COUNT(*) AS yesterday_count FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE(); -- 前日の売上合計 SELECT SUM(amount) AS yesterday_sales FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE(); -- 時間帯別件数(前日の1時間ごと集計) SELECT HOUR(order_date) AS hr, COUNT(*) AS cnt FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE() GROUP BY HOUR(order_date) ORDER BY hr;
MySQL 8.0以降の関数
MySQL 8.0ではNOW(3)でミリ秒精度、UTC_TIMESTAMP()でUTC現在時刻、CONVERT_TZ()でタイムゾーン変換が使えます。前日計算ではCURDATE()で十分ですが、時刻精度が重要な場合はNOW(6)(マイクロ秒)を使い分けてください。
PostgreSQLで前日のデータを取得する
-- ⭕ SARGable推奨 SELECT * FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '1 day' AND order_date < CURRENT_DATE; -- INTEGER演算でも可(PostgreSQL独自) SELECT * FROM orders WHERE order_date >= CURRENT_DATE - 1 AND order_date < CURRENT_DATE; -- TIMESTAMP型との混在が気になる時はtimestampキャスト SELECT * FROM orders WHERE order_date >= (CURRENT_DATE - 1)::timestamp AND order_date < CURRENT_DATE::timestamp;
-- timestamptz カラムの場合はタイムゾーン意識が必要 -- 日本の前日(UTCではなくJSTで)取得 SELECT * FROM orders WHERE order_date AT TIME ZONE 'Asia/Tokyo' >= (CURRENT_DATE - 1) AND order_date AT TIME ZONE 'Asia/Tokyo' < CURRENT_DATE; -- SARGableにしたい場合は右辺を変換する(左辺は素のまま) SELECT * FROM orders WHERE order_date >= ((CURRENT_DATE - 1) AT TIME ZONE 'Asia/Tokyo') AND order_date < (CURRENT_DATE AT TIME ZONE 'Asia/Tokyo');
PostgreSQLのAT TIME ZONEはカラム側に適用するとインデックスが効かなくなるため、本番大規模テーブルでは右辺(比較値)側にだけ変換をかけるのが王道。DB設計段階でtimestamptzを一貫して使うとこの手の問題が激減します。
SQL Serverで前日のデータを取得する
-- ⭕ SARGable推奨 SELECT * FROM orders WHERE order_date >= CAST(DATEADD(day, -1, GETDATE()) AS DATE) AND order_date < CAST(GETDATE() AS DATE); -- 変数に入れて可読性UP DECLARE @yesterday DATE = CAST(DATEADD(day, -1, GETDATE()) AS DATE); DECLARE @today DATE = CAST(GETDATE() AS DATE); SELECT * FROM orders WHERE order_date >= @yesterday AND order_date < @today;
-- UTC保存→JSTで前日取得(SQL Server 2016+)
SELECT *
FROM orders
WHERE order_date >= CAST(DATEADD(day, -1,
SYSDATETIMEOFFSET() AT TIME ZONE 'Tokyo Standard Time') AS DATE)
AND order_date < CAST(SYSDATETIMEOFFSET() AT TIME ZONE 'Tokyo Standard Time' AS DATE);
GETDATE()とSYSDATETIME()の違い:GETDATE()はミリ秒精度のdatetime型(ms単位丸め)、SYSDATETIME()は100ナノ秒精度のdatetime2型。精度が重要な場合はSYSDATETIME()、UTC時間が必要ならSYSUTCDATETIME()を使い分けます。
Oracleで前日のデータを取得する
-- ⭕ SARGable推奨 SELECT * FROM orders WHERE order_date >= TRUNC(SYSDATE) - 1 AND order_date < TRUNC(SYSDATE); -- CURRENT_DATE(セッションタイムゾーンに依存) SELECT * FROM orders WHERE order_date >= TRUNC(CURRENT_DATE) - 1 AND order_date < TRUNC(CURRENT_DATE);
-- UTC保存→JSTで前日取得 SELECT * FROM orders WHERE order_date >= (TRUNC(CAST(SYSTIMESTAMP AT TIME ZONE 'Asia/Tokyo' AS DATE)) - 1) AND order_date < TRUNC(CAST(SYSTIMESTAMP AT TIME ZONE 'Asia/Tokyo' AS DATE)); -- パーティション列と組み合わせ(業務RDBで超高速) SELECT /*+ FULL(o) PARALLEL(o 4) */ * FROM orders PARTITION FOR (TRUNC(SYSDATE) - 1) o;
Oracle TRUNCの重要性
OracleのSYSDATEとCURRENT_DATEは時刻込みで返ります。TRUNC()で00:00:00に切り下げないと、”前日”の定義がずれます。カラム側には絶対TRUNC()をかけない(SARGable破壊)、右辺の比較値だけTRUNC()するのが鉄則。
タイムゾーン完全対応:UTC保存→JSTで前日
グローバルSaaSやクラウドインフラのDBはUTC保存が定石。日本のユーザーから「昨日」と言われたらJSTで解釈しなければなりません。UTCそのままでCURDATE()を使うとJSTの9時間前になり、前日が早朝にカットされる事故が起きます。
-- UTC保存データをJSTで前日取得
SELECT *
FROM logs
WHERE log_time >= CONVERT_TZ(
CONCAT(CURDATE() - INTERVAL 1 DAY, ' 00:00:00'),
'+09:00', '+00:00'
)
AND log_time < CONVERT_TZ(
CONCAT(CURDATE(), ' 00:00:00'),
'+09:00', '+00:00'
);
-- JST 2026-04-21 00:00:00 は UTC 2026-04-20 15:00:00
-- シンプルに範囲を構築 SELECT * FROM logs WHERE log_time >= ((CURRENT_DATE - 1) AT TIME ZONE 'Asia/Tokyo') AND log_time < (CURRENT_DATE AT TIME ZONE 'Asia/Tokyo');
SELECT *
FROM logs
WHERE log_time >= CAST(DATEADD(day, -1,
SYSDATETIMEOFFSET() AT TIME ZONE 'Tokyo Standard Time') AS DATETIME)
AND log_time < CAST(SYSDATETIMEOFFSET() AT TIME ZONE 'Tokyo Standard Time' AS DATETIME);
SELECT *
FROM logs
WHERE log_time >= (FROM_TZ(CAST(TRUNC(SYSTIMESTAMP AT TIME ZONE 'Asia/Tokyo') - 1 AS TIMESTAMP), 'Asia/Tokyo')
AT TIME ZONE 'UTC')
AND log_time < (FROM_TZ(CAST(TRUNC(SYSTIMESTAMP AT TIME ZONE 'Asia/Tokyo') AS TIMESTAMP), 'Asia/Tokyo')
AT TIME ZONE 'UTC');
タイムゾーンのよくある事故:①UTC保存なのにJSTでCURDATE()を投げて9時間早朝が前日扱いに②夏時間導入国(US等)でDSTによる23/25時間の日が発生③timestamp型とtimestamptz型の混在④サーバーTZ・DBセッションTZ・アプリTZが三者三様で「いつの前日か」が不明瞭。UTC保存+アプリ側でTZ表示が最もシンプルで安全です。
実務8パターン:そのまま使える前日SQL集
①日次売上レポート
-- MySQL:前日の商品カテゴリ別売上 SELECT category, COUNT(*) AS order_count, SUM(amount) AS total_sales, AVG(amount) AS avg_sales FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE() GROUP BY category ORDER BY total_sales DESC;
②前日比(Day-over-Day)計算
-- 前々日と前日を比較
SELECT
SUM(CASE WHEN order_date >= CURDATE() - INTERVAL 1 DAY THEN amount END) AS yesterday,
SUM(CASE WHEN order_date >= CURDATE() - INTERVAL 2 DAY
AND order_date < CURDATE() - INTERVAL 1 DAY THEN amount END) AS day_before,
(SUM(CASE WHEN order_date >= CURDATE() - INTERVAL 1 DAY THEN amount END)
- SUM(CASE WHEN order_date >= CURDATE() - INTERVAL 2 DAY
AND order_date < CURDATE() - INTERVAL 1 DAY THEN amount END))
/ SUM(CASE WHEN order_date >= CURDATE() - INTERVAL 2 DAY
AND order_date < CURDATE() - INTERVAL 1 DAY THEN amount END) * 100 AS dod_percent
FROM orders
WHERE order_date >= CURDATE() - INTERVAL 2 DAY
AND order_date < CURDATE();
③ログのアーカイブ(前日分を別テーブルへ)
-- Step1: 前日分をアーカイブテーブルへ INSERT INTO access_logs_archive SELECT * FROM access_logs WHERE log_time >= CURDATE() - INTERVAL 1 DAY AND log_time < CURDATE(); -- Step2: 元テーブルから前日分削除(バッチサイズ制限推奨) DELETE FROM access_logs WHERE log_time >= CURDATE() - INTERVAL 1 DAY AND log_time < CURDATE() LIMIT 10000; -- LIMITで分割実行してロック範囲を狭める
④昨日登録されたユーザーへの通知
SELECT u.id, u.email, u.name, u.created_at FROM users u WHERE u.created_at >= CURDATE() - INTERVAL 1 DAY AND u.created_at < CURDATE() AND u.verified = 0; -- verified=0 の未認証ユーザーだけに再送信通知を送る等
⑤前日の異常検知(閾値超過)
SELECT DATE_FORMAT(log_time, '%Y-%m-%d %H:00:00') AS hour, COUNT(*) AS error_count FROM error_logs WHERE log_time >= CURDATE() - INTERVAL 1 DAY AND log_time < CURDATE() AND severity = "ERROR" GROUP BY DATE_FORMAT(log_time, '%Y-%m-%d %H:00:00') HAVING COUNT(*) > 100 ORDER BY hour; -- 1時間当たり100件超のエラーを検出
⑥前日未処理のジョブ洗い出し
SELECT *
FROM jobs
WHERE scheduled_at >= CURDATE() - INTERVAL 1 DAY
AND scheduled_at < CURDATE()
AND status IN ('pending', 'retrying');
-- 前日予定のpending/retrying を検出→再投入対象
⑦前日データを用いたランキング
SELECT product_id, product_name, SUM(quantity) AS total_qty, RANK() OVER (ORDER BY SUM(quantity) DESC) AS daily_rank FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE o.order_date >= CURDATE() - INTERVAL 1 DAY AND o.order_date < CURDATE() GROUP BY product_id, product_name ORDER BY total_qty DESC LIMIT 10;
⑧パーティション+前日抽出で超高速化
-- MySQL/PostgreSQL: RANGE PARTITION BY date SELECT * FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE(); -- パーティションプルーニングが効き前日分のみスキャン -- Oracle: PARTITION FOR でさらに明示 SELECT * FROM orders PARTITION FOR (TRUNC(SYSDATE) - 1);
パーティション活用で1億件規模でも数秒。日次パーティション(PARTITION BY RANGE)で毎日1パーティション作る設計にすれば、前日抽出=1パーティション全件スキャン=超高速。前日データだけアーカイブテーブルへ移す運用にも好相性です。
cron/EventBridge/Lambdaで定時実行
前日SQLは定期バッチとの組み合わせが本領発揮。実運用でよく使うパターンをまとめます。
Linux cron + mysqlクライアント
# 毎朝6:00に前日の集計バッチ実行(ログ付き) 0 6 * * * /usr/bin/mysql -h db.example.com -u batch -p"$PASS" mydb \ -e "CALL daily_summary_batch()" >> /var/log/batch.log 2>&1 # Dockerベース(compose経由) 0 6 * * * cd /opt/app && /usr/bin/docker compose exec -T db \ mysql -uroot -proot mydb -e "CALL daily_summary_batch()" \ >> /var/log/batch.log 2>&1
AWS EventBridge + Lambda + RDS
import pymysql
import os
from datetime import datetime, timedelta, timezone
def handler(event, context):
# JST基準の前日を計算(Lambdaタイムゾーンに依存せず明示)
jst = timezone(timedelta(hours=9))
today_jst = datetime.now(jst).date()
yesterday_jst = today_jst - timedelta(days=1)
conn = pymysql.connect(
host=os.environ["DB_HOST"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
db=os.environ["DB_NAME"],
)
try:
with conn.cursor() as cur:
cur.execute(
"SELECT COUNT(*), SUM(amount) FROM orders "
"WHERE order_date >= %s AND order_date < %s",
(yesterday_jst, today_jst),
)
count, total = cur.fetchone()
finally:
conn.close()
return {"count": count, "total": float(total or 0), "date": str(yesterday_jst)}
# cron(分 時 日 月 曜日 年) UTC指定
cron(0 21 * * ? *) # UTC 21:00 = JST 06:00 毎日
# Terraform例
resource "aws_cloudwatch_event_rule" "daily_batch" {
name = "daily-batch"
schedule_expression = "cron(0 21 * * ? *)"
}
cron時刻を決める時に気をつけること:①サーバーTZとバッチ集計TZが違う可能性(UTCサーバーで JST 6:00→UTC 21:00)、②深夜バッチが跨日で動くと「前日」の定義が曖昧になる(0:00直前に起動して0:01に完了)、③重い処理は分散して早朝帯に集中させない。アプリコード側でJST基準の日付を明示計算するのが最も事故が少ないです。
インデックスが効いているか確認:EXPLAIN診断
-- EXPLAINを付けるだけ EXPLAIN SELECT * FROM orders WHERE order_date >= CURDATE() - INTERVAL 1 DAY AND order_date < CURDATE(); -- 見るべき列 -- type: range(◎)/ ref(◯)/ ALL(✗フルスキャン) -- key: 使われたインデックス名(NULLは未使用) -- rows: 想定読み込み行数(少ないほど良い) -- Extra: "Using index condition" があれば効いている
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '1 day' AND order_date < CURRENT_DATE; -- 見るべきキーワード -- "Index Scan using orders_order_date_idx"(◎) -- "Seq Scan"(✗フルスキャン) -- "Bitmap Index Scan"(◯中程度) -- actual rows vs estimated rows が大きくズレたら統計情報更新
-- SSMS: Ctrl+Mで実行プラン表示を有効化 -- または SET STATISTICS IO ON; SET STATISTICS TIME ON; SET STATISTICS IO ON; SET STATISTICS TIME ON; SELECT * FROM orders WHERE order_date >= CAST(DATEADD(day, -1, GETDATE()) AS DATE) AND order_date < CAST(GETDATE() AS DATE); -- 見るべきキーワード -- "Index Seek"(◎) -- "Index Scan"(△) -- "Table Scan"(✗フルスキャン)
EXPLAIN PLAN FOR SELECT * FROM orders WHERE order_date >= TRUNC(SYSDATE) - 1 AND order_date < TRUNC(SYSDATE); SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); -- 見るべきキーワード -- "INDEX RANGE SCAN"(◎) -- "INDEX FAST FULL SCAN"(△) -- "TABLE ACCESS FULL"(✗フルスキャン)
EXPLAIN結果の改善フロー
- フルスキャンが出ている → カラムへの関数適用をチェック(SARGable違反)
- インデックスが無い →
CREATE INDEX idx_order_date ON orders(order_date); - 推定行数と実測が大きくズレる → 統計情報更新(
ANALYZE TABLE/DBMS_STATS) - 依然として遅い → パーティション導入検討
よくある質問
CURDATE() - INTERVAL 2 DAY(前々日)/INTERVAL 7 DAY(1週間前)。Oracle:TRUNC(SYSDATE) - 2/- 7。PostgreSQL:CURRENT_DATE - 2/- 7。SQL Server:DATEADD(day, -2, GETDATE())/-7。前月/前年については【SQL】前月のデータを取得する完全ガイド/【SQL】前年のデータを取得する方法を参照。BETWEEN '2026-04-21 00:00:00' AND '2026-04-21 23:59:59'は23:59:59.500は拾えても、DB・カラム型によっては23:59:59.999が境界で事故を起こすことが。半開区間>= 前日 AND < 当日なら精度に関係なく正確で、連続する範囲の結合も安全です。DATE(col)のように関数適用するとインデックスが使えなくなります(カラムがDATE型でも同じ)。ただしPostgreSQLとOracleには関数ベースインデックス(Function-Based Index)を作る手があります:CREATE INDEX idx_date ON orders(DATE(order_date));とはいえ、素直に範囲指定する方がシンプルで保守性も高いです。CONVERT_TZ(ts, '+00:00', '+09:00')で変換、PostgreSQLならts AT TIME ZONE 'Asia/Tokyo'、SQL Server 2016+ではAT TIME ZONE 'Tokyo Standard Time'。カラム側ではなく右辺(比較値)側に変換を適用するとSARGableを保てます。本記事「タイムゾーン完全対応」セクション参照。where col >= :start and col < :endのようにプレースホルダ使用)。複数のクエリで揃った基準日を保証でき、バッチ完了時刻の揺らぎによる結果不整合を防げます。ANALYZE TABLE orders;、PostgreSQL:ANALYZE orders;、SQL Server:UPDATE STATISTICS orders;、Oracle:EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ORDERS');で統計を更新。定期的に統計を取る設定も重要です。DELETE ... LIMIT 10000;を繰り返し、PostgreSQL:DELETE ... WHERE ctid IN (SELECT ctid FROM ... LIMIT 10000);、SQL Server:DELETE TOP (10000) ... ;。パーティション導入済みならそのままDROP PARTITIONが最速です。Asia/Tokyoの代わりにAmerica/Los_Angeles等を指定するとDB側が自動的に切替時の時刻調整(23時間/25時間の日)を処理してくれます。手動でオフセット-08:00/-07:00を切り替える方式はバグの温床なので避けてください。CURDATE() - INTERVAL 1 DAYやDATEADD(day, -1, ...)はカレンダー計算として正確に前日を返します。3月1日の前日は2月28日(平年)or 2月29日(閏年)、1月1日の前日は12月31日と自動処理されます。月またぎ・年またぎを意識せず同じSQLで動くのが半開区間方式の強みです。関連記事
- 【SQL】日付の比較完全ガイド — 比較演算子/DATETIME落とし穴/SARGable
- 【SQL】日付の範囲指定完全ガイド — BETWEEN/半開区間/月別・四半期・年度パターン
- 【SQL】時間を無視して日付のみで比較する方法 — DATE型/TRUNC/CAST使い分け
- 【SQL】前月のデータを取得する完全ガイド — 前月初〜月末の正確な計算
- 【SQL】前年のデータを取得する方法 — 前年同月比/LAG関数
- 【SQL】INSERTで日付・日時を登録する完全ガイド — 型選択/現在日時/タイムゾーン
- 【SQL】WHERE句で複数条件を組み合わせる完全ガイド — AND/OR/SARGable/HAVING vs WHERE
- 【SQL】誕生日から年齢を算出する方法 — 日付計算の応用
- 【Docker】MySQL CLI完全ガイド — 本記事のSQLをDocker環境で実行する
まとめ
- 推奨構文は半開区間:
col >= 前日00:00 AND col < 当日00:00 - カラムに関数を適用しない(SARGable維持)でインデックス効果
- 4種DBMS:
CURDATE()(MySQL)/CURRENT_DATE(PostgreSQL)/CAST(GETDATE() AS DATE)(SQL Server)/TRUNC(SYSDATE)(Oracle) - BETWEENで
23:59:59を書くとミリ秒のレコードを取り逃がす事故の元 - UTC保存はJST変換を右辺側(比較値)に適用でSARGable維持
- DST/閏年/月またぎ/年またぎも半開区間ならそのまま動く
- 実務パターン:売上集計/前日比/アーカイブ/通知/異常検知/ランキング/パーティション
- cron/EventBridge時刻はUTCで指定、バッチ内部で基準日を事前計算+変数化
- EXPLAIN/EXPLAIN ANALYZE/STATISTICS IO/DBMS_XPLAN で“range”/”Index Scan”/”INDEX RANGE SCAN”を確認
- 大量DELETEはバッチ分割、パーティション前提なら
DROP PARTITIONが最速
前日データ取得は見かけシンプルでも、SARGable・DATETIME境界・タイムゾーンの3つを外すと本番で数時間の夜間バッチ遅延やデータ欠損事故に直結します。本記事の半開区間パターンと4DBMS別構文を押さえておけば、どのRDBMSを使う案件でも迷わず書けて、パフォーマンスも最大化できます。関連する前月/前年/日付比較/範囲指定もあわせて参照してください。

