Oracle から PostgreSQL へ(またはその逆へ)移行するとき、SYSDATE を now() に置き換えて終わり、、としていないでしょうか。PostgreSQL の now() はトランザクション開始時刻を返すので、Oracle の「SQL文の時刻」とは意味が違います。しかも、どちらの向きに移すかで、壊れ方が違います。
※ PostgreSQL 側は PostgreSQL 15 で動かして確かめた結果です。Oracle 側は手元に環境がないので、未検証です(マニュアルなどの知識をもとに書いています)。
基本の違い
| 返る時刻 | Oracle | PostgreSQL |
|---|---|---|
| トランザクション開始時刻 | (ない) | now() CURRENT_TIMESTAMP LOCALTIMESTAMP など |
| SQL文の時刻 | SYSDATE SYSTIMESTAMP CURRENT_TIMESTAMP | statement_timestamp() |
| 呼んだ瞬間の時刻 | (ない) | clock_timestamp() |
同じ CURRENT_TIMESTAMP でも、Oracle では「SQL文の時刻」、PostgreSQL では「トランザクション開始時刻」です(→ PostgreSQL now() と CURRENT_TIMESTAMP の違い)。
短いトランザクションなら問題ない
オンライン画面の更新のような短いトランザクションなら、トランザクション開始と SQL文の時刻の差は数ミリ秒です。実際には困りません。問題になるのは、バッチ・ストアドプロシージャ・ループのような長いトランザクションです。
Oracle → PostgreSQL:派手に壊れる
| Oracle で動いていたもの | PostgreSQL で now() に置き換えると |
|---|---|
| ストアドの中で SYSTIMESTAMP の差を取って、処理時間をログに出す | 処理時間がいつも0になる |
| 「開始から5分たつまで待つ」のようなループ | ループが終わらない(時刻が進まないので) |
| 1つのトランザクションで大量に INSERT し、登録日時で順番を判断する | 全部の行が同じ時刻になり、順番が分からない |
| 登録日時を主キーや一意キーの一部にしている | 同じトランザクションの中で一意キー違反になる |
| 更新日時で差分を抜き出すバッチ | トランザクション開始時刻が入るので、コミット遅延による抽出漏れが起きやすくなる |
ループが終わらない(確認済み)
「1秒たつまで、0.1秒ずつ待つ」ループです。そのままだと本当に終わらないので、30回で打ち切るようにしています。
DO $$
DECLARE t0 timestamptz; i int;
BEGIN
t0 := now(); i := 0;
WHILE now() < t0 + interval '1 second' AND i < 30 LOOP
PERFORM pg_sleep(0.1); i := i + 1;
END LOOP;
RAISE NOTICE 'now() : % 回まわった', i;
t0 := clock_timestamp(); i := 0;
WHILE clock_timestamp() < t0 + interval '1 second' AND i < 30 LOOP
PERFORM pg_sleep(0.1); i := i + 1;
END LOOP;
RAISE NOTICE 'clock_timestamp(): % 回まわった', i;
END $$;
-- NOTICE: now() : 30 回まわった ← 1秒たっても抜けない(打ち切りで止まった)
-- NOTICE: clock_timestamp(): 10 回まわった ← 1秒で抜けた
一意キー違反(確認済み)
CREATE TABLE op_log (created_at timestamptz PRIMARY KEY, msg text); BEGIN; INSERT INTO op_log VALUES (now(), '処理A'); INSERT INTO op_log VALUES (now(), '処理B'); -- ERROR: duplicate key value violates unique constraint "op_log_pkey" ROLLBACK; -- statement_timestamp() なら、SQL文ごとに値が変わるので通る BEGIN; INSERT INTO op_log VALUES (statement_timestamp(), '処理A'); INSERT INTO op_log VALUES (statement_timestamp(), '処理B'); COMMIT;
どちらも、テストで短いデータしか流していないと気づかず、本番の大きなバッチで初めて表に出ます。ただ、エラーや処理が終わらないという形で表に出るので、気づくことはできます。
PostgreSQL → Oracle:静かに壊れる
こちらのほうが怖いです。PostgreSQL のマニュアルでは、now() がトランザクションの中で変わらないのは、同じトランザクションの中の更新に同じ時刻が付くようにするため、という趣旨で説明されています。つまり、PostgreSQL の開発者はそれを当てにしてコードを書くことがあります。Oracle には「トランザクション開始時刻」を返す関数がないので、移した途端にその前提が崩れます。しかもエラーにならず、データが少しずつ合わなくなります。
| PostgreSQL で当てにしていたこと | Oracle に移すと(未検証) |
|---|---|
| ヘッダーと明細を同じトランザクションで INSERT し、登録日時が同じもの同士を結びつける | 時刻が少しずれて、結びつかない行が出る |
| INSERT した直後に、WHERE created_at = now() で「いま入れた行」を探す | 時刻が進んでいるので、見つからない |
| 1つのトランザクションで更新した行を、更新日時で「同じ処理の分」として扱う | 行ごとに時刻がばらばらになり、まとめられない |
| 日付の境目をまたぐバッチで、CURRENT_DATE を「今日」として使う | 夜中の0時を過ぎたところで「今日」が変わり、前半と後半で違う日付が入る |
PostgreSQL では、次のように「0.5秒待ってから明細を入れても、ヘッダーと時刻が一致する」ことを確かめました。Oracle の SYSTIMESTAMP では、ここが一致しなくなるはずです。
BEGIN; INSERT INTO header VALUES (1, now()); SELECT pg_sleep(0.5); INSERT INTO detail VALUES (1, 1, now()), (1, 2, now()); SELECT count(*) FROM header h JOIN detail d ON d.created_at = h.created_at; -- 2 ← PostgreSQL では、0.5秒たっていても一致する SELECT count(*) FROM header WHERE created_at = now(); -- 1 ← いま入れた行が見つかる COMMIT;
日付の境目の例は特に危険です。PostgreSQL では、23:50 に始まったバッチは最後まで「今日 = 開始した日」です。Oracle では、0:00 を過ぎた瞬間に TRUNC(SYSDATE) が翌日になります。しかもこれは、バッチがたまたま0時をまたいだ日にしか起きません。
※ Oracle でも、1つの SQL文の中では SYSDATE は変わりません。INSERT … SELECT を1文で書いている場合は問題ありません。ずれるのは、複数の SQL文にまたがるときです。
時刻関数のほかの違い
| 違い | Oracle | PostgreSQL | 起きること |
|---|---|---|---|
| タイムゾーン | SYSDATE は DBサーバーの OS のタイムゾーン | セッションの timezone 設定 | RDS は初期設定が UTC。Oracle(OS が JST)から RDS に移すと9時間ずれる |
| DATE 型 | 時・分・秒まで持つ | 日付だけ | SYSDATE を CURRENT_DATE に置き換えると、時刻が消える |
| 秒までに丸める | SYSDATE は秒で切り捨て | CURRENT_TIMESTAMP(0) は四捨五入 | 最大0.5秒先の「未来の時刻」になる |
| 日付の計算 | SYSDATE + 1 で1日後 | + interval ‘1 day’ と書く | そのままではエラーになるので、すぐ気づける |
置き換え方の目安(Oracle → PostgreSQL)
| Oracle | PostgreSQL(動きをそろえる場合) |
|---|---|
| SYSTIMESTAMP CURRENT_TIMESTAMP | statement_timestamp() |
| SYSDATE | date_trunc(‘second’, statement_timestamp())::timestamp |
| ループの中や、処理時間の計測で使っている箇所 | clock_timestamp() |
SELECT statement_timestamp(),
date_trunc('second', statement_timestamp())::timestamp AS sysdate_like;
-- 2026-10-12 13:05:29.021332+09 | 2026-10-12 13:05:29
- SYSDATE の置き換えに
CURRENT_TIMESTAMP(0)を使うと、四捨五入になります。切り捨てたいのでdate_truncを使っています。 - DB の
timezone設定を、Oracle のサーバーと同じ(Asia/Tokyoなど)にそろえることも必要です。 - 移行ツールにも、
SYSDATEをトランザクション開始時刻を返す関数に置き換えるものがあるようです。どう置き換えるかはツールの版や設定によると思うので、使うツールの変換ルールを確認してください(未検証)。
どちらの向きにも効く対策:処理の基準日時を1回だけ取る
バッチの開始時に、基準日時を1回だけ取り(DB からでもアプリからでもよい)、以降の SQL にはその値をバインド変数で渡します。SQL の中に now() や SYSDATE を直接書きません。
- PostgreSQL と Oracle の違いに左右されない
- 日付の境目をまたいでも、「今日」が変わらない
- テストのときに基準日時を固定できる(「月末の日付で動かしてみる」が簡単にできる)
業務システムでよく使われる「業務日付」や「処理基準日時」という考え方は、まさにこの問題を避ける仕組みでもあります。
移行のときのチェックリスト
- 洗い出す:ストアド、トリガー、ビュー、列の DEFAULT、アプリの SQL から、SYSDATE・SYSTIMESTAMP・CURRENT_TIMESTAMP・CURRENT_DATE・now() を検索する
- 仕分ける:見つけた箇所が、長いトランザクション(バッチ、ストアド、ループ)の中で使われているかを確かめる
- 移行ツールの変換ルールを確かめる:何に置き換えているか
- タイムゾーンをそろえる:Oracle のサーバーの OS と、PostgreSQL の timezone 設定
- 長いトランザクションでテストする:時刻の値が期待どおりに進むか(または進まないか)。日付の境目をまたぐケースも
なお、アプリ(Java など)で時刻を取ってバインド変数で渡している箇所は、この影響を受けません。DB の時計を使っている箇所だけが対象です。
