[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
---
|
||||
id: db-bitemporal-data
|
||||
title: Bitemporal Data — valid time + transaction time
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [database, time, vibe-coding]
|
||||
tech_stack: { language: "SQL", applicable_to: ["Database"] }
|
||||
applied_in: []
|
||||
aliases: [bitemporal, valid time, transaction time, system time, SCD, slowly changing dimension, history]
|
||||
---
|
||||
|
||||
# Bitemporal Data
|
||||
|
||||
> "이 fact 가 valid 한 기간" + "이 fact 가 DB 에 있던 기간". **Bitemporal = 둘 다**. 회계 / 의료 / legal 가 필수. SCD (Slowly Changing Dimension) 의 generalization.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Valid time (effective): 현실 의 fact 시점.
|
||||
- Transaction time (system): DB 안 시점.
|
||||
- 둘 다 = bitemporal.
|
||||
- "내가 1주 전에 알았던 사실 가 무엇" 가능.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### 일반 (no time)
|
||||
```sql
|
||||
CREATE TABLE customers (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR,
|
||||
address VARCHAR
|
||||
);
|
||||
|
||||
UPDATE customers SET address = 'New' WHERE id = 1;
|
||||
-- → 옛 address 잃음.
|
||||
```
|
||||
|
||||
### Valid time (effective from / to)
|
||||
```sql
|
||||
CREATE TABLE customers (
|
||||
id INT,
|
||||
name VARCHAR,
|
||||
address VARCHAR,
|
||||
valid_from DATE NOT NULL,
|
||||
valid_to DATE NOT NULL DEFAULT '9999-12-31',
|
||||
PRIMARY KEY (id, valid_from)
|
||||
);
|
||||
|
||||
-- 변경 시
|
||||
UPDATE customers SET valid_to = '2026-05-09' WHERE id = 1 AND valid_to = '9999-12-31';
|
||||
INSERT INTO customers VALUES (1, 'Alice', 'New', '2026-05-09', '9999-12-31');
|
||||
|
||||
-- "2025-01-01 의 address?"
|
||||
SELECT address FROM customers
|
||||
WHERE id = 1 AND valid_from <= '2025-01-01' AND valid_to > '2025-01-01';
|
||||
```
|
||||
|
||||
→ SCD Type 2.
|
||||
|
||||
### Transaction time (system)
|
||||
```sql
|
||||
CREATE TABLE customers (
|
||||
id INT,
|
||||
name VARCHAR,
|
||||
sys_from TIMESTAMP NOT NULL,
|
||||
sys_to TIMESTAMP NOT NULL DEFAULT '9999-12-31',
|
||||
PRIMARY KEY (id, sys_from)
|
||||
);
|
||||
|
||||
-- "1주 전 DB 에 있던 record?"
|
||||
SELECT * FROM customers
|
||||
WHERE id = 1 AND sys_from <= NOW() - INTERVAL '1 week'
|
||||
AND sys_to > NOW() - INTERVAL '1 week';
|
||||
```
|
||||
|
||||
→ "내가 어제 본 거" — even if data 가 retro 변경.
|
||||
|
||||
### Bitemporal (둘 다)
|
||||
```sql
|
||||
CREATE TABLE policies (
|
||||
policy_id INT,
|
||||
premium DECIMAL,
|
||||
|
||||
valid_from DATE, -- 현실 effective
|
||||
valid_to DATE DEFAULT '9999-12-31',
|
||||
|
||||
sys_from TIMESTAMP, -- DB transaction
|
||||
sys_to TIMESTAMP DEFAULT '9999-12-31',
|
||||
|
||||
PRIMARY KEY (policy_id, valid_from, sys_from)
|
||||
);
|
||||
```
|
||||
|
||||
### Insert (bitemporal)
|
||||
```sql
|
||||
-- 새 policy
|
||||
INSERT INTO policies (policy_id, premium, valid_from, valid_to, sys_from, sys_to)
|
||||
VALUES (1, 100, '2026-01-01', '9999-12-31', NOW(), '9999-12-31');
|
||||
```
|
||||
|
||||
### Update (premium 가 retroactive 변경)
|
||||
```sql
|
||||
-- 2026-05-09 가 발견: 2026-01-01 부터 premium 가 110 였다 (오타).
|
||||
|
||||
-- 1. 옛 row close
|
||||
UPDATE policies SET sys_to = NOW()
|
||||
WHERE policy_id = 1 AND sys_to = '9999-12-31';
|
||||
|
||||
-- 2. 새 row (valid 가 1월 1일 부터)
|
||||
INSERT INTO policies VALUES (1, 110, '2026-01-01', '9999-12-31', NOW(), '9999-12-31');
|
||||
```
|
||||
|
||||
→ "이 시점 에 우리 가 아는 premium" 가 query 가능.
|
||||
|
||||
### Query: As-of (valid + system)
|
||||
```sql
|
||||
-- "2026-03-01 시점 의 policy. 1주 전 DB 의 view."
|
||||
SELECT premium FROM policies
|
||||
WHERE policy_id = 1
|
||||
AND valid_from <= '2026-03-01' AND valid_to > '2026-03-01'
|
||||
AND sys_from <= NOW() - INTERVAL '1 week' AND sys_to > NOW() - INTERVAL '1 week';
|
||||
```
|
||||
|
||||
→ 4 가지 query 가능:
|
||||
- 현재 valid + 현재 system (default)
|
||||
- 과거 valid + 현재 system (history)
|
||||
- 현재 valid + 과거 system ("when did we know?")
|
||||
- 과거 valid + 과거 system (full audit)
|
||||
|
||||
### SQL:2011 Temporal
|
||||
```sql
|
||||
-- Postgres + extension (BTRIM 또는 system-versioned tables 가 옵션)
|
||||
CREATE TABLE customers (...) WITH SYSTEM VERSIONING;
|
||||
|
||||
SELECT * FROM customers FOR SYSTEM_TIME AS OF TIMESTAMP '2026-05-01';
|
||||
```
|
||||
|
||||
→ MariaDB / SQL Server / IBM DB2 가 native 지원.
|
||||
|
||||
### Use case: 보험
|
||||
```
|
||||
2026-01-01: Policy 시작, premium $100.
|
||||
2026-03-01: Customer 가 새 car (premium $120).
|
||||
2026-04-01: 발견 - 2026-01-01 부터 premium $110 였어야 함.
|
||||
|
||||
쿼리:
|
||||
- "2026-02-15 의 premium" = $100 ($110 retroactive 적용 안 했을 시 본 거).
|
||||
- "2026-02-15 의 premium (현재 truth)" = $110.
|
||||
|
||||
→ 보험 / regulatory 가 "we knew on date X" 답 필요.
|
||||
```
|
||||
|
||||
### Use case: 회계
|
||||
```
|
||||
"2026-Q1 매출 = ?"
|
||||
|
||||
- Q1 끝날 시점 답 = $1M.
|
||||
- 4월 의 retroactive correction 후 답 = $1.05M.
|
||||
|
||||
→ Audit: 우리 가 Q1 close 때 본 = $1M.
|
||||
정정 후 = $1.05M.
|
||||
둘 다 record 유지.
|
||||
```
|
||||
|
||||
### Pitfall: complexity
|
||||
```
|
||||
Bitemporal 가 어려움.
|
||||
- 매 update = 2-3 row
|
||||
- Index 가 복잡 (composite)
|
||||
- Query 가 복잡
|
||||
|
||||
→ 진짜 필요할 때 만.
|
||||
대부분 = SCD Type 2 (valid time 만) 충분.
|
||||
```
|
||||
|
||||
### Implementation: trigger
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION close_old_row() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE policies SET sys_to = NOW()
|
||||
WHERE policy_id = NEW.policy_id AND valid_from = NEW.valid_from AND sys_to = '9999-12-31';
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER policy_versioning BEFORE INSERT ON policies
|
||||
FOR EACH ROW EXECUTE FUNCTION close_old_row();
|
||||
```
|
||||
|
||||
### XTDB (bitemporal native)
|
||||
```clojure
|
||||
(xt/submit-tx node
|
||||
[[::xt/put {:xt/id :alice :address "New"}
|
||||
#inst "2026-05-09"]]) ; valid time
|
||||
|
||||
(xt/q (xt/db node #inst "2026-05-08")
|
||||
'{:find [?addr] :where [[:alice :address ?addr]]})
|
||||
; ↑ as-of yesterday
|
||||
```
|
||||
|
||||
→ Bitemporal first-class.
|
||||
|
||||
### Datomic (similar)
|
||||
```clojure
|
||||
(d/q '[:find ?addr
|
||||
:where [?e :customer/address ?addr]]
|
||||
(d/as-of db tx-id))
|
||||
```
|
||||
|
||||
→ Immutable + time travel.
|
||||
|
||||
### Snapshot tables (simpler alternative)
|
||||
```sql
|
||||
-- 매일 entire DB snapshot
|
||||
CREATE TABLE customers_snapshot_2026_05_09 AS SELECT * FROM customers;
|
||||
|
||||
-- "2026-05-09 의 truth"
|
||||
SELECT * FROM customers_snapshot_2026_05_09;
|
||||
```
|
||||
|
||||
→ Storage 큼, query simple.
|
||||
|
||||
### Event sourcing (related)
|
||||
```
|
||||
Event log (append-only) → state.
|
||||
"2026-05-09 시점 의 state" = event log replay until then.
|
||||
|
||||
→ Bitemporal 의 different angle.
|
||||
```
|
||||
|
||||
→ [[Backend_Event_Sourcing]].
|
||||
|
||||
### Audit log (simpler)
|
||||
```sql
|
||||
CREATE TABLE customer_audit (
|
||||
audit_id BIGSERIAL,
|
||||
customer_id INT,
|
||||
changed_at TIMESTAMP,
|
||||
old_data JSONB,
|
||||
new_data JSONB
|
||||
);
|
||||
|
||||
-- Trigger 가 매 변경 추가
|
||||
```
|
||||
|
||||
→ Full bitemporal 가 X. "언제 무엇 변경" 만.
|
||||
|
||||
### When 진짜 bitemporal?
|
||||
```
|
||||
✓ Insurance (regulatory)
|
||||
✓ Banking / accounting
|
||||
✓ Healthcare (EHR — patient history vs DB record)
|
||||
✓ Legal (contracts vs amendments)
|
||||
✓ ERP (price catalog history)
|
||||
|
||||
✗ 일반 web app
|
||||
✗ E-commerce
|
||||
✗ Internal tool
|
||||
```
|
||||
|
||||
→ "내가 X 시점 에 알았던 거?" 가 비즈니스 질문 일 때 만.
|
||||
|
||||
### Performance
|
||||
```
|
||||
Composite index:
|
||||
CREATE INDEX ON policies (policy_id, valid_from, valid_to, sys_from, sys_to);
|
||||
|
||||
→ Range query 빠름.
|
||||
Storage 가 일반 의 N x (N = 평균 history depth).
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 일반 web | No history |
|
||||
| Audit log 필요 | Audit table / trigger |
|
||||
| 현재 + 과거 valid | SCD Type 2 (valid time only) |
|
||||
| 회계 / 보험 / 법 | Full bitemporal |
|
||||
| Event-driven | Event sourcing |
|
||||
| Functional / immutable | Datomic / XTDB |
|
||||
| Ad-hoc snapshot | Daily backup |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Bitemporal 없이 retroactive correction**: 옛 truth 잃음.
|
||||
- **Soft delete 만 + history 없음**: 변경 추적 안 됨.
|
||||
- **모든 table 가 bitemporal**: complexity.
|
||||
- **Index 없이**: query 가 full scan.
|
||||
- **Trigger error → silent**: data corrupt.
|
||||
- **`9999-12-31` 가 magic value**: NULL 보다 좋지만 명시 필요.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Bitemporal 진짜 필요 = 회계 / 법 / 의료.
|
||||
- 대부분 = valid time (SCD Type 2) 충분.
|
||||
- XTDB / Datomic 가 native 지원.
|
||||
- Audit log 가 cheap alternative.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[DB_Audit_Log_Patterns]]
|
||||
- [[Backend_Event_Sourcing]]
|
||||
- [[DB_Soft_Delete_Patterns]]
|
||||
Reference in New Issue
Block a user