这是 ClickHouse 官方工程博客在 2026 年发布的一份综合优化指南。它的标题里有”definitive”这个词确实不算夸张——不是因为它穷举了所有优化手段,而是因为它始终把每项优化技术挂回到 ClickHouse 的底层架构上。读过之后,我对”为什么优化有效”的理解明显比”该怎么调参”更扎实。

从三条原则出发

整份指南围绕三条原则展开:更快查询读更少数据(列式存储的天然优势)、避免查询时做不必要的工作(把计算挪到写入时,用物化视图预聚合)、先过滤再处理(缩小数据集后再做 join 和聚合)。这三条不算新,但每一条在 ClickHouse 的物理实现下都有比普通数据库更直接的性能反馈。

架构基础

指南花了相当篇幅解释底层机制,这恰恰是它比一般”调参清单”有价值的地方:

  • 列式存储意味着 SELECT * 的代价比传统数据库严重得多,但也意味着宽表只要只选几列,成本接近窄表。
  • 稀疏主索引在 8.87M 行上只生成 ~1,083 个索引条目(约 97KB),通过 granule(默认 8,192 行)做二分搜索定位目标数据块。和 B-tree 比,索引本身小了一个数量级,代价是最多可能多读一个 granule 的无关数据。
  • Granule 是 ClickHouse 不可分割的读取单位,不能读半个 granule。这意味着主键排在后面的列,其过滤效率严重依赖于前置列基数的分布——如果第一列基数极高(如 UUID),后面的列几乎退化成了全扫描。
  • 后台 Merge 不只是压缩碎片,还是 deduplication、预聚合、TTL 过期和物化视图状态维护的载体,这就是为什么 ClickHouse 在”查询时快”的同时还能把大量计算挪到了写入后的后台。
  • Pipeline 并行执行让每个 CPU 核心独立处理一段数据范围,EXPLAIN PIPELINE 输出里的 × N 就是并行度。

优化清单

1. ORDER BY 设计(最重要,100× 提升)

物理排序是 ClickHouse 优化的第一杠杆。好的 ORDER BY 让查询跳过 99% 的 granule;不好的 ORDER BY 让全表扫描成为默认行为。三条规则:WHERE 里高频出现的列优先、低基数列放前面、时序数据要包含时间组件。

2. 避免 Nullable 列

每个 Nullable 列额外增加一个 UInt8 null mask,从存储、内存到计算全链路都有开销。先用 countIf(col IS NULL) 确认实际是否有 NULL,再决定是否用默认值替代。

3. LowCardinality 类型

对少于 10,000 个唯一值的 String 列,LowCardinality(String) 用字典编码把重复字符串压缩成整数引用,查询时直接在字典上操作。

4. 选择最小数据类型

UInt8 vs Int64 在单行上差 7 bytes,放在十亿行表里就是 7GB。对时间列,Date 比 DateTime 省一半,DateTime 比 DateTime64 又省一半。

5. 增量物化视图

把聚合从查询时挪到 INSERT 时,适合仪表盘和周期性报表。但物化视图和普通视图不一样——它只在 INSERT 时触发,不响应源表 mutation 或 partition 变更。

6. PREWHERE

先用条件列做过滤,再读取 SELECT 列。适合过滤完大部分行但 SELECT 列很宽的查询。ClickHouse 通常会自动应用,但显式使用可保证行为。

7. 近似函数

uniq() 用 HyperLogLog 在 ~12KB 固定内存里算近似去重,比 COUNT(DISTINCT) 快 10-100 倍,误差通常在 1-2%。uniqCombined() 支持跨分布式查询合并。

8. 反范式化

ClickHouse 在反范式化的宽表上跑得最好。列式压缩大幅降低重复字段的存储惩罚,JOIN 的 hash table 构建成本则被完全省掉。

9. Projections

一个表只能有一种物理排序,Projections 就是为不同查询模式维护不同排序的副本。ClickHouse 查询时自动选择最优 projection,无需改查询代码。25.6 版本开始支持单查询内组合多个 projection。

10. 字符串哈希

对长字符串做 GROUP BY 时,先用 cityHash64() 把变长字符串转成固定 64 位整数再分组,比直接按字符串聚合快 5-10 倍。

11. Skip Indexes

二级索引,应该在优化完主键和 projection 之后再用。关键是必须和 ORDER BY 有相关性,否则只有写入成本没有查询收益。

12. 分区

主要用于数据生命周期管理,不是性能优化工具。维持 10-100 个分区,超过一千个会导致严重性能退化。

13. 多趟分组

optimize_aggregation_in_order 开启后,可以先在更细粒度做 in-order 聚合,再在外层做 hash 聚合,避免单次聚合吃尽内存。

14. 字典

用内存常驻的字典替代表关联 JOIN,O(1) 查询、自动刷新、支持外部数据源。

性能监控体系

指南给出了一套完整的监控方法:system.query_log 找慢查询、EXPLAIN indexes = 1 看 granule 扫描比例、send_logs_level = 'trace' 看细节、normalized_query_hash 找高频重复查询。优化顺序也是明确的:优先优化那些每天执行上万次的查询,而不是偶尔跑一次的分析查询。

对我的启发

这篇指南最让我受用的是它的”物理直觉”培养方式。它不是在说”不开这个配置会慢”,而是解释 granule 为什么这么设计、为何前置列基数决定了后续列的过滤效率、后台 merge 不仅是碎片清理而是计算载体。读完后的感觉是:ClickHouse 不是一个跑起来很快的黑盒,而是一个所有加速手段都能在架构图上找到对应物理原因的透明系统。这才是”优化”的前提。