coredot.today
기술 탐색으로 돌아가기
인프라·DB성장

이중 시간 SQL (SQL:2011)

'언제 참이었나'와 '언제 알았나'를 두 기간으로 적는 SQL 표준 — PostgreSQL 18·19와 MariaDB에서 어디까지 되나

직접 해보기 4라이브 실행실측빠른 시작
종류
프로토콜
사실 확인
2026.10.24
docker run -d -p 3307:3306 -e MARIADB_ROOT_PASSWORD=bt -e MARIADB_DATABASE=bt mariadb:11.8

TRY IT

직접 해보기

OVERVIEW

개요

무엇인가

사실마다 시계를 둘 다는 생각입니다. 세상의 시간(유효 시간 — 언제 참이었나)과 기록의 시간(트랜잭션 시간 — 시스템이 언제 알았나)을 따로 적으면, "지금 아는 바로 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 문서가 시연용으로 소개하는 방법이고, 운영에서는 막아야 합니다 — 아래 실측).

sql
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 OF19beta3에서 동작, 19beta4에서 syntax error. 9월 15일 동시 수정 유실 문제로 되돌려졌다
동시 수정 재현 (19beta3, READ COMMITTED)세션 B가 바꾸려던 6~8월이 통째로 빠짐. 두 세션 모두 오류 없이 커밋
같은 경쟁, MariaDB 11.8READ 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를 한 가상 머신에서 동시에 돌려 자원을 나눠 쓴 값이고, 동시성 재현은 조합마다 한 번씩입니다. 절대값보다 자릿수로 읽습니다.

LIVE

라이브 실행

대화에서 시간 붙은 사실 뽑기

Graphiti·이중 시간 테이블이 다루는 문제를 손으로 해 봅니다. 대화 기록에서 사실을 뽑고, 언제부터 참이었는지와 나중 발언으로 무효가 된 사실을 표시합니다.

콘솔에서 키 발급받기

키는 이 브라우저에서 core.today 게이트웨이로만 전송되고 코어닷투데이 서버를 거치지 않습니다. 실행 비용은 키 주인의 크레딧에서 차감됩니다.

예시 입력
모델에게 보내는 시스템 프롬프트 보기
대화 기록에서 사람·대상에 관한 사실을 추출해 시간 그래프의 간선처럼 만든다.
각 사실: 주어, 관계, 대상, valid_from(언제부터 참인지, 모르면 null), invalidated_by(이 사실을 무효로 만든 나중 사실의 번호, 없으면 null).
나중 발언이 앞의 사실과 충돌하면 앞 사실을 지우지 말고 invalidated_by로 닫는다.
JSON 하나만 출력한다: {"facts": [{"id": 1, "subject": ..., "relation": ..., "object": ..., "valid_from": ..., "invalidated_by": ...}]}

134 / 1,500자 · 모델 gpt-5.6-luna · 출력 최대 700 토큰

DEEP DIVE

더 깊이 읽기