乐于分享
好东西不私藏

软件测试面试:ClickHouse自身什么情况会变慢

软件测试面试:ClickHouse自身什么情况会变慢
面试官问:在【采集数据 → Kafka消费 → ClickHouse入库】这一条链路中,亿级数据查询突然变慢,除了磁盘IO等外部因素,ClickHouse自身什么情况会变慢?
这是在这次面试面试,它又要凉了中的第9个问题。前面已经聊了磁盘IO怎么排查(看过这两篇文章软件测试面试:磁盘IO看哪些指标-1软件测试面试:磁盘IO看哪些指标-2的朋友应该不陌生),当时面试官接着追问:"抛开其他层面,ClickHouse数据库自身在什么情况下会变慢?"

考察目的:是不是真的用过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 数量和大小分布    SELECT      table,    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_size   FROM system.parts   WHERE database = 'your_db'     AND table = 'your_table'   AND active   GROUP BY table 

解决方向:

方案
说明
降低写入频率
应用层攒批(比如每5秒或每1000条写一次)
使用 Buffer 表引擎
内存缓冲 + 定时刷盘,减少 parts 碎片
调整 merge 参数
增大 background_pool_size,调小 parts_to_delay_insert 阈值
手动触发 merge
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被占用,查询在同一个物理资源上竞争。

排查:

-- 查看是否有正在执行的 mutation     SELECT      database, table, command,     create_time,     parts_to_do,     is_done FROM system.mutations    WHERE is_done = 0   ORDER BY create_time DESC

查询结果,需要关注 is_done=0 的行和 parts_to_do 数值较大的行。如果is_done=0parts_to_do 列有数值如 1500+,说明"Mutation 正在重写 parts,查询性能受影响"。

影响程度:

mutation 类型
重写范围
影响
DELETE
按条件重写匹配的 parts
中等
UPDATE
重写匹配的 parts
较大
ALTER DELETE 列
全表重写
极大,数小时到数天

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
0(无限制)
GROUP BY 内存上限,超了写磁盘
max_bytes_before_external_sort
0
ORDER BY 内存上限
max_memory_usage
10GB
单查询最大内存
join_algorithm
auto
JOIN 算法(auto/partial_merge/hash)

慢的现象:

-- 查询执行日志中看到 External 字样     SELECT toStartOfMinute(event_time),     formatReadableSize(peak_memory_usage),   query_duration_ms   FROM system.query_log   WHERE query LIKE '%GROUP BY%'    AND type = 'QueryFinish'  ORDER BY query_duration_ms DESC   LIMIT 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_A      JOIN distributed_B ON xxx      -- 大表被广播到所有节点,网络和内存爆炸 

8. 数据倾斜——某个分片特别大

分布式表背后是多个副本/分片。如果分片键设计不当导致数据倾斜,某个分片的数据量是其他分片的 N 倍,查询到这个分片时变成单点瓶颈。

排查:

SELECT      hostName() AS shard,    formatReadableSize(sum(bytes_on_disk)) AS disk_usage,    count() AS parts_count     FROM system.parts  WHERE table = 'your_table' AND active    GROUP BY shard  ORDER BY sum(bytes_on_disk) DESC 

四、其他 ClickHouse 自身特性相关

9. 物化视图未触发或设计不当

物化视图是 ClickHouse 的"预聚合加速器",但它也有坑:

  • 同步物化视图没有触发
    INSERT 时物化视图对应的 target 表出问题(如磁盘满),数据未写入,查询时拿不到预聚合结果
  • 物化视图与源表数据不一致
    只支持 INSERT 触发,源表做 DELETE/UPDATE(mutation)后,物化视图不会同步更新
  • 物化视图写入成为 INSERT 的瓶颈
    链式物化视图嵌套过多,原始 INSERT 被拖慢
-- 排查物化视图状态   SELECT   database, 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 设置的表      SELECT      database, table,      engine,     sorting_key,     ttl_info    FROM system.tables  WHERE ttl_info != ''
补充什么是TTL:
TTL(Time To Live,存活时间)是 ClickHouse 自带的自动数据过期 / 冷热分层机制,根据时间字段自动做两件事:
  • 删除过期数据(归档、清理老旧数据)

  • 移动数据到低成本存储(冷热分层,本地盘→S3 / 对象存储)

底层是后台线程异步执行,不需要手动定时删数据。