Quick Reference
SELECT *
FROM events
WHERE created_at >= NOW() - INTERVAL '7 days';생일·영업일처럼 달력만 필요하면 date, 실제 발생 순간이면 timestamptz, 기간 자체는 interval을 씁니다. 특정 local day의 TIMESTAMPTZ 행은 보고 기준 zone에서 시작·끝 instant를 계산해 반열린 범위로 찾습니다. plain timestamp index를 살리려면 열을 date로 변환해 비교하기보다 raw range를 기본으로 하고, expression index는 필요한 경우에만 같은 expression으로 설계합니다.
문법
timestamptz가 timestamp보다 안전한 이유
PostgreSQL의 timestamp는 타임존 정보를 저장하지 않습니다. 같은 값이라도 서버 로케일이나 클라이언트 세션 타임존에 따라 다르게 해석될 수 있습니다. timestamptz는 내부적으로 UTC로 저장하고 조회 시 세션 타임존으로 변환하므로, 여러 지역에서 접속하거나 서버 타임존 설정이 달라질 수 있는 환경에서 일관성을 보장합니다. 새 테이블을 설계한다면 시각 컬럼은 timestamptz를 기본으로 선택하는 것이 실무 관용입니다.
-- 세션 타임존을 바꿔도 UTC 기준으로 저장된 값은 올바르게 변환된다
SET timezone = 'Asia/Seoul';
SELECT NOW(); -- 2026-04-02 15:00:00+09
SET timezone = 'UTC';
SELECT NOW(); -- 2026-04-02 06:00:00+00date, timestamptz, interval은 각각 날짜, 시각, 기간을 맡는다
입문 단계에서는 "날짜만 저장하면 date", "실제 시점을 저장하면 timestamptz", "얼마나 차이나는지 표현하면 interval"로 구분하면 됩니다. 이 역할을 섞기 시작하면 필터 경계와 타임존 처리에서 오류가 자주 납니다.
SELECT
CURRENT_DATE AS today,
NOW() AS current_instant,
INTERVAL '7 days' AS seven_days;interval로 기간 산술을 표현하면 경계 계산 실수를 줄인다
INTERVAL '7 days'는 "7일"이라는 기간 자체를 표현하는 타입입니다. NOW() - INTERVAL '7 days'는 현재 시각에서 7일 전 시각을 동적으로 계산합니다. INTERVAL '1 month'는 고정된 일 수가 아니라 달력상 한 달을 더하고 빼므로, 기간이 정확히 몇 시간이어야 하는지와 달력 기준인지도 구분해야 합니다.
-- 지난 1개월 주문 조회
SELECT * FROM orders
WHERE created_at >= NOW() - INTERVAL '1 month';
-- 3일 후 만료되는 토큰 계산
SELECT token, expires_at
FROM auth_tokens
WHERE expires_at BETWEEN NOW() AND NOW() + INTERVAL '3 days';date와 timestamptz는 "달력 기준"인지 "실제 시점 기준"인지로 고르면 된다
생일, 정산일, 영업일처럼 달력 날짜만 중요하면 date가 맞고, 로그인 시각, 결제 시각, 이벤트 발생 시각처럼 실제 순간이 중요하면 timestamptz가 맞습니다. 두 타입을 섞어 쓰면 하루 경계나 사용자 타임존 기준 해석이 쉽게 어긋납니다.
-- 달력 기준
billing_date DATE
-- 실제 시점 기준
created_at TIMESTAMPTZ날짜 추출 조건은 raw range와 expression index를 구분한다
WHERE created_at::date = DATE '2026-04-01'처럼 컬럼을 변환하면 plain created_at index와 query 형태가 달라집니다. 일반적인 시간 범위 검색은 raw range로 바꾸면 plain index 후보를 유지할 수 있습니다. 날짜 추출 자체가 반복되는 workload는 같은 expression의 index를 검토할 수 있지만, TIMESTAMPTZ의 날짜 기준 zone과 expression immutability를 먼저 고정해야 합니다.
-- plain created_at index와는 형태가 다른 조건
WHERE created_at::date = DATE '2026-04-01'
-- KST 기준 하루를 TIMESTAMPTZ instant 범위로 표현
WHERE created_at >= TIMESTAMPTZ '2026-04-01 00:00:00+09'
AND created_at < TIMESTAMPTZ '2026-04-02 00:00:00+09'date, time, timestamp, interval의 역할 차이
각 타입은 저장하는 정보의 범위가 다릅니다. date는 날짜만(연월일), time은 하루 내 시각만, timestamp/timestamptz는 날짜와 시각을 함께 담습니다. interval은 "기간" 자체를 표현하며 더하거나 빼는 산술 연산에 씁니다. 이벤트 로그나 트랜잭션 기록에는 timestamptz, 생일처럼 타임존이 의미 없는 날짜에는 date가 적합합니다.
SELECT
'2026-04-02'::date, -- date
'14:30:00'::time, -- time
'2026-04-02 14:30:00'::timestamp, -- timestamp (타임존 없음)
'2026-04-02 14:30:00+09'::timestamptz, -- timestamptz (UTC 변환 저장)
INTERVAL '2 hours 30 minutes'; -- interval선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 시각 정보를 저장할 때 (기본 선택) | TIMESTAMPTZ |
| 타임존이 불필요한 날짜 전용 컬럼 | DATE |
| 동적 기간 필터링 | NOW() - INTERVAL '...' |
| 날짜 컬럼을 조건으로 인덱스 활용 | 범위 비교 (>=, <) |
주의할 점
WHERE DATE(created_at) = '2026-04-01'처럼 컬럼에 함수를 씌우면 인덱스를 사용하지 못해
전체 테이블을 스캔합니다. 날짜 단위 필터는 범위 비교(>= '2026-04-01' AND < '2026-04-02')로
작성하는 것이 인덱스 활용과 성능 면에서 안전합니다. timestamp와 timestamptz 혼용도
타임존 변환 오류의 원인이 되므로 처음부터 타입을 통일하는 편이 좋습니다.
WHERE created_at >= '2026-04-01'
AND created_at < '2026-04-01' + INTERVAL '1 day'상한 경계를 <= '2026-04-01 23:59:59'처럼 직접 쓰면 밀리초나 마이크로초 값을 놓칠 수 있습니다. 날짜 범위는 보통 >= 시작과 < 다음 경계 패턴이 더 안전합니다.
WHERE created_at >= '2026-04-01 00:00:00'
AND created_at <= '2026-04-01 23:59:59'이 패턴은 초 단위보다 더 세밀한 값이 있으면 마지막 구간을 놓칩니다. 특히 timestamptz 컬럼에서는 로컬 시간 문자열과 세션 타임존까지 섞여 오해가 커질 수 있으므로, 날짜 범위는 항상 반열린 구간으로 잡는 편이 안전합니다.
참고 링크
2 sources