# Postgres 19를 기대하며: 드디어 '시간'을 다룰 때

> Clean Markdown view of GeekNews topic #30991. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30991](https://news.hada.io/topic?id=30991)
- GeekNews Markdown: [https://news.hada.io/topic/30991.md](https://news.hada.io/topic/30991.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-01T09:48:02+09:00
- Updated: 2026-07-01T09:48:02+09:00
- Original source: [pgedge.com](https://www.pgedge.com/blog/looking-forward-to-postgres-19-its-about-time)
- Points: 1
- Comments: 0

## Topic Body

- 데이터가 **특정일자(지난 화요일)** 시점에 어떤 모습이었는지 묻는 질문에 대응하기 위해, 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가 양시점의 양쪽 절반을 모두 갖추면 가능성이 크게 확장됨

## Comments



_No public comments on this page._
