무엇인가
사실마다 시계를 둘 다는 생각입니다. 세상의 시간(유효 시간 — 언제 참이었나)과 기록의 시간(트랜잭션 시간 — 시스템이 언제 알았나)을 따로 적으면, "지금 아는 바로 2월 20일 월급은 얼마였나"와 "2월 28일 급여일에 우리는 얼마라고 알고 있었나"에 둘 다 답할 수 있습니다. 1985~1986년 스노드그래스(Snodgrass)와 안(Ahn)의 데이터베이스 연구에서 나왔고, 2011년 SQL 표준(SQL:2011)에 들어갔습니다. 표준 해설은 IBM의 Kulkarni·Michels가 2012년 SIGMOD Record에 쓴 글이 가장 짧고 정확합니다.
Graphiti 같은 에이전트 기억의 '두 개의 시계'도 이 뿌리에서 왔습니다. 다른 점은 입력입니다. 이중 시간 테이블은 정형 데이터를 받고 기간을 닫는 일을 앱이 명시하며, Graphiti는 대화에서 언어 모델이 그 판단을 대신합니다.
어떻게 동작하나
| 개념 | 표준 문법 | 뜻 |
|---|---|---|
| 기간 | PERIOD FOR p (start_col, end_col) | 새 타입이 아니라 열 두 개에 붙인 이름. 시작 포함·끝 제외 |
| 시스템 시간 | WITH SYSTEM VERSIONING | 기록의 시간. DB가 채우고 사용자는 못 바꾼다 |
| 겹침 금지 키 | PRIMARY KEY (id, p WITHOUT OVERLAPS) | 같은 id의 유효 기간이 겹치지 않게 |
| 시간 외래 키 | FOREIGN KEY (id, PERIOD p) REFERENCES … | 자식 행의 기간 내내 부모가 있어야(여러 부모 행에 걸쳐도 됨) |
| 기간 일부 수정 | UPDATE t FOR PORTION OF p FROM a TO b | 겹치는 행을 둘·셋으로 쪼개 그 구간만 바꾼다 |
| 기록 시점 질의 | FOR SYSTEM_TIME AS OF t | 그때 DB에 저장돼 있던 대로 읽는다 |
표준에는 유효 시간에 대한 AS OF 문법이 없습니다. 유효 시간은 평범한 WHERE로 묻습니다. 두 기능 모두 선택 기능이라 DB마다 지원 범위가 다릅니다. 2026년 10월 현재 MariaDB 11.8·12.3은 거의 전부(유효 기간, 겹침 금지 키, FOR PORTION OF, 시스템 버전, 기록 시점 질의)를 기본으로 갖췄고, 시간 외래 키만 없습니다. PostgreSQL 18은 범위 타입 열 하나로 기간을 표현하는 겹침 금지 키와 시간 외래 키까지만 있고(btree_gist 필요), 시스템 시간은 트리거나 확장으로 만들어야 합니다.
빠른 시작
MariaDB에서 소급 인상과 무급 휴직을 넣는 예입니다. 실습용으로 세션 시각을 그날로 맞춰 가며 넣었습니다(MariaDB 문서가 시연용으로 소개하는 방법이고, 운영에서는 막아야 합니다 — 아래 실측).
CREATE TABLE salaries (
emp VARCHAR(20), monthly INT,
valid_from DATE, valid_to DATE,
PERIOD FOR valid_period(valid_from, valid_to),
UNIQUE (emp, valid_period WITHOUT OVERLAPS)
) WITH SYSTEM VERSIONING;
SET @@timestamp = UNIX_TIMESTAMP('2026-01-01 09:00:00');
INSERT INTO salaries VALUES ('김지훈', 3000000, '2026-01-01', '9999-12-31');
SET @@timestamp = UNIX_TIMESTAMP('2026-03-15 09:00:00'); -- 소급 인상을 알게 된 날
UPDATE salaries FOR PORTION OF valid_period FROM '2026-02-15' TO '9999-12-31'
SET monthly = 3500000 WHERE emp = '김지훈';
-- 2월 28일 급여일에 시스템이 알던 2월 20일 월급 → 3000000
SELECT monthly FROM salaries FOR SYSTEM_TIME AS OF TIMESTAMP '2026-02-28 18:00:00'
WHERE '2026-02-20' >= valid_from AND '2026-02-20' < valid_to;
같은 질의에서 FOR SYSTEM_TIME AS OF를 빼면 지금 아는 바의 답 3,500,000이 나옵니다. 둘 다 맞는 답이고, 앞의 것은 "그날 왜 300만 원을 줬나"를, 뒤의 것은 "지금 얼마를 더 줘야 하나"를 설명합니다.
코어닷투데이 실측
2026년 10월 11일 postgres:18.6, postgres:19beta3·19beta4, mariadb:11.8(11.8.9)·12.3(12.3.3), ghcr.io/xtdb/xtdb:2.1.0을 도커로 띄워 같은 급여·요금제 시나리오를 넣었습니다.
| 측정 | 결과 |
|---|---|
| PostgreSQL 18 기록 시간 직접 구현 | 트리거·함수·뷰 SQL 56줄. 기간을 쪼갤 때 옛 행을 먼저 줄이고 새 조각을 나중에 넣어야 시간 키가 거부하지 않았다 |
PostgreSQL 19 FOR PORTION OF | 19beta3에서 동작, 19beta4에서 syntax error. 9월 15일 동시 수정 유실 문제로 되돌려졌다 |
| 동시 수정 재현 (19beta3, READ COMMITTED) | 세션 B가 바꾸려던 6~8월이 통째로 빠짐. 두 세션 모두 오류 없이 커밋 |
| 같은 경쟁, MariaDB 11.8 | READ COMMITTED·REPEATABLE READ 모두 올바른 결과. SERIALIZABLE은 B가 실패 |
MariaDB 기록 시각 되돌리기 (기본 secure_timestamp=NO) | 오류 없이 받아들였고, 7월 3일 기록된 이력 한 줄이 흔적 없이 사라짐. --secure-timestamp=YES로 띄우자 거부 |
| 고객 1만 명 · 사건 13만 건 | 한 고객 질의 1ms 안팎, 전체 두 시점 집계 PostgreSQL 15.5ms · MariaDB 38.5ms. 저장 30.1MB · 30.6MB |
요금제 네 질문(지금 요금제, 3/10 청구서가 왜 Basic이었나 등)에는 MariaDB·XTDB·PostgreSQL 트리거 구현이 모두 같은 답을 냈습니다. 시간 기능을 쓴다고 질의가 갑자기 느려지지는 않았고, 이력은 변경 수만큼 늘었습니다(사건 13만 건에 행 25만 개 안팎).
언제 쓰고, 언제 피하나
- 잘 맞는다: 계약·요금제·급여·주소처럼 정답이 정형 시스템에 있고 "그때 값"(유효 시간)이나 "그때 우리가 알던 값"(감사)을 물어야 하는 데이터. 겹침은 DB 제약이 막고 동시성은 격리 수준이 지킵니다. 이번 실험에서 언어 모델은 한 번도 필요하지 않았습니다.
- 필요 없다: "지금 값"만 필요하면 시간 테이블은 과합니다. 대화·문서처럼 비정형 입력에서 사실을 뽑아야 한다면 그 일은 Graphiti 같은 시간 그래프의 몫이고, 정형 원본이 있는 사실만 이중 시간 DB를 원본으로 두는 혼합 설계가 현실적입니다.
한계와 주의
- PostgreSQL 19에도
FOR PORTION OF는 없다. 남은 조각을 계산하는 도우미 함수와 문서의 '시간 테이블' 절(시스템 시간은 미지원이라고 적음)만 남았습니다. 확장temporal_tables·periods가 있지만periods는 README 기준 PostgreSQL 15까지 호환을 적고 있습니다. - 기간을 쪼개는 연산은 동시성이 미묘하다. 같은 대상의 기간을 동시에 고칠 수 있다면 REPEATABLE READ 이상에서 실패·재시도를 쓰거나 대상별로 직렬화합니다. MariaDB에서 재현이 안 됐다고 "안전하다"로 읽지는 않습니다.
- 감사용이면 기록 시각 조작부터 막는다. MariaDB는 기본 설정에서 아무 사용자나 세션 시각을 바꿀 수 있습니다.
- '타임 트래블'은 대개 기록의 시간만이다. Iceberg·BigQuery·DuckLake의 시간 여행은 스냅숏을 되돌려 보는 기능이고, Flink의
FOR SYSTEM_TIME AS OF는 이름과 달리 사건 시간을 씁니다. 이름만 보고 판단하지 않습니다. - 성능 측정은 세 DB를 한 가상 머신에서 동시에 돌려 자원을 나눠 쓴 값이고, 동시성 재현은 조합마다 한 번씩입니다. 절대값보다 자릿수로 읽습니다.



