ClickHouseNotes

第 15 章:向量化执行

zjc 于 2026-01-15 发布

这是《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 列文件由压缩块组成。读取时会按需要解压,不一定整列全部解压。

影响压缩和读取的因素:

  1. 数据类型宽度;
  2. 列的局部性;
  3. 值重复度;
  4. 排序键设计;
  5. Codec 配置;
  6. 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;

线程越多不一定越快:

  1. 小查询调度开销增加;
  2. 并发查询互相抢占 CPU;
  3. 聚合 hash table 内存增加;
  4. 分布式查询放大资源消耗。

常见策略:

查询类型 线程策略
小点查 较少线程
大扫描 适度增加
高并发报表 全局排队和限额
后台补数 独立资源池

15.6 聚合执行

SELECT city_id, event_type, count(), sum(amount)
FROM analytics.events_local
GROUP BY city_id, event_type;

执行过程:

每个线程构建局部聚合状态
        |
        v
合并局部状态
        |
        v
输出最终分组

高基数分组会带来:

  1. 大量 hash key;
  2. 内存增长;
  3. 状态合并成本;
  4. 溢写或查询失败。

优化方向:

  1. 减少维度组合;
  2. 先过滤再聚合;
  3. 使用低基数类型;
  4. 预聚合;
  5. 近似函数;
  6. 限制查询内存。

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 等扩展,但外部调用通常比内置向量化函数慢得多。

使用原则:

  1. 优先内置函数;
  2. 写入侧预处理;
  3. UDF 只处理小规模数据;
  4. 控制超时和资源;
  5. 避免在 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

建议:

  1. 少用 SELECT *
  2. 高频字段类型尽量窄;
  3. 低基数字符串用 LowCardinality;
  4. 避免高基数字符串分组;
  5. 能写入时固化就不要查询时解析;
  6. 避免复杂嵌套表达式;
  7. 近似场景使用近似聚合;
  8. 大结果集分页或落盘导出。

本章小结

向量化执行依赖列存、连续数据块、类型特化和批量内核。开发者能做的不是手写 SIMD,而是让数据类型、排序、压缩和 SQL 形态更适配批量执行,同时通过查询日志持续观察 CPU、内存和 IO。

思考题

  1. 为什么列存更适合向量化?
  2. max_threads 是否越大越好?
  3. 高基数 GROUP BY 为什么耗内存?
  4. Codec 如何影响 CPU 和磁盘?
  5. 哪些 SQL 写法会削弱向量化收益?