PostgreSQL「timestamp out of range」の原因と対処
- 作成日 2025.08.29
- 更新日 2025.10.06
- その他
タイトル直後の概要。サポート範囲外の日時が生成・解釈・変換のどこかで混入すると「timestamp out of range」が出る。単位ミス(秒/ミリ秒/マイクロ秒)、タイムゾーン変換、巨大なinterval加算、パースフォーマット不一致、移行時の特殊値などが主因。発生条件の整理、再現コード、修正手順、予防のためのチェック・関数・制約・運用のコツまでまとめた。
- 1. 発生条件(まずここを押さえる)
- 2. 型の有効範囲と誤解しやすいポイント
- 3. 最短再現:to_timestamp に桁を間違えて渡す
- 4. エポックの単位ミスを見分ける(秒/ミリ秒/マイクロ秒)
- 5. タイムゾーン変換が引き起こすオーバーフロー
- 6. 巨大な interval 加算・減算が原因のケース
- 7. 文字列パース(to_timestamp(text, fmt))の罠
- 8. 移行時の地雷:MySQL の ‘0000-00-00’ と不正日付
- 9. BCE と年 0 の扱い(別系エラーとの見分け)
- 10. infinity / -infinity の使いどころ
- 11. 実運用の防御策(制約・型選択・変換関数)
- 12. 安全変換:ユニット自動判定の関数(貼って使える)
- 13. デバッグのための診断スニペット
- 14. アプリ/ETL での具体的な対処例
- 15. クエリ全体を安全化するための設計ヒント
- 16. エラーを素早く再現→修正→回避するための最小手順
発生条件(まずここを押さえる)
・評価結果の型が timestamp(または timestamptz)になる式が、内部表現の上下限を超える
・典型箇所は INSERT/UPDATE の代入、SELECT 中の to_timestamp、AT TIME ZONE、キャスト(::timestamp / ::timestamptz)、interval 加減算、関数の戻り値
・text からのパース(to_timestamp(text, fmt) / 直接キャスト)で日付フィールドが異常値
・timestamptz ではローカル→UTC 変換のオフセットが加わるため、端の値で上下限をまたぎやすい
型の有効範囲と誤解しやすいポイント
timestamp / timestamptz は約「BC 4713 年」〜「AD 294276 年」まで(マイクロ秒精度)。date 型はさらに広い。timestamptz は UTC 正規化されるため、解釈時のタイムゾーン次第でわずかに範囲チェック結果が変わる。年 0 は存在しない(… 1 BC → AD 1)。秒未満は 6 桁まで保持される。
最短再現:to_timestamp に桁を間違えて渡す
-- JavaScript の "ミリ秒" をそのまま渡してしまう例
SELECT to_timestamp(1692873600000); -- NG: 多くの環境で 'timestamp out of range'
-- 修正:秒に直す(浮動小数で割る)
SELECT to_timestamp(1692873600000/1000.0); -- OK
-- "マイクロ秒" なら 1e6 で割る
SELECT to_timestamp(1692873600000000/1000000.0); -- OKエポックの単位ミスを見分ける(秒/ミリ秒/マイクロ秒)
10 桁前後=秒、13 桁=ミリ秒、16〜17 桁=マイクロ秒が目安。秒を期待する API に 13/16 桁を渡すと簡単に範囲外へ飛ぶ。整数割り算に注意(1000 で割ると整数のままになることがある)。
-- 安全変換(桁数で自動判定)
WITH v(n) AS (VALUES (1692873600::bigint), (1692873600000), (1692873600000000))
SELECT n,
CASE
WHEN length(n::text) >= 16 THEN to_timestamp(n/1000000.0)
WHEN length(n::text) >= 13 THEN to_timestamp(n/1000.0)
ELSE to_timestamp(n::double precision)
END AS ts
FROM v;タイムゾーン変換が引き起こすオーバーフロー
timestamptz は「そのローカル時刻を指定 TZ と解釈して UTC へ直す」。範囲ギリギリの値に大きなオフセット(例: +14, -12)を適用すると上下限を越えることがある。
-- ローカル時刻を JST として timestamptz に上げる
SET TIME ZONE 'Asia/Tokyo';
SELECT ('294276-12-31 23:59:59.999999'::timestamp)::timestamptz; -- 環境によっては NG
-- 対策:範囲内であることを先に確認してから変換
WITH c(ts) AS (VALUES ('294276-12-31 23:59:59.999999'::timestamp))
SELECT CASE
WHEN ts <= '294276-12-31 23:59:59.999999'::timestamp
THEN ts::timestamptz
ELSE NULL
END
FROM c;巨大な interval 加算・減算が原因のケース
-- 未来に大きく飛ばすとオーバーフロー
SELECT '2000-01-01'::timestamp + interval '500000000 days'; -- NG
-- 対策:演算前にクランプ(上限・下限で丸める)
WITH base(ts) AS (VALUES ('2000-01-01'::timestamp))
SELECT LEAST(
GREATEST(ts + interval '100 years',
'0001-01-01'::timestamp),
'2100-01-01'::timestamp
) AS safe_ts
FROM base;文字列パース(to_timestamp(text, fmt))の罠
フォーマットと実データの不一致、12/24 時制の混同、タイムゾーン略称の誤解釈で異常な値ができる。
-- "2025/08/22 23:59:59" を MM-DD と誤って読むと別日になるなど
SELECT to_timestamp('2025/08/22 23:59:59', 'YYYY/MM/DD HH24:MI:SS'); -- OK
-- TZ 付き文字列は timestamptz へ
SELECT to_timestamp('2025-08-22T23:59:59+09', 'YYYY-MM-DD"T"HH24:MI:SSOF')::timestamptz;
-- 事前バリデーション(パース可能かつ範囲内のみ変換)
WITH raw(s) AS (VALUES ('2025-08-22 23:59:59'), ('999999-01-01 00:00:00'))
SELECT s,
CASE
WHEN s ~ '^\d{4}-\d{2}-\d{2} ' -- 簡易パターン例
AND to_timestamp(s, 'YYYY-MM-DD HH24:MI:SS')
BETWEEN '0001-01-01'::timestamp AND '2100-01-01'::timestamp
THEN to_timestamp(s, 'YYYY-MM-DD HH24:MI:SS')
ELSE NULL
END AS ts
FROM raw;移行時の地雷:MySQL の ‘0000-00-00’ と不正日付
MySQL の「0 日付」や 30/31 のない月日を許容していたデータをそのまま投げると、PostgreSQL ではパース段階でエラー。NULL 置換か「ダミー最小値」を定義して正規化する。
-- 受け入れテーブル側で CHECK を設ける
CREATE TABLE t_migrate(
id bigint primary key,
ts timestamp
, CHECK (ts IS NULL OR ts >= '1900-01-01'::timestamp)
);
-- ETL 側で 0 日付を NULL に
INSERT INTO t_migrate(id, ts)
SELECT id,
CASE WHEN src_date IN ('0000-00-00','0000-00-00 00:00:00') THEN NULL
ELSE to_timestamp(src_epoch/1000.0) END
FROM ext;BCE と年 0 の扱い(別系エラーとの見分け)
PostgreSQL には年 0 がない。’0000-01-01′ は「date/time field value out of range」を返しやすい。一方、桁過大などで内部範囲を超えたときは「timestamp out of range」になる。ログでエラーメッセージを区別して原因を絞る。
infinity / -infinity の使いどころ
無期限などの意味で infinity を扱える(ただしアプリ層やドライバが非対応な場合がある)。
SELECT 'infinity'::timestamp, '-infinity'::timestamp; -- OK
-- 期限切れ未設定を infinity に統一
CREATE TABLE licenses(
user_id bigint primary key,
expires_at timestamp NOT NULL DEFAULT 'infinity'
);実運用の防御策(制約・型選択・変換関数)
・テーブルに CHECK 制約で上限・下限を設定(例:1900〜2100)
・アプリ入力は文字列で受けず、エポック(秒)で統一して受け取る
・秒/ミリ秒/マイクロ秒は関数で正規化してから保存
・タイムゾーンはサーバ全体のポリシー(例:UTC)を固定し、入出力で明示する
安全変換:ユニット自動判定の関数(貼って使える)
-- Bigint のエポック値を「秒/ミリ秒/マイクロ秒」自動判定して timestamp を返す
CREATE OR REPLACE FUNCTION safe_to_timestamp(v bigint)
RETURNS timestamp LANGUAGE sql IMMUTABLE AS $$
SELECT CASE
WHEN v IS NULL THEN NULL
WHEN length(v::text) >= 16 THEN to_timestamp(v/1000000.0)
WHEN length(v::text) >= 13 THEN to_timestamp(v/1000.0)
ELSE to_timestamp(v::double precision)
END
$$;
-- timestamptz 版(タイムゾーンポリシーに合わせて使用)
CREATE OR REPLACE FUNCTION safe_to_timestamptz(v bigint)
RETURNS timestamptz LANGUAGE sql IMMUTABLE AS $$
SELECT (safe_to_timestamp(v))::timestamptz
$$;
-- 使用例
SELECT safe_to_timestamp(1692873600000), safe_to_timestamptz(1692873600);デバッグのための診断スニペット
-- 1) 実行中のタイムゾーンを確認
SHOW TIME ZONE;
-- 2) 文字列→タイムスタンプがどの型へ落ちるか確認
SELECT pg_typeof('2025-08-22 12:34:56'::timestamp), pg_typeof(('2025-08-22 12:34:56')::timestamptz);
-- 3) 端の値に触れていないかクイックチェック
SELECT
MIN(my_ts) < '0001-01-01'::timestamp AS has_too_small,
MAX(my_ts) > '2100-01-01'::timestamp AS has_too_large
FROM my_table;
-- 4) to_timestamp に渡す前に桁数と想定範囲を確認
WITH raw(v) AS (VALUES (999999999999999::bigint))
SELECT v,
length(v::text) AS digits,
CASE WHEN v BETWEEN 0 AND 32503680000 THEN 'within 3000-01-01'
ELSE 'suspicious' END AS range_hint
FROM raw;アプリ/ETL での具体的な対処例
-- 例: ログ取り込み(秒/ミリ秒混在)を一括正規化し、範囲外は捨てる
INSERT INTO events(id, occurred_at)
SELECT id,
CASE
WHEN val IS NULL THEN NULL
WHEN length(val::text) BETWEEN 9 AND 11 THEN to_timestamp(val::double precision)
WHEN length(val::text) = 13 THEN to_timestamp(val/1000.0)
WHEN length(val::text) BETWEEN 16 AND 17 THEN to_timestamp(val/1000000.0)
ELSE NULL
END AS occurred_at
FROM staging
WHERE
-- 変換結果が安全範囲に収まるものだけ
CASE
WHEN length(val::text) BETWEEN 9 AND 11 THEN to_timestamp(val::double precision)
WHEN length(val::text) = 13 THEN to_timestamp(val/1000.0)
WHEN length(val::text) BETWEEN 16 AND 17 THEN to_timestamp(val/1000000.0)
END
BETWEEN '1970-01-01'::timestamp AND '2100-01-01'::timestamp;クエリ全体を安全化するための設計ヒント
・保存型は一本化(アプリ内部は timestamptz で UTC、入出力でのみローカルに)
・入力は必ず単位付きのフィールド名に(例: event_time_ms, created_at_s)
・API/キュー/ETL の境界で正規化を完了させる
・「安全上限/下限」はビジネス要件に合わせて CHECK で明文化
・Grafana / BI に渡すときは date_trunc で分解能を落とし事故を抑止
エラーを素早く再現→修正→回避するための最小手順
- ログのメッセージ文言を確認(timestamp out of range / date/time field value out of range)
- 発生式を特定(to_timestamp / キャスト / AT TIME ZONE / interval)
- 値の桁数と型を確認(pg_typeof、length、::bigint)
- to_timestamp の単位を矯正、または CASE で安全化
- タイムゾーンを固定して再実行、または timestamptz/ timestamp のどちらに寄せるか決定
- 最後に CHECK 制約と安全変換関数を入れて恒久対策
-
前の記事
kotlin Listの要素を次の値とPairに変換する 2025.08.28
-
次の記事
PostgreSQL「ERROR: division by zero」の原因と対処 2025.09.01
コメントを書く