# 11. Database & Storage Architect
## 11.1 目標與整體觀念
在任何 MLOps 或大型分散式系統中,資料庫與儲存架構要同時滿足三個目標:
1. **一致性與正確性(Correctness)**:確保交易與資料不產生矛盾
2. **可擴充性(Scalability)**:能隨著資料量與使用者數量成長而水平擴充
3. **可維運性(Operability)**:易於備份、監控、調整 schema 與擴充節點
Kleppmann 在 *Designing Data-Intensive Applications* 中,將這類系統稱為「資料密集型應用」,並強調要從資料模型、儲存引擎、分散式協定三個層面整體設計,而不是只看單一資料庫產品。
### 關鍵架構考量點
```
資料庫架構三角
┌─────────────────┐
│ Consistency │
│ (一致性) │
└────────┬────────┘
│
┌─────┴─────┐
│ │
┌──────▼─────┐ ┌──▼────────┐
│ Availability│ │Performance│
│ (可用性) │ │ (效能) │
└─────────────┘ └───────────┘
```
---
## 11.2 資料庫 Schema 設計
### 11.2.1 Schema 的角色
Schema 決定:
- 資料如何被分割、儲存與索引
- 查詢路徑與未來可否容易擴充或分片
- MLOps 中特徵抽取、標註、訓練資料抽樣的難易度
在集中式資料庫中,多半以「正規化 → 避免重複」為主;而在分散式資料庫或雲端環境,還要考慮「資料局部性(locality)」與「存取模式(access pattern)」。
### 11.2.2 設計重點
#### 1. 明確定義主鍵(Primary Key)
- 採用穩定、不易變動的識別子(如 `user_id`、`device_id`)
- 避免以易變欄位(如 email)當主鍵
- 在分散式系統中,主鍵常同時是「分片 key」的一部分
```sql
-- ✅ 良好的主鍵設計
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
org_id INT NOT NULL,
email VARCHAR(255),
created_at TIMESTAMP
);
-- ❌ 避免使用易變欄位作為主鍵
CREATE TABLE users (
email VARCHAR(255) PRIMARY KEY, -- 不推薦
...
);
```
#### 2. 針對存取模式設計
- 先列出主要查詢,如:「依使用者 + 時間區間查詢」、「依任務狀態查詢」
- Schema 不是只為了「資料漂亮」,而是要讓主要查詢走到有效率的索引與分片
```sql
-- 設計範例:支援常見查詢模式
CREATE TABLE training_runs (
org_id INT,
user_id BIGINT,
run_id UUID,
status VARCHAR(50),
created_at TIMESTAMP,
PRIMARY KEY (org_id, user_id, created_at),
INDEX idx_status (org_id, status, created_at)
);
```
#### 3. 適度反正規化(Denormalization)
- 避免跨多表 join,尤其在分片後跨節點 join 成本很高
- 對於常被一起查詢的欄位,可以接受適度重複,換取查詢簡化與效能
**正規化 vs. 反正規化比較**
| 面向 | 正規化 | 反正規化 |
|------|--------|----------|
| 資料重複 | 最小化 | 接受部分重複 |
| 寫入效能 | 較快(單點寫入) | 較慢(多處更新) |
| 讀取效能 | 需要 JOIN | 直接讀取 |
| 適用場景 | 寫多讀少 | 讀多寫少 |
| 分散式友善度 | 較低 | 較高 |
#### 4. 多租戶(Multi-tenant)設計
- 若同一系統服務多個機構,常見作法是在所有主表加上 `tenant_id` 或 `org_id`
- 這個欄位通常也是分片 key 的一部分,有助於隔離負載與資料
```sql
-- 多租戶設計模式
CREATE TABLE medical_records (
tenant_id INT,
patient_id BIGINT,
record_id UUID,
data JSONB,
created_at TIMESTAMP,
PRIMARY KEY (tenant_id, patient_id, created_at)
);
-- 資料隔離查詢
SELECT * FROM medical_records
WHERE tenant_id = 123
AND patient_id = 456
AND created_at >= '2025-01-01';
```
Google Spanner 的 schema 設計文件特別強調「設計主鍵與層級結構,讓同一個分片上的資料能對應主要存取模式」,以避免熱點與跨節點查詢。
---
## 11.3 分片(Sharding)設計
### 11.3.1 為什麼要分片
當單一資料庫節點在 CPU、記憶體、磁碟或 I/O 已達瓶頸時,就必須「水平切分」資料:
- 每個節點只存「全部資料的一部分」
- 查詢時依據分片 key 將請求路由到正確節點
- 這是多數「雲原生分散式資料庫」的核心能力
### 11.3.2 常見分片方式
#### 1. Range Sharding(範圍分片)
**原理**:依 key 範圍拆分
```
User ID Range Sharding 示例:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Shard A │ │ Shard B │ │ Shard C │
│ ID: 1-1M │ │ ID: 1M-2M │ │ ID: 2M-3M │
└──────────────┘ └──────────────┘ └──────────────┘
```
**優點**:
- 支援範圍查詢
- 排序友好
- 易於理解和實作
**風險**:
- 如果新資料只落在某段(如時間序列),容易產生「熱點分片」
- 資料分佈可能不均勻
**適用場景**:
- 時間序列資料(配合定期重平衡)
- 地理位置資料
- 字母順序資料
#### 2. Hash Sharding(雜湊分片)
**原理**:對 key 做 hash,再依 hash 結果分配 shard
```
Hash Sharding 流程:
User ID → Hash Function → Hash Value mod N → Shard Assignment
Example:
user_id: 12345
↓ hash()
hash: 8a7f2c...
↓ mod 3
shard: 2 → Shard C
```
**優點**:
- 負載分佈均勻
- 不易產生熱點
- 自動平衡
**缺點**:
- 範圍查詢需要打到多個分片,成本較高
- 重新分片時需要大量資料搬移
**適用場景**:
- 點查詢為主的應用
- 需要均勻負載分佈
- 高併發寫入
#### 3. Directory / Lookup-based Sharding(目錄式分片)
**原理**:維護一張「分片對照表」,紀錄 key 區間對應哪個 shard
```
Directory-Based Sharding:
┌─────────────────────┐
│ Lookup Table │
├──────────┬──────────┤
│ Key Range│ Shard ID │
├──────────┼──────────┤
│ A-F │ Shard 1 │
│ G-M │ Shard 2 │
│ N-Z │ Shard 3 │
└──────────┴──────────┘
↓
Route to correct shard
```
**優點**:
- 彈性高,可以動態調整
- 支援複雜的分片策略
- 易於遷移和重新平衡
**缺點**:
- 需要額外維護 metadata
- Lookup table 可能成為瓶頸
- 增加了一層查詢延遲
#### 4. Geographical Sharding(地理位置分片)
**原理**:根據地理位置分配資料到就近的分片
```
Geographical Sharding:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Asia Shard │ │ Europe Shard │ │ US Shard │
│ (Tokyo) │ │ (Frankfurt) │ │ (Virginia) │
└──────────────┘ └──────────────┘ └──────────────┘
```
**優點**:
- 減少跨區域延遲
- 符合資料主權要求
- 更好的用戶體驗
**適用場景**:
- 全球化應用
- 需要符合 GDPR 等法規
- 對延遲敏感的應用
### 11.3.3 分片 key 設計原則
**應該包含**:
- 租戶/組織 ID(`org_id`)
- 主實體 ID(`user_id`、`device_id`等)
**應該避免**:
- 單調遞增純時間戳作為唯一 key(會導致最新寫入集中到同一分片)
**時間查詢的解決方案**:
- 以 `(org_id, user_id)` 分片,再在 shard 內用時間排序
- 或採用時間分區表(partitioned table)搭配 range sharding
```sql
-- ✅ 良好的複合分片鍵設計
CREATE TABLE events (
org_id INT,
user_id BIGINT,
event_id UUID,
timestamp TIMESTAMP,
data JSONB,
PRIMARY KEY (org_id, user_id, event_id)
) PARTITION BY HASH (org_id, user_id);
-- ❌ 避免單一時間戳作為分片鍵
CREATE TABLE events (
timestamp TIMESTAMP PRIMARY KEY, -- 會造成熱點
...
);
```
### 11.3.4 自動分片管理
實務上通常會搭配「自動 split / merge」機制:
- 當某個分片過熱或過大時自動拆分
- Spanner 以自動切分 tablet 的方式將資料分散到多個伺服器
```
自動分片分裂示例:
Initial:
┌────────────────────────┐
│ Shard A (0-1000) │
│ Size: 100GB (過大!) │
└────────────────────────┘
↓ Auto-split
After:
┌─────────────┐ ┌─────────────┐
│ Shard A1 │ │ Shard A2 │
│ (0-500) │ │ (501-1000) │
│ 50GB │ │ 50GB │
└─────────────┘ └─────────────┘
```
**參考架構圖資源**:
- [Azure Sharding Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/sharding)
- [GeeksforGeeks Database Sharding Guide](https://www.geeksforgeeks.org/system-design/database-sharding-a-system-design-concept/)
- [DigitalOcean Sharding Tutorial](https://www.digitalocean.com/community/tutorials/understanding-database-sharding)
---
## 11.4 複製(Replication)與一致性(Consistency)
雖然你章節標題沒有特別寫「複製」,但在分散式資料庫中,分片與複製是一起討論的。
### 11.4.1 複製目的
1. **容錯**:單一節點故障不會造成資料遺失
2. **讀取擴充**:讀取可以分散到多個副本
3. **跨區部署**:資料靠近使用者或系統所在區域
### 11.4.2 常見模式
#### 1. Master-Slave Replication(主從複製)
**架構圖**:
```
Master-Slave Replication:
┌──────────┐
│ Master │ ← All Writes
│ Database │
└─────┬────┘
│ Replication Log
┌─────────────┼─────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Slave1 │ │ Slave2 │ │ Slave3 │
│ (讀) │ │ (讀) │ │ (讀) │
└────────┘ └────────┘ └────────┘
```
**運作流程**:
1. 所有寫入走 master
2. Master 將變更記錄到 binary log/WAL
3. Slave 從 master 拉取並重放日誌
4. 讀取可以分散到任何 slave
**優點**:
- 實作簡單
- 讀取可擴充
- 備份不影響主庫
**缺點**:
- 寫入無法擴充(單一 master 瓶頸)
- 存在複製延遲(Replication Lag)
- Master 故障需要手動或自動提升 slave
**一致性等級**:
- **同步複製**:等待所有 slave 確認(強一致,但慢)
- **非同步複製**:不等待 slave(快,但最終一致)
- **半同步複製**:等待至少一個 slave(平衡)
```sql
-- MySQL 配置半同步複製
-- 在 Master 上
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
-- 在 Slave 上
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
```
**Failover 處理**:
```
Master 故障處理流程:
1. 偵測 master 不可用
2. 選擇最新的 slave (檢查 replication lag)
3. 將選定的 slave 提升為新 master
4. 更新其他 slave 指向新 master
5. 更新應用程式連線
```
#### 2. Multi-Master Replication(多主複製)
**架構圖**:
```
Multi-Master Replication:
┌──────────┐ ←──────→ ┌──────────┐
│ Master 1 │ 雙向複製 │ Master 2 │
│(讀 + 寫) │ ←──────→ │(讀 + 寫) │
└──────────┘ └──────────┘
↕ ↕
可接受寫入 可接受寫入
```
**優點**:
- 寫入可擴充
- 無單點故障
- 支援跨區域寫入
- 高可用性
**缺點**:
- 寫入衝突處理複雜
- 需要衝突解決策略
- 一致性較弱(通常是最終一致)
**衝突解決策略**:
1. **Last Write Wins (LWW)**:使用時間戳,最後寫入者勝出
2. **Application-level resolution**:應用層處理衝突
3. **CRDT (Conflict-free Replicated Data Types)**:無衝突資料型別
```python
# 衝突解決示例 - Last Write Wins
def resolve_conflict(value1, value2):
if value1.timestamp > value2.timestamp:
return value1
else:
return value2
```
#### 3. Google Spanner: 全球分散式強一致
Google Spanner 則採用「同步複製 + TrueTime」實作強一致性分散式交易,被視為 global-scale 分散式資料庫的代表架構。
**Spanner 架構核心元件**:
```
Spanner Architecture:
┌──────────────────────────────────────────┐
│ Spanner Universe │
├──────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │ Zone 1 │ │ Zone 2 │ ... │
│ ├────────────┤ ├────────────┤ │
│ │ Spanserver │ │ Spanserver │ │
│ │ - Tablet │ │ - Tablet │ │
│ │ - Paxos │ │ - Paxos │ │
│ └────────────┘ └────────────┘ │
│ ↓ ↓ │
│ ┌─────────────────────────────┐ │
│ │ Colossus (Storage) │ │
│ │ Distributed File System │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ TrueTime API │ │
│ │ GPS + Atomic Clocks │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────┘
```
**關鍵技術**:
1. **Paxos 共識算法**:確保副本間一致性
2. **TrueTime API**:提供全球時間戳,誤差界限在微秒級
3. **2PC (Two-Phase Commit)**:跨分片事務
**TrueTime 如何運作**:
```
TrueTime API:
┌────────────────────────────────┐
│ TrueTime.now() │
│ Returns: [earliest, latest] │
│ │
│ 例如: [t-ε, t+ε] │
│ 其中 ε 是時鐘不確定性 │
└────────────────────────────────┘
Transaction Commit:
1. 取得 TrueTime: [t1, t2]
2. 等待直到 t2 確定已過去
3. 此時可以保證全球順序一致性
```
**Spanner 的資料層級**:
```
Spanner Data Hierarchy:
Universe
└─ Database
└─ Table
└─ Tablet (Split)
└─ Paxos Group
└─ Replica 1, 2, 3, ...
```
**Interleaving (父子表交錯)**:
```sql
-- Spanner 的 Interleaving 範例
CREATE TABLE Customers (
customer_id INT64,
name STRING(100),
) PRIMARY KEY (customer_id);
CREATE TABLE Invoices (
customer_id INT64,
invoice_id INT64,
amount FLOAT64,
) PRIMARY KEY (customer_id, invoice_id),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;
```
這樣 Customer 和其 Invoices 會儲存在同一個分片上,避免跨節點查詢。
**參考資源**:
- [Google Spanner 論文 (OSDI 2012)](https://research.google.com/archive/spanner-osdi2012.pdf)
- [Spanner: Google's Globally-Distributed Database](https://research.google/pubs/spanner-googles-globally-distributed-database-2/)
- [Life of Spanner Reads & Writes](https://cloud.google.com/spanner/docs/whitepapers/life-of-reads-and-writes)
### 11.4.3 一致性模型比較
| 一致性模型 | 說明 | 優點 | 缺點 | 適用場景 |
|-----------|------|------|------|----------|
| 強一致性 | 所有副本即時同步 | 資料一致 | 效能較差 | 金融交易 |
| 最終一致性 | 副本最終會同步 | 高效能 | 可能讀到舊資料 | 社交媒體 |
| 因果一致性 | 保證因果關係順序 | 平衡效能與一致性 | 實作複雜 | 協作應用 |
| 讀寫法定人數 | N 個副本中 R+W>N | 可調整一致性 | 需要協調 | 通用分散式系統 |
在教學或計畫書中,通常會先讓讀者理解:「分片解決規模問題,複製解決可靠性問題;一致性則是兩者之間的協調結果。」
---
## 11.5 索引(Index)設計
### 11.5.1 索引的作用
索引是為了加速查詢,類似「書本的目錄」。
設計不當會導致:
- 寫入成本增加(每次寫入需更新多個索引)
- 儲存空間浪費
- 查詢規劃器選錯索引,反而變慢
### 11.5.2 B-Tree vs LSM-Tree
資料庫索引的兩大主流結構:
#### B-Tree (B+ Tree)
**結構圖**:
```
B-Tree Structure:
[50|100]
/ | \
[10|30] [60|80] [110|130]
/ | \ / | \ / | \
[5|8][15][40][55][70][95][105][120][140]
↓
Data Pages
```
**特性**:
- 平衡樹結構,深度一致
- 每個節點包含多個 key
- 葉節點包含實際資料或資料指標
- 支援原地更新(in-place update)
**讀寫特性**:
| 操作 | B-Tree | 說明 |
|------|--------|------|
| 讀取 | O(log N) | 優秀的讀取效能 |
| 寫入 | 較慢 | 需要隨機寫入,可能觸發頁分裂 |
| 範圍查詢 | 優秀 | 葉節點連結,順序掃描 |
| 空間效率 | 中等 | 可能有碎片化 |
**優點**:
- 讀取效能優秀
- 成熟穩定,廣泛使用
- 支援範圍查詢
- 點查詢快速
**缺點**:
- 寫入放大(Write Amplification)較高
- 隨機寫入效能較差(特別是 HDD)
- 可能需要碎片整理
**使用案例**:
- PostgreSQL、MySQL InnoDB
- Oracle Database
- SQLite
- 適合讀多寫少的場景
#### LSM-Tree (Log-Structured Merge-Tree)
**結構圖**:
```
LSM-Tree Architecture:
┌─────────────────────────────────────┐
│ Memtable (記憶體) │
│ 寫入首先進入這裡,快速寫入 │
└──────────────┬──────────────────────┘
│ 達到閾值時 flush
▼
┌─────────────────────────────────────┐
│ Level 0: SSTable Files (2-4 個) │
│ [SST1] [SST2] [SST3] [SST4] │
└──────────────┬──────────────────────┘
│ Compaction
▼
┌─────────────────────────────────────┐
│ Level 1: SSTable Files (~10 個) │
│ [SST...] × N │
└──────────────┬──────────────────────┘
│ Compaction
▼
┌─────────────────────────────────────┐
│ Level 2: SSTable Files (~100 個) │
│ [SST...] × N │
└──────────────┬──────────────────────┘
│
...
│
┌─────────────────────────────────────┐
│ Level N: 最大最冷的資料 │
└─────────────────────────────────────┘
```
**寫入流程**:
```
LSM-Tree Write Path:
1. Write → WAL (Write-Ahead Log)
↓
2. Write → Memtable (in-memory)
↓ (when full)
3. Flush → SSTable (Level 0)
↓ (background)
4. Compaction → Merge to Level 1, 2, ...
```
**讀取流程**:
```
LSM-Tree Read Path:
1. Check Memtable
├─ Found? → Return
└─ Not Found → Continue
2. Check Bloom Filter (每層)
├─ Negative? → Skip this SSTable
└─ Possible? → Continue
3. Search Level 0 SSTables
├─ Found? → Return
└─ Not Found → Continue
4. Search Level 1, 2, ... (由新到舊)
└─ Found? → Return
```
**特性**:
- 分層儲存結構
- 寫入先進記憶體,再批次寫入磁碟
- 使用 Compaction 合併和清理資料
- 支援 Bloom Filter 加速查詢
**Compaction 策略**:
1. **Size-Tiered Compaction (STCS)**:
- 相似大小的 SSTable 合併
- 寫入放大較小
- 空間放大較大
2. **Leveled Compaction (LCS)**:
- 嚴格分層,每層大小固定
- 空間放大較小
- 寫入放大較大
```
Compaction 示例:
Level 0: [A][B][C][D] (4個 SSTables, 每個 10MB)
↓ Compact
Level 1: [Merged_ABCD] (1個 SSTable, 40MB)
```
**讀寫特性**:
| 操作 | LSM-Tree | 說明 |
|------|----------|------|
| 寫入 | O(1) amortized | 優秀的寫入效能 |
| 讀取 | O(log N) - O(N) | 可能需要查多層 |
| 範圍查詢 | 較慢 | 需要合併多個 SSTable |
| 空間效率 | 較高 | 但有空間放大 |
**優點**:
- 寫入效能優秀(順序寫入)
- 適合 SSD
- 壓縮效率高
- 寫入放大可控
**缺點**:
- 讀取可能較慢(需查多層)
- Compaction 會消耗 CPU 和 I/O
- 讀放大(Read Amplification)較高
- 空間放大需要管理
**使用案例**:
- RocksDB、LevelDB
- Apache Cassandra
- ScyllaDB
- HBase
- 適合寫多讀少的場景
#### 效能放大比較
```
放大係數比較:
┌──────────────┬──────────┬──────────┐
│ 放大類型 │ B-Tree │ LSM-Tree │
├──────────────┼──────────┼──────────┤
│ 寫入放大 │ 較高 │ 較低 │
│ 讀取放大 │ 較低 │ 較高 │
│ 空間放大 │ 中等 │ 中等 │
└──────────────┴──────────┴──────────┘
寫入放大 = 實際寫入磁碟 / 應用寫入
讀取放大 = 實際讀取磁碟 / 應用讀取
空間放大 = 實際佔用空間 / 資料大小
```
**混合架構**:
某些現代資料庫(如 CockroachDB)採用混合方式:
- 底層使用 LSM-Tree 儲存引擎
- 上層提供 B-Tree 語意的 SQL 介面
### 11.5.3 實務設計原則
#### 1. 以查詢為中心設計索引
```sql
-- 分析常見查詢
-- 查詢 1: 按組織和時間範圍查詢
SELECT * FROM events
WHERE org_id = ?
AND created_at BETWEEN ? AND ?;
-- 查詢 2: 按狀態查詢
SELECT * FROM tasks
WHERE org_id = ?
AND status = 'pending';
-- 針對性建立索引
CREATE INDEX idx_events_org_time
ON events(org_id, created_at);
CREATE INDEX idx_tasks_org_status
ON tasks(org_id, status);
```
#### 2. 複合索引(Composite Index)
索引欄位順序很重要:
```sql
-- ✅ 正確:最左前綴原則
CREATE INDEX idx_composite
ON table (org_id, user_id, created_at);
-- 支援以下查詢:
-- 1. WHERE org_id = ?
-- 2. WHERE org_id = ? AND user_id = ?
-- 3. WHERE org_id = ? AND user_id = ? AND created_at > ?
-- ❌ 無法有效支援:
-- WHERE user_id = ? (沒有 org_id)
-- WHERE created_at > ? (沒有前面的欄位)
```
**索引順序選擇策略**:
1. 等值查詢欄位在前
2. 範圍查詢欄位在後
3. 選擇性高的欄位在前
```sql
-- 範例:按查詢頻率和選擇性設計
-- 假設查詢模式:
-- - org_id: 高選擇性,總是使用
-- - status: 低選擇性 (只有幾個值)
-- - created_at: 範圍查詢
CREATE INDEX idx_optimal
ON orders (org_id, created_at, status);
-- org_id 在最前(高選擇性 + 總是使用)
-- created_at 其次(範圍查詢)
-- status 最後(低選擇性)
```
#### 3. 避免過度索引
```sql
-- ❌ 過度索引示例
CREATE INDEX idx1 ON users(email);
CREATE INDEX idx2 ON users(email, name); -- 可能多餘
CREATE INDEX idx3 ON users(email, name, age); -- 更多餘
CREATE INDEX idx4 ON users(created_at);
CREATE INDEX idx5 ON users(updated_at);
CREATE INDEX idx6 ON users(is_active); -- 低選擇性,效益低
-- ✅ 精簡索引
CREATE UNIQUE INDEX idx_email ON users(email); -- 唯一索引
CREATE INDEX idx_active_created
ON users(is_active, created_at)
WHERE is_active = true; -- 部分索引
```
**索引成本評估**:
```
每個索引的代價:
┌────────────────────┬─────────────────┐
│ 操作 │ 影響 │
├────────────────────┼─────────────────┤
│ INSERT │ 每個索引 +1 寫入 │
│ UPDATE (索引欄位) │ 每個索引重建 │
│ DELETE │ 每個索引標記刪除 │
│ 儲存空間 │ 索引大小累加 │
│ 記憶體 │ 索引緩存空間 │
└────────────────────┴─────────────────┘
```
#### 4. 分散式資料庫中的二級索引
在 Spanner 這類系統中,二級索引本身也會被分片與複製:
```sql
-- Spanner 二級索引範例
CREATE INDEX idx_user_email
ON users(email)
STORING (name, created_at); -- 包含額外欄位避免回表
-- 此索引會:
-- 1. 自己被分片儲存
-- 2. 被複製以確保可用性
-- 3. 與主表可能不在同一個分片
```
**全局索引 vs 局部索引**:
```
Global Index (全局索引):
┌─────────────────────────────┐
│ 索引表(獨立分片) │
│ key → shard_location │
└─────────────────────────────┘
優點: 查詢快,缺點: 寫入慢
Local Index (局部索引):
每個資料分片維護自己的索引
優點: 寫入快,缺點: 查詢需要掃描所有分片
```
#### 5. 索引維護策略
```sql
-- 監控索引使用情況 (PostgreSQL)
SELECT
schemaname,
tablename,
indexname,
idx_scan, -- 索引被使用次數
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0 -- 找出從未使用的索引
ORDER BY schemaname, tablename;
-- 重建碎片化的索引
REINDEX INDEX idx_name;
-- 分析索引效能
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'test@example.com';
```
### 11.5.4 進階索引技術
#### 1. 部分索引(Partial Index)
```sql
-- 只索引活躍用戶
CREATE INDEX idx_active_users
ON users(email)
WHERE is_active = true;
-- 節省空間且提高效能
```
#### 2. 表達式索引(Expression Index)
```sql
-- 對計算欄位建索引
CREATE INDEX idx_lower_email
ON users(LOWER(email));
-- 支援不區分大小寫查詢
SELECT * FROM users
WHERE LOWER(email) = 'test@example.com';
```
#### 3. 覆蓋索引(Covering Index)
```sql
-- 包含查詢所需的所有欄位,避免回表
CREATE INDEX idx_covering
ON orders(user_id, created_at)
INCLUDE (amount, status);
-- 此查詢可以只從索引取得資料
SELECT amount, status
FROM orders
WHERE user_id = ?
AND created_at > ?;
```
**參考資源**:
- [TiKV: B-Tree vs LSM-Tree 詳細分析](https://tikv.org/deep-dive/key-value-engine/b-tree-vs-lsm/)
- [USENIX: Revisiting B+-tree vs. LSM-tree](https://www.usenix.org/publications/loginonline/revisit-b-tree-vs-lsm-tree-upon-arrival-modern-storage-hardware-built)
---
## 11.6 快取(Caching)設計
### 11.6.1 為什麼需要快取
當相同的查詢或資料被頻繁讀取時,直接打資料庫會造成:
- 資料庫負載升高
- 延遲變大
- 成本增加
透過快取,可以把熱門資料放在較快的媒介(記憶體/分散式快取),降低主資料庫負擔。
**快取效益**:
```
無快取 vs 有快取:
┌────────────────────┬──────────┬──────────┐
│ 指標 │ 無快取 │ 有快取 │
├────────────────────┼──────────┼──────────┤
│ 平均回應時間 │ 100ms │ 5ms │
│ 資料庫 QPS │ 1000 │ 100 │
│ 快取命中率 │ N/A │ 90% │
│ 成本(相對) │ 1.0x │ 0.3x │
└────────────────────┴──────────┴──────────┘
```
### 11.6.2 快取層級
#### 1. 應用程式內快取(In-Process Cache)
```python
# 使用 LRU Cache
from functools import lru_cache
@lru_cache(maxsize=1000)
def get_user_profile(user_id):
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
```
**優點**:
- 延遲最低(無網路開銷)
- 實作簡單
- 無額外成本
**缺點**:
- 無法跨伺服器共享
- 記憶體受限於單機
- 擴展困難
**適用場景**:
- 單體應用
- 計算密集型快取
- 配置資料
#### 2. 分散式快取(Redis / Memcached)
**架構圖**:
```
分散式快取架構:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ App 1 │ │ App 2 │ │ App 3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼─────────────┘
▼
┌─────────────────┐
│ Redis Cluster │
│ ┌───┬───┬───┐ │
│ │ M1│ M2│ M3│ │
│ └───┴───┴───┘ │
└─────────────────┘
▼
┌─────────────────┐
│ Database │
└─────────────────┘
```
**Redis 特性**:
- 支援多種資料結構(String, Hash, List, Set, Sorted Set)
- 持久化選項(RDB, AOF)
- 發布/訂閱功能
- Lua 腳本支援
- 叢集模式
**Memcached 特性**:
- 純記憶體,無持久化
- 簡單的 key-value 儲存
- 多執行緒
- 極致效能
**比較**:
| 特性 | Redis | Memcached |
|------|-------|-----------|
| 資料結構 | 豐富(5+種) | 簡單(key-value) |
| 持久化 | 支援 | 不支援 |
| 複製 | 支援 | 不支援 |
| 事務 | 支援 | 不支援 |
| Lua 腳本 | 支援 | 不支援 |
| 多執行緒 | 單執行緒* | 多執行緒 |
| 效能 | 優秀 | 極致 |
*Redis 6.0 後支援多執行緒 I/O
**適合存放的資料**:
- Session 資料
- 使用者設定
- 熱門查詢結果
- 排行榜(使用 Sorted Set)
- 計數器
- 即時資料
```python
# Redis 使用範例
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 快取使用者資料
def get_user(user_id):
cache_key = f"user:{user_id}"
# 1. 先查快取
cached = r.get(cache_key)
if cached:
return json.loads(cached)
# 2. 查詢資料庫
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
# 3. 寫入快取(設定過期時間)
r.setex(cache_key, 3600, json.dumps(user))
return user
```
### 11.6.3 快取更新策略
#### 1. Cache-Aside (Lazy Loading)
**流程圖**:
```
Cache-Aside Pattern:
┌─────────┐
│ App │
└────┬────┘
│
│ 1. Read
▼
┌──────────┐ 2. Miss
│ Cache │ ────────┐
└──────────┘ │
▲ │
│ │ 3. Query
│ ▼
│ ┌──────────┐
│ 4.Write │ Database │
└─────────└──────────┘
5. Return Data
```
**程式碼範例**:
```python
def get_data(key):
# 1. 嘗試從快取讀取
data = cache.get(key)
# 2. 快取命中,直接返回
if data is not None:
return data
# 3. 快取未命中,查詢資料庫
data = database.query(key)
# 4. 寫入快取
cache.set(key, data, ttl=3600)
# 5. 返回資料
return data
def update_data(key, value):
# 1. 更新資料庫
database.update(key, value)
# 2. 刪除快取(或更新快取)
cache.delete(key)
# 或: cache.set(key, value, ttl=3600)
```
**優點**:
- 實作簡單
- 只快取實際需要的資料
- 快取失效後自動重新載入
**缺點**:
- 首次讀取慢(cache miss)
- 可能出現快取不一致
- 驚群效應(Thundering Herd)
#### 2. Read-Through
**流程圖**:
```
Read-Through Pattern:
┌─────────┐
│ App │
└────┬────┘
│ 1. Read
▼
┌──────────┐
│ Cache │
│ Layer │
└────┬─────┘
│ 2. Cache Miss
│ (自動查詢)
▼
┌──────────┐
│ Database │
└──────────┘
```
**特點**:
- 快取層自己負責載入資料
- 應用不需要處理快取邏輯
- 類似 Cache-Aside,但邏輯在快取層
#### 3. Write-Through
**流程圖**:
```
Write-Through Pattern:
┌─────────┐
│ App │
└────┬────┘
│ 1. Write
▼
┌──────────┐ 2. Sync Write
│ Cache │ ────────────┐
└──────────┘ │
▼
┌──────────┐
│ Database │
└──────────┘
│
│ 3. Ack
▼
Response
```
**程式碼範例**:
```python
def update_data(key, value):
# 1. 同時寫入快取
cache.set(key, value, ttl=3600)
# 2. 寫入資料庫(同步)
database.update(key, value)
# 3. 兩者都成功才返回
return True
```
**優點**:
- 資料一致性強
- 快取總是最新的
- 讀取效能好
**缺點**:
- 寫入延遲增加(需等待兩個操作)
- 可能寫入不常用的資料
- 系統耦合度高
#### 4. Write-Behind (Write-Back)
**流程圖**:
```
Write-Behind Pattern:
┌─────────┐
│ App │
└────┬────┘
│ 1. Write
▼
┌──────────┐ 2. Async
│ Cache │ ─────────┐
└──────────┘ │
│ │ (Background)
│ 3. Ack │
│ ▼
│ ┌──────────┐
└───────────│ Database │
└──────────┘
```
**特點**:
- 寫入先更新快取
- 背景程序非同步同步到資料庫
- 寫入效能最好,但風險最高
**優點**:
- 寫入效能極佳
- 減少資料庫負載
- 批次寫入效率高
**缺點**:
- 資料可能遺失(快取故障時)
- 實作複雜
- 不適合關鍵資料
```python
# Write-Behind 範例(簡化)
class WriteBehindCache:
def __init__(self):
self.cache = {}
self.dirty_keys = set()
self.start_background_sync()
def write(self, key, value):
# 1. 立即寫入快取
self.cache[key] = value
self.dirty_keys.add(key)
# 2. 立即返回
return True
def background_sync(self):
while True:
time.sleep(5) # 每 5 秒同步一次
for key in list(self.dirty_keys):
value = self.cache[key]
database.update(key, value)
self.dirty_keys.remove(key)
```
### 11.6.4 快取模式比較
| 模式 | 讀取 | 寫入 | 一致性 | 複雜度 | 適用場景 |
|------|------|------|--------|--------|----------|
| Cache-Aside | 應用控制 | 應用控制 | 最終一致 | 低 | 通用場景 |
| Read-Through | 快取自動 | 應用控制 | 最終一致 | 中 | 讀密集 |
| Write-Through | 任意 | 同步寫入 | 強一致 | 中 | 一致性重要 |
| Write-Behind | 任意 | 非同步寫入 | 弱一致 | 高 | 寫密集,可容忍資料遺失 |
### 11.6.5 快取失效策略
#### 1. TTL (Time To Live)
```python
# 設定過期時間
cache.setex("user:123", 3600, user_data) # 1 小時後過期
# 階梯式 TTL
CACHE_TTL = {
'user_profile': 3600, # 1 小時
'config': 86400, # 1 天
'hot_data': 300, # 5 分鐘
'analytics': 7200 # 2 小時
}
```
#### 2. 主動失效(Active Invalidation)
```python
def update_user(user_id, data):
# 1. 更新資料庫
db.update(user_id, data)
# 2. 主動刪除相關快取
cache.delete(f"user:{user_id}")
cache.delete(f"user:profile:{user_id}")
cache.delete(f"user:permissions:{user_id}")
```
#### 3. 標籤式失效(Tag-based Invalidation)
```python
# 使用標籤管理相關快取
def set_with_tags(key, value, tags):
cache.set(key, value)
for tag in tags:
cache.sadd(f"tag:{tag}", key)
def invalidate_by_tag(tag):
keys = cache.smembers(f"tag:{tag}")
for key in keys:
cache.delete(key)
# 使用範例
set_with_tags("user:123:profile", data, ["user:123", "profiles"])
invalidate_by_tag("user:123") # 刪除該用戶所有快取
```
### 11.6.6 快取常見問題與解決方案
#### 1. Cache Stampede (快取雪崩)
**問題**:熱門 key 過期時,大量請求同時打到資料庫
**解決方案**:
```python
import threading
locks = {}
def get_with_lock(key):
# 1. 檢查快取
data = cache.get(key)
if data:
return data
# 2. 使用鎖確保只有一個請求查詢資料庫
if key not in locks:
locks[key] = threading.Lock()
with locks[key]:
# Double-check
data = cache.get(key)
if data:
return data
# 3. 查詢資料庫
data = database.query(key)
# 4. 寫入快取
cache.setex(key, 3600, data)
return data
```
#### 2. Cache Penetration (快取穿透)
**問題**:查詢不存在的資料,繞過快取直接打資料庫
**解決方案**:
```python
# 方案 1: 快取空值
def get_user(user_id):
cached = cache.get(f"user:{user_id}")
if cached == "NULL": # 空值標記
return None
if cached:
return cached
user = db.query(user_id)
if user:
cache.setex(f"user:{user_id}", 3600, user)
else:
cache.setex(f"user:{user_id}", 60, "NULL") # 快取空值,短 TTL
return user
# 方案 2: Bloom Filter
from pybloom_live import BloomFilter
user_bloom = BloomFilter(capacity=1000000, error_rate=0.001)
def get_user_with_bloom(user_id):
# 先檢查 Bloom Filter
if user_id not in user_bloom:
return None # 一定不存在
# 可能存在,繼續正常流程
return get_user(user_id)
```
#### 3. Cache Avalanche (快取崩潰)
**問題**:大量快取同時過期,資料庫瞬間壓力暴增
**解決方案**:
```python
import random
def set_with_jitter(key, value, base_ttl):
# 在基礎 TTL 上加上隨機時間
jitter = random.randint(0, base_ttl // 10)
actual_ttl = base_ttl + jitter
cache.setex(key, actual_ttl, value)
# 使用範例
set_with_jitter("user:123", user_data, 3600) # 3600±360 秒
```
**參考資源**:
- [AWS: Database Caching Strategies Using Redis](https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/caching-patterns.html)
- [Redis.io: Caching Patterns](https://redis.io/solutions/caching/)
- [Azure: Cache-Aside Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside)
---
## 11.7 儲存規劃(Hot / Warm / Cold Storage)
### 11.7.1 多層儲存的動機
不同資料有不同特性:
- **熱資料(Hot)**:近期頻繁存取,例如最近一個月交易
- **溫資料(Warm)**:偶爾查詢,例如過去一年紀錄
- **冷資料(Cold)**:主要為合規與稽核保存,幾乎不查
雲端與混合雲架構中,將資料分層儲存可以同時兼顧成本與效能。
**儲存層級金字塔**:
```
Storage Tier Pyramid:
┌──────────┐
│ Hot │ ← 最快、最貴、容量小
│ (SSD) │
├──────────┤
│ Warm │ ← 中速、中價、容量中
│ (HDD) │
├──────────┤
│ Cold │ ← 最慢、最便宜、容量大
│(Object) │
├──────────┤
│ Frozen │ ← 歸檔、極少存取
│ (Glacier)│
└──────────┘
成本比例(以 AWS 為例):
Hot (SSD): $0.10/GB/月
Warm (HDD): $0.045/GB/月
Cold (S3): $0.023/GB/月
Frozen (Glacier): $0.004/GB/月
```
### 11.7.2 實務規劃範例
#### Hot Storage (熱儲存)
**技術選擇**:
- RDBMS(PostgreSQL / MySQL / 分散式 SQL)
- 高效能儲存、SSD
- 記憶體快取(Redis)
**保留範圍**:最近 3-6 個月業務必要資料
**用途**:
- 線上交易(OLTP)
- 即時查詢
- 儀表板
- 應用程式主資料
**效能指標**:
- 延遲: < 10ms
- IOPS: > 10,000
- 可用性: 99.99%
```sql
-- Hot Storage 資料庫設計
CREATE TABLE transactions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_created (created_at)
)
-- 只保留最近 3 個月
PARTITION BY RANGE (created_at);
-- 建立分區
CREATE TABLE transactions_2025_01
PARTITION OF transactions
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
```
#### Warm Storage (溫儲存)
**技術選擇**:
- 資料倉儲(BigQuery、Snowflake、Redshift)
- 時序資料庫(InfluxDB、TimescaleDB)
- 部分物件儲存
**保留範圍**:1-3 年
**用途**:
- 報表分析(OLAP)
- 模型訓練
- 歷史分析
- 趨勢查詢
**效能指標**:
- 延遲: < 1s
- 批次處理優先
- 可用性: 99.9%
```sql
-- Warm Storage 範例(資料倉儲)
CREATE TABLE analytics_events (
event_id STRING,
user_id INT64,
event_type STRING,
properties JSON,
event_time TIMESTAMP
)
PARTITION BY DATE(event_time)
CLUSTER BY user_id, event_type;
-- 查詢最佳化
SELECT
DATE(event_time) as date,
event_type,
COUNT(*) as count
FROM analytics_events
WHERE event_time BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY 1, 2;
```
#### Cold Storage (冷儲存)
**技術選擇**:
- 物件儲存(S3、GCS、Azure Blob)
- 冷儲存階層
- 備份系統
- 歸檔系統
**保留範圍**:依法規,例如 5-7 年以上
**用途**:
- 合規要求
- 稽核備查
- 災難恢復
- 罕見回溯
**效能指標**:
- 延遲: 分鐘到小時級別
- 檢索成本考量
- 可用性: 99%
```python
# 資料生命週期管理範例(AWS S3)
import boto3
s3 = boto3.client('s3')
# 設定生命週期規則
lifecycle_config = {
'Rules': [
{
'Id': 'Move to Cold Storage',
'Status': 'Enabled',
'Filter': {'Prefix': 'logs/'},
'Transitions': [
# 30 天後移到 S3 IA
{
'Days': 30,
'StorageClass': 'STANDARD_IA'
},
# 90 天後移到 Glacier
{
'Days': 90,
'StorageClass': 'GLACIER'
},
# 365 天後移到 Deep Archive
{
'Days': 365,
'StorageClass': 'DEEP_ARCHIVE'
}
],
# 7 年後刪除
'Expiration': {
'Days': 2555 # 7 * 365
}
}
]
}
s3.put_bucket_lifecycle_configuration(
Bucket='my-bucket',
LifecycleConfiguration=lifecycle_config
)
```
### 11.7.3 資料遷移策略
#### 自動分層(Auto-Tiering)
```
Data Lifecycle Flow:
┌──────────┐ 3個月後 ┌──────────┐ 1年後 ┌──────────┐
│ Hot │ ────────→ │ Warm │ ──────→ │ Cold │
│ SSD │ │ HDD │ │ Object │
└──────────┘ └──────────┘ └──────────┘
即時存取 分析查詢 歸檔合規
自動化策略:
- 按時間自動遷移
- 按存取頻率
- 按業務規則
- 按成本優化
```
#### ETL 流程範例
```python
# 資料分層 ETL 範例
from datetime import datetime, timedelta
class DataTieringETL:
def __init__(self):
self.hot_db = HotDatabase()
self.warm_dw = WarmDataWarehouse()
self.cold_storage = ColdObjectStorage()
def daily_tiering_job(self):
# 1. Hot → Warm: 遷移 90 天前的資料
cutoff_warm = datetime.now() - timedelta(days=90)
old_data = self.hot_db.query(
f"SELECT * FROM events WHERE created_at < '{cutoff_warm}'"
)
if old_data:
# 寫入 Warm
self.warm_dw.batch_insert(old_data)
# 從 Hot 刪除
self.hot_db.delete(
f"DELETE FROM events WHERE created_at < '{cutoff_warm}'"
)
# 2. Warm → Cold: 遷移 1 年前的資料
cutoff_cold = datetime.now() - timedelta(days=365)
archive_data = self.warm_dw.query(
f"SELECT * FROM events WHERE created_at < '{cutoff_cold}'"
)
if archive_data:
# 壓縮並寫入 Cold
compressed = self.compress(archive_data)
self.cold_storage.upload(
f"archive/{cutoff_cold.year}/events.parquet.gz",
compressed
)
# 從 Warm 刪除
self.warm_dw.delete(
f"DELETE FROM events WHERE created_at < '{cutoff_cold}'"
)
```
### 11.7.4 混合查詢策略
當資料分散在多個儲存層級時,需要智能查詢路由:
```python
class UnifiedDataAccessLayer:
def query_events(self, start_date, end_date):
results = []
now = datetime.now()
# 1. 判斷資料範圍分佈
hot_cutoff = now - timedelta(days=90)
warm_cutoff = now - timedelta(days=365)
# 2. 路由到對應的儲存層
if end_date > hot_cutoff:
# 查詢 Hot
hot_results = self.hot_db.query(
start=max(start_date, hot_cutoff),
end=end_date
)
results.extend(hot_results)
if start_date < hot_cutoff and end_date > warm_cutoff:
# 查詢 Warm
warm_results = self.warm_dw.query(
start=max(start_date, warm_cutoff),
end=min(end_date, hot_cutoff)
)
results.extend(warm_results)
if start_date < warm_cutoff:
# 查詢 Cold
cold_results = self.cold_storage.query(
start=start_date,
end=min(end_date, warm_cutoff)
)
results.extend(cold_results)
# 3. 合併結果
return self.merge_and_sort(results)
```
### 11.7.5 Elasticsearch 多層架構案例
Elasticsearch 提供了完整的 Hot-Warm-Cold-Frozen 架構:
```
Elasticsearch Data Tiers:
┌─────────────────────────────────┐
│ Hot Tier (高效能 SSD) │
│ - 最新資料 │
│ - 高頻查詢 │
│ - Indexing + Search │
│ 節點配置: data_hot │
└──────────────┬──────────────────┘
│ ILM Policy
▼
┌─────────────────────────────────┐
│ Warm Tier (低效能 SSD/HDD) │
│ - 較舊資料 │
│ - 低頻查詢 │
│ - Read-only │
│ 節點配置: data_warm │
└──────────────┬──────────────────┘
│
▼
┌─────────────────────────────────┐
│ Cold Tier (HDD/部分掛載) │
│ - 歷史資料 │
│ - 罕見查詢 │
│ - Searchable Snapshots │
│ 節點配置: data_cold │
└──────────────┬──────────────────┘
│
▼
┌─────────────────────────────────┐
│ Frozen Tier (快照) │
│ - 歸檔資料 │
│ - 極少查詢 │
│ - Partial Mount from Snapshot │
│ 節點配置: data_frozen │
└─────────────────────────────────┘
```
**ILM Policy 範例**:
```json
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_age": "7d",
"max_size": "50gb"
}
}
},
"warm": {
"min_age": "30d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
}
}
},
"cold": {
"min_age": "90d",
"actions": {
"searchable_snapshot": {
"snapshot_repository": "my_repository"
}
}
},
"frozen": {
"min_age": "365d",
"actions": {
"searchable_snapshot": {
"snapshot_repository": "my_repository",
"storage": "shared_cache"
}
}
},
"delete": {
"min_age": "2555d",
"actions": {
"delete": {}
}
}
}
}
}
```
### 11.7.6 成本優化計算
**範例計算**:假設有 100TB 資料
```
全部使用 Hot Storage (SSD):
100TB × $0.10/GB/月 = 100,000GB × $0.10 = $10,000/月
分層儲存策略:
- Hot (10TB): 10,000GB × $0.10 = $1,000/月
- Warm (30TB): 30,000GB × $0.045 = $1,350/月
- Cold (60TB): 60,000GB × $0.023 = $1,380/月
總計: $3,730/月
節省: ($10,000 - $3,730) / $10,000 = 62.7%
```
**參考資源**:
- [Elastic: Data Tiers Documentation](https://www.elastic.co/docs/manage-data/lifecycle/data-tiers)
- [Azure: Hot, Warm, Cold Data Path Design](https://www.futurelearn.com/info/courses/introduction-azure-architect-technologies/0/steps/325799)
- [CTERA: Hot vs Warm vs Cold Storage](https://www.ctera.com/company/blog/differences-hot-warm-cold-file-storage/)
---
## 11.8 完整架構整合範例
### 11.8.1 MLOps 場景的完整架構
```
完整 Database & Storage 架構圖:
┌──────────────────────────────────────────────────────────────┐
│ 應用層 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ML 訓練 │ │ 特徵提取 │ │ 推論服務 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼─────────────────────────┘
│ │ │
│ ┌────▼────────────▼────┐
│ │ 快取層 (Redis) │
│ │ - Session │
│ │ - 熱門查詢 │
│ └────────┬─────────────┘
│ │
│ ┌────────▼─────────────┐
│ │ 主資料庫叢集 │
│ │ ┌───┬───┬───┐ │
│ │ │M1 │M2 │M3 │ │
│ │ └───┴───┴───┘ │
│ │ Sharding + Replica │
│ └────────┬─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ Hot Storage │ │ Warm Storage │
│ (3 個月) │ │ (1-3 年) │
│ │ │ │
│ 訓練資料 │ │ 資料倉儲 │
│ 特徵庫 │ │ - BigQuery │
│ 即時推論 │ │ - Snowflake │
└──────┬──────┘ └────────┬────────┘
│ │
│ ┌────────▼────────┐
│ │ Cold Storage │
│ │ (3+ 年) │
│ │ │
└─────────│ - S3 Glacier │
│ - 合規歸檔 │
│ - 模型版本 │
└─────────────────┘
```
### 11.8.2 關鍵決策矩陣
**選擇資料庫時的考量**:
| 需求 | 推薦方案 | 原因 |
|------|----------|------|
| 強一致性交易 | PostgreSQL / Spanner | ACID 保證 |
| 寫入密集 | Cassandra / ScyllaDB | LSM-Tree 優化寫入 |
| 讀取密集 | MySQL / PostgreSQL | B-Tree 優化讀取 |
| 時序資料 | InfluxDB / TimescaleDB | 時間優化 |
| 文件儲存 | MongoDB / CouchDB | 靈活 Schema |
| 全文搜尋 | Elasticsearch | 倒排索引 |
| 圖資料 | Neo4j / JanusGraph | 關係查詢 |
| 快取 | Redis / Memcached | 記憶體速度 |
**分片策略選擇**:
| 場景 | 推薦策略 | 理由 |
|------|----------|------|
| 用戶資料 | Hash Sharding | 均勻分佈 |
| 時序日誌 | Range Sharding + 定期重平衡 | 支援範圍查詢 |
| 多租戶 SaaS | Directory Sharding (按 org_id) | 資料隔離 |
| 全球化應用 | Geographical Sharding | 減少延遲 |
**複製策略選擇**:
| 需求 | 推薦策略 | 權衡 |
|------|----------|------|
| 強一致性 | 同步複製 | 延遲較高 |
| 高吞吐量 | 非同步複製 | 可能資料延遲 |
| 跨區域 | 非同步多主 | 需要衝突解決 |
| 讀取擴展 | 主從複製 | 簡單實作 |
---
## 11.9 最佳實踐總結
### 11.9.1 設計原則
1. **以查詢模式驅動設計**
- 先了解 80% 的查詢模式
- Schema 和索引配合查詢優化
- 避免過早優化
2. **適度反正規化**
- 分散式環境中接受部分重複
- 減少跨節點 JOIN
- 權衡一致性與效能
3. **漸進式擴展**
- 從單機開始,根據需求擴展
- 避免過早分片
- 保留擴展空間
4. **資料生命週期管理**
- 自動化冷熱資料分層
- 定期清理過期資料
- 備份與歸檔策略
5. **監控與調優**
- 持續監控關鍵指標
- 定期檢視慢查詢
- 根據實際負載調整
### 11.9.2 效能優化檢查清單
**資料庫層面**:
- [ ] 主鍵設計合理
- [ ] 索引覆蓋主要查詢
- [ ] 避免過度索引
- [ ] 查詢使用正確的索引
- [ ] 批次操作替代逐筆操作
- [ ] 連線池配置適當
**分片層面**:
- [ ] 分片鍵選擇合理
- [ ] 避免熱點分片
- [ ] 跨分片查詢最小化
- [ ] 自動平衡機制啟用
**快取層面**:
- [ ] 快取命中率 > 80%
- [ ] TTL 設定合理
- [ ] 快取更新策略明確
- [ ] 避免快取穿透/雪崩
**儲存層面**:
- [ ] 冷熱資料分離
- [ ] 生命週期策略自動化
- [ ] 壓縮與歸檔啟用
- [ ] 成本定期審核
### 11.9.3 故障處理與災難恢復
**備份策略**:
```
3-2-1 備份原則:
- 3 份資料副本
- 2 種不同儲存媒介
- 1 份異地備份
備份頻率建議:
┌──────────────┬──────────┬──────────┐
│ 資料類型 │ 頻率 │ 保留期 │
├──────────────┼──────────┼──────────┤
│ 關鍵交易資料 │ 即時 │ 永久 │
│ 業務資料 │ 每日 │ 90 天 │
│ 分析資料 │ 每週 │ 30 天 │
│ 日誌資料 │ 每日 │ 7 天 │
└──────────────┴──────────┴──────────┘
```
**災難恢復(DR)流程**:
```
RTO (Recovery Time Objective) 目標:
┌────────────────┬──────────┐
│ 系統等級 │ RTO │
├────────────────┼──────────┤
│ 關鍵系統 │ < 1 小時│
│ 重要系統 │ < 4 小時│
│ 一般系統 │ < 24小時│
└────────────────┴──────────┘
RPO (Recovery Point Objective) 目標:
┌────────────────┬──────────┐
│ 資料類型 │ RPO │
├────────────────┼──────────┤
│ 金融交易 │ 0 (即時)│
│ 用戶資料 │ < 15 分 │
│ 分析資料 │ < 1 小時│
└────────────────┴──────────┘
```
---
## 11.10 延伸閱讀與參考文獻
### 核心書籍
1. **Martin Kleppmann**, *Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems*, O'Reilly, 2017
- 資料密集型應用的聖經
- 深入探討資料模型、儲存引擎、分散式系統
2. **Alex Petrov**, *Database Internals: A Deep Dive into How Distributed Data Systems Work*, O'Reilly, 2019
- 資料庫內部實作細節
- B-Tree、LSM-Tree 深入分析
### 重要論文
1. **J. C. Corbett et al.**, "Spanner: Google's Globally Distributed Database," OSDI 2012
- https://research.google.com/archive/spanner-osdi2012.pdf
- 全球分散式資料庫的里程碑
2. **Giuseppe DeCandia et al.**, "Dynamo: Amazon's Highly Available Key-value Store," SOSP 2007
- NoSQL 設計的基礎
- 最終一致性模型
3. **Fay Chang et al.**, "Bigtable: A Distributed Storage System for Structured Data," OSDI 2006
- 列式資料庫設計
- LSM-Tree 應用
### 線上資源
**分片與分散式**:
- [Azure Sharding Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/sharding)
- [GeeksforGeeks: Database Sharding](https://www.geeksforgeeks.org/system-design/database-sharding-a-system-design-concept/)
- [DigitalOcean: Understanding Database Sharding](https://www.digitalocean.com/community/tutorials/understanding-database-sharding)
**Google Spanner**:
- [Google Cloud: Spanner 文件](https://cloud.google.com/spanner/docs)
- [Life of Spanner Reads & Writes](https://cloud.google.com/spanner/docs/whitepapers/life-of-reads-and-writes)
- [Spanner Schema Design Best Practices](https://cloud.google.com/spanner/docs/schema-design)
**複製與一致性**:
- [GeeksforGeeks: Replication Types](https://www.geeksforgeeks.org/types-of-database-replication-system-design/)
- [Medium: Master-Master vs Master-Slave](https://medium.com/@techsuneel99/database-replication-master-slave-architecture-1a1246397a22)
**索引結構**:
- [TiKV: B-Tree vs LSM-Tree](https://tikv.org/deep-dive/key-value-engine/b-tree-vs-lsm/)
- [USENIX: B+-tree vs LSM-tree 再探討](https://www.usenix.org/publications/loginonline/revisit-b-tree-vs-lsm-tree-upon-arrival-modern-storage-hardware-built)
- [ScyllaDB: LSM-Tree 詳解](https://www.scylladb.com/glossary/log-structured-merge-tree/)
**快取模式**:
- [AWS: Database Caching Strategies Using Redis](https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/welcome.html)
- [Redis.io: Caching Solutions](https://redis.io/solutions/caching/)
- [Azure: Cache-Aside Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside)
**儲存分層**:
- [Elastic: Data Tiers](https://www.elastic.co/docs/manage-data/lifecycle/data-tiers)
- [Opster: Elasticsearch Hot-Warm-Cold Architecture](https://opster.com/guides/elasticsearch/capacity-planning/elasticsearch-hot-warm-cold-frozen-architecture/)
- [Azure: Hot, Warm, Cold Data Path](https://www.futurelearn.com/info/courses/introduction-azure-architect-technologies/0/steps/325799)
**效能與監控**:
- [Azure: Data Performance Strategies](https://learn.microsoft.com/en-us/azure/architecture/framework/data/performance-best-practices)
- [Google SRE Book: Monitoring Distributed Systems](https://sre.google/sre-book/monitoring-distributed-systems/)
### 實作工具與平台
**關聯式資料庫**:
- PostgreSQL: https://www.postgresql.org/
- MySQL: https://www.mysql.com/
- Google Cloud Spanner: https://cloud.google.com/spanner
**NoSQL 資料庫**:
- MongoDB: https://www.mongodb.com/
- Cassandra: https://cassandra.apache.org/
- ScyllaDB: https://www.scylladb.com/
**快取系統**:
- Redis: https://redis.io/
- Memcached: https://memcached.org/
**資料倉儲**:
- Google BigQuery: https://cloud.google.com/bigquery
- Snowflake: https://www.snowflake.com/
- Amazon Redshift: https://aws.amazon.com/redshift/
**時序資料庫**:
- InfluxDB: https://www.influxdata.com/
- TimescaleDB: https://www.timescale.com/
**搜尋引擎**:
- Elasticsearch: https://www.elastic.co/
- OpenSearch: https://opensearch.org/
---
## 11.11 實戰練習題
### 練習 1: Schema 設計
**場景**:設計一個多租戶的 IoT 資料收集系統
**要求**:
1. 每個組織有多個設備
2. 每個設備每秒產生 10 條資料
3. 需要支援即時查詢和歷史分析
4. 預計 1000 個組織,每個 100 台設備
**思考**:
- 主鍵如何設計?
- 需要哪些索引?
- 如何分片?
- 如何處理冷熱資料?
### 練習 2: 快取策略
**場景**:電商平台商品資訊快取
**要求**:
1. 商品庫存即時性要求高
2. 商品詳情可接受秒級延遲
3. 熱門商品查詢頻繁
4. 促銷期間流量暴增
**思考**:
- 使用哪種快取模式?
- TTL 如何設定?
- 如何避免快取雪崩?
- 庫存更新如何處理?
### 練習 3: 儲存分層
**場景**:醫療影像系統
**要求**:
1. 最近 1 個月影像需要快速存取
2. 1 年內影像偶爾查詢
3. 歷史影像需保留 10 年(合規要求)
4. 影像檔案大小 10-50MB
**思考**:
- 如何設計儲存分層?
- 自動化策略如何實作?
- 查詢時如何路由?
- 成本如何優化?
---
## 附錄:架構圖索引
本教材中提到的所有架構圖資源整理:
### 分片架構
1. [Azure Sharding Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/sharding)
2. [Database Sharding Patterns](https://www.geeksforgeeks.org/system-design/database-sharding-a-system-design-concept/)
### 複製架構
1. [Master-Slave Replication](https://www.geeksforgeeks.org/dbms/single-master-and-multi-master-replication-in-dbms/)
2. [Master-Master Replication](https://akashrajpurohit.com/blog/exploring-master-master-replication-in-databases-how-it-works-and-its-benefits/)
### Google Spanner
1. [Spanner Architecture Overview](https://research.google/pubs/spanner-googles-globally-distributed-database-2/)
2. [Life of Reads and Writes](https://cloud.google.com/spanner/docs/whitepapers/life-of-reads-and-writes)
### 索引結構
1. [B-Tree vs LSM-Tree](https://tikv.org/deep-dive/key-value-engine/b-tree-vs-lsm/)
2. [Database Index Structures](https://dev.to/aws-builders/understanding-database-indexes-and-their-data-structures-hashes-ss-tables-lsm-trees-and-b-trees-2dk5)
### 快取模式
1. [AWS Caching Patterns](https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/caching-patterns.html)
2. [Redis Caching Solutions](https://redis.io/solutions/caching/)
### 儲存分層
1. [Elasticsearch Data Tiers](https://www.elastic.co/docs/manage-data/lifecycle/data-tiers)
2. [Hot-Warm-Cold Architecture](https://opster.com/guides/elasticsearch/capacity-planning/elasticsearch-hot-warm-cold-frozen-architecture/)
---
**文件版本**: v1.0
**最後更新**: 2025-11-16
**作者**: Database Architecture Team
**授權**: CC BY-NC-SA 4.0