본문 바로가기
빌드노트 검색

빌드노트

PostgreSQL now()가 트랜잭션 안에서 계속 같은 값을 준다

오래 열린 트랜잭션에서 now()가 반복되는 이유를 짧은 SQL로 확인한다. 트랜잭션 시작, SQL 시작, 호출 순간 중 기록할 의미에 따라 now(), statement_timestamp(), clock_timestamp()를 고르는 기준을 설명한다.

PostgreSQL에서 트랜잭션을 열어 둔 채 여러 번 기록했는데 now() 값이 모두 같을 수 있다. now()는 호출한 순간이 아니라 현재 트랜잭션의 시작 시각을 반환한다. 같은 값이 반복됐다는 사실만으로 서버 시계가 멈췄다고 판단할 수는 없다.

저장하려는 시간이 무엇인지에 따라 함수를 고른다.

  • 한 트랜잭션에서 변경한 행에 같은 기준 시각을 붙이려면 now()를 쓴다. transaction_timestamp()와 같은 뜻이며 CURRENT_TIMESTAMP로 바꿔도 기준은 그대로다.
  • 각 SQL의 시작을 기록하되 그 SQL이 처리하는 행에는 같은 값을 붙이려면 statement_timestamp()를 쓴다.
  • 처리 도중 함수를 호출한 순간을 기록하려면 clock_timestamp()를 쓴다. 한 SQL 안에서도 값이 달라질 수 있다.

이 구분은 PostgreSQL 18의 현재 시각 함수 문서 (새 탭에서 열림)에 명시돼 있다. 시간대를 바꾸는 문제보다 먼저, 기록할 시각의 의미를 정해야 한다.

잠깐 기다려도 now()만 그대로였다

초기화를 마친 별도 PostgreSQL 18.6 컨테이너에서 한 psql 연결로 확인했다. 실제 서비스의 장애 기록이 아니라, 두 번의 짧은 대기를 넣은 재현이다. 직접 확인하려면 접속 가능한 PostgreSQL 실습 서버와 psql을 준비하고, 새 연결에서 아래 SQL을 차례로 입력한다. 전체를 하나의 psql -c 문자열로 보내지 않는다.

SET TIME ZONE 'UTC';
BEGIN;
SELECT 'A' AS sample, now() AS tx, statement_timestamp() AS stmt, clock_timestamp() AS wall;
SELECT pg_sleep(0.2);
SELECT 'B' AS sample, now() AS tx, statement_timestamp() AS stmt, clock_timestamp() AS wall;
SELECT pg_sleep(0.2);
SELECT 'C' AS sample, now() AS tx, statement_timestamp() AS stmt, clock_timestamp() AS wall;
COMMIT;
SELECT 'D' AS sample, now() AS tx, statement_timestamp() AS stmt, clock_timestamp() AS wall;

pg_sleep(0.2)는 현재 연결을 0.2초 이상 기다리게 한다. 정확히 0.2초에 돌아온다는 뜻은 아니다. 대기 함수의 조건 (새 탭에서 열림)

아래는 실제 출력에서 날짜와 시간대 표기를 덜어 낸 것이다. 네 행 모두 2026년 10월 21일 UTC였으며, 소수초는 원래 출력대로 남겼다.

sample | tx              | stmt            | wall
A      | 05:20:38.626701 | 05:20:38.626755 | 05:20:38.626869
B      | 05:20:38.626701 | 05:20:38.828492 | 05:20:38.828552
C      | 05:20:38.626701 | 05:20:39.030072 | 05:20:39.030127
D      | 05:20:39.030295 | 05:20:39.030295 | 05:20:39.030321

A부터 C까지 tx는 같았고, stmt와 wall은 증가했다. 기다린 뒤에도 now()가 바뀌지 않는 조건을 재현한 것이다. COMMIT 뒤 별도 요청으로 실행한 D에서는 새 트랜잭션이 시작돼 tx도 달라졌다. D의 값은 앞 트랜잭션의 커밋 시각을 뜻하지 않는다.

자기 결과에서 A부터 C의 now()가 달라졌다면 같은 연결과 트랜잭션이 유지됐는지부터 본다. 반대로 세 열이 전부 같아 보이면 도구가 소수초를 숨기는지도 확인한다. 초 단위 표시만으로는 이 짧은 대기를 구별하지 못할 수 있다.

한 SQL이 여러 행을 만들 때의 차이

같은 실습 연결에서 다음 SQL도 실행했다. 행마다 짧은 대기가 있어, 한 SQL의 실행 도중 시간이 흐르는 모습을 볼 수 있다. 마지막 pg_sleep 열은 시각 값이 아니라 대기용이다.

SELECT n, now() AS tx, statement_timestamp() AS stmt,
       clock_timestamp() AS wall, pg_sleep(0.1)
FROM generate_series(1, 3) AS g(n);

실제 세 행의 tx와 stmt는 각각 한 값으로 같았다. wall은 05:20:39.030654, 05:20:39.13182, 05:20:39.232963로 달랐다. 문장마다 하나의 시각이 필요할 때 clock_timestamp()를 쓰면, 같은 문장이 처리한 행에도 서로 다른 시각이 붙을 수 있다. 반대로 statement_timestamp()는 오래 실행되는 문장 안의 각 처리 순간을 보여 주지 않는다.

여기서 문장 시작은 더 정확히는 서버가 클라이언트의 최근 명령 메시지를 받은 시점이다. 이번 추가 비교에서도 여러 SQL을 한 요청으로 묶자, 대기 전후의 statement_timestamp()가 같았다. 세미콜론으로 나눴다는 사실만으로 요청까지 나뉜 것은 아니다. psql의 입력 모드에서는 명령을 개별 전송하지만, 하나의 -c 문자열은 통째로 전송한다. 두 방식의 차이는 psql의 명령 전송 설명 (새 탭에서 열림)에 있다.

따라서 오래 열린 트랜잭션에서 각 실행의 시작을 남기려면 statement_timestamp()를, 실행 도중의 호출 시각을 남기려면 clock_timestamp()를 선택한다. 같은 업무 변경 묶음에 일관된 시간을 붙이려던 목적이라면 now()의 반복은 그 목적에 맞는 동작이다.

이번 실험은 한 연결, 고정된 UTC 표시, 짧은 대기에서의 동일·증가 관계를 확인했다. 애플리케이션의 연결 풀·드라이버가 요청을 묶는 방식과 호스트 시계 보정 중의 동작은 확인하지 않았다. 관찰된 clock_timestamp()의 증가를 매 호출의 유일성이나 항상 증가하는 순번의 보장으로 쓰지는 않는다.

← 전체 글 보기