Postgres AT TIME ZONE 'UTC'는 생각과 다르게 동작한다

Footguns with Postgres "at time zone 'UTC'"

Postgres AT TIME ZONE 'UTC'는 생각과 다르게 동작한다

Postgres의 AT TIME ZONE 'UTC'는 timestamptz를 timestamp로 변환해 타입을 바꿔버린다. 이 때문에 timestamp와 timestamptz의 동등 비교는 항상 false가 되고, 월을 더할 때도 타임존에 따라 결과가 달라진다. UTC 기반 제품에서는 월별 증감을 계산할 때 AT TIME ZONE 'UTC'를 두 번 써야 올바른 결과를 얻을 수 있다.

timestamp와 timestamptz의 동등 비교는 항상 false가 된다.
  1. heurekamala

    글에서 "timestamp와 timestamptz를 비교하면 항상 false가 나온다"라고 했는데, 이는 틀렸습니다. Postgres는 백그라운드에서 timestamp를 timestamptz로 아주 조용히 변환합니다. 이때 세션의 time zone을 사용할지 여부는 TimeZone 설정에 따라 true가 될 수도 false가 될 수도 있습니다. 이건 "항상 false"보다 더 나쁩니다. 프로덕션에서 UTC로 설정하면 잘 동작하지만, 캘리포니아 개발자의 노트북에서는 동작하지 않습니다.

    방금 테스트해봤습니다:

    SET TIME ZONE 'America/Los_Angeles';

    SELECT '2026-03-01 00:00:00'::timestamp = '2026-03-01 00:00:00+00'::timestamptz; /* f */

    SET TIME ZONE 'UTC';

    SELECT '2026-03-01 00:00:00'::timestamp = '2026-03-01 00:00:00+00'::timestamptz; /* t */

    가능하다면 Postgres 16을 사용하세요. 글에 나온 AT TIME ZONE 'UTC'를 두 번 쓰는 방식은 거기서는 필요 없습니다. 대신 세 번째 인자로 time zone을 받는 date_add가 있습니다:

    date_add(b.month_start, interval '1 month', 'UTC')

    이렇게 하면 UTC 기준으로 월을 더하고, 결과는 timestamptz로 유지됩니다.

  2. ulrikrasmussen

    SQL 표준은 시간 처리가 정말 끔찍합니다. `timestamp` 타입은 사실 timestamp가 아닙니다. 고유한 시점을 인코딩하지 않고, 단지 날짜와 시간을 저장할 뿐이며 이는 timezone을 기준으로 해석되어야 합니다. 이름이 "datetime"이어야 합니다.

    Java Instant를 데이터베이스와 주고받는 것도 올바르게 하기 놀랍도록 어려운 작업이며, JDBC의 setTimestamp/getTimestamp 메서드를 사용하면 완전히 잘못 처리되기 때문에 더 나쁩니다. 이는 함정이 있는 나쁜 설계라서가 아니라, 구현이 그냥 틀렸기 때문입니다. 레거시 date/time API를 사용하기 때문에 달력 날짜가 충분히 과거인 instant를 다루면 데이터가 손상됩니다. 그 API는 과거 날짜에 대해 그레고리력을 사용하도록 전환하기 때문입니다.

    `timestamp with time zone`이라는 이름도 오해의 소지가 있습니다. 실제로 time zone을 저장하지 않고, java.time.Instant처럼 epoch 이후 초 수를 저장하기 때문입니다(해상도는 다르지만). "with time zone" 부분은 단지 값을 표기하는 텍스트 형식에서 날짜/시간 부분 뒤에 time zone을 포함해 timestamp를 고유하게 식별한다는 의미일 뿐이고, 값이 파싱된 후에는 time zone은 버려지고 저장되지 않습니다. 이는 예를 들어 Java의 `ZonedDateTime`과 다릅니다. ZonedDateTime은 실제로 offset을 저장하므로 (Instant, TimeZone) 쌍에 해당합니다.

  3. 1a527dd5

    나의 바이블: https://wiki.postgresql.org/wiki/Don't_Do_This

  4. Macha

    내 경험상 (Postgres 개발자의 사용 권장에도 불구하고) timestamptz는 UTC로의 변환을 감싼 래퍼에 불과하기 때문에 거의 쓸모없는 데이터 타입입니다.

    - 과거 이벤트를 저장하나요? 그냥 UTC로 저장하세요. timestamptz가 대신 변환해줄 수도 있지만, 실제로는 timestamp보다 더 난해한 인터페이스입니다.

    - 미래의 UTC 시간을 저장하나요? 좋습니다. 그냥 UTC를 쓰세요. 위와 같습니다.

    - 미래의 인간 시간을 저장하나요? 그러면 timestamptz는 적극적으로 해롭습니다. 왜냐하면 성급하게 UTC로 변환해버리기 때문에, 이벤트가 도래할 때쯤 업데이트된 tzdb를 제때 받더라도 쓰기 시점에 무슨 일이 있었는지 알 수 없어 datetime이 모호해집니다. 일반 timestamp + 문자열 timezone 컬럼을 쓰는 게 덜 망가진 방법입니다(정렬이 필요하면 비정규화된 _utc 컬럼도 쓸 수 있고, tzdb를 업데이트할 때 재생성하거나 약간의 off-by-one 오류를 감수해야 한다는 점을 이해하고 있어야 합니다. 하지만 적어도 timestamptz와 달리 입력 값이 무엇이었는지 알 때 이 작업을 할 수 있습니다).

  5. layer8

    > + INTERVAL '1 months'로 월을 더하는 것은 timezone에 의존적이다. […]

    월을 더하는 것은 어차피 잘 정의되지 않습니다. date를 사용하더라도 월의 일수가 28을 초과하는 경우에는 그렇습니다. 시스템이 이런 계산을 일반적으로 허용하는 것(애플리케이션 코드가 도메인별 비즈니스 규칙을 구현하는 것과 달리)은 실수라고 생각합니다.

이 날의 다른 글

2026-09-28