きっかけ

あるバッチ処理で、本来なら反映されているはずのデータが一部だけ反映されないという事象がありました。ロジックそのものにはおかしなところが見当たらず、原因を追っていくと、最終的に行き着いたのはコードの不具合ではなく「NOW()が返す時刻の意味」に対する思い込みでした。似たような処理を別の場所に流用する際に、この関数の仕様を意識していなかったことが遠因になっていたようです。今回はその原因調査で得た知見をもとに、NOW()の挙動そのものを整理しておきます。

TL;DR

PostgreSQLのNOW()は、呼び出された瞬間の時刻ではなく、トランザクション開始時刻を返します。この仕様を知らずに「前回処理日時をNOW()で更新する」という実装をすると、トランザクションの組み方次第で記録される値の意味が変わってしまい、思わぬ不具合につながることがあります。この記事では、NOW()の仕様そのものと、その挙動が引き起こしうる問題のパターンを整理します。

NOW()はトランザクション開始時刻を返す

PostgreSQLの公式ドキュメントでは、NOW()はtransaction start timeを返す関数として定義されています。つまり、トランザクションの中でどれだけ時間が経過しても、そのトランザクション内でNOW()を何度呼び出しても、返る値は同じです。

以下のSQLで確認できます。

BEGIN;
SELECT NOW(); -- 例: 2026-07-29 10:00:00.000000
SELECT pg_sleep(3);
SELECT NOW(); -- 3秒経過しても同じ値が返る
COMMIT;

これはPostgreSQL独自の仕様というより、SQL標準に沿った動作です。SQL標準ではCURRENT_TIMESTAMPも同様にトランザクション内で一定であることが求められています。

呼び出すたびに現在時刻を取得したい場合は、NOW()やCURRENT_TIMESTAMPではなくclock_timestamp()を使う必要があります。clock_timestamp()は呼び出された瞬間の実時刻を返すため、トランザクションの途中で値が変化します。

BEGIN;
SELECT clock_timestamp(); -- 呼び出しごとに異なる値
SELECT pg_sleep(3);
SELECT clock_timestamp(); -- 3秒後の時刻が返る
COMMIT;

statement_timestamp()という選択肢もあり、こちらは「そのSQL文が開始された時刻」を返します。トランザクション内の各文ごとに値が変わる点でNOW()と異なり、文の実行中は値が変化しない点でclock_timestamp()と異なります。

関数 基準になる時刻 トランザクション内での挙動
NOW() / CURRENT_TIMESTAMP トランザクション開始時刻 常に同じ値
statement_timestamp() 文の実行開始時刻 文ごとに変わる
clock_timestamp() 呼び出された瞬間の実時刻 呼び出すたびに変わる

なぜこの仕様が問題になりうるのか

NOW()がトランザクション開始時刻を返すという仕様自体は、トランザクションの一貫性を保つうえで理にかなっています。同一トランザクション内の複数のテーブル・複数の行に同じタイムスタンプを刻みたい場面では、むしろ望ましい挙動です。

問題が起きやすいのは、この値を「処理の基準時刻」として後から利用するケースです。典型的には「前回この処理が実行された日時」を記録し、次回の処理で「その日時以降に発生した更新だけを対象にする」といった、差分抽出のための基準値として使う場合です。

このとき、トランザクションの粒度によって記録される値の意味が変わってしまいます。

処理全体を一つのトランザクションで実行する構造であれば、NOW()は「その処理を開始する直前の時刻」を指します。処理の内容が外部APIへの問い合わせであっても、DBへの書き込みであっても、記録される値は「処理を始める前の時点」なので、その後に発生した更新は次回の基準値以降として扱われ、取りこぼされません。

一方、対象データを1件ずつ個別のトランザクションで処理するような構造では、最後の1件を処理し終えるまで記録が確定しません。最後に走るトランザクションでNOW()を評価することになるため、記録される値は実質的に「処理がほぼ終わった時点の時刻」に近づきます。処理を開始してから完了するまでの間に発生した更新は、次回の基準値より前の出来事として扱われてしまい、次回の抽出対象から漏れる可能性があります。

つまり、同じ「NOW()で前回処理日時を更新する」という一行のコードでも、それを囲むトランザクションの粒度によって「処理前の時刻」を意味するのか「処理後に近い時刻」を意味するのかが変わってしまいます。これがそのまま基準値として使われると、取得漏れという形でしか表面化しないため、気づきにくいという厄介さがあります。

設計上の対処の方向性

この種の問題への対処は、大きく分けて次の2つの方向があります。

一つは、記録するタイミングを明示的にコントロールする方法です。処理を開始する直前にアプリケーション側で時刻を取得しておき、処理がすべて正常に終わった後で、その「開始前に取得しておいた時刻」を書き込みます。書き込む値はNOW()ではなく、あらかじめ変数として保持しておいた時刻になるため、トランザクションの粒度に関係なく「処理開始前の時刻」という意味を安定して持たせられます。

もう一つは、そもそもトランザクションを分割しない設計にする方法です。1件ずつ処理する必要がある場合でも、基準値の更新自体は全件の処理が終わった後に一度だけ、単一のトランザクションで行うようにすれば、NOW()を使ってもその瞬間は「全体の処理が終わった直後」という一貫した意味になります。ただしこの場合は「前回処理日時」が実質的に処理完了時刻になるため、その間に発生した更新を取りこぼさない設計になっているかは別途確認が必要です。

どちらの方法を選ぶにしても、重要なのは「この基準値はいつの時点を指すべきか」を先に決めてから実装することです。NOW()という関数を使うこと自体が問題なのではなく、その関数が返す値の意味を意識せずに「前回の日時」として扱ってしまうことが問題の本質です。

まとめ

  • NOW()はトランザクション開始時刻を返す。呼び出すたびに変わる値が欲しい場合はclock_timestamp()、文単位の時刻が欲しい場合はstatement_timestamp()を使う
  • 「前回処理日時」のように後から基準値として使う値をNOW()で記録する場合、トランザクションの粒度によって記録される値の意味が変わる
  • 単一トランザクションでの記録は「処理開始前の時刻」に、行単位トランザクションでの記録は「処理完了に近い時刻」になりやすい
  • 対処としては、基準時刻を明示的に変数として保持してから書き込むか、基準値の更新自体を全体処理の完了後に一度だけ行うという設計が考えられる