【完全ガイド】ORA-01839: date not valid for month specified の原因と解決方法|うるう年・INTERVAL・ADD_MONTHS 徹底解説

【完全ガイド】ORA-01839: date not valid for month specified の原因と解決方法|うるう年・INTERVAL・ADD_MONTHS 徹底解説

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/30 4/31 等の異常値
  • アプリの日付ピッカーからの無効入力
  • 年月日を分割保存した後の再結合
  • UI では 2/29 に見えるが実は 3/1(アプリ側で丸め)

さらに、多くの日本語記事が「その日は存在しない」で終わりますが、実務では:

  • INTERVAL '1' MONTH vs ADD_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 を根本から解決できるようになります。


目次

結論:その月にその日は存在しない

時間がない方向けに、最速の対処を先に示します。

エラーメッセージの読み方

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’ MONTHADD_MONTHS
1/31 + 1ヶ月ORA-018392/28(or 2/29)
1/30 + 1ヶ月ORA-018392/28
1/28 + 1ヶ月2/282/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 公式


まとめ

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/29LAST_DAY
4/31, 6/31 等LAST_DAY
INTERVAL ‘1’ MONTHADD_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)もあわせてご確認ください。