데이터베이스에 타임스탬프 저장: DATETIME vs INT vs BIGINT
타임스탬프에 잘못된 열 타입을 고르면 시간대 드리프트, 2038년 오버플로, 깨진 범위 쿼리, 혼란스러운 API 출력을 낳습니다. MySQL과 PostgreSQL에서 네이티브 datetime 타입, BIGINT 에포크 값, 문자열을 비교합니다.
타임스탬프를 저장하는 세 가지 방법
대부분의 데이터베이스는 적어도 세 가지 옵션을 제공합니다: 네이티브 datetime 타입(TIMESTAMP, DATETIME, TIMESTAMPTZ), 단순 정수(INT 또는 BIGINT), 문자열(VARCHAR). 각각은 저장 크기, 쿼리 편의성, 시간대 처리, 미래 대비에서 다른 절충점을 가집니다. 대부분의 제품 데이터베이스에서는 네이티브 datetime 열이 최선의 기본값입니다. 데이터베이스가 값을 익명의 숫자가 아니라 시간으로 비교, 인덱싱, 절단, 그룹화, 포맷할 수 있기 때문입니다.
- 네이티브 datetime 타입 — 날짜 산술, 시간대 변환, 가독성에 최적
- BIGINT 정수 — 고처리량 삽입과 단순 숫자 범위 쿼리에 좋음
- VARCHAR 문자열 — 거의 항상 잘못됨: 날짜의 문자열 비교는 엄격한 ISO 8601 형식에서만 동작
- INT 정수 — 2038년 경계를 완전히 확인하지 않았다면 미래 타임스탬프에는 피할 것
MySQL: TIMESTAMP vs DATETIME vs INT
MySQL에는 비슷해 보이지만 매우 다르게 동작하는 두 날짜-시간 타입이 있으며, 그중 하나는 엄격한 만료일이 있습니다. TIMESTAMP는 UTC와 세션 시간대 간 자동 변환을 원할 때 편리하지만, 역사적인 32비트 범위 때문에 미래 지향적인 제품 데이터에는 위험합니다. DATETIME은 당신이 제공한 날짜와 시간을 그대로 저장하며, 애플리케이션이 기록 전에 UTC로 표준화할 때 보통 더 명확합니다.
- TIMESTAMP: 내부적으로 32비트 Unix 초로 저장 — 1970-01-01부터 2038-01-19까지로 제한
- TIMESTAMP: 삽입/읽기 시 UTC와 세션 시간대 간 자동 변환
- DATETIME: 날짜-시간을 그대로 저장, 시간대 없음. 범위 1000-01-01부터 9999-12-31. Y2038의 영향 없음.
- DATETIME: 시간대를 변환하지 않음 — UTC를 애플리케이션 수준에서 제어
- 권장: 2038년 제한을 피하기 위해 새 테이블에는 명시적 UTC 값과 함께 DATETIME 사용
PostgreSQL: TIMESTAMPTZ가 올바른 선택
PostgreSQL의 TIMESTAMP WITH TIME ZONE(TIMESTAMPTZ)은 타임스탬프를 내부적으로 UTC 마이크로초로 저장하고 출력 시 세션 시간대로 변환합니다. 실제 시간의 한 순간을 나타내므로 대부분의 사용 사례에 가장 안전하고 올바른 옵션입니다. 이름은 오해를 부를 수 있습니다: TIMESTAMPTZ는 America/New_York 같은 원래 시간대 레이블을 저장하지 않습니다. 순간을 저장한 뒤 현재 세션 시간대에 따라 표시합니다.
- TIMESTAMPTZ: UTC 저장, 출력 시 세션 시간대로 변환 — 이식 가능하고 일광 절약에 안전
- TIMESTAMP(시간대 없음): 리터럴 값을 변환 없이 저장 — 시간대 비인식 데이터에만 사용
- EXTRACT(EPOCH FROM col): 임의의 TIMESTAMP 열에서 Unix 초를 float로 반환
- TO_TIMESTAMP(epoch): Unix 초를 다시 TIMESTAMPTZ로 변환
인덱싱과 쿼리 성능
일반적인 애플리케이션 테이블에서는 네이티브 datetime 열과 BIGINT 에포크 열 사이의 성능 차이가 결정 요인인 경우가 드뭅니다. 쿼리 형태, 인덱스 설계, 파티셔닝, 행 수가 더 중요합니다. 먼저 의미를 올바르게 유지하는 타입을 고르고, 그다음 애플리케이션이 실제로 실행하는 범위 쿼리에 맞춰 인덱싱하세요.
- 세 타입 모두 B-tree 인덱스와 효율적인 범위 쿼리를 지원
- BIGINT 정수는 초대용량 테이블에서 동등/범위 스캔에 근소하게 더 빠름
- 네이티브 datetime 타입은 날짜 부분 인덱스 쿼리를 허용: WHERE created_at::date = '2024-01-01'
- VARCHAR 타임스탬프는 성능이 최악 — 문자열 비교는 날짜를 이해하지 못함
BIGINT 에포크 저장이 합리적일 때
BIGINT는 데이터가 이벤트 같고, 추가가 많으며, 이미 다른 시스템이 Unix 시간으로 생성한 경우에 합리적입니다. 분석 파이프라인, 텔레메트리 스트림, 큐, 컴팩트한 바이너리 프로토콜은 숫자 값이 비교가 빠르고 언어 중립적이기 때문에 에포크 밀리초를 자주 씁니다. 절충점은 가독성입니다: 사람에게는 변환기가 필요하고 SQL 날짜 산술은 더 장황해집니다.
- JavaScript 클라이언트가 이벤트를 직접 생성하면 Unix 밀리초에 BIGINT를 사용
- 소스 시스템이 Unix 스타일이고 초 정밀도면 충분하면 Unix 초에 BIGINT를 사용
- 단위를 열 이름에 문서화: created_at_ms가 created_at_epoch보다 명확
- 분석가에게 읽기 쉬운 SQL 쿼리가 필요하면 생성 datetime 열을 추가
- 32비트 범위 제한 때문에 미래 지향적인 현대 타임스탬프에는 INT를 피할 것
권장 스키마 패턴
대부분의 웹 애플리케이션에서는 순간을 UTC로 저장하고, 로컬 벽시계 의도를 재구성해야 할 때만 사용자의 선호 시간대를 별도로 저장하세요. America/New_York 오전 9:00로 예정된 회의는 정확한 UTC 순간에 생성된 이벤트 로그와 다릅니다; 이런 경우는 다르게 모델링하세요.
- 이벤트 로그: PostgreSQL은 created_at TIMESTAMPTZ, MySQL은 UTC의 created_at DATETIME
- JavaScript 이벤트 수집: created_at_ms BIGINT와 명확한 API 문서
- 반복 로컬 일정: local_date, local_time, timezone_id를 저장하고 다음 순간을 계산
- 만료 타임스탬프: expires_at를 네이티브 datetime, 또는 명시적 Unix 초의 expires_at_seconds로
- 감사 테이블: 읽기 쉬운 디버깅을 위해 created_at와 updated_at를 둘 다 네이티브 datetime 열로 유지
데이터베이스 타임스탬프 FAQ
- 데이터베이스에 UTC를 저장할까요, 로컬 시간을 저장할까요?
- 이벤트 타임스탬프에는 UTC를 저장하고 표시할 때 로컬 시간으로 변환하세요. 반복 회의나 영업시간처럼 사용자의 로컬 벽시계 의도가 중요할 때는 시간대 식별자를 별도로 저장하세요.
- BIGINT가 TIMESTAMP보다 나은가요?
- 일반적으로는 아닙니다. BIGINT는 숫자 에포크 파이프라인에 유용하지만, 네이티브 datetime 타입이 SQL 날짜 산술, 읽기 쉬운 디버깅, 시간대 인식 출력에 더 쉽습니다.
- MySQL은 TIMESTAMP를 써야 하나요, DATETIME을 써야 하나요?
- 새 애플리케이션 테이블에는 UTC 값의 DATETIME이 종종 더 안전합니다. 2038년 범위 제한을 피하고 세션 시간대 변환에 조용히 의존하지 않기 때문입니다.
- 타임스탬프를 UTC로 저장할까요, 시간대와 함께 저장할까요?
- 순간을 UTC로 저장하고(PostgreSQL은 TIMESTAMPTZ, MySQL은 UTC 값의 DATETIME) 표시할 때 로컬 시간으로 변환하세요. 반복 회의처럼 사용자의 로컬 벽시계 의도를 재구성해야 할 때만 별도의 IANA 시간대 열을 유지하세요.