【完全ガイド】ORA-01839: date not valid for month specified の原因と解決方法|うるう年・INTERVAL・ADD_MONTHS 徹底解説
- 作成日 2026.08.03
- Oracle Database
Oracle 開発者が必ず一度は遭遇する日付エラー:
SELECT TO_DATE('2025-02-29', 'YYYY-MM-DD') FROM DUAL;
-- ORA-01839: date not valid for month specified
「その月に存在しない日付」というシンプルなメッセージ。2025 年はうるう年でないので、2月に29日は存在しません。
しかし、このエラーは日付計算で本当に厄介なパターンで発生します:
-- ❌ 落とし穴:INTERVAL 加算
SELECT DATE '2025-01-31' + INTERVAL '1' MONTH FROM DUAL;
-- ORA-01839: 2月31日は存在しない!
-- ✅ 正解:ADD_MONTHS
SELECT ADD_MONTHS(DATE '2025-01-31', 1) FROM DUAL;
-- 2025-02-28(月末に自動補正)
このシンプルな違いが、本番運用での事故を引き起こします:
- 月次バッチが特定月にだけ失敗
- 契約更新日の計算エラー
- サブスクリプションの翌月同日課金
- CSV データに紛れる
2/304/31等の異常値 - アプリの日付ピッカーからの無効入力
- 年月日を分割保存した後の再結合
- UI では 2/29 に見えるが実は 3/1(アプリ側で丸め)
さらに、多くの日本語記事が「その日は存在しない」で終わりますが、実務では:
INTERVAL '1' MONTHvsADD_MONTHS(d, 1)の根本的な違いLAST_DAYによる月末補正パターン- うるう年判定の SQL(実は簡単)
DEFAULT ... ON CONVERSION ERROR(12c+)- VALIDATE_CONVERSION による事前チェック(12c+)
- CSV/ETL での無効日付処理
- Rails / Java / Python からの防御的コーディング
さらに、Oracle 日付エラー4大シリーズの中で、ORA-01839 は最も見落とされがち:
ORA-01843: not a valid month ← 月が範囲外
ORA-01830: date format ends before ← フォーマット早期終了
ORA-01858: non-numeric character ← 数値以外の文字
ORA-01839: date not valid for month ← 日と月の整合性 ← 本記事
+ ORA-01847: day of month must be... ← 日の範囲外
+ ORA-01841: year must be between... ← 年の範囲外
本記事では、ORA-01839: date not valid for month specified の完全な原因と解決方法を、リファレンスとして実用的に整理します。うるう年、月末日の罠、INTERVAL vs ADD_MONTHS、LAST_DAY 補正、10大発生パターン、日付エラー6兄弟の完全比較、5つの解決策、Rails/Java/Python 対応、実践シナリオ、FAQまで完全網羅。この1本で ORA-01839 を根本から解決できるようになります。
- 1. 結論:その月にその日は存在しない
- 2. まず理解する:Oracle の日付検証
- 3. 【原因①】うるう年でない年の2月29日(最頻出)
- 4. 【原因②】30日までの月(4/6/9/11月)の31日
- 5. 【原因③】INTERVAL 加算の落とし穴(超危険)
- 6. 【原因④】CSV / ETL データの無効日付
- 7. 【原因⑤】年月日の分割保存後の再結合
- 8. 【原因⑥】サブスクリプションの翌月同日課金
- 9. 【原因⑦】外部システム日付データの混入
- 10. 【原因⑧】バッチ処理の月末問題
- 11. 【原因⑨】うるう秒 vs うるう年の混同
- 12. 【原因⑩】タイムゾーン変換での日付シフト
- 13. 診断ツール完全リファレンス
- 14. 5つの解決策 完全リファレンス
- 15. Rails / Java / Python 対応
- 16. 実践シナリオ
- 17. トラブルシューティング
- 18. よくある質問(FAQ)
- 19. 参考リンク
- 20. まとめ
結論:その月にその日は存在しない
時間がない方向けに、最速の対処を先に示します。
エラーメッセージの読み方
ORA-01839: date not valid for month specified
↑ 指定された月に対して日付が無効
日(DD)の値がその月の最大日数を超えている。
各月の最大日数
1月: 31 7月: 31
2月: 28/29 8月: 31
3月: 31 9月: 30
4月: 30 10月: 31
5月: 31 11月: 30
6月: 30 12月: 31
うるう年判定:
- 4 で割り切れる
- ただし 100 で割り切れる年は除外
- ただし 400 で割り切れる年は含む
例:
- 2024 → うるう年(4で割り切れる)
- 2025 → 平年
- 2100 → 平年(100で割り切れるが 400 で割り切れない)
- 2000 → うるう年(400で割り切れる)
最速の診断
-- ① 12c+ の VALIDATE_CONVERSION
SELECT VALIDATE_CONVERSION('2025-02-29' AS DATE, 'YYYY-MM-DD') FROM DUAL;
-- 1 = 変換可能, 0 = 不可
-- ② 12.2+ の DEFAULT ON CONVERSION ERROR
SELECT TO_DATE('2025-02-29' DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD')
FROM DUAL;
-- NULL(エラーなし)
-- ③ 無効データ検出
SELECT * FROM my_table
WHERE VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 0;
5つの解決策
| # | 手法 | 使う場面 |
|---|---|---|
| ① | LAST_DAY で月末補正 | 月末日の演算 |
| ② | ADD_MONTHS を使う | 月加算 |
| ③ | VALIDATE_CONVERSION | 事前チェック |
| ④ | DEFAULT ON CONVERSION ERROR | 変換失敗時デフォルト |
| ⑤ | REGEXP + アプリロジック | 入力検証 |
日付エラー4部作+2
| エラー | 意味 |
|---|---|
| ORA-01843 | 月が無効(’JAN’ 等の変換) |
| ORA-01830 | フォーマット早期終了 |
| ORA-01858 | 数値以外の文字(英字混入) |
| ORA-01839 | 月に対して日が無効 ← 本記事 |
| ORA-01847 | 日が範囲外(32以上等) |
| ORA-01841 | 年が範囲外(-4713〜9999) |
詳細は以下で解説します。
まず理解する:Oracle の日付検証
DATE 型の内部表現
Oracle の DATE 型は年月日+時分秒を保持:
バイト構成: 世紀・年・月・日・時・分・秒 (7バイト)
範囲: 4712 BC 〜 9999 AD
日付の妥当性チェック
Oracle は厳格に検証:
- 月(1-12): ORA-01843 / ORA-01858
- 日(1-31): ORA-01847
- 日と月の整合性: ORA-01839 ← 本記事
- 年(-4713〜9999): ORA-01841
型変換の順序
1. 文字列の書式解析(TO_DATE のフォーマット)
2. 各要素の抽出(年・月・日)
3. 各要素の範囲チェック
4. 月と日の整合性チェック ← ここで ORA-01839
5. DATE 型の構築
DATE の詳細は Oracle DATE vs TIMESTAMP の違いの記事も参照してください。
【原因①】うるう年でない年の2月29日(最頻出)
症状
SELECT TO_DATE('2025-02-29', 'YYYY-MM-DD') FROM DUAL;
-- ORA-01839(2025は平年)
SELECT TO_DATE('2024-02-29', 'YYYY-MM-DD') FROM DUAL;
-- 2024-02-29(うるう年)
うるう年の判定 SQL
-- SQL でうるう年判定
SELECT year,
CASE
WHEN MOD(year, 400) = 0 THEN 'うるう年'
WHEN MOD(year, 100) = 0 THEN '平年'
WHEN MOD(year, 4) = 0 THEN 'うるう年'
ELSE '平年'
END AS type
FROM (SELECT LEVEL + 2020 year FROM DUAL CONNECT BY LEVEL <= 10);
-- または単純化
SELECT year,
CASE WHEN TO_CHAR(TO_DATE('01-03-' || year, 'DD-MM-YYYY') - 1, 'DD') = '29'
THEN 'うるう年' ELSE '平年' END AS type
FROM ...;
解決
A. 存在確認:
-- 2/29 か 2/28 か判定
SELECT NVL(
TO_DATE('2025-02-29' DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD'),
TO_DATE('2025-02-28', 'YYYY-MM-DD')
) FROM DUAL;
-- 2025-02-28
B. LAST_DAY 活用:
SELECT LAST_DAY(TO_DATE('2025-02-01', 'YYYY-MM-DD')) FROM DUAL;
-- 2025-02-28
【原因②】30日までの月(4/6/9/11月)の31日
症状
SELECT TO_DATE('2025-04-31', 'YYYY-MM-DD') FROM DUAL;
-- ORA-01839: 4月は30日まで
SELECT TO_DATE('2025-06-31', 'YYYY-MM-DD') FROM DUAL;
-- ORA-01839
SELECT TO_DATE('2025-09-31', 'YYYY-MM-DD') FROM DUAL;
-- ORA-01839
SELECT TO_DATE('2025-11-31', 'YYYY-MM-DD') FROM DUAL;
-- ORA-01839
覚え方(英語圏)
30 days hath September,
April, June, and November,
All the rest have 31,
Except for February...
解決
A. 月末補正:
DECLARE
v_target_date DATE;
BEGIN
BEGIN
v_target_date := TO_DATE('2025-04-31', 'YYYY-MM-DD');
EXCEPTION
WHEN OTHERS THEN
-- 月末に補正
v_target_date := LAST_DAY(TO_DATE('2025-04-01', 'YYYY-MM-DD'));
END;
END;
/
B. LEAST 関数:
SELECT LEAST(
31,
TO_NUMBER(TO_CHAR(LAST_DAY(TO_DATE('2025-04-01', 'YYYY-MM-DD')), 'DD'))
) AS actual_day FROM DUAL;
-- 30(4月の場合)
【原因③】INTERVAL 加算の落とし穴(超危険)
症状(多くの人がハマる)
-- 1月31日 + 1ヶ月 = ?
SELECT DATE '2025-01-31' + INTERVAL '1' MONTH FROM DUAL;
-- ORA-01839: 2月31日は存在しない!
-- 3年後の 2/29
SELECT DATE '2024-02-29' + INTERVAL '3' YEAR FROM DUAL;
-- ORA-01839: 2027-02-29 は存在しない
原因
INTERVAL は「単純な月加算」:
2025-01-31 + INTERVAL '1' MONTH
= 2025-02-31 ← 数学的にはこれ
= ORA-01839 ← しかし無効な日付
解決:ADD_MONTHS を使う
-- ADD_MONTHS は月末を自動補正
SELECT ADD_MONTHS(DATE '2025-01-31', 1) FROM DUAL;
-- 2025-02-28(月末に自動補正)
SELECT ADD_MONTHS(DATE '2024-02-29', 36) FROM DUAL;
-- 2027-02-28(3年後の月末)
INTERVAL vs ADD_MONTHS 完全比較
| 項目 | INTERVAL ‘1’ MONTH | ADD_MONTHS |
|---|---|---|
| 1/31 + 1ヶ月 | ORA-01839 | 2/28(or 2/29) |
| 1/30 + 1ヶ月 | ORA-01839 | 2/28 |
| 1/28 + 1ヶ月 | 2/28 | 2/28 |
| 月末の特殊処理 | なし | あり |
| 推奨 | ❌ | ✅ |
ADD_MONTHS の月末保持
-- ADD_MONTHS は月末なら月末を保持
SELECT ADD_MONTHS(DATE '2025-01-31', 1) FROM DUAL; -- 2025-02-28(月末)
SELECT ADD_MONTHS(DATE '2025-02-28', 1) FROM DUAL; -- 2025-03-31(月末)
-- 中旬は普通に
SELECT ADD_MONTHS(DATE '2025-01-15', 1) FROM DUAL; -- 2025-02-15
【原因④】CSV / ETL データの無効日付
症状
CSV データ:
id,date_str
1,2025-01-15
2,2025-02-29 ← 平年の2/29
3,2025-04-31 ← 4月31日
4,2025-13-05 ← 13月(別エラー)
診断
-- 全ての無効日付を検出(12c+)
SELECT id, date_str
FROM staging
WHERE VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 0;
解決:クレンジング
INSERT INTO final (id, event_date)
SELECT
id,
TO_DATE(date_str DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD')
FROM staging;
-- または NULL でなく代替日付
SELECT NVL(
TO_DATE(date_str DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD'),
TRUNC(SYSDATE) -- 今日をデフォルト
) FROM staging;
Data Pump の詳細は Oracle Data Pump 使い方の記事も参照してください。
【原因⑤】年月日の分割保存後の再結合
症状
アプリで年月日を別列に保存:
CREATE TABLE events (
event_id NUMBER,
event_year NUMBER,
event_month NUMBER,
event_day NUMBER
);
INSERT INTO events VALUES (1, 2025, 2, 29); -- INSERT は成功
INSERT INTO events VALUES (2, 2025, 4, 31);
-- 後で DATE 型に変換
SELECT TO_DATE(
event_year || '-' || event_month || '-' || event_day,
'YYYY-MM-DD'
) FROM events;
-- ORA-01839
解決
アプリで検証 or DB CHECK 制約:
ALTER TABLE events ADD CONSTRAINT chk_valid_date
CHECK (
VALIDATE_CONVERSION(
event_year || '-' || event_month || '-' || event_day AS DATE,
'YYYY-MM-DD'
) = 1
);
CHECK 制約の詳細は ORA-02290: check constraint violated の記事も参照してください。
【原因⑥】サブスクリプションの翌月同日課金
症状
契約日: 1/31、次回課金: 2/31? 3/1?
-- ❌ 危険
next_billing := contract_date + INTERVAL '1' MONTH; -- ORA-01839
-- ✅ 正しい
next_billing := ADD_MONTHS(contract_date, 1); -- 月末なら月末
業務要件次第
A. 月末なら月末(保守的):
ADD_MONTHS(d, 1)
B. 常に同日、翌月に存在しなければ月初:
SELECT CASE
WHEN TO_DATE('2025-02-31' DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD') IS NULL
THEN DATE '2025-03-01'
ELSE TO_DATE('2025-02-31', 'YYYY-MM-DD')
END FROM DUAL;
C. 前月末:
LAST_DAY(ADD_MONTHS(d, 1) - INTERVAL '1' MONTH) -- 微妙
【原因⑦】外部システム日付データの混入
症状(Excel からのコピペ等)
Excel の日付形式: 2025/2/29
テキストとして入力 → SQL で TO_DATE → ORA-01839
解決
入力段階でバリデーション:
// フロントエンド
const isValidDate = (dateStr) => {
const date = new Date(dateStr);
return date instanceof Date && !isNaN(date);
};
SQL 層で防御:
-- 12c+ の DEFAULT
SELECT TO_DATE(user_input DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD')
FROM DUAL;
【原因⑧】バッチ処理の月末問題
症状
毎月30日にバッチ実行 → 2月ハマる:
-- スケジュール: 毎月30日
-- 2月30日は存在しない
INSERT INTO reports (report_date)
VALUES (TO_DATE('2025-02-30', 'YYYY-MM-DD'));
-- ORA-01839
解決
LAST_DAY で月末に補正:
INSERT INTO reports (report_date)
VALUES (LEAST(
TO_DATE('2025-02-01', 'YYYY-MM-DD') + 29, -- 30日目
LAST_DAY(TO_DATE('2025-02-01', 'YYYY-MM-DD')) -- 月末
));
または DBMS_SCHEDULER のカレンダー式:
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'MONTH_END_BATCH',
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN my_procedure; END;',
repeat_interval => 'FREQ=MONTHLY; BYMONTHDAY=-1', -- 月末
enabled => TRUE
);
END;
/
【原因⑨】うるう秒 vs うるう年の混同
症状(別エラーだが関連)
-- うるう秒 (23:59:60) は Oracle 未対応
SELECT TO_TIMESTAMP('2025-06-30 23:59:60', 'YYYY-MM-DD HH24:MI:SS') FROM DUAL;
-- ORA-01852 or ORA-01847
解決
Oracle は POSIX 時間(うるう秒は無視)。うるう年のみ気にする。
【原因⑩】タイムゾーン変換での日付シフト
症状
-- TZ 変換で日付が変わる
SELECT CAST(
TIMESTAMP '2025-02-28 22:00:00 America/New_York'
AT TIME ZONE 'Asia/Tokyo' AS DATE
) FROM DUAL;
-- 2025-03-01(+14時間)
表示上は 2/29 になり得るが、実際は無効。
解決
明示的な TZ 指定:
CAST(TIMESTAMP '2025-02-28 22:00:00' AS DATE)
-- TZ 変換前に文字列で処理
TIMESTAMP の詳細は Oracle DATE vs TIMESTAMP の違いの記事も参照してください。
診断ツール完全リファレンス
VALIDATE_CONVERSION(Oracle 12c+)
-- 個別チェック
SELECT VALIDATE_CONVERSION('2025-02-29' AS DATE, 'YYYY-MM-DD') FROM DUAL;
-- 0
-- 一括検出
SELECT * FROM my_table
WHERE VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 0;
DEFAULT ON CONVERSION ERROR(Oracle 12.2+)
-- エラー時デフォルト値
SELECT TO_DATE('2025-02-29' DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD')
FROM DUAL;
-- NULL
is_valid_date ユーザー関数
CREATE OR REPLACE FUNCTION is_valid_date(
p_str VARCHAR2,
p_fmt VARCHAR2 DEFAULT 'YYYY-MM-DD'
) RETURN VARCHAR2 DETERMINISTIC IS
v_date DATE;
BEGIN
v_date := TO_DATE(p_str, p_fmt);
RETURN 'Y';
EXCEPTION
WHEN OTHERS THEN
RETURN 'N';
END;
/
-- 使用
SELECT id, date_str, is_valid_date(date_str) AS valid
FROM staging;
REGEXP による事前チェック
-- 基本的な形式チェック(月・日の妥当性は不十分)
WHERE REGEXP_LIKE(date_str,
'^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$')
-- 完璧ではない(例: 4月31日を通す)
完全な妥当性チェック(クエリ)
-- 実際に変換できるか
CASE
WHEN VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 1
THEN 'VALID'
ELSE 'INVALID'
END
12c+ 使用推奨。REGEXP は月末日の判定不可。
5つの解決策 完全リファレンス
解決策① LAST_DAY で月末補正
-- 特定月の月末を取得
SELECT LAST_DAY(TO_DATE('2025-02-01', 'YYYY-MM-DD')) FROM DUAL;
-- 2025-02-28
SELECT LAST_DAY(TO_DATE('2024-02-01', 'YYYY-MM-DD')) FROM DUAL;
-- 2024-02-29(うるう年)
解決策② ADD_MONTHS
-- INTERVAL よりも安全
SELECT ADD_MONTHS(DATE '2025-01-31', 1) FROM DUAL;
-- 2025-02-28
-- ❌ 避ける
SELECT DATE '2025-01-31' + INTERVAL '1' MONTH FROM DUAL;
-- ORA-01839
解決策③ VALIDATE_CONVERSION(12c+)
SELECT CASE VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD')
WHEN 1 THEN TO_DATE(date_str, 'YYYY-MM-DD')
ELSE NULL
END
FROM staging;
解決策④ DEFAULT ON CONVERSION ERROR(12.2+)
-- 変換失敗時にデフォルト
SELECT TO_DATE(date_str DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD')
FROM staging;
解決策⑤ アプリ側 + REGEXP
-- 基本パターンチェック(月末日は不十分)
WHERE REGEXP_LIKE(date_str, '^\d{4}-\d{2}-\d{2}$')
AND VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 1
Rails / Java / Python 対応
Rails ActiveRecord
モデル側のバリデーション:
class Event < ApplicationRecord
validates :event_date, presence: true
validate :valid_date_format
private
def valid_date_format
return unless event_date_before_type_cast.is_a?(String)
Date.parse(event_date_before_type_cast)
rescue ArgumentError
errors.add(:event_date, "無効な日付")
end
end
Ruby の Date.parse:
begin
date = Date.parse("2025-02-29")
rescue ArgumentError => e
# invalid date
Rails.logger.error "無効な日付: #{e.message}"
end
# または valid_date?
Date.valid_date?(2025, 2, 29) # false(平年)
Date.valid_date?(2024, 2, 29) # true(うるう年)
Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事も参照してください。
Java (JDBC)
// Java 8+ の LocalDate(推奨)
try {
LocalDate date = LocalDate.parse("2025-02-29");
ps.setObject(1, date);
} catch (DateTimeParseException e) {
logger.error("無効な日付: " + e.getMessage());
}
// うるう年判定
boolean isLeap = Year.isLeap(2024); // true
Python (oracledb)
from datetime import datetime
try:
dt = datetime.strptime("2025-02-29", "%Y-%m-%d")
except ValueError as e:
print(f"無効な日付: {e}")
# うるう年判定
import calendar
calendar.isleap(2024) # True
calendar.isleap(2025) # False
# 月の最大日数
last_day = calendar.monthrange(2025, 2)[1] # 28
実践シナリオ
シナリオ1:契約更新日の計算
CREATE OR REPLACE FUNCTION next_billing_date(
p_current DATE,
p_months NUMBER
) RETURN DATE DETERMINISTIC IS
BEGIN
-- ADD_MONTHS を使用(月末補正付き)
RETURN ADD_MONTHS(p_current, p_months);
END;
/
-- 使用
SELECT next_billing_date(DATE '2025-01-31', 1) FROM DUAL;
-- 2025-02-28
SELECT next_billing_date(DATE '2025-01-31', 12) FROM DUAL;
-- 2026-01-31
シナリオ2:うるう年一覧の生成
SELECT year
FROM (SELECT LEVEL + 2020 year FROM DUAL CONNECT BY LEVEL <= 20)
WHERE VALIDATE_CONVERSION(year || '-02-29' AS DATE, 'YYYY-MM-DD') = 1;
-- 2024, 2028, ...
シナリオ3:CSV データクレンジング
CREATE TABLE staging_errors (
original_row VARCHAR2(4000),
error_type VARCHAR2(50)
);
-- クレンジング付き移行
INSERT INTO events (event_id, event_date)
SELECT
id,
TO_DATE(date_str DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD')
FROM staging
WHERE VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 1;
-- エラーログ
INSERT INTO staging_errors
SELECT id || ',' || date_str, 'INVALID_DATE'
FROM staging
WHERE VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 0;
シナリオ4:月末バッチのスケジューリング
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'MONTHEND_CLOSING',
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN monthend_procedure; END;',
start_date => TRUNC(SYSDATE, 'MM'),
repeat_interval => 'FREQ=MONTHLY; BYMONTHDAY=-1',
enabled => TRUE
);
END;
/
BYMONTHDAY=-1 で月末を自動指定。
シナリオ5:Rails でのサブスクリプション課金
class Subscription < ApplicationRecord
def next_billing_date
# Ruby の Date#next_month は Oracle ADD_MONTHS と同じロジック
started_at.to_date.next_month
# 1/31 → 2/28
end
# または明示的
def calculate_next_billing
ActiveRecord::Base.connection.select_value(<<-SQL)
SELECT ADD_MONTHS(started_at, 1) FROM subscriptions WHERE id = #{id}
SQL
end
end
Solid Queue の詳細は Solid Queue 使い方の記事も参照してください。
シナリオ6:日付ピッカーの入力検証
-- CHECK 制約でDBレベル保証
ALTER TABLE events ADD CONSTRAINT chk_valid_date
CHECK (
event_date IS NULL OR
event_date BETWEEN DATE '1900-01-01' AND DATE '2100-12-31'
);
シナリオ7:レポートでの月末日集計
-- 各月の月末売上
SELECT
TRUNC(order_date, 'MM') AS month,
LAST_DAY(TRUNC(order_date, 'MM')) AS month_end,
SUM(amount) AS total
FROM orders
GROUP BY TRUNC(order_date, 'MM');
分析関数の詳細は Oracle 分析関数(OVER/PARTITION BY)の記事も参照してください。
シナリオ8:Docker Oracle でのテスト
docker exec -it oracle-xe sqlplus scott/tiger <<EOF
SELECT VALIDATE_CONVERSION('2025-02-29' AS DATE, 'YYYY-MM-DD') FROM DUAL;
-- 0
SELECT ADD_MONTHS(DATE '2025-01-31', 1) FROM DUAL;
-- 2025-02-28
EOF
Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。
シナリオ9:Data Pump 後の日付検証
-- インポート後の妥当性チェック
SELECT COUNT(*) FROM imported_data
WHERE VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 0;
Data Pump の詳細は Oracle Data Pump 使い方の記事も参照してください。
シナリオ10:Kamal デプロイ後の日付処理検証
docker exec -it db-container sqlplus / as sysdba <<EOF
-- サブスクリプション課金日を全て検証
SELECT COUNT(*) FROM subscriptions
WHERE VALIDATE_CONVERSION(TO_CHAR(next_billing_date, 'YYYY-MM-DD') AS DATE) = 0;
EOF
Kamal 2 デプロイの詳細は Kamal 2 デプロイの記事を参照してください。
トラブルシューティング
VALIDATE_CONVERSION が使えない
Oracle 11g 以前: ユーザー関数 is_valid_date を作成。
DEFAULT ON CONVERSION ERROR が使えない
12.2 未満: 例外処理で対応。
実行時にしか発覚しない
CHECK 制約で事前防止:
ALTER TABLE t ADD CONSTRAINT chk
CHECK (VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD') = 1);
タイムゾーン変換で日付シフト
-- TZ 変換前に DATE に変換
ADD_MONTHS の月末保持動作
2025-01-31 + 1 → 2025-02-28(月末保持)
2025-01-30 + 1 → 2025-02-28(月末に補正、意図と違う可能性)
「月末なら月末」ロジックが期待通りか要確認。
PostgreSQL からの移行
PG では date '2025-02-29' はエラー(Oracle と同様)。
よくある質問(FAQ)
Q1. INTERVAL と ADD_MONTHS どちらを使うべき?
ADD_MONTHS 推奨(月末補正あり)。
Q2. うるう年判定は?
VALIDATE_CONVERSION(year || '-02-29' AS DATE, 'YYYY-MM-DD')
-- 1 = うるう年, 0 = 平年
Q3. LAST_DAY の使い方
LAST_DAY(TO_DATE('2025-02-15', 'YYYY-MM-DD'))
-- 2025-02-28(その月の月末)
Q4. ORA-01839 と ORA-01847 の違い
- 01839: 月に対して日が無効(2/29 平年、4/31)
- 01847: 日が範囲外(32以上等)
Q5. Rails での対応
Date.valid_date? で事前チェック、Date.parse で例外捕捉。
Q6. 月末日の自動補正
ADD_MONTHS は月末を自動補正、INTERVAL はしない。
Q7. CHECK 制約で防げる?
可能(12c+ の VALIDATE_CONVERSION 使用)。
Q8. うるう秒との違い
Oracle はうるう秒非対応(POSIX 時間)。うるう年のみ気にする。
Q9. Autonomous DB での挙動
同じ。VALIDATE_CONVERSION / DEFAULT ON CONVERSION ERROR 完全対応。
Q10. 2000 年問題との関連
Oracle は内部的に4桁年、Y2K 影響なし。
Q11. 世紀の扱い
RR フォーマットで自動判定(50-99 → 1900, 00-49 → 2000)。
Q12. パフォーマンスへの影響
VALIDATE_CONVERSION は各行評価、大量データで注意。
参考リンク
Oracle 公式
- Oracle Database Error Messages: ORA-01839
- Oracle Database SQL Language Reference: ADD_MONTHS
- Oracle Database SQL Language Reference: LAST_DAY
- Oracle Database SQL Language Reference: VALIDATE_CONVERSION
まとめ
ORA-01839: date not valid for month specified の要点を再整理します。
エラーの本質
日(DD)の値がその月の最大日数を超えている
→ 例: 2025-02-29(平年)、2025-04-31(4月30日まで)
→ Oracle が厳格に検証して拒否
各月の最大日数
31日まで: 1,3,5,7,8,10,12
30日まで: 4,6,9,11
28/29日: 2(うるう年判定)
うるう年:
- 4 で割り切れる
- 100 で割り切れる年は除外
- 400 で割り切れる年は含む
日付エラー6兄弟
| エラー | 意味 |
|---|---|
| ORA-01843 | 月が無効 |
| ORA-01830 | フォーマット早期終了 |
| ORA-01858 | 数値以外の文字 |
| ORA-01839 | 日と月の整合性 |
| ORA-01847 | 日が範囲外 |
| ORA-01841 | 年が範囲外 |
10大原因
| # | 原因 | 対処 |
|---|---|---|
| ① | 平年の2/29 | LAST_DAY |
| ② | 4/31, 6/31 等 | LAST_DAY |
| ③ | INTERVAL ‘1’ MONTH | ADD_MONTHS へ |
| ④ | CSV 無効日付 | VALIDATE_CONVERSION |
| ⑤ | 年月日分割 | CHECK 制約 |
| ⑥ | サブスクリプション | ADD_MONTHS |
| ⑦ | 外部システム | 入力検証 |
| ⑧ | 月末バッチ | BYMONTHDAY=-1 |
| ⑨ | うるう秒 | 別問題 |
| ⑩ | TZ 変換 | DATE キャスト前 |
5つの解決策
-- ① LAST_DAY で月末補正
LAST_DAY(TO_DATE('2025-02-01', 'YYYY-MM-DD'))
-- ② ADD_MONTHS(INTERVAL より安全)
ADD_MONTHS(DATE '2025-01-31', 1) -- 2025-02-28
-- ③ VALIDATE_CONVERSION(12c+)
VALIDATE_CONVERSION(date_str AS DATE, 'YYYY-MM-DD')
-- ④ DEFAULT ON CONVERSION ERROR(12.2+)
TO_DATE(date_str DEFAULT NULL ON CONVERSION ERROR, 'YYYY-MM-DD')
-- ⑤ REGEXP + アプリロジック
REGEXP_LIKE(date_str, '^\d{4}-\d{2}-\d{2}$')
INTERVAL vs ADD_MONTHS
-- ❌ INTERVAL
DATE '2025-01-31' + INTERVAL '1' MONTH -- ORA-01839
-- ✅ ADD_MONTHS
ADD_MONTHS(DATE '2025-01-31', 1) -- 2025-02-28
うるう年判定
-- SQL で判定
VALIDATE_CONVERSION(year || '-02-29' AS DATE, 'YYYY-MM-DD')
-- Ruby
Date.valid_date?(year, 2, 29)
-- Python
calendar.isleap(year)
-- Java
Year.isLeap(year)
これらの知識は、Oracle でのサブスクリプション課金・レポート集計・CSV/ETL・バッチ処理・スケジューラ設計・Rails / Java / Python 開発・データ品質管理など、あらゆる場面で活用できます。本記事をブックマークしておけば、ORA-01839 に出会っても冷静に的確に対処できるようになります。
本記事は2026年6月時点の情報をもとに、Oracle Database 19c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。
-
前の記事
【完全ガイド】ORA-00955: name is already used by an existing object の原因と解決方法|23ai IF NOT EXISTS・CREATE OR REPLACE 徹底解説 2026.07.31
-
次の記事
【完全ガイド】ORA-04068: existing state of packages discarded の原因と解決方法|PRAGMA SERIALLY_REUSABLE・EBR・24時間運用 徹底解説 2026.08.03
コメントを書く