[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-10 22:08:15 +09:00
parent 21ac3ed255
commit 504fd5fb42
3011 changed files with 380280 additions and 206977 deletions
+302
View File
@@ -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]]