【SQL】前日のデータを取得する方法完全ガイド|MySQL/PostgreSQL/SQL Server/Oracle・SARGable・タイムゾーン・実務8パターン

【SQL】前日のデータを取得する方法完全ガイド|MySQL/PostgreSQL/SQL Server/Oracle・SARGable・タイムゾーン・実務8パターン SQL

「前日のデータだけ取得したい」——日次バッチ/レポート/通知/アーカイブ削除など、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共通のベストプラクティスです。

DBMS 現在日付の取得 推奨の前日取得(SARGable)
MySQL CURDATE() col >= CURDATE() - INTERVAL 1 DAY AND col < CURDATE()
PostgreSQL CURRENT_DATE col >= CURRENT_DATE - 1 AND col < CURRENT_DATE
SQL Server CAST(GETDATE() AS DATE) col >= CAST(DATEADD(day,-1,GETDATE()) AS DATE) AND col < CAST(GETDATE() AS DATE)
Oracle TRUNC(SYSDATE) col >= TRUNC(SYSDATE) - 1 AND col < TRUNC(SYSDATE)

黄金律:半開区間 >= 前日00:00 AND < 当日00:00。カラムを素のまま左辺に置く/関数適用しない/開始含む・終了含まない、の3点でインデックスが効き、境界事故も起きません。

なぜ関数適用はダメなのか:SARGableの本質

SARGable(Search ARGument Able)とは「検索引数としてインデックスが使える書き方」のこと。WHERE句の左辺にカラムをそのまま置くとインデックスを利用できますが、関数で加工するとDBはカラム値を全件計算せざるを得ずフルスキャンになります。

非SARGable(インデックス無効化)
-- ❌ 遅い: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);
SARGable(インデックス有効)
-- ⭕ 速い:カラムは素のまま、比較対象を計算
SELECT * FROM orders
WHERE order_date &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  CURDATE();

-- ⭕ 速い:Oracle
SELECT * FROM orders
WHERE order_date &gt;= TRUNC(SYSDATE) - 1
  AND order_date &lt;  TRUNC(SYSDATE);

-- ⭕ 速い:SQL Server
SELECT * FROM orders
WHERE order_date &gt;= CAST(DATEADD(day,-1,GETDATE()) AS DATE)
  AND order_date &lt;  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 の境界レコードも含まれてしまう
-- → 前日だけ取得したいのに当日の先頭も混ざる
正解:半開区間([start, end))
-- ⭕ 半開区間:開始含む、終了含まない
SELECT * FROM events
WHERE event_time &gt;= '2026-04-21 00:00:00'
  AND event_time &lt;  '2026-04-22 00:00:00';

-- ⭕ 動的計算版(MySQL)
SELECT * FROM events
WHERE event_time &gt;= CURDATE() - INTERVAL 1 DAY
  AND event_time &lt;  CURDATE();

半開区間[start, end)の数学的な美しさ

  • 連続する期間が重複なく完全につながる(前日 + 当日 = 2日分)
  • ミリ秒/ナノ秒/将来のTimestamptz変更でも壊れない
  • 分単位データでも秒単位データでも同じ書き方で動く
  • プログラミング言語のrange(start, end)と同じ流儀

MySQLで前日のデータを取得する

基本構文(推奨)
-- ⭕ SARGable推奨(半開区間)
SELECT *
FROM orders
WHERE order_date &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  CURDATE();

-- DATE_SUB()派(好みで)
SELECT *
FROM orders
WHERE order_date &gt;= DATE_SUB(CURDATE(), INTERVAL 1 DAY)
  AND order_date &lt;  CURDATE();

-- カラムがDATE型しか入っていないケース(時刻なし)
-- これならシンプルに「= 前日」で良い
SELECT *
FROM daily_reports
WHERE report_date = CURDATE() - INTERVAL 1 DAY;
便利関数との組み合わせ
-- 前日の総件数
SELECT COUNT(*) AS yesterday_count
FROM orders
WHERE order_date &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  CURDATE();

-- 前日の売上合計
SELECT SUM(amount) AS yesterday_sales
FROM orders
WHERE order_date &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  CURDATE();

-- 時間帯別件数(前日の1時間ごと集計)
SELECT HOUR(order_date) AS hr, COUNT(*) AS cnt
FROM orders
WHERE order_date &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  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 &gt;= CURRENT_DATE - INTERVAL '1 day'
  AND order_date &lt;  CURRENT_DATE;

-- INTEGER演算でも可(PostgreSQL独自)
SELECT *
FROM orders
WHERE order_date &gt;= CURRENT_DATE - 1
  AND order_date &lt;  CURRENT_DATE;

-- TIMESTAMP型との混在が気になる時はtimestampキャスト
SELECT *
FROM orders
WHERE order_date &gt;= (CURRENT_DATE - 1)::timestamp
  AND order_date &lt;  CURRENT_DATE::timestamp;
TIMESTAMP WITH TIME ZONE対応
-- timestamptz カラムの場合はタイムゾーン意識が必要
-- 日本の前日(UTCではなくJSTで)取得
SELECT *
FROM orders
WHERE order_date AT TIME ZONE 'Asia/Tokyo' &gt;= (CURRENT_DATE - 1)
  AND order_date AT TIME ZONE 'Asia/Tokyo' &lt;  CURRENT_DATE;

-- SARGableにしたい場合は右辺を変換する(左辺は素のまま)
SELECT *
FROM orders
WHERE order_date &gt;= ((CURRENT_DATE - 1) AT TIME ZONE 'Asia/Tokyo')
  AND order_date &lt;  (CURRENT_DATE AT TIME ZONE 'Asia/Tokyo');

PostgreSQLのAT TIME ZONEカラム側に適用するとインデックスが効かなくなるため、本番大規模テーブルでは右辺(比較値)側にだけ変換をかけるのが王道。DB設計段階でtimestamptzを一貫して使うとこの手の問題が激減します。

SQL Serverで前日のデータを取得する

基本構文(推奨)
-- ⭕ SARGable推奨
SELECT *
FROM orders
WHERE order_date &gt;= CAST(DATEADD(day, -1, GETDATE()) AS DATE)
  AND order_date &lt;  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 &gt;= @yesterday
  AND order_date &lt;  @today;
SYSUTCDATETIMEとタイムゾーン(SQL Server 2016+)
-- UTC保存→JSTで前日取得(SQL Server 2016+)
SELECT *
FROM orders
WHERE order_date &gt;= CAST(DATEADD(day, -1,
    SYSDATETIMEOFFSET() AT TIME ZONE 'Tokyo Standard Time') AS DATE)
  AND order_date &lt;  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 &gt;= TRUNC(SYSDATE) - 1
  AND order_date &lt;  TRUNC(SYSDATE);

-- CURRENT_DATE(セッションタイムゾーンに依存)
SELECT *
FROM orders
WHERE order_date &gt;= TRUNC(CURRENT_DATE) - 1
  AND order_date &lt;  TRUNC(CURRENT_DATE);
TIMESTAMP WITH TIME ZONE対応
-- UTC保存→JSTで前日取得
SELECT *
FROM orders
WHERE order_date &gt;= (TRUNC(CAST(SYSTIMESTAMP AT TIME ZONE 'Asia/Tokyo' AS DATE)) - 1)
  AND order_date &lt;  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のSYSDATECURRENT_DATE時刻込みで返ります。TRUNC()で00:00:00に切り下げないと、”前日”の定義がずれます。カラム側には絶対TRUNC()をかけない(SARGable破壊)、右辺の比較値だけTRUNC()するのが鉄則。

タイムゾーン完全対応:UTC保存→JSTで前日

グローバルSaaSやクラウドインフラのDBはUTC保存が定石。日本のユーザーから「昨日」と言われたらJSTで解釈しなければなりません。UTCそのままでCURDATE()を使うとJSTの9時間前になり、前日が早朝にカットされる事故が起きます。

MySQL:CONVERT_TZ
-- UTC保存データをJSTで前日取得
SELECT *
FROM logs
WHERE log_time &gt;= CONVERT_TZ(
    CONCAT(CURDATE() - INTERVAL 1 DAY, ' 00:00:00'),
    '+09:00', '+00:00'
  )
  AND log_time &lt;  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
PostgreSQL:AT TIME ZONE
-- シンプルに範囲を構築
SELECT *
FROM logs
WHERE log_time &gt;= ((CURRENT_DATE - 1) AT TIME ZONE 'Asia/Tokyo')
  AND log_time &lt;  (CURRENT_DATE AT TIME ZONE 'Asia/Tokyo');
SQL Server:AT TIME ZONE (2016+)
SELECT *
FROM logs
WHERE log_time &gt;= CAST(DATEADD(day, -1,
    SYSDATETIMEOFFSET() AT TIME ZONE 'Tokyo Standard Time') AS DATETIME)
  AND log_time &lt;  CAST(SYSDATETIMEOFFSET() AT TIME ZONE 'Tokyo Standard Time' AS DATETIME);
Oracle:FROM_TZ + AT TIME ZONE
SELECT *
FROM logs
WHERE log_time &gt;= (FROM_TZ(CAST(TRUNC(SYSTIMESTAMP AT TIME ZONE 'Asia/Tokyo') - 1 AS TIMESTAMP), 'Asia/Tokyo')
    AT TIME ZONE 'UTC')
  AND log_time &lt;  (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 &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  CURDATE()
GROUP BY category
ORDER BY total_sales DESC;

②前日比(Day-over-Day)計算

前日比%
-- 前々日と前日を比較
SELECT
  SUM(CASE WHEN order_date &gt;= CURDATE() - INTERVAL 1 DAY THEN amount END) AS yesterday,
  SUM(CASE WHEN order_date &gt;= CURDATE() - INTERVAL 2 DAY
            AND order_date &lt;  CURDATE() - INTERVAL 1 DAY THEN amount END) AS day_before,
  (SUM(CASE WHEN order_date &gt;= CURDATE() - INTERVAL 1 DAY THEN amount END)
    - SUM(CASE WHEN order_date &gt;= CURDATE() - INTERVAL 2 DAY
               AND order_date &lt;  CURDATE() - INTERVAL 1 DAY THEN amount END))
    / SUM(CASE WHEN order_date &gt;= CURDATE() - INTERVAL 2 DAY
               AND order_date &lt;  CURDATE() - INTERVAL 1 DAY THEN amount END) * 100 AS dod_percent
FROM orders
WHERE order_date &gt;= CURDATE() - INTERVAL 2 DAY
  AND order_date &lt;  CURDATE();

③ログのアーカイブ(前日分を別テーブルへ)

INSERT SELECT + DELETE
-- Step1: 前日分をアーカイブテーブルへ
INSERT INTO access_logs_archive
SELECT * FROM access_logs
WHERE log_time &gt;= CURDATE() - INTERVAL 1 DAY
  AND log_time &lt;  CURDATE();

-- Step2: 元テーブルから前日分削除(バッチサイズ制限推奨)
DELETE FROM access_logs
WHERE log_time &gt;= CURDATE() - INTERVAL 1 DAY
  AND log_time &lt;  CURDATE()
LIMIT 10000;
-- LIMITで分割実行してロック範囲を狭める

④昨日登録されたユーザーへの通知

通知対象抽出
SELECT u.id, u.email, u.name, u.created_at
FROM users u
WHERE u.created_at &gt;= CURDATE() - INTERVAL 1 DAY
  AND u.created_at &lt;  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 &gt;= CURDATE() - INTERVAL 1 DAY
  AND log_time &lt;  CURDATE()
  AND severity = "ERROR"
GROUP BY DATE_FORMAT(log_time, '%Y-%m-%d %H:00:00')
HAVING COUNT(*) &gt; 100
ORDER BY hour;
-- 1時間当たり100件超のエラーを検出

⑥前日未処理のジョブ洗い出し

バッチ処理の見逃し検出
SELECT *
FROM jobs
WHERE scheduled_at &gt;= CURDATE() - INTERVAL 1 DAY
  AND scheduled_at &lt;  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 &gt;= CURDATE() - INTERVAL 1 DAY
  AND o.order_date &lt;  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 &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  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クライアント

crontab例
# 毎朝6:00に前日の集計バッチ実行(ログ付き)
0 6 * * * /usr/bin/mysql -h db.example.com -u batch -p"$PASS" mydb \
  -e "CALL daily_summary_batch()" &gt;&gt; /var/log/batch.log 2&gt;&amp;1

# Dockerベース(compose経由)
0 6 * * * cd /opt/app && /usr/bin/docker compose exec -T db \
  mysql -uroot -proot mydb -e "CALL daily_summary_batch()" \
  &gt;&gt; /var/log/batch.log 2&gt;&amp;1

AWS EventBridge + Lambda + RDS

Lambda(Python)で前日集計
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 &gt;= %s AND order_date &lt; %s",
                (yesterday_jst, today_jst),
            )
            count, total = cur.fetchone()
    finally:
        conn.close()

    return {"count": count, "total": float(total or 0), "date": str(yesterday_jst)}
EventBridge (CloudWatch Events) ルール
# 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診断

MySQL EXPLAIN
-- EXPLAINを付けるだけ
EXPLAIN SELECT * FROM orders
WHERE order_date &gt;= CURDATE() - INTERVAL 1 DAY
  AND order_date &lt;  CURDATE();

-- 見るべき列
-- type: range(◎)/ ref(◯)/ ALL(✗フルスキャン)
-- key:  使われたインデックス名(NULLは未使用)
-- rows: 想定読み込み行数(少ないほど良い)
-- Extra: "Using index condition" があれば効いている
PostgreSQL EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE order_date &gt;= CURRENT_DATE - INTERVAL '1 day'
  AND order_date &lt;  CURRENT_DATE;

-- 見るべきキーワード
-- "Index Scan using orders_order_date_idx"(◎)
-- "Seq Scan"(✗フルスキャン)
-- "Bitmap Index Scan"(◯中程度)
-- actual rows vs estimated rows が大きくズレたら統計情報更新
SQL Server 実行プラン
-- 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 &gt;= CAST(DATEADD(day, -1, GETDATE()) AS DATE)
  AND order_date &lt;  CAST(GETDATE() AS DATE);

-- 見るべきキーワード
-- "Index Seek"(◎)
-- "Index Scan"(△)
-- "Table Scan"(✗フルスキャン)
Oracle EXPLAIN PLAN / DBMS_XPLAN
EXPLAIN PLAN FOR
SELECT * FROM orders
WHERE order_date &gt;= TRUNC(SYSDATE) - 1
  AND order_date &lt;  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 TABLEDBMS_STATS
  • 依然として遅い → パーティション導入検討

よくある質問

Q前々日や1週間前のデータも取得したい
A数字を変えるだけでOKです。MySQL: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】前年のデータを取得する方法を参照。
QBETWEENで「前日00:00〜23:59」と書いても同じ?
A同じ結果がほぼ得られますが完全一致ではありませんBETWEEN '2026-04-21 00:00:00' AND '2026-04-21 23:59:59'23:59:59.500は拾えても、DB・カラム型によっては23:59:59.999が境界で事故を起こすことが。半開区間>= 前日 AND < 当日なら精度に関係なく正確で、連続する範囲の結合も安全です。
QカラムがDATE型なら関数適用してもインデックスは使える?
AMySQL/PostgreSQLでは、DATE(col)のように関数適用するとインデックスが使えなくなります(カラムがDATE型でも同じ)。ただしPostgreSQLとOracleには関数ベースインデックス(Function-Based Index)を作る手があります:CREATE INDEX idx_date ON orders(DATE(order_date));とはいえ、素直に範囲指定する方がシンプルで保守性も高いです。
QUTCで保存されているテーブルを「日本時間で前日」取得したい
AMySQLならCONVERT_TZ(ts, '+00:00', '+09:00')で変換、PostgreSQLならts AT TIME ZONE 'Asia/Tokyo'、SQL Server 2016+ではAT TIME ZONE 'Tokyo Standard Time'カラム側ではなく右辺(比較値)側に変換を適用するとSARGableを保てます。本記事「タイムゾーン完全対応」セクション参照。
Qバッチが深夜0:00過ぎに起動して途中で日付が切り替わる
Aバッチ内部でSQLを発行する直前に日付を計算・固定するのが安全。アプリコードで「基準日」を変数に入れてからクエリを投げます(where col >= :start and col < :endのようにプレースホルダ使用)。複数のクエリで揃った基準日を保証でき、バッチ完了時刻の揺らぎによる結果不整合を防げます。
QEXPLAINしたらrowsが全件に近い数字だった
A統計情報が古い可能性。MySQL:ANALYZE TABLE orders;、PostgreSQL:ANALYZE orders;、SQL Server:UPDATE STATISTICS orders;、Oracle:EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ORDERS');で統計を更新。定期的に統計を取る設定も重要です。
Q件数が多すぎてDELETEがロックで止まる
A大量削除はバッチサイズで分割するのが鉄則。MySQL:DELETE ... LIMIT 10000;を繰り返し、PostgreSQL:DELETE ... WHERE ctid IN (SELECT ctid FROM ... LIMIT 10000);、SQL Server:DELETE TOP (10000) ... ;。パーティション導入済みならそのままDROP PARTITIONが最速です。
Q夏時間(DST)の導入国でも正しく動く?
AUTC保存+適切なタイムゾーン変換を使っていれば、DSTでも正しく計算されます。「日本時間で前日」の要件なら日本はDSTが無いので問題なし、US等のDST地域ではAsia/Tokyoの代わりにAmerica/Los_Angeles等を指定するとDB側が自動的に切替時の時刻調整(23時間/25時間の日)を処理してくれます。手動でオフセット-08:00-07:00を切り替える方式はバグの温床なので避けてください。
Q閏年・月末またぎでもOK?
Aはい。CURDATE() - INTERVAL 1 DAYDATEADD(day, -1, ...)カレンダー計算として正確に前日を返します。3月1日の前日は2月28日(平年)or 2月29日(閏年)、1月1日の前日は12月31日と自動処理されます。月またぎ・年またぎを意識せず同じSQLで動くのが半開区間方式の強みです。

関連記事

まとめ

  • 推奨構文は半開区間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を使う案件でも迷わず書けて、パフォーマンスも最大化できます。関連する前月前年日付比較範囲指定もあわせて参照してください。