資料庫筆記
ClickHouse
開源欄式 OLAP 資料庫,用 SQL 對大量資料做即時聚合分析。官方把「即時」定義為查詢結果在 1 秒內回傳。
1. ClickHouse 是什麼
ClickHouse 是一個開源(Apache 2.0)的欄式 OLAP 資料庫,使用 SQL 查詢,原始設計目標是對大量資料做即時聚合分析。官方將「即時」定義為查詢結果在 1 秒內回傳。
1.1 OLTP 與 OLAP
資料庫的工作負載大致分兩類,設計取向完全不同:
| OLTP(如 PostgreSQL、MySQL) | OLAP(如 ClickHouse) | |
|---|---|---|
| 典型操作 | 單筆或少量資料的讀寫、更新 | 掃描大量資料做彙總(COUNT、SUM、GROUP BY) |
| 每次查詢讀取的資料 | 少數幾列 | 數百萬至數十億列,但只用少數幾欄 |
| 儲存方式 | 行式(同一列的欄位放一起) | 欄式(同一欄的資料放一起) |
| 交易 | 完整 ACID | 有限 |
| 資料變動 | 頻繁更新、刪除 | 以新增為主,很少修改 |
1.2 為什麼欄式儲存適合分析
假設一張表有 50 個欄位,查詢只需要其中 2 欄:
- 行式:必須把整列讀進來,再丟掉 48 欄,浪費大量 I/O。
- 欄式:只讀那 2 欄的檔案,I/O 量約為前者的幾十分之一。
- 同一欄的資料型態相同、數值相近,壓縮率很高,進一步降低磁碟與記憶體頻寬需求。
代價是:要取回「一整列」或修改單列時,需要碰多個欄位檔案,成本較高。這也是它不適合 OLTP 的根本原因。
1.3 定位
- 它不是 PostgreSQL / MySQL 的替代品,而是搭配使用的分析引擎。
- 官方也建議:需要頻繁單列讀寫的應用,以 PostgreSQL 作為 system of record,再用 ClickHouse 做分析。
2. 適合與不適合
2.1 適合的資料與場景
| 資料類型 | 為什麼適合 |
|---|---|
| 日誌、事件、追蹤(trace) | 持續大量寫入、寫入後幾乎不修改、查詢以時間範圍+聚合為主 |
| 時序資料、指標 | 依時間排序與分割區,TTL 可自動過期清理 |
| Clickstream、使用者行為 | 資料量大、寬表、多維度分析 |
| 即時儀表板、報表 | 秒級聚合查詢,可搭配 Materialized View 預先聚合 |
共同特徵:資料量大、以新增為主、查詢是掃描與聚合、欄位多但每次只用少數幾欄。
2.2 不適合的情境
| 情境 | 原因 |
|---|---|
| 頻繁 UPDATE / DELETE 單列 | 更新是背景 mutation,刪除只是標記,成本高 |
| 需要跨語句交易 | 無完整多語句交易(實驗性功能有嚴格限制) |
| 以主鍵取單列為主 | 稀疏索引不是為單列點查設計 |
| 小量頻繁寫入(每筆一次 INSERT) | 每次 INSERT 產生一個 part,太多 part 會拖慢合併與查詢;官方建議批次寫入 |
| 需要唯一性約束 | 主鍵不保證唯一 |
3. 為什麼快、特點
3.1 效能來源
- 欄式儲存 + 高壓縮:只讀需要的欄位;同欄資料壓縮後,磁碟讀取量更小。
- 稀疏主鍵索引:資料依主鍵排序後切成 granule(預設約 8192 列),索引只記錄每個 granule 的起點,因此索引小到可常駐記憶體,並能跳過不相關區塊。
- 向量化執行:以「一批列」為單位處理,而非逐列,配合 SIMD 指令加速。
- 多核與多節點平行:單一查詢可同時使用所有 CPU 核心,叢集可跨節點平行。
- 批次寫入設計:每次 INSERT 直接寫成不可變的 part,背景再合併,寫入路徑很輕。
3.2 功能特點
| 特點 | 說明 |
|---|---|
| 豐富的資料型態 | 數值、字串、日期時間、Array、Tuple、Map、JSON、LowCardinality、UUID、Enum 等 |
| 多種 JOIN | 預設 hash join;另有 parallel_hash、grace_hash、full_sorting_merge、ASOF 等 |
| TTL | 資料過期可自動刪除、重新壓縮、搬到其他磁碟或做聚合;在背景合併時才評估,不是即時 |
| 分層儲存 | 熱資料放 SSD,冷資料搬到 HDD 或 S3 |
| Materialized View | INSERT 時增量更新,適合預先聚合 |
| 跳躍索引、Projection | 針對非主鍵條件加速查詢 |
| 複製與分片 | 非同步多主複製,可選 quorum 同步寫入;大多數故障可自動恢復 |
| 近似計算 | 可用取樣或近似聚合函式,以精度換速度 |
3.3 要記住的取捨
ClickHouse 的快,來自放棄傳統資料庫的一些保證:不保證主鍵唯一、不即時更新、不提供完整交易。
4. 缺點與限制
| 項目 | 限制 | 對策 |
|---|---|---|
| 更新/刪除成本高 | 更新是背景 mutation;刪除只是標記 | 資料模型以「新增」為主 |
| 交易有限 | 無完整多語句交易;INSERT 預設不 fsync | 確認寫入設定,不依賴 rollback |
| 主鍵不唯一 | 主鍵只管排序與索引,重複 key 不報錯 | 不當唯一約束;用 FINAL 或應用層去重 |
| 去重/刪除語意 | ReplacingMergeTree 只在合併時去重 |
Langfuse 的刪除列會留在表中 |
| JOIN 限制多 | 無 JOIN 順序最佳化;預設不溢寫磁碟 | 先過濾,小表放右邊 |
| 小量頻繁寫入 | 每次 INSERT 一個 part,過多會拖慢合併 | 批次寫入或非同步 INSERT |
| 維運複雜 | 叢集需 Keeper;shard/replica 自行規劃 | 自管成本高時考慮雲端版 |
細節補充
- 更新/刪除:
ALTER TABLE ... UPDATE非原子;DELETE FROM內部寫入_row_exists標記,讀取時多一道過濾,大量刪除會拖慢 SELECT。 - 交易:單表單次 INSERT 有 ACID,但預設不 fsync(論文指出),斷電可能遺失近期資料,耐久性取決於
fsync_after_insert或insert_quorum。跨語句交易為實驗性:需 Keeper、僅限非 Replicated 的 MergeTree,雲端版不支援。 - 主鍵/去重:
ReplacingMergeTree只在合併時去重,查詢時可能讀到重複。is_deleted需搭配ver,預設仍保留該 key 最後一列(即使是刪除列)。 - JOIN:右表先處理;
max_rows_in_join等為硬上限;JOIN key 為 NULL 的列不會被 join;多個 JOIN 時PREWHERE、USING不可用。
5. 核心概念
5.1 MergeTree 引擎家族
主力表引擎,建表時必須指定 ORDER BY(排序鍵,預設即主鍵):
CREATE TABLE events
(
ts DateTime,
user_id UInt64,
action LowCardinality(String),
value Float64
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ts)
ORDER BY (user_id, ts);
ORDER BY:決定資料在磁碟上的排序,也是稀疏索引的依據。選得好壞,直接影響查詢速度。PARTITION BY:依條件切分資料,方便整個分割區刪除或搬移;官方不建議切得太細。- 同一家族還有
ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree等,以及加上Replicated前綴的複製版本。
5.2 Part 與 Merge
- 每次 INSERT 寫成一個不可變的 part(含排序後的各欄資料)。
- 背景執行緒把小 part 合併成大 part。
- Part 有兩種格式:
Wide(每欄一個檔案)與Compact(所有欄位一個檔案,利於小量寫入)。 - TTL、
ReplacingMergeTree的去重、刪除標記的實際清除,都是在合併時才發生。這就是為什麼它們「不即時」。
5.3 ReplacingMergeTree:用新增模擬更新
做法是:要更新時,插入一筆同 key、版本更高的新列;合併時保留版本最大的那列。
ENGINE = ReplacingMergeTree(event_ts, is_deleted)
ORDER BY (project_id, id)
event_ts:版本欄位,值較大者勝出。is_deleted:標記刪除,需搭配版本欄位。- 合併之前,查詢可能讀到新舊多個版本;需要精確結果時用
FINAL(代價較高)。 - Langfuse 的
traces、observations、scores就是用ReplicatedReplacingMergeTree(event_ts, is_deleted)。
5.4 複製、分片與 Keeper
| 概念 | 說明 |
|---|---|
| Replica(副本) | 同一份資料的多個複本,提供高可用;複製為非同步 |
| Shard(分片) | 把資料水平切到多個節點,擴充容量與吞吐 |
| Keeper | 負責協調的服務(Raft,通常 3 個),保存複製的全域 log;也用於 insert 去重 |
| Distributed 表 | 跨 shard 查詢的入口表;本身不存資料 |
- 重送相同的 INSERT 時,伺服器會依最近若干個 part 的 hash 去重(冪等)。
- 雲端版改用
SharedMergeTree,儲存與運算分離,不需自己管 shard。
5.5 加速查詢的輔助機制
| 機制 | 用途 | 注意 |
|---|---|---|
| 跳躍索引(minmax、set、bloom_filter、text) | 針對非主鍵條件,跳過整個區塊 | 不是 B-tree;要與排序鍵有相關性才有效,官方建議最後才考慮 |
| Projection | 以不同排序或預聚合存一份,查詢自動選用 | 不支援帶 FINAL 的 SELECT |
| Materialized View | INSERT 時增量更新到另一張表 | 只處理新寫入的資料 |
| 欄位統計 | 協助 PREWHERE 最佳化 | 需 use_statistics=1;部分類型建立成本高 |
6. 跟一般 SQL 差多少
基本查詢(SELECT / WHERE / GROUP BY / JOIN / 視窗函式)與標準 SQL 差不多,差異主要在寫入與建表:
| 項目 | 一般 SQL | ClickHouse |
|---|---|---|
| 建表 | CREATE TABLE 即可 |
必須指定 ENGINE 與 ORDER BY |
| 更新 | UPDATE ... SET |
ALTER TABLE ... UPDATE(背景 mutation) |
| 刪除 | 立即生效 | 標記刪除,合併後才實際移除 |
| 交易 | BEGIN / COMMIT |
無完整多語句交易 |
| 主鍵 | 保證唯一 | 排序/稀疏索引,不保證唯一 |
| NULL | 欄位預設可為 NULL | 需明確寫 Nullable(T),官方指出幾乎總是影響效能 |
| 函式名 | 不分大小寫 | 多數函式名區分大小寫 |
| 額外語法 | — | PREWHERE、FINAL、ARRAY JOIN、LIMIT BY、ASOF JOIN、SETTINGS |