PostgreSQL timestamp vs timestamptz, 내부에 실제로 저장되는 값은?
PostgreSQL은 timestamp와 timestamptz를 모두 하나의 64비트 정수로 유지합니다. 즉, 1970-01-01 00:00:00 UTC를 기준으로 한 마이크로초 단위의 값입니다. 두 타입의 차이는 데이터를 사람이 읽을 수 있는 형식으로 포매팅할 때만 드러납니다.
왜 이 지점에서 자주 헷갈릴까요?
- 같은 날짜를 두 컬럼에 넣었는데, 쿼리 결과가 다르게 나옵니다
- 애플리케이션은
2025-07-29 10:00을 삽입했는데, 다른 팀에서는02:00으로 보입니다 - 프런트엔드가 렌더링한 ISO 문자열이 백엔드 로그와 일치하지 않습니다
복숭아 통조림 두 캔: 라벨이 없는 것과 있는 것
| 데이터 타입 | 정식 이름 | 저장되는 값 | SELECT 시 동작 |
|---|---|---|---|
timestamp | timestamp without time zone | 원본 마이크로초 카운트 | 그대로 반환됩니다. Postgres는 시간대를 추측하지 않습니다 |
timestamptz | timestamp with time zone | 동일한 마이크로초 카운트 | 텍스트를 보내기 직전에 세션의 TimeZone 설정을 적용합니다 |
비유
timestamp= 원산지 라벨이 없는 복숭아 통조림입니다. 과일이라는 사실은 알지만 어디서 만들어졌는지는 알 수 없습니다.timestamptz= “Made in UTC+8”이 당당하게 찍혀 있는 통조림입니다. 뚜껑을 여는 사람이 영양 성분표를 환산할지 말지 직접 결정할 수 있습니다.
내부 구조: 결국은 거대한 숫자 하나
2000-01-01 00:00:00 UTC → 0
2000-01-01 00:00:01 UTC → 1 000 000
- 단위: 마이크로초(100만 분의 1초)
- 범위: 기원전 4713년 ~ 서기 294276년 — 인디아나 존스도 인정할 만한 범위입니다
timestamp와timestamptz의 저장 구조는 완전히 동일합니다. 해석 방식만 다를 뿐입니다
15초 데모
-- 클라이언트는 상하이 시간 기준으로 동작
SET TimeZone = 'Asia/Shanghai';
CREATE TABLE demo (
created_ts timestamp,
created_tz timestamptz
);
INSERT INTO demo VALUES ('2025-07-29 10:00', '2025-07-29 10:00');
| 쿼리 | 결과 | 이유 |
|---|---|---|
SELECT created_ts FROM demo; | 2025-07-29 10:00:00 | 원본 값 그대로, 시간대 계산 없음 |
SELECT created_tz FROM demo; | 2025-07-29 10:00:00+08 | 출력 시점에 시간대 라벨이 붙음 |
SET TimeZone = 'UTC'; 이후 SELECT | 2025-07-29 02:00:00+00 | 같은 순간을 다른 렌즈로 본 것 |
타임스탬프 산술과 인터벌
PostgreSQL 타임스탬프에서 가장 실용적인 부분 중 하나가 인터벌 산술입니다. 두 타입 모두 마이크로초 카운트를 저장하므로, 인터벌을 직접 더하고 뺄 수 있습니다.
-- 3시간 30분 더하기
SELECT '2025-07-29 10:00'::timestamptz + INTERVAL '3 hours 30 minutes';
-- → 2025-07-29 13:30:00+08
-- 두 타임스탬프의 차이 구하기
SELECT '2025-07-30 09:00'::timestamptz - '2025-07-29 10:00'::timestamptz;
-- → 23:00:00 (인터벌 값)
-- 특정 필드만 뽑아내기
SELECT EXTRACT(EPOCH FROM '2025-07-29 10:00:00+08'::timestamptz);
-- → 1753768800 (초 단위 Unix 타임스탬프)
-- 하루 경계로 자르기 (일간 집계에 유용)
SELECT date_trunc('day', '2025-07-29 15:42:19+08'::timestamptz);
-- → 2025-07-29 00:00:00+08
EXTRACT(EPOCH FROM ...) 함수는 Unix epoch 초를 요구하는 외부 시스템에 타임스탬프를 넘겨야 할 때 특히 유용합니다. 반대로, epoch 값을 다시 타임스탬프로 변환할 수도 있습니다.
SELECT to_timestamp(1753768800);
-- → 2025-07-29 10:00:00+08 ('Asia/Shanghai' 세션 기준)
미묘하지만 중요한 지점이 하나 있습니다. timestamp(시간대 없음)로 하는 인터벌 산술은 DST(일광 절약 시간) 전환을 전혀 반영하지 않지만, timestamptz는 이를 존중합니다. 즉, DST 경계를 가로지르는 timestamptz 값에 INTERVAL '1 day'를 더하면 정확히 24시간 뒤가 아니라 같은 벽시계 시각이 올바르게 반환되는 셈입니다.
인덱싱과 성능 고려 사항
timestamp와 timestamptz는 모두 8바이트 정수로 저장되므로, 저장 공간이나 인덱스 성능에서 차이가 없습니다. 비교 연산이 결국 정수 비교이기 때문에 B-tree 인덱스도 두 타입에서 동일하게 동작합니다.
다만 실무에서 고려해야 할 몇 가지 지점이 있습니다.
- 범위 쿼리:
WHERE created_at > '2025-07-01'은 두 타입 모두에서 인덱스를 효율적으로 활용합니다.timestamptz의 경우 PostgreSQL이 리터럴을 UTC로 변환한 뒤 비교하므로 인덱스가 그대로 사용됩니다. - 파티션 키: 타임스탬프 컬럼을 기준으로 범위 파티셔닝을 할 때는 일반적으로
timestamptz가 더 안전합니다. 파티션 경계가 항상 UTC로 명확하기 때문입니다.timestamp를 쓰면'2025-07-01 00:00'같은 경계가 세션마다 다른 의미로 해석될 수 있습니다. - 함수 기반 인덱스: 시간 부분을 무시하고 날짜만으로 자주 조회한다면,
date_trunc('day', created_at)에 인덱스를 걸어 일간 집계 쿼리 속도를 높이는 방법을 고려하세요.
흔한 함정과 즉석 해결책
1. 사용자마다 시계가 다르게 보임
- 원인: 클라이언트마다 서로 다른
TimeZone설정으로timestamptz를 읽고 있습니다 - 해결: 모든 컬럼을
timestamp로 통일하고 하나의 시간대로 합의하거나, 연결 초기화 시점에SET TimeZone = 'UTC'를 강제하십시오
애플리케이션 코드에서 자주 쓰는 패턴은 커넥션 풀을 초기화할 때 시간대를 한 번만 세팅해 두는 방식입니다.
-- 연결 설정(예: pg 풀 구성)에서
SET timezone = 'UTC';
이렇게 하면 모든 세션이 동일한 UTC 표현을 보게 되고, 로컬 시간으로의 변환은 애플리케이션 계층이 전담하게 됩니다.
2. “벽시계 시각”을 저장해야 하는데 타입을 잘못 고른 경우
- 업무 캘린더(영업시간, 마감일)에는
timestamp를 사용하세요 - 국경을 넘나드는 워크플로(주문, 로그)는 UTC로
timestamptz에 저장하세요
판단 기준은 간단합니다. 질문이 “이 일이 어느 순간에 일어났는가?”라면 timestamptz를, “벽시계에 찍히는 시각은 무엇인가?”라면 timestamp를 사용하십시오.
3. 시간이 어긋나는 API
timestamptz는 항상 오프셋이 포함된 ISO-8601 문자열(Z또는+08:00)로 전송하세요- 로컬 시간 포매팅은 UI에 맡기세요
4. 서로 다른 타입 간 타임스탬프 비교
비교나 조인에서 timestamp와 timestamptz를 섞어 쓰는 것은 은근히 자주 발생하는 버그의 원인입니다.
-- 위험: 암묵적 캐스트에 세션 시간대가 적용됨
SELECT * FROM orders o
JOIN schedules s ON o.created_tz = s.start_ts;
-- PostgreSQL이 s.start_ts를 세션 시간대 기준으로 timestamptz에 캐스팅
-- 세션마다 다른 조인 결과가 나올 수 있음!
해결: 타입이 다른 값을 비교할 때는 항상 명시적으로 캐스팅하거나, 도메인별로 하나의 타입으로 표준화하십시오.
5. ORM 기본값의 함정
많은 ORM(Django, SQLAlchemy, ActiveRecord)이 기본값으로 시간대 없는 timestamp를 사용합니다. 마이그레이션 파일을 확인해 보세요. 여러 시간대의 사용자를 상대하는 애플리케이션이라면 기본값을 timestamptz로 덮어써야 합니다. Django에서는 설정에 USE_TZ = True를 넣으시고, SQLAlchemy에서는 DateTime(timezone=True)를 사용하십시오.
치트 시트: 어떤 타입을 쓸까?
로컬 캘린더만 쓰는 경우 → timestamp
그 외 글로벌한 모든 것 → timestamptz (UTC로 저장)
- 재무 보고서, 수업 시간표 →
timestamp - 감사 로그, 이커머스 주문 →
timestamptz
Go Tools로 몇 초 만에 검증하기
| 필요 | 도구 | 사용법 |
|---|---|---|
| SQL로 얻은 epoch 값 확인 | Unix 타임스탬프 변환기 | 1753768800을 붙여 넣고 변환을 누르세요 |
| 두 시간대를 한눈에 비교 | Unix 타임스탬프 변환기 | 10:00 Asia/Shanghai을 입력하세요 |
| 시간 필드가 포함된 JSON을 정돈 | JSON 포맷터 | 페이로드를 붙여 넣고 예쁘게 정렬한 뒤 살펴보세요 |
모든 도구는 브라우저 안에서만 동작합니다. 데이터가 사용자의 기기를 벗어나지 않습니다.
자주 묻는 질문
PostgreSQL의 timestamp와 timestamptz의 차이는 무엇인가요?
timestamp(시간대 없는 타임스탬프, timestamp without time zone)는 날짜·시간 값을 아무 시간대 문맥 없이 있는 그대로 저장합니다. timestamptz(시간대 포함 타임스탬프, timestamp with time zone)는 입력을 UTC로 변환해 저장하고, 조회 시점에 세션 시간대로 다시 변환합니다. 분산 시스템에서 시간대 관련 버그를 예방해 주므로 거의 모든 경우에 timestamptz를 사용하십시오.
PostgreSQL은 실제로 timestamptz에 시간대를 저장하나요?
아닙니다. 이름과 달리 PostgreSQL은 시간대 자체를 저장하지 않습니다. 입력을 UTC로 변환한 뒤 UTC 값(2000-01-01을 기준으로 한 마이크로초 카운트)만을 저장합니다. 조회 시점에는 세션의 timezone 설정에 지정된 시간대로 UTC에서 다시 변환하며, 원래의 시간대 정보는 폐기됩니다.
PostgreSQL 세션의 시간대는 어떻게 바꾸나요?
SET timezone = 'America/New_York';을 실행하면 세션 시간대를 변경할 수 있습니다. 이 설정은 timestamptz 값이 표시되고 해석되는 방식에 영향을 줍니다. 서버 전체 기본값은 postgresql.conf에서 timezone을 지정하면 됩니다. 모호함을 피하려면 약어(CST 등)가 아닌 IANA 시간대 이름(Asia/Shanghai 등)을 항상 사용하십시오.
이벤트 시각을 저장할 때 timestamp와 timestamptz 중 어느 쪽을 써야 하나요?
사용자 액션, API 호출, 감사 로그, 예약 이벤트 등 거의 모든 경우에 timestamptz를 사용하십시오. timestamp(시간대 없는 타임스탬프)는 특정 순간과 결부되지 않는 추상적인 시각에만 써야 합니다. 예를 들어 “매장은 09:00에 연다”라는 문장은 특정 UTC 순간이 아니라 로컬 시간대의 오전 9시를 뜻하므로 이 경우가 해당됩니다.
PostgreSQL은 timestamptz에서 일광 절약 시간(DST)을 어떻게 처리하나요?
PostgreSQL은 내부적으로 모든 값을 UTC로 저장하므로 timestamptz에서 DST를 올바르게 처리합니다. 값을 조회할 때는 세션 시간대의 현재 DST 규칙을 적용해 UTC에서 변환합니다. 덕분에 같은 UTC 순간이라도 DST 전환 전후로 서로 다른 로컬 시각이 정확하게 표시되는 셈입니다.
정밀도 처리, 시간대 모범 사례, JavaScript·Python·Go 코드 예제까지 Unix 타임스탬프를 한 번에 훑고 싶다면, Unix 타임스탬프 가이드: epoch 초·ms·시간대·DST를 참고하세요.
마무리
- Postgres의 두 시간 타입은 모두 마이크로초 카운터이며, 라벨의 유무가 전부입니다
- 타입을 잘못 고르면 뒤죽박죽인 타임스탬프와 어긋난 계산이 따라옵니다
- 알맞은 도구로 테스트·변환·검증하면 디버깅 시간을 몇 시간 단위로 아낄 수 있습니다