總覽與痛點
先問「為什麼需要」,再談產品名稱——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 往往等於重建表。
一列長什麼樣子
先建立畫面:什麼叫「寬」、什麼叫「稀疏」,以及一筆資料在 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」。
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 的 WHERE 會變成 scatter-gather:coordinator 問遍所有節點再合併。節點愈多愈慢。ALLOW FILTERING 不是救命稻草,是生產紅燈。設計時先列出 Top 查詢,再反推 schema。
| 查詢型態 | 行為 | 延遲直覺 |
|---|---|---|
| 帶 PK 的 point lookup | 路由到少數 replica,讀單列 | 最低 |
| 帶 PK + clustering 範圍 | 同一 partition 內順序掃描 | 低(wide-column 強項) |
| 無 PK / 低基數當 PK | 全叢集掃描,或熱點擠在單一 partition | 危險 |
依查詢設計資料模型
思維翻轉:不是先正規化再優化,而是「一個查詢一張(物化)表」。
關聯式教科書:消除冗餘 → 正規化 → 需要時再 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 匯出
寫一筆訂單時發生什麼
orders_by_id 與 orders_by_user——同一筆業務事實,存成兩個查詢視角。
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 往往變成「每個節點都掃一輪」。正確做法仍是查詢驅動的物化表,搜尋需求另掛 Elasticsearch 等側車。
Cassandra 與 HBase:兩種架構選擇
模型相同、取捨不同——一個偏線上可用性,一個偏 Hadoop 生態與單區強一致。
學會「partition + clustering」之後,再看兩個常見實作如何落地。
Cassandra:Masterless Ring
無中心 master。節點用 Gossip 交換狀態;client 可連任一節點當 coordinator。寫入時 coordinator 依 partition key 找 replica,並行推送,再依 Consistency Level(CL) 等足夠回應。
以常見 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 擅長高吞吐寫入與大範圍 scan,並與 Hadoop 生態整合;若你要毫秒級線上 API 且無 HDFS 依賴,實務上更常選 Cassandra / Scylla 這類路徑。
互動演示:資料長相與查詢路由
同一組控制項同時回答兩件事:這個 partition 裡資料長什麼樣?這次查詢打到哪些節點?
左側是 Wide Row 視圖(partition 內的 clustering rows 與稀疏欄位);右側是 4 節點 Ring(coordinator、replica 命中、scatter-gather、hot partition)。請依序試:
- 收件箱 + Partition 範圍——理想路徑:單 partition 範圍掃描。
- 訂單 + 帶 PK——單列 lookup。
- 錯誤設計 (status) + 帶 PK——看 hot partition(不是全叢集掃描)。
- 任一模型 + 無 PK 全表掃描——看 scatter-gather。
對照:高效查詢 = 少數 replica + partition 內掃描;hot partition = 仍打少數節點但負載畸形;scatter-gather = 問遍全 ring。