Skip to content
DEE
Database Engineering Essentials

[DEE-403] 列族建模

INFO

圍繞查詢而非實體來建模表格。在像 Apache Cassandra 這樣的列族存儲中,你為每個查詢模式設計一個表格、接受反正規化,並讓分區鍵和叢集欄位組織資料以實現高效讀取。

背景

列族資料庫(也稱為寬欄存儲)—— 包括 Apache Cassandra、ScyllaDB 和 Google Bigtable —— 專為大量寫入吞吐量、水平擴展和跨資料中心的高可用性而設計。它們透過使用分區鍵將資料分散到叢集中,並使用叢集欄位在每個分區內組織資料列。

資料模型與關聯式資料庫有本質上的不同。在關聯式資料庫中,你將資料正規化為實體,然後撰寫帶有 join 的查詢來回答問題。在 Cassandra 中,你從需要回答的查詢開始,為每個查詢專門設計一個表格。Join 不存在。反正規化不是妥協 —— 而是預期的設計方法。

資料按層級組織:分區鍵決定哪個節點存放資料,叢集欄位決定該分區內資料列的排序順序。讀取單一分區的查詢是 Cassandra 能執行的最高效操作。跨越多個分區的查詢(scatter-gather)代價高昂,在延遲敏感的路徑上應避免使用。

Cassandra 最佳實踐建議將分區保持在 100 MB 磁碟大小和 100,000 列以下。超過這些限制會造成壓縮壓力、增加讀取延遲,以及修復期間潛在的堆積記憶體壓力。

原則

  • MUST在設計表格前識別所有查詢模式。每個主要存取模式通常對應一個專屬表格。
  • MUST選擇能將資料均勻分散到叢集中且不會無限增長的分區鍵。
  • SHOULD使用叢集欄位在分區內排序資料,讓範圍查詢(例如時間範圍)更有效率。
  • MUST NOT使用次要索引來替代適當的表格設計。Cassandra 中的次要索引需要對所有節點進行 scatter-gather 查詢,在規模化時效能不佳。
  • SHOULD接受反正規化並跨表格複製資料以服務不同的查詢模式。在 Cassandra 中寫入是廉價的;跨分區讀取是昂貴的。

視覺化

範例

時間序列感測器資料

查詢:「取得感測器 X 在時間 T1 到 T2 之間的所有讀數。」

cql
CREATE TABLE sensor_readings (
    sensor_id    TEXT,
    reading_date DATE,
    reading_ts   TIMESTAMP,
    temperature  DOUBLE,
    humidity     DOUBLE,
    pressure     DOUBLE,
    PRIMARY KEY ((sensor_id, reading_date), reading_ts)
) WITH CLUSTERING ORDER BY (reading_ts DESC);

關鍵設計決策:

  • 複合分區鍵 (sensor_id, reading_date) —— 按感測器和日期分散資料,防止任何單一分區無限增長(每個分區一天的讀數)。
  • 叢集欄位 reading_ts DESC —— 最新讀數排在前面,符合最常見的查詢模式(最近讀數)。
cql
-- 高效:單一分區讀取搭配叢集欄位的範圍查詢
SELECT * FROM sensor_readings
WHERE sensor_id = 's-001'
  AND reading_date = '2025-06-01'
  AND reading_ts >= '2025-06-01T08:00:00Z'
  AND reading_ts <= '2025-06-01T09:00:00Z';

使用者活動動態

查詢 1:「取得使用者 X 最新的 20 筆活動。」 查詢 2:「取得使用者 X 在特定日期的所有活動。」

cql
CREATE TABLE user_activity (
    user_id      UUID,
    activity_date DATE,
    activity_ts  TIMESTAMP,
    activity_type TEXT,
    details      TEXT,
    PRIMARY KEY ((user_id, activity_date), activity_ts)
) WITH CLUSTERING ORDER BY (activity_ts DESC);
cql
-- 今天的最新活動
SELECT * FROM user_activity
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000
  AND activity_date = '2025-06-15'
LIMIT 20;

與關聯式的比較:每個查詢模式一個表格

關聯式方法Cassandra 方法
一個 orders 表格,使用不同的 WHERE 子句和 JOIN 查詢orders_by_customer 表格用於「依客戶取得訂單」
同一表格,不同索引orders_by_status 表格用於「依狀態取得訂單」
同一表格,不同 joinorders_by_date 表格用於「依日期範圍取得訂單」
正規化以避免冗餘跨三個表格複製訂單資料
新增索引以支援新查詢為新查詢模式建立新表格

在 Cassandra 中,反正規化的儲存成本與跨分區查詢的延遲成本相比微不足道。

常見錯誤

錯誤為何有害修正方式
無限分區增長 —— 僅使用 sensor_id 作為時間序列資料的分區鍵單一感測器的分區永遠增長。大型分區造成壓縮壓力、GC 暫停和讀取緩慢。在分區鍵中加入時間分桶(例如 (sensor_id, reading_date))以限制分區大小
過多的小分區 —— 使用高基數的叢集欄位作為分區鍵的一部分每個查詢需要讀取許多微小分區(scatter-gather)。讀取延遲隨分區數量線性增長。設計分區鍵使單一查詢從一個分區讀取。將高基數欄位移到叢集欄位。
過度依賴次要索引 —— 使用 CREATE INDEX 來避免建立新表格Cassandra 中的次要索引是本地索引,需要查詢每個節點。它們適用於低基數欄位和小結果集,但在規模化時會失效。為查詢模式建立專屬表格。謹慎使用實體化視圖(它們有已知的一致性問題)。
關聯式思維 —— 將資料正規化到不同表格並嘗試多表查詢Cassandra 沒有 join。從多個表格讀取需要多次往返,增加延遲和失敗機率。積極反正規化。將資料寫入每個需要它的表格。僅在為同一分區鍵維護反正規化表格的一致性時使用批次(LOGGED BATCH)。
在生產查詢中使用 ALLOW FILTERINGALLOW FILTERING 強制執行全表掃描。它是為臨時探索而存在的,不是為生產工作負載。重新設計表格,使查詢能僅靠分區鍵 + 叢集欄位限制來完成。

相關 DEE

參考資料