这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 向量化执行是 ClickHouse 快的重要原因之一。它把“一行一行解释执行”变成“一批一批计算”,让 CPU、缓存、SIMD 指令和编译优化充分发挥。
15.1 行执行与向量化执行
行执行:
row1.amount + row1.tax
row2.amount + row2.tax
row3.amount + row3.tax
向量化执行:
amount = [100, 200, 300]
tax = [6, 12, 18]
result = amount + tax = [106, 212, 318]
差异:
| 维度 | 行执行 | 向量化执行 |
|---|---|---|
| 处理单位 | 单行 | 一批值 |
| 指令分发 | 多 | 少 |
| CPU cache | 不友好 | 友好 |
| SIMD | 难 | 易 |
| 类型特化 | 一般 | 强 |
15.2 列存与向量化
列存让同一列的值连续保存:
amount.bin: 100, 200, 300, ...
执行器可以一次读取一批 amount,直接调用向量化加法、比较或聚合内核。
这也是不建议在热点路径频繁使用复杂 UDF、字符串拆散和高分支逻辑的原因:它们会打断批量处理。
15.3 查询流水线
读取分区
-> 读取列块
-> 解压 / 反序列化
-> 表达式向量化计算
-> 过滤位图
-> 聚合 hash table
-> 排序 / 合并
-> 输出
其中任何一个环节都可能成为瓶颈:
| 瓶颈 | 现象 |
|---|---|
| 磁盘 IO | read_bytes 高、IO wait 高 |
| 解压 CPU | CPU 高、扫描大 |
| 聚合内存 | memory_usage 高 |
| 排序 | 外部排序或内存不足 |
| 网络 | 分布式节点间传输慢 |
| 结果输出 | result_rows 大 |
15.4 压缩块
ClickHouse 列文件由压缩块组成。读取时会按需要解压,不一定整列全部解压。
影响压缩和读取的因素:
- 数据类型宽度;
- 列的局部性;
- 值重复度;
- 排序键设计;
- Codec 配置;
- part 大小。
示例:
CREATE TABLE analytics.events_codec
(
event_date Date CODEC(DoubleDelta, ZSTD(1)),
user_id UInt64 CODEC(T64, ZSTD(1)),
event_type LowCardinality(String),
amount Decimal64(2) CODEC(ZSTD(1))
)
ENGINE = MergeTree
ORDER BY (event_date, event_type, user_id);
Codec 选择要测试,不是压缩率越高越好,解压 CPU 也可能成为瓶颈。
15.5 max_threads
查询线程数可以通过设置控制:
SELECT count()
FROM analytics.events_local
SETTINGS max_threads = 8;
线程越多不一定越快:
- 小查询调度开销增加;
- 并发查询互相抢占 CPU;
- 聚合 hash table 内存增加;
- 分布式查询放大资源消耗。
常见策略:
| 查询类型 | 线程策略 |
|---|---|
| 小点查 | 较少线程 |
| 大扫描 | 适度增加 |
| 高并发报表 | 全局排队和限额 |
| 后台补数 | 独立资源池 |
15.6 聚合执行
SELECT city_id, event_type, count(), sum(amount)
FROM analytics.events_local
GROUP BY city_id, event_type;
执行过程:
每个线程构建局部聚合状态
|
v
合并局部状态
|
v
输出最终分组
高基数分组会带来:
- 大量 hash key;
- 内存增长;
- 状态合并成本;
- 溢写或查询失败。
优化方向:
- 减少维度组合;
- 先过滤再聚合;
- 使用低基数类型;
- 预聚合;
- 近似函数;
- 限制查询内存。
15.7 表达式优化
避免对每行做重复复杂表达式:
SELECT
toDate(event_time) AS event_date,
toLowerCase(trim(platform)) AS platform_clean
FROM analytics.events_local;
更好做法是写入时固化:
event_date Date,
platform LowCardinality(String)
查询只读取已经标准化后的列,减少运行时计算。
15.8 外部函数与 UDF
ClickHouse 支持可执行 UDF 等扩展,但外部调用通常比内置向量化函数慢得多。
使用原则:
- 优先内置函数;
- 写入侧预处理;
- UDF 只处理小规模数据;
- 控制超时和资源;
- 避免在 WHERE 中调用高成本外部函数。
15.9 观察执行指标
SELECT
query_id,
query_duration_ms,
read_rows,
read_bytes,
memory_usage,
ProfileEvents
FROM system.query_log
WHERE type = 'QueryFinish'
ORDER BY query_duration_ms DESC
LIMIT 20;
关注事件:
| 指标 | 含义 |
|---|---|
| CPU 时间 | 计算成本 |
| 解压字节 | 解压压力 |
| 读取文件数 | 小文件问题 |
| 聚合状态数 | 分组基数 |
| 溢写临时文件 | 内存压力 |
| 网络字节 | 分布式传输 |
15.10 编写向量化友好的 SQL
建议:
- 少用
SELECT *; - 高频字段类型尽量窄;
- 低基数字符串用 LowCardinality;
- 避免高基数字符串分组;
- 能写入时固化就不要查询时解析;
- 避免复杂嵌套表达式;
- 近似场景使用近似聚合;
- 大结果集分页或落盘导出。
本章小结
向量化执行依赖列存、连续数据块、类型特化和批量内核。开发者能做的不是手写 SIMD,而是让数据类型、排序、压缩和 SQL 形态更适配批量执行,同时通过查询日志持续观察 CPU、内存和 IO。
思考题
- 为什么列存更适合向量化?
- max_threads 是否越大越好?
- 高基数 GROUP BY 为什么耗内存?
- Codec 如何影响 CPU 和磁盘?
- 哪些 SQL 写法会削弱向量化收益?