숏컷 코드
SELECT *
FROM events
WHERE created_at >= NOW() - INTERVAL '7 days';문법
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일 전 시각을 동적으로 계산해 쿼리마다 항상 올바른 범위를 반환합니다. 애플리케이션 코드에서 날짜 문자열을 직접 계산해 파라미터로 넘기는 것보다, DB 안에서 INTERVAL로 계산하면 타임존 변환 실수나 월말 경계 오류를 피할 수 있습니다.
-- 지난 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날짜 컬럼에 함수를 씌우면 인덱스를 사용하지 못한다
WHERE DATE(created_at) = '2026-04-01'처럼 컬럼에 함수를 적용하면, PostgreSQL은 인덱스 조건으로 해당 표현식을 그대로 쓸 수 없어 Seq Scan으로 전락합니다. 같은 의도를 범위 비교로 바꾸면 created_at 인덱스를 그대로 활용할 수 있습니다. 이 원칙은 EXTRACT, DATE_TRUNC, 타입 캐스트에도 동일하게 적용됩니다.
-- 인덱스 비활용 (Seq Scan)
WHERE DATE(created_at) = '2026-04-01'
-- 인덱스 활용 (Index Scan)
WHERE created_at >= '2026-04-01'
AND created_at < '2026-04-02'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