考察目的:是不是真的用过ClickHouse,有没有深入理解它的内部机制,还是只会写个SELECT count(*)。
我答得不好,那就学呗,全方位了解ClickHouse 自身层面的慢查询原因。
以下大量内容是通过AI整理,包括图片也由AI生成,如果介意,可以不用往下读。
首先,我们先看结论,看下整体思路:全局排查优先级总结。
面试时如果被问到,按这个优先级来排查 ClickHouse 自身问题:
️① 先看 system.query_log → 确认是突然变慢还是持续慢,慢在哪个环节↓️② 看 system.mutations → 是否有 ALTER/DELETE/UPDATE 在重写数据↓️③ 看 system.parts → parts 数量是否过多、分区是否合理↓️④ 看 system.metrics → 内存使用、merge 线程是否饱和↓️⑤ 看集群各节点 → 是否存在数据倾斜↓⑥ 最后看表 DDL → ORDERBY/PARTITIONBY 设计是否合理

如果只想看个思路的朋友,看到这里就行了。下面是详细的内容,文章较长,欢迎一起学习。
一、MergeTree 存储引擎层面
ClickHouse 的核心是 MergeTree 系列引擎,很多慢查询的根因在"写"上积累的问题,最终传导到了"读"。
1. Parts 数量过多——"碎片化"问题
ClickHouse 的数据是以 part(数据片段) 为单位组织的。每次 INSERT 都会生成一个或多个 part,后台 merge 线程会异步地将小 parts 合并为大 parts。
慢的原因:
查询时需要对匹配的 parts 逐一扫描,parts 数量越多,打开和读取的文件数量越多,查询延迟越高。
触发场景:
高频小批量写入(比如每秒几百次INSERT,每次几行) Merge 速度跟不上写入速度 表设置了 max_bytes_to_merge_at_max_space_in_pool等参数限制了 merge 行为
怎么确认:
-- 查看表的 parts 数量和大小分布SELECTtable,count() AS parts_count,formatReadableSize(sum(bytes_on_disk)) AS total_size,formatReadableSize(min(bytes_on_disk)) AS min_part_size,formatReadableSize(max(bytes_on_disk)) AS max_part_sizeFROM system.partsWHERE database = 'your_db'AND table = 'your_table'AND activeGROUP BY table

解决方向:
background_pool_size,调小 parts_to_delay_insert 阈值 | |
OPTIMIZE TABLE xxx FINAL |
2. 分区键(PARTITION BY)设计不当
分区过大:
单个分区包含N亿行,查询时即便带了分区条件,扫描量依然巨大 分区内 merge 压力大,parts 堆积
分区过细:
按天分区没问题,按小时/分钟分区就过头了 上千个分区,元数据膨胀,查询时分区裁剪遍历开销大

建议:
3. 排序键(ORDER BY)设计不当——索引失效
这是最容易被忽略的性能杀手。ClickHouse 的主索引是稀疏索引,默认每 8192 行才记录一个标记(mark),索引精度全靠 ORDER BY 列的顺序和排列来保证。
慢的原因:
查询条件没有命中 ORDER BY 的前缀列,导致全表扫描或大范围扫描。
举例:
-- 不好的设计ORDER BY (timestamp, user_id, event_type)-- 查询条件只有 event_typeSELECT count() FROM events WHERE event_type = 'login'-- 索引无法命中,因为 event_type 是 ORDER BY 的第三列
改进:
-- 把高频过滤的列前置ORDER BY (event_type, timestamp, user_id)

二、后台操作抢资源
4. Mutations(ALTER/DELETE/UPDATE)正在执行
ClickHouse 的 DELETE 和 UPDATE 是异步重写操作(mutation),不是传统数据库的原地修改。
慢的原因:
Mutation 操作会触发后台重写 parts,大量IO和CPU被占用,查询在同一个物理资源上竞争。
排查:
-- 查看是否有正在执行的 mutationSELECTdatabase, table, command,create_time,parts_to_do,is_doneFROM system.mutationsWHERE is_done = 0ORDER BY create_time DESC
查询结果,需要关注
is_done=0的行和parts_to_do数值较大的行。如果is_done=0,parts_to_do列有数值如 1500+,说明"Mutation 正在重写 parts,查询性能受影响"。
影响程度:
5. Merge 操作滞后
Merge 是 ClickHouse 后台合并 parts 的过程。如果 merge 跟不上写入,除了 parts 变多,还会导致:
每个 part 都有独立的索引文件,查询时打开的文件句柄增多 系统表 system.parts查询变慢,影响监控和运维
排查:
-- 查看后台 merge 状态SELECT * FROM system.merges
三、查询执行引擎层面
6. 内存不足导致磁盘溢写
ClickHouse 对 GROUP BY、ORDER BY、JOIN、DISTINCT 等操作设置了内存上限,超过后会溢写到磁盘,性能断崖式下降。
关键参数:
max_bytes_before_external_group_by | ||
max_bytes_before_external_sort | ||
max_memory_usage | ||
join_algorithm |
慢的现象:
-- 查询执行日志中看到 External 字样SELECT toStartOfMinute(event_time),formatReadableSize(peak_memory_usage),query_duration_msFROM system.query_logWHERE query LIKE '%GROUP BY%'AND type = 'QueryFinish'ORDER BY query_duration_ms DESCLIMIT 20
如果 peak_memory_usage 远超 max_memory_usage 但查询还在执行,说明走了磁盘溢写——慢,但不报错。

7. 分布式查询放大
分布式表(Distributed)自身不存数据,只做路由。如果使用不当,查询会被放大:
场景一:查询被路由到所有分片
-- 不带分片键条件,每个节点都要扫描全量SELECT count() FROM distributed_events-- 内部 = 在每个 shard 执行 SELECT count() FROM local_events
场景二:GLOBAL IN / GLOBAL JOIN 数据汇总到发起节点
分布式子查询使用 GLOBAL IN 时,数据会被拉取到发起节点构建临时数据集,如果子查询结果很大,发起节点内存撑不住,还会走磁盘。
场景三:distributed_product_mode 策略不当
-- 分布式表之间 JOIN,默认是广播式SELECT * FROM distributed_AJOIN distributed_B ON xxx-- 大表被广播到所有节点,网络和内存爆炸
8. 数据倾斜——某个分片特别大
分布式表背后是多个副本/分片。如果分片键设计不当导致数据倾斜,某个分片的数据量是其他分片的 N 倍,查询到这个分片时变成单点瓶颈。
排查:
SELECThostName() AS shard,formatReadableSize(sum(bytes_on_disk)) AS disk_usage,count() AS parts_countFROM system.partsWHERE table = 'your_table' AND activeGROUP BY shardORDER BY sum(bytes_on_disk) DESC

四、其他 ClickHouse 自身特性相关
9. 物化视图未触发或设计不当
物化视图是 ClickHouse 的"预聚合加速器",但它也有坑:
- 同步物化视图没有触发
INSERT 时物化视图对应的 target 表出问题(如磁盘满),数据未写入,查询时拿不到预聚合结果 - 物化视图与源表数据不一致
只支持 INSERT 触发,源表做 DELETE/UPDATE(mutation)后,物化视图不会同步更新 - 物化视图写入成为 INSERT 的瓶颈
链式物化视图嵌套过多,原始 INSERT 被拖慢
-- 排查物化视图状态SELECTdatabase, table,total_rows,total_bytes,formatReadableSize(total_bytes)FROM system.tablesWHERE engine = 'MaterializedView'AND database = 'your_db'
10. 大量过期 TTL 数据待清理
TTL 机制也是通过后台 merge 来物理删除数据。如果 TTL 设置不合理(比如过期条件涉及复杂计算),或者过期数据量巨大,merge 线程忙于清理,正常查询的 merge 被延后。
排查:
-- 查看有 TTL 设置的表SELECTdatabase, table,engine,sorting_key,ttl_infoFROM system.tablesWHERE ttl_info != ''
删除过期数据(归档、清理老旧数据)
移动数据到低成本存储(冷热分层,本地盘→S3 / 对象存储)
底层是后台线程异步执行,不需要手动定时删数据。
夜雨聆风