01

總覽與痛點

先問「為什麼需要」,再談產品名稱——Wide-column 解決的是海量寫入與已知查詢下的水平擴展。

想像你要做一個訊息收件箱:每個使用者每天收到數百則訊息,系統要支援「打開收件箱 → 依時間倒序看最近 50 則」。若用關聯式一張大表加索引,寫入高峰時 B-tree 索引維護與熱頁競爭會先撞牆;若用純 KV,又很難在單一 key 下做「時間範圍掃描」。

寬列儲存(Wide-column Store)正是為這類場景而生:資料依 Partition Key 水平切到不同節點,同一 partition 內再用 Clustering Key 排序,讓「已知查詢」變成打到少數節點的範圍掃描。欄位可以稀疏、動態出現——同一張表裡,有人只有 3 個欄位、有人有 50 個,都不必先宣告完整 schema。

座標它在儲存版圖的位置

• 關聯式:任意 JOIN / ad-hoc 查詢強,水平寫入擴展弱。
• Document:單文件靈活,跨文件關聯與大規模範圍掃描通常要另想辦法。
• 純 KV:延遲極低,但 value 是不透明 blob,範圍查詢靠應用層拼。
• Wide-column:為已知 access pattern 優化——用 partition 換寫入擴展,用 clustering 換範圍掃描。

何時不該用先排除這些需求

若你需要頻繁的跨實體 JOIN、複雜交易(多列 ACID)、或營運隨時發明的 ad-hoc 報表——先回到關聯式或「關聯式 + 搜尋/分析側車」。Wide-column 的代價是:查詢必須在建模時就想好,事後改 access pattern 往往等於重建表。

一句話答法Wide-column 用 partition 水平切寫入、用 clustering 做 partition 內範圍掃描,適合海量、已知查詢的場景;它不是「沒 schema 的 SQL」。

02

一列長什麼樣子

先建立畫面:什麼叫「寬」、什麼叫「稀疏」,以及一筆資料在 partition 裡如何堆疊。

先忘掉產品名稱。把收件箱想成:每個使用者是一個抽屜(partition),抽屜裡的訊息依時間排好(clustering),每則訊息可以有不同欄位(稀疏)。

Partition Key
user_id
Clustering Key
message_id(時間倒序)
動態欄位(稀疏)
alice msg-1003 sender, subject, body, attachment_url
msg-1002 sender, subject, body
msg-1001 sender, subject (無 body——欄位可缺)
bob msg-2001 sender, subject, body, priority

同一張「表」裡:alice 的 partition 有 3 則訊息、bob 只有 1 則;有的列多了 attachment,有的缺 body——這就是「寬」與「稀疏」。

術語對照(先記概念,產品細節留到第 5 節再說):

  • Partition(分片單元):同一 Partition Key 的所有資料必須落在同一組 replica 上,才能做高效範圍掃描。
  • Clustering row:partition 內依 Clustering Key 排序的一「列」;查詢常是「某個 partition 裡最近 N 列」。
  • Column / Qualifier:列上的動態欄位名;不必像關聯式先 CREATE 所有欄位。
  • 多版本 / TTL:cell 可帶時間戳或過期時間,適合時序與「只留最近 N 版」。
關鍵「寬」指的是什麼

不是說每一列都有幾百個欄位,而是:一個 partition 可以水平長出很多 clustering rows,每列又可掛不同欄位。關聯式的「一列」通常對應固定欄位;wide-column 的「一列」更像「某個排序鍵下的一組 cell」。

一句話答法Wide-column 的基本畫面是「Partition Key 決定抽屜,Clustering Key 決定抽屜內排序,欄位可稀疏動態出現」。

03

Partition Key 與 Clustering Key

PRIMARY KEY 在這裡決定「資料落哪台機器」與「partition 內怎麼掃」——不是關聯式那種單純唯一約束。

以 Cassandra 的 CQL 為例(語意最清楚;HBase 用 Row Key 設計表達同一件事):

-- 收件箱:依 user_id 分 partition,依 message_id 排序
CREATE TABLE inbox_by_user (
  user_id       UUID,
  message_id    TIMEUUID,
  sender        TEXT,
  subject       TEXT,
  body          TEXT,
  PRIMARY KEY (user_id, message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);

-- 高效:一定帶 partition key
SELECT * FROM inbox_by_user
WHERE user_id = ? AND message_id > ?
LIMIT 50;
  • Partition Key(上例的 user_id):決定 token → 落在哪個節點的哪組 replica。同一 key 的資料必定在一起。
  • Clustering Key(上例的 message_id):只在 partition 內排序,支援 WHERE pk = ? AND ck > ? 這類區間。
  • Static column(進階):屬於整個 partition 的共用欄位(例如使用者顯示名稱),不隨每則訊息重複存。
陷阱沒帶 Partition Key = 全叢集掃描

沒有 partition key 的 WHERE 會變成 scatter-gather:coordinator 問遍所有節點再合併。節點愈多愈慢。ALLOW FILTERING 不是救命稻草,是生產紅燈。設計時先列出 Top 查詢,再反推 schema。

查詢型態 行為 延遲直覺
帶 PK 的 point lookup 路由到少數 replica,讀單列 最低
帶 PK + clustering 範圍 同一 partition 內順序掃描 低(wide-column 強項)
無 PK / 低基數當 PK 全叢集掃描,或熱點擠在單一 partition 危險
一句話答法Partition key 決定資料落哪組機器;clustering key 決定 partition 內怎麼排、怎麼掃——沒帶 partition key 的查詢等同全表掃描。

04

依查詢設計資料模型

思維翻轉:不是先正規化再優化,而是「一個查詢一張(物化)表」。

關聯式教科書:消除冗餘 → 正規化 → 需要時再 denormalize。Wide-column 幾乎反過來:先列出讀寫查詢,再為每條查詢設計 table。沒有跨 partition JOIN。

實戰:電商訂單的三個查詢

  • Q1:依 user_id 看最近訂單列表
  • Q2:依 order_id 查單筆詳情
  • Q3:依 product_id 看某商品近期訂單(營運,流量較低可批次)
-- Q1:使用者時間線
CREATE TABLE orders_by_user (
  user_id UUID, order_id TIMEUUID, total DECIMAL, status TEXT,
  PRIMARY KEY (user_id, order_id)
) WITH CLUSTERING ORDER BY (order_id DESC);

-- Q2:訂單主檔(order_id 當 partition key)
CREATE TABLE orders_by_id (
  order_id UUID PRIMARY KEY,
  user_id UUID, total DECIMAL, status TEXT, items TEXT
);

-- Q3 流量大再建 orders_by_product;否則用批次從 Q2 匯出

寫一筆訂單時發生什麼

t0 · API 使用者送出「建立訂單」。
t1 · 寫入放大 同步(或經 Kafka fan-out)寫入 orders_by_id 與 orders_by_user——同一筆業務事實,存成兩個查詢視角。
t2 · 讀路徑 「我的訂單列表」只打 Q1 的 PK=user_id;「訂單詳情頁」只打 Q2 的 PK=order_id。兩邊都是 partition-local,不需 JOIN。
關鍵寫入放大是付費換讀取

在關聯式這叫冗餘;在 Cassandra 這是為讀取效能付的設計代價。面試要主動講 trade-off,並提到可用 CDC / 訊息佇列做 eventual 同步,降低雙寫失敗導致的視角不一致。

避免 Hot Partition

若把低基數欄位(如 status = pending)當 partition key,幾乎所有熱資料會擠進同一個抽屜,單一節點被打爆。解法包括:複合 partition key(((user_id, bucket)), …)、salt、或乾脆別用低基數當 PK——改物化正確的查詢表。

陷阱Secondary Index 濫用

高基數欄位上的 secondary index 往往變成「每個節點都掃一輪」。正確做法仍是查詢驅動的物化表,搜尋需求另掛 Elasticsearch 等側車。

一句話答法Wide-column 建模是「一個查詢一張表」:用寫入放大與反正規化換 partition-local 讀取,絕不指望 ad-hoc JOIN。

05

Cassandra 與 HBase:兩種架構選擇

模型相同、取捨不同——一個偏線上可用性,一個偏 Hadoop 生態與單區強一致。

學會「partition + clustering」之後,再看兩個常見實作如何落地。

Cassandra:Masterless Ring

無中心 master。節點用 Gossip 交換狀態;client 可連任一節點當 coordinator。寫入時 coordinator 依 partition key 找 replica,並行推送,再依 Consistency Level(CL) 等足夠回應。

W + R > RF → 讀寫重疊,較不易讀到過舊副本

以常見 RF=3 為例:

  • 寫 ONE、讀 ONE:吞吐高,但可能讀到尚未修好的舊副本。
  • 寫 QUORUM(2)、讀 QUORUM(2):W+R=4 > 3,較能讀到較新資料。
  • 多 DC 常用 LOCAL_QUORUM,避免跨洋等 quorum。
Consistency Level 行為 適用
ONE 任一副本回應即成功 高吞吐、可容忍短暫 stale
QUORUM 過半副本確認 需要較新資料
LOCAL_QUORUM 本地 DC 內過半 多 DC,避免跨洋延遲
ALL 全部副本確認 極少用;一節點掛即失敗

HBase:Master-based + HDFS

HMaster 管 Region 分配與 schema(通常不在熱資料路徑上);RegionServer 服務連續 row key 範圍;ZooKeeper 協調選舉。寫入走 WAL → MemStore → HFile(LSM 風格),單 Region 內語意接近強一致。較常出現在「與 Hadoop 批次共存」的離線/近線路徑,而非毫秒級純 OLTP。

維度 Cassandra HBase
控制平面 Gossip,無中心 master HMaster + ZooKeeper
儲存 本地 SSTable HDFS 上的 HFile
預設一致性 最終一致(CL 可調) 單 Region 強一致
最佳場景 線上服務、低延遲寫入 批次分析、離線特徵庫
典型痛點 hot partition、repair 成本 Region split、major compaction IO
陷阱把 HBase 當低延遲 OLTP 銀彈

HBase 擅長高吞吐寫入與大範圍 scan,並與 Hadoop 生態整合;若你要毫秒級線上 API 且無 HDFS 依賴,實務上更常選 Cassandra / Scylla 這類路徑。

一句話答法兩者都是 wide-column 思維;Cassandra 用 masterless + 可調 CL 換線上擴展,HBase 用 master + HDFS 換與批次生態共存的強一致路徑。

06

互動演示:資料長相與查詢路由

同一組控制項同時回答兩件事:這個 partition 裡資料長什麼樣?這次查詢打到哪些節點?

左側是 Wide Row 視圖(partition 內的 clustering rows 與稀疏欄位);右側是 4 節點 Ring(coordinator、replica 命中、scatter-gather、hot partition)。請依序試:

  1. 收件箱 + Partition 範圍——理想路徑:單 partition 範圍掃描。
  2. 訂單 + 帶 PK——單列 lookup。
  3. 錯誤設計 (status) + 帶 PK——看 hot partition(不是全叢集掃描)。
  4. 任一模型 + 無 PK 全表掃描——看 scatter-gather。
互動 · Wide Row 與查詢路由 切換模型與查詢,同時觀察資料長相與節點命中
WIDE ROW · PARTITION 內長相
RING · 4 NODES(RF = 3)
QUERY PLAN
METRICS
選擇資料模型與查詢模式,按「執行查詢」。左側看資料長相,右側看路由。
閒置 Coordinator Replica 命中 Hot partition Scatter-gather

對照:高效查詢 = 少數 replica + partition 內掃描;hot partition = 仍打少數節點但負載畸形;scatter-gather = 問遍全 ring。

一句話答法建模階段就要保證查詢帶有效 partition key;低基數 PK 造成 hot spot,無 PK 造成 scatter-gather——兩者病徵不同,解法也不同。