Postgres 19를 기대하며: 드디어 '시간'을 다룰 때
요약
Postgres 19에서 SQL:2011 표준을 준수하는 네이티브 시점 이력 테이블(Temporal Table) 지원 도입 소식을 다룹니다. 기존의 복잡한 확장 기능이나 수동 제약 조건 대신, 엔진 레벨에서 시간 범위의 중첩을 방지하고 데이터의 시점 추적을 직관적으로 처리할 수 있게 됩니다.
핵심 포인트
- Postgres 19에서 SQL:2011 표준 기반 Temporal 테이블 네이티브 지원
- FOR PORTION OF 구문을 통한 UPDATE/DELETE 시 행 자동 분할 지원
- 기존 btree_gist 확장 및 배제 제약 방식의 복잡성 해결
- 유효 시간(Application Time) 관리를 위한 엔진 레벨의 정합성 보장
- 데이터가
특정일자(지난 화요일) 시점에 어떤 모습이었는지 묻는 질문에 대응하기 위해, Postgres 19가 네이티브 시점 이력 테이블(temporal table) 지원을 도입, 별도 감사 트리거 시스템 없이 변경 전후 데이터 추적 가능
SQL:2011 표준이 10여 년 전 정의한 Temporal 테이블을 Postgres가 뒤늦게 코어에 반영, 다른 DB 엔진보다 늦게 합류 - 기존
valid_from
/valid_to
두 컬럼과 btree_gist
확장 기반 배제 제약(exclusion constraint) 대신, 단일 범위 타입(range type) 컬럼과 WITHOUT OVERLAPS
제약으로 더 직관적인 표현 제공
FOR PORTION OF
구문으로 UPDATE·DELETE 시 행 자동 분할, 시간축의 공백·중첩 방지를 엔진이 처리
- 이번 도입은 양시점(bi-temporal) 시스템 중
유효 시간(application time) 절반에 해당, 시스템 시간(system time)은 미지원이지만 향후 릴리스의 기반을 마련
기존 방식 - The Old-Fashioned Way
- 제품 가격을 시간순으로 추적하려는 첫 시도는
valid_from
, valid_to
두 날짜 컬럼과 valid_from < valid_to
CHECK 제약으로 구성
-
그러나 동일 제품에 대해
날짜 범위가 겹치는 두 행 삽입을 막지 못함 (예: 제품 42번이 같은 화요일에 $9.99이자 $14.99) -
전통적 해법은
btree_gist
확장과 배제 제약(exclusion constraint) 사용
EXCLUDE USING gist (product_id WITH =, daterange(valid_from, valid_to) WITH &&)
형태로 겹치는 행 삽입 시 오류 발생
- 이 방식의 문제점
GiST는 Postgres 고유 색인으로 경험이 필요하며, 선택적 확장이라는 점에서 진입 장벽 존재 - 배제 제약 구문이 직관적이지 않아 표준 접근으로 떠올리기 어려움
- 테이블 자체에
시점 인식이 내장되지 않음, 시간 범위 변경 시 행을 수동으로 분할·결합해야 하며 시점 정합성 부담이 응용 프로그램에 전가됨
시간의 짧은 역사 - A Brief History of Time
- SQL:2011 표준이 유효 시간 기간(
APPLICATION TIME
), WITHOUT OVERLAPS
제약, 시점 데이터 조작용 FOR PORTION OF
구문 도입
-
Henrietta Dombrovskaya(Hetti)가 Chad Slaughter와 함께
pg_bitemporal 확장 개발, PL/pgSQL로 Postgres 내에서 양시점 테이블을 관리하는 프레임워크 -
2015년부터 여러 컨퍼런스에서 개념 발표,
유효 시간(valid time)(현실 세계에서 사실이 참인 시점)과 거래 시간(transaction time)(DB가 사실을 기록한 시점)을 동시에 추적하는 방법 시연 -
두 시간 차원의 구분
-
유효 시간은 "이 가격은 1월부터 6월까지 유효"라는 의미
-
거래 시간은 "이 행은 3월 12일 오후 3시 47분에 삽입, 4월 3일 오전 9시 1분에 대체됨"이라는 DB 관점
-
둘을 결합하면 "당시 알고 있던 정보 기준으로 지난 화요일 가격을 무엇이라
생각했는가"에 답하는 양시점 테이블 구성 -
pg_bitemporal은
EXCLUDE USING gist
를 이중으로 사용, effective
범위(유효 시간)와 asserted
범위(거래 시간)에 각각 하나씩 적용
-
양시점 삽입·갱신·정정·비활성·삭제용 함수와 시점 추론을 위한
앨런 구간 관계(Allen's interval relationships) 구현 제공 -
확장의 한계
-
쿼리 플래너가 시점 조건을 인식하도록 바꾸거나, 엔진 레벨 제약 시스템과 통합하거나, 네이티브 조작 구문을 제공할 수 없음 → 코어 진입 필요
-
Postgres 19가 양시점 시스템의
유효 시간 절반 수용, 전체는 아니지만 큰 진전
범위로 해결 - Ranges to the Rescue
- Postgres 19 방식은 별도
valid_from
/valid_to
대신 단일 범위 타입 컬럼 valid_at DATERANGE
사용
PRIMARY KEY (product_id, valid_at WITHOUT OVERLAPS)
로 btree_gist
확장과 배제 제약 불필요
WITHOUT OVERLAPS
는 product_id
가 임의의 시점에서 유일하되 범위가 겹치지 않으면 동일 제품의 여러 행 허용
- 내부적으로는 여전히 GiST 색인을 쓰고 키의 비-시점 컬럼에
btree_gist
가 필요하나, 제약 초기화 시 의존성을 Postgres가 자동 처리
- 범위 표기법:
[
는 포함, )
는 제외 → [2025-01-01, 2025-07-01)
은 1월 1일 포함, 7월 1일 미포함
- 마지막 Gadget 행
[2026-01-01,)
는 종료일이 열린(open-ended) 범위로 현재 가격에 정의된 종료일 없음
- 겹침 보호 동작
- 잘못된 범위
[2025-03-01, 2025-01-01)
삽입 시 "범위 하한이 상한보다 작거나 같아야 한다"는 오류
- 겹치는 범위
[2025-03-01, 2025-09-01)
삽입 시 products_pkey
배제 제약 위반 오류
- 범위 사용 하나로
두 가지 검증 동시 확보
잘라내고 나누기 - Slicing and Dicing
-
상품의 가격을 2025년 3월~9월에만 $10.99로 변경하는 경우, 기존 방식은 행을 수동 분할·삽입해야 했고 실수 시 공백·중첩 발생
-
시점 이력 테이블에서는 의도한 대로 직접 표현
UPDATE products FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-09-01' SET price = 10.99 WHERE product_id = 1 -
결과적으로 Widget 행이 3개에서
5개로 증가 -
기존 $9.99 행을
[2025-01-01, 2025-03-01)
로 축소
- $10.99 신규 행을 남은 범위에 추가
- 기존 $12.99 행을
[2025-09-01, 2026-01-01)
로 축소
- $10.99 신규 행을 남은
[2025-07-01, 2025-09-01)
범위에 추가
- $10.99가 두 행으로 나뉜 이유는
FOR PORTION OF
가 매칭되는 각 행을 독립적으로 처리, 인접 범위를 사후 병합(coalesce)하지 않기 때문
-
최종 결과는 공백·중첩 없음, 순수 배제 논리(exclusion logic)로는 없던 이점
-
예외 상황(edge case)
FOR PORTION OF
범위가 단일 기존 행 내부에 완전히 들어가면 최대 2개의 잔여 행(앞·뒤) 생성
-
기존 경계와 정확히 일치하면 잔여 행 불필요
-
새로 생기는 시점 잔여 행은
INSERT
권한이 필요 없으나, 기존 INSERT
트리거는 발동 → 감사 로깅이나 SECURITY DEFINER
트리거 함수에서 유의 필요
이력 지우기 - Erasing History
FOR PORTION OF
는 DELETE
에서도 동작, 특정 상품을 2025년 6월~10월 카탈로그에서 일시 제거하는 걸 예로 들어보면
DELETE FROM products FOR PORTION OF valid_at FROM '2025-06-01' TO '2025-10-01' WHERE product_id = 2
- 결과
- 6월~10월 구간이 잘려 나가고,
[2025-04-01, 2026-01-01)
을 덮던 $22.99 행이 6월에 끝나는 행과 10월에 시작하는 두 잔여 행으로 분할
-
공백 전후 가격 데이터는 원래 값으로 보존,
DELETE가 행 수를 증가시키는 결과 -
시점 이력 테이블 관리 메커니즘이 모두 자동 처리, 응용 프로그램 레벨에서 과도 삭제나 고아 조각(orphaned fragment) 위험 제거
광고의 진실 - Truth in Advertising
- 시점 이력 테이블은
시점 외래 키(temporal foreign key) 없이는 완성되지 않으며, Postgres 19는PERIOD
키워드로 이를 지원
FOREIGN KEY (product_id, PERIOD valid_at) REFERENCES products (product_id, PERIOD valid_at)
형태
PERIOD
키워드는 외래 키 자체가 시점 기반임을 알림
- 참조 대상 product가 variant의
valid_at
범위 전체 기간 동안 존재해야 함
-
참조 테이블의 모든 매칭 행의 조합이 참조하는 행의 기간을 완전히 덮어야 함
-
product 시간 범위를 넘는 variant 생성 시도(
[2025-01-01, 2027-01-01)
)는 거부
-
Widget 가격이 2026년 중반까지만 정의되어 2027년까지 유효 주장하는 variant는 외래 키 제약 위반으로 거부
-
한 가지 중요한 제한
-
시점 외래 키는 참조 동작(referential action)으로
NO ACTION
만 지원, CASCADE
·SET NULL
·SET DEFAULT
제외
- variant가 의존하는 product 행 삭제 시 항상 오류, 연쇄 시점 연산의 복잡성 때문이며 응용 프로그램이 명시적으로 처리해야 함
작은 걸음들 - Baby Steps
-
현재 확보된 기능: 겹침 방지가 적용된 유효 시간 기반 시점 이력 테이블, 시점 데이터 조작, 시점 외래 키
-
가장 큰 누락은 거래 시간이라고도 불리는
시스템 시간(system time) -
유효 시간은 사실이 현실에서 참인 시점을 추적, 시스템 시간은 DB가 그 사실을 인지하는 시점을 추적, 둘 다 활용하는 시스템 다수
-
이 영역은 2015년부터 pg_bitemporal 확장이 채워온 부분
-
트리거로 시스템 시간 모사(emulation)는 가능하나, 다른 신규 시점 기능처럼 엔진이 투명하게 관리하는 것과는 다름
-
시점 이력 테이블 문서도 시스템 시간이 네이티브 미지원이며 모사 가능함을 명시, Postgres 20 이후 도입 여부는 미정이나 기반은 이미 마련됨
맺음말 - Final Thoughts
EXCLUDE USING gist
방식은 동작하지만 비교적 조잡한 우회책, pg_bitemporal 같은 확장이 개념을 증명하고 논의를 이어옴
- GiST 배제 제약보다 훨씬 직관적인 접근
- 기본 키의
WITHOUT OVERLAPS
는 평이한 영어처럼 읽히고, FOR PORTION OF
는 동작을 그대로 표현
-
시점 갱신·삭제 시
자동 행 분할이 한 부류의 잠재적 버그 제거 -
SQL:2011에서 Postgres 19까지의 여정은 길었으며, Hetti와 커뮤니티가 수년간 패턴의 필요성과 실현 가능성 증명, 이제 코어에 반영됨
-
향후 릴리스의 시스템 시간 지원 주목 필요, Postgres가 양시점의 양쪽 절반을 모두 갖추면 가능성이 크게 확장됨
댓글과 토론
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기