Oracle DATE型の比較・変換エラーを即解決
目次
OracleのDATE型は日付だけを入れる型と思われがちですが、年・月・日に加えて時・分・秒まで保持します。この一点を見落としてSQLを書くと、WHERE句で1件もヒットしない、環境を変えたらORA-01858で落ちる、日付条件なのにインデックスが使われず全表走査になる、といった症状が出ます。発生頻度の高い原因から、確認コマンドと対処を並べます。
結論:Oracle DATE型のトラブルは「時刻部分」と「書式の暗黙変換」がほとんど
DATE型のトラブルは、次の2つでほぼ説明がつきます。
1つ目は、時刻が入った列を等値比較しているケース。DATE型は固定7バイトで、世紀・年・月・日・時・分・秒を格納します(小数秒とタイムゾーンは持ちません)。SYSDATEや画面入力から登録された値には時刻が入るため、日付だけを指定した等値比較は0時0分0秒の行しか拾いません。
-- NG: 2024-04-01 00:00:00 のレコードしかヒットしない
SELECT * FROM orders WHERE order_date = DATE '2024-04-01';
-- OK: 半開区間で範囲指定する(インデックスも効く)
SELECT * FROM orders
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-04-02';
2つ目は、文字列との比較で暗黙変換が起きているケースです。DATE型と文字列を比較すると、Oracleは文字列側をTO_DATE(文字列, NLS_DATE_FORMAT)として変換します。書式がセッションのNLS_DATE_FORMAT任せになるため、開発環境では動いて本番でORA-01858やORA-01861が出ます。
-- NG: NLS_DATE_FORMAT 依存。環境によって落ちる
SELECT * FROM orders WHERE order_date = '2024-04-01';
-- OK: ANSI 日付リテラル(書式固定・NLS 非依存)
SELECT * FROM orders WHERE order_date = DATE '2024-04-01';
-- OK: 書式を明示した TO_DATE
SELECT * FROM orders WHERE order_date = TO_DATE('2024-04-01', 'YYYY-MM-DD');
原則は、DATE型と文字列を直接比較しないこと。日付リテラルはDATE 'YYYY-MM-DD'か書式指定付きのTO_DATEで書きます。
原因の切り分け手順:この順番で確認する
症状から当てにいくより、次の順で確認するほうが速いです。
格納値を秒単位まで表示して時刻部分の有無を確認する
まず、データに時刻が入っているかを確定させます。SQL*PlusやGUIツールの既定表示はNLS_DATE_FORMATに従うため、時刻が切り捨てられて見えることがあります。TO_CHARで書式を明示して確認してください。
SELECT order_id,
TO_CHAR(order_date, 'YYYY-MM-DD HH24:MI:SS') AS order_date_full
FROM orders
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-04-02'
ORDER BY order_date;
時刻つきの行数を数えておくと、アプリ側の修正範囲を判断できます。
SELECT COUNT(*) AS total,
COUNT(CASE WHEN order_date <> TRUNC(order_date) THEN 1 END) AS has_time
FROM orders;
has_timeが0でなければ、等値比較しているSQLはすべて範囲条件へ書き換える対象です。
比較・代入で文字列からの暗黙変換が起きていないか確認する
暗黙変換は実行計画の述語情報にINTERNAL_FUNCTIONやTO_DATEとして現れるため、DBMS_XPLANで確認できます。
EXPLAIN PLAN FOR
SELECT * FROM orders WHERE order_date = '2024-04-01';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, 'ALL'));
逆にVARCHAR2(8)へ'20240401'のように文字列で日付を持つ設計では、DATE側が文字列に変換されるか、文字列側がDATEに変換されるかで挙動とインデックス利用可否が変わります。カラム型に合わせて比較値の型を揃えるのが基本で、TO_CHAR(date_col, 'YYYYMMDD') = char_colのようにカラム側へ関数を当てると、そのカラムのインデックスは使えません。
12.2以降なら、変換できない値を事前に洗い出す関数が使えます。移行や取り込み時のデータ検証に便利です。
-- 書式に合わない行だけを抽出
SELECT id, date_str
FROM staging_tbl
WHERE VALIDATE_CONVERSION(date_str AS DATE, 'YYYYMMDD') = 0;
-- 変換できない行は NULL にして処理を止めない
SELECT TO_DATE(date_str DEFAULT NULL ON CONVERSION ERROR, 'YYYYMMDD')
FROM staging_tbl;
セッションの NLS_DATE_FORMAT とタイムゾーン設定を確認する
同じSQLなのに環境で結果が違う場合は、ここを見ます。クライアントの環境変数やセッション設定(ALTER SESSION)が、データベース既定を上書きします。
-- 現在のセッションに実際に効いている値
SELECT * FROM v$nls_parameters
WHERE parameter IN ('NLS_DATE_FORMAT','NLS_DATE_LANGUAGE',
'NLS_TIMESTAMP_FORMAT','NLS_LANGUAGE','NLS_TERRITORY');
-- データベース側の既定値
SELECT * FROM nls_database_parameters
WHERE parameter IN ('NLS_DATE_FORMAT','NLS_DATE_LANGUAGE');
-- DB とセッションのタイムゾーン
SELECT DBTIMEZONE, SESSIONTIMEZONE FROM dual;
NLS_DATE_FORMATが米国既定のDD-MON-RRになっていれば、'2024-04-01'という文字列の暗黙変換はエラーになります。JDBCアプリではoracle.jdbc.*のプロパティや-Duser.languageの影響も受けるため、アプリ実行時のセッションで上記クエリを流して確認するのが確実です。
接続時にALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'を流す運用は、表示の統一には有効でも、暗黙変換に依存したSQLの根本対策にはなりません。別経路の接続で崩れます。SQL側を書式非依存にするのが本筋です。
ORA-01858 / ORA-01861 / ORA-01843 など、エラー別の対処
変換系エラーは、書式文字列と入力文字列のどこがズレているかで原因が特定できます。
| エラー | 意味 | 典型的な原因と対処 |
|---|---|---|
| ORA-01858 | 数値が必要な位置に非数値がある | 書式'YYYYMMDD'に'2024-04-01'を渡した。区切り文字の有無を書式と一致させる |
| ORA-01861 | リテラルが書式文字列と一致しない | 区切り文字や桁数の不一致。FX修飾子を付けている場合は桁数も厳格判定になる |
| ORA-01843 | 月が無効 | MMに13以上、またはMON/MONTH使用時にNLS_DATE_LANGUAGEが不一致('JAN'と'1月') |
| ORA-01847 | 日が月の範囲外 | 2月30日などの不正値、DDとMMの順序取り違え |
| ORA-01830 | 書式が入力を変換しきる前に終了 | 書式'YYYY-MM-DD'に'2024-04-01 10:00:00'を渡した。時刻まで書式に含める |
| ORA-01840 | 入力値が書式に対して短い | 固定桁書式に桁落ちした値。ゼロ埋めするかLPADで整える |
| ORA-01841 | 年が有効範囲外 | 桁ズレで年に0や巨大値が入っている |
| ORA-00932 | データ型が一致しない | DATEとTIMESTAMP/INTERVALの混在演算、CASEの各分岐で型が異なる |
MON/MONTH/DAYを含む変換は、言語を引数で固定すると環境差に強くなります。
SELECT TO_DATE('01-APR-2024', 'DD-MON-YYYY', 'NLS_DATE_LANGUAGE=ENGLISH')
FROM dual;
2桁年のYYとRRの違いも押さえておきます。RRは世紀を推測する書式で、現在の年を基準に'99'は1999年、'01'は2001年として解釈されます。意図しない世紀になるため、新規開発では4桁年のYYYYを使い、RRは既存データの読み替えに限定するのが安全です。
バッチの一部の行だけで落ちる場合は、エラー行の特定が先です。VALIDATE_CONVERSION(12.2以降)が使えない古いバージョンでは、正規表現で書式に合わない値を抽出します。
SELECT id, date_str
FROM staging_tbl
WHERE NOT REGEXP_LIKE(date_str, '^[0-9]{4}-[0-9]{2}-[0-9]{2}$');
日付条件でインデックスが効かない・検索が遅いときの対処
日付検索が遅い原因は、述語にカラムを包む関数が入っているか、統計情報が古くて見積りを外しているかのどちらかがほとんどです。
実行計画で TRUNC や TO_CHAR が述語に入っていないか確認する
実際に動いた計画を見るのが確実です。
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST +PREDICATE'));
Predicate Informationのfilterやaccessに次のような形が出ていたら、その条件でカラムの通常インデックスは使えません。
filter(TRUNC(INTERNAL_FUNCTION("ORDER_DATE"))=TO_DATE(...))
filter(TO_CHAR(INTERNAL_FUNCTION("ORDER_DATE"),'YYYYMM')='202404')
もう一つ見るべきはE-Rows(推定行数)とA-Rows(実測行数)の乖離です。日付カラムは値が単調に増えるため、統計取得後に追加された新しい日付範囲を指定すると、オプティマイザが該当行はほぼ無いと見積もってNested Loopを選び、実際には大量行が返って極端に遅くなります。統計の鮮度と、DBA_TAB_COL_STATISTICSのHIGH_VALUEを確認してください。
SELECT table_name, last_analyzed, num_rows, stale_stats
FROM dba_tab_statistics
WHERE owner = 'APPUSER' AND table_name = 'ORDERS';
推定行数と実測行数の乖離を起点に追う手順は、他のRDBMSでも共通する部分があります。PostgreSQL EXPLAIN ANALYZEの見方も参考になります。
範囲条件への書き換えと関数ベースインデックスの使い分け
第一選択は範囲条件(半開区間)への書き換えです。カラムに関数を当てないため、既存のインデックスとパーティション・プルーニングがそのまま使えます。
-- NG: TRUNC でカラムを包んでいる
WHERE TRUNC(order_date) = DATE '2024-04-01'
-- OK: 半開区間
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-04-02'
-- 月単位も同じ考え方
WHERE order_date >= DATE '2024-04-01'
AND order_date < ADD_MONTHS(DATE '2024-04-01', 1)
BETWEEN ... AND TO_DATE('2024-04-01 23:59:59','YYYY-MM-DD HH24:MI:SS')はDATE型なら動きますが、将来カラムをTIMESTAMPに変更したときに23:59:59.5が漏れます。半開区間で書くほうが安全です。
パッケージ製品や改修困難な既存コードでSQLを書き換えられない場合は、関数ベースインデックスを検討します。
CREATE INDEX idx_orders_trunc_date ON orders (TRUNC(order_date));
使うときの判断基準は次のとおりです。
- インデックス定義の式とSQL内の式が一字一句同じでないと使われない(
TRUNC(order_date)とTRUNC(order_date,'DD')は別物として扱われ得る) - 式の統計が必要なため、作成後に対象表の統計を取り直す
- DMLのたびに式を評価するので更新コストが増える。更新の多い表では範囲条件への書き換えを優先する
QUERY_REWRITE_ENABLEDが無効化されている環境では利用されない
月次集計のようにTO_CHAR(order_date,'YYYYMM')でグルーピングする処理は、インデックスを足すより、集計対象期間を範囲条件で絞り込んでからGROUP BYする形に直すほうが効果が大きいケースが多いです。
この記事のテーマをさらに学べる本
Oracle の SQL を体系的に学べる試験学習書で、日付や書式の扱いを含む SQL 知識の土台固めに使えます。
楽天ブックスで詳しく見るよくある質問
DATE型とTIMESTAMP型はどう使い分ける?
判断軸は、小数秒が必要か、タイムゾーンを扱うかです。
秒精度で足りて単一タイムゾーン運用ならDATEを選びます。固定7バイトで最も軽量で、date2 - date1が日数のNUMBERになるため日数計算が書きやすい。小数秒が必要なログやイベント時刻はTIMESTAMP(n)で、既定は6桁です。複数タイムゾーンのユーザーが同一データを参照する場合はTIMESTAMP WITH TIME ZONEを使い、WITH LOCAL TIME ZONEはDBタイムゾーンで格納してセッションのタイムゾーンで返します。
注意点として、TIMESTAMP同士の減算結果はINTERVAL DAY TO SECONDです。DATE前提で(t2 - t1) * 24のような計算をしているとORA-00932などで落ちるため、型変更時は演算部分も合わせて見直します。
また、JDBCでDATEがjava.sql.Dateになるかjava.sql.Timestampになるかはドライバのバージョンや設定で変わり、時刻部分が落ちる原因になります。使用中のドライバのドキュメントで確認してください。
SYSDATE・CURRENT_DATE・SYSTIMESTAMPの違いは?
| 関数 | 戻り型 | 基準となるタイムゾーン |
|---|---|---|
SYSDATE |
DATE | データベースサーバーOSの時刻 |
CURRENT_DATE |
DATE | セッションタイムゾーン |
SYSTIMESTAMP |
TIMESTAMP WITH TIME ZONE | データベースサーバーOSの時刻 |
CURRENT_TIMESTAMP |
TIMESTAMP WITH TIME ZONE | セッションタイムゾーン |
LOCALTIMESTAMP |
TIMESTAMP | セッションタイムゾーン |
サーバーとクライアントのタイムゾーンが異なる環境では、SYSDATEとCURRENT_DATEが別の値を返します。登録日時はサーバー時刻で統一するなど方針を決め、混在させないでください。
SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') AS sysdate_v,
TO_CHAR(CURRENT_DATE, 'YYYY-MM-DD HH24:MI:SS') AS current_date_v,
SESSIONTIMEZONE, DBTIMEZONE
FROM dual;
DATE型同士の差分から「何日何時間」を求められる?
DATEの減算は日数(小数を含むNUMBER)を返すので、そこから分解します。
SELECT d2 - d1 AS diff_days,
(d2 - d1) * 24 AS diff_hours,
NUMTODSINTERVAL(d2 - d1, 'DAY') AS diff_interval,
EXTRACT(DAY FROM NUMTODSINTERVAL(d2 - d1,'DAY')) AS dd,
EXTRACT(HOUR FROM NUMTODSINTERVAL(d2 - d1,'DAY')) AS hh,
MONTHS_BETWEEN(d2, d1) AS diff_months
FROM (SELECT TO_DATE('2024-04-01 09:30:00','YYYY-MM-DD HH24:MI:SS') d1,
TO_DATE('2024-04-03 18:45:00','YYYY-MM-DD HH24:MI:SS') d2
FROM dual);
月単位はMONTHS_BETWEEN、月末判定はLAST_DAY、週や時間単位の切り捨てはTRUNC(date,'IW')/TRUNC(date,'HH')を使います。浮動小数の誤差が気になる秒単位の集計では、NUMTODSINTERVALでINTERVALに変換してからEXTRACTするか、ROUND((d2-d1)*86400)で秒に丸めます。
再発防止:DATE型を安全に扱う設計ルールと、次に学ぶこと
コーディング規約として次を決めておくと、同じ事故を繰り返しません。
- 日付リテラルは
DATE 'YYYY-MM-DD'か書式指定付きTO_DATEで書き、文字列との直接比較を禁止する - 日付範囲は半開区間(
>= 開始 AND < 翌日/翌月)で書き、BETWEEN ... 23:59:59は使わない - 述語のカラム側に関数を当てない。
TRUNC/TO_CHARはSELECTリストやGROUP BY側で使う - 時刻を持たせない列は
CHECK (col = TRUNC(col))で担保し、登録時もTRUNC(SYSDATE)を使う - 日付を
VARCHAR2やNUMBERで持たない。ソート・比較・範囲検索・不正値チェックのすべてで不利になる - 時刻の基準(サーバー時刻かセッション時刻か)をプロジェクトで統一する。混在させると差分計算が合わなくなる
- 日付カラムを含む表の統計情報は鮮度を保つ。単調増加カラムは見積りを外しやすい
実運用では、ロックで止まる、表領域が足りない、夜間だけ遅いといった別系統の問題と絡み合います。症状別の入口を押さえておくと調査が速いので、Oracle Database運用トラブルの切り分け|障害と性能劣化も合わせて確認してください。
OracleからPostgreSQLへの移行では、DATE型の扱いが最初のつまずきどころになります。PostgreSQLのdateは時刻を持たず、時刻を含めるならtimestampを使うため、OracleのDATEをそのままdateにマッピングすると時刻が消えます。移行時の型対応と検証観点はOracle PostgreSQL移行の注意点|症状別の切り分けと対処にまとめています。
書式モデルの一覧や、各バージョンで追加された関数(VALIDATE_CONVERSION、ON CONVERSION ERRORなど)の対応状況は、使用しているバージョンのOracle公式ドキュメント(SQL Language ReferenceのDatetime Format Models)で確認してください。