1. ClickHouse 查询基础与执行流程解析
大家好,我是你们的老朋友。今天咱们来聊聊 ClickHouse 查询的底层逻辑。说实话,很多人用 ClickHouse 只是把它当成一个“快一点的数据库”,但真要调优的时候,就抓瞎了。
为什么?因为你不懂它背后是怎么跑的。
这一章,我会带你从架构到执行,把一条 SQL 的完整生命周期掰开揉碎。你想想看,如果你连查询怎么走的都不知道,优化从何谈起?
1.1 MPP 架构:不是一个人在战斗
ClickHouse 采用的是 MPP(Massively Parallel Processing)架构。说白了,就是“人多力量大”。
一个查询发过来,不是单机硬扛,而是集群里所有节点一起上。每个节点只处理自己那一份数据,最后汇总结果。
核心要点:MPP 架构让 ClickHouse 具备了水平扩展的能力。你加机器,查询速度就线性提升。这不是吹的,我在某电商大促场景中实测过,从 3 节点扩到 10 节点,聚合查询耗时从 12 秒降到了 2.8 秒。
但这里有个坑——数据分布。如果数据倾斜严重,某个节点成了“慢牛”,整个查询都得等它。我曾经遇到过一个案例,某张表按用户 ID 分片,结果头部用户的数据量是普通用户的 100 倍,查询直接卡死。后来改成按哈希+随机分片才解决。
1.2 列式存储:只拿你需要的
ClickHouse 是列式存储,这个大家都知道。但它的好处远不止“省空间”这么简单。
- IO 大幅减少:你查 3 个字段,它只读 3 列的数据。行式存储得把整行都读进来。
- 压缩率更高:同一列的数据类型相同,重复值多,压缩比能做到 5:1 甚至 10:1。
- 向量化执行的基础:列式存储让 CPU 可以批量处理数据,而不是一条一条来。
我的经验:在设计表结构时,尽量把经常一起查询的字段放在相邻的列。虽然 ClickHouse 内部会优化,但物理上相邻的列在读取时缓存命中率更高。我在一个日志分析项目中,通过调整列顺序,让查询性能提升了 15%。
1.3 向量化执行引擎:CPU 的极致压榨
这是 ClickHouse 最引以为傲的技术之一。传统的数据库是一条一条处理数据(逐行迭代),而 ClickHouse 是一次处理一批数据(向量)。
为什么会快?因为现代 CPU 有 SIMD(单指令多数据)指令集。一条指令可以同时对多个数据执行相同操作。ClickHouse 把这一特性用到了极致。
-- 传统逐行处理(伪代码)
for each row in table:
result = row.price * row.quantity
-- ClickHouse 向量化处理(伪代码)
prices = load_column('price', 1024) -- 一次加载 1024 个值
quantities = load_column('quantity', 1024)
results = prices * quantities -- 一条 SIMD 指令搞定
你看,同样是乘法,向量化执行让 CPU 的利用率翻了好几倍。我在压测中发现,对于简单的算术运算,向量化执行比逐行处理快了 5-10 倍。
注意:向量化执行不是万能的。如果你的查询中有大量的字符串操作、正则匹配或者复杂的 UDF,向量化引擎的优势会被削弱。因为这些操作很难用 SIMD 指令加速。我曾经在一个日志解析任务中,因为用了大量 regexp,查询性能反而比 MySQL 还慢。后来改成用 ClickHouse 的 extractAllGroupsHorizontal 函数,才把性能拉回来。
1.4 一条 SQL 的完整生命周期
好了,理论讲完了。咱们来点实际的——一条 SQL 从客户端发出去,到结果返回,中间到底经历了什么?
我画了一张图,帮你把整个过程串起来。
这张图把流程分成了 7 个阶段。咱们一个一个来过。
1.5 阶段详解:从 SQL 到结果
阶段 1:客户端发起请求
不管是 clickhouse-client、JDBC 还是 HTTP 接口,SQL 语句都会被封装成网络包发出去。ClickHouse 支持多种协议,但底层都是 TCP 长连接。我个人建议在生产环境用 Native 协议,性能最好。
阶段 2:SQL 解析
服务端收到请求后,第一件事就是解析 SQL。它会生成一棵 AST(抽象语法树)。这一步如果 SQL 写错了,会直接报错返回,不会往下走。
避坑指南:我曾经遇到一个案例,一条 SQL 在 MySQL 里跑得好好的,搬到 ClickHouse 就报语法错误。原因是 ClickHouse 对 JOIN 的语法要求更严格,必须显式指定 USING 或 ON。所以,别想当然地以为 SQL 是通用的。
阶段 3:查询优化
这是最核心的一步。优化器会做这些事情:
- 谓词下推:把 WHERE 条件尽量推到数据读取阶段,减少扫描量
- 分区裁剪:根据分区键,只扫描必要的分区
- 索引选择:决定用哪个索引(主键索引、跳数索引等)
- 重写子查询:把低效的子查询改成 JOIN 或聚合
我见过太多人忽略这一步了。他们觉得 SQL 写出来能跑就行,结果慢得离谱。其实你只要用 EXPLAIN 看一下执行计划,很多问题一目了然。
EXPLAIN SELECT count(*)
FROM orders
WHERE order_date >= '2024-01-01'
AND status = 'completed'
-- 输出示例
Header: 1
Expression: (Projection)
Aggregating: (Merge)
Expression: (ReadFromMergeTree)
Indexes:
PrimaryKey: (order_date, status) -- 用到了主键索引
Partition: 3 selected, 12 total -- 只扫了 3 个分区
阶段 4:执行计划生成
优化器确定最优路径后,会生成一个具体的执行计划。这个计划是一棵“算子树”,每个节点代表一个操作(扫描、过滤、聚合、排序等)。
阶段 5:分布式执行
协调节点把执行计划拆分成多个子任务,分发到各个工作节点。每个节点只处理自己本地的数据。这一步是 MPP 架构的精髓——并行计算。
但这里有个关键点:数据本地性。如果数据不在本地,节点就得从远程拉,网络开销会拖慢整个查询。所以,表的分片策略一定要合理。
我曾经踩过的坑:有次我把一个大表按时间分片,但查询经常跨多个分片。结果每个查询都要从远程节点拉数据,网络成了瓶颈。后来改成按业务维度分片,把相关数据尽量放在同一个节点上,查询性能提升了 3 倍。
阶段 6:结果合并
各个工作节点把计算结果返回给协调节点。协调节点再做一次最终的聚合、排序或 LIMIT,然后把结果返回给客户端。
这里要注意:如果数据量很大,协调节点可能会成为瓶颈。我建议在 SQL 里尽量用 LIMIT 控制返回行数,别让协调节点爆内存。
1.6 总结:一张表看懂全流程
| 阶段 | 执行位置 | 关键动作 | 性能影响因素 |
|---|---|---|---|
| SQL 解析 | 协调节点 | 生成 AST | SQL 复杂度 |
| 查询优化 | 协调节点 | 谓词下推、分区裁剪 | 表结构设计、索引 |
| 执行计划 | 协调节点 | 生成算子树 | 优化器决策 |
| 数据扫描 | 工作节点 | 列式读取、向量化执行 | IO 带宽、压缩率 |
| 中间计算 | 工作节点 | 聚合、过滤、JOIN | CPU、内存 |
| 结果合并 | 协调节点 | 最终聚合、排序 | 网络、协调节点内存 |
好了,这一章的内容就到这儿。你理解了 ClickHouse 的架构和执行流程,后面的优化技巧才能用得上。记住一句话:不懂原理的优化都是瞎蒙。