news 2026/10/2 7:35:40

ClickHouse容量统计全解析:从system.parts到分区治理实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ClickHouse容量统计全解析:从system.parts到分区治理实战

接手ClickHouse集群之后,最先被我翻烂的表就是system.parts。无论是排查磁盘告警、评估数据增长趋势,还是确认分区合并是否健康,所有容量相关的答案,最终都落在这张表上。不少刚接触ClickHouse的朋友习惯去information_schema.tables查大小,查完回来一脸问号——表明明有几十个G,那里面显示的却是0。这个问题的根子不在SQL写错了,而是ClickHouse的容量统计压根不走标准元数据那一套,真正干活的家伙是system.parts。

这篇文章就围绕system.parts展开,把我日常用来查数据库容量、表大小、分区大小的方法全部梳理一遍。你会看到底层存储模型为什么决定了这套查询方式,也会拿到可以直接抄走的SQL脚本,以及我在生产环境里踩过的坑。适合刚接手ClickHouse集群的运维同学,也适合正在做容量评估和成本治理的开发朋友。

1. 为什么查容量首选system.parts——先理解ClickHouse的存储模型

刚用ClickHouse时我也犯过嘀咕:明明是一张表,为什么目录里碎成一片片的小文件夹,每个文件夹名字还长得像乱码。等搞清楚这套机制之后,再看容量查询就顺理成章了。

1.1 一次数据写入到底发生了什么

ClickHouse最常用的表引擎是MergeTree家族,它的存储单位叫part(分区内的数据片段)。每当你执行一次INSERT,数据不会直接写进某个大文件,而是先在内存里攒成一个part,再落盘成一个独立目录。比如一次插入几千行,就可能生成一个包含bin文件、mrk标记文件、checksums.txt校验文件的子目录。这个目录就是part。

part之间有排序键的约束,同一分区内可能存在多个part,它们的范围是有序但不重叠的。后台线程会定期把多个小part合并成一个大part,这个过程叫merge。合并完成之后,旧的part会被标记为 inactive,最终由清理线程物理删除。

这个机制直接决定了容量统计的方式:一张表的磁盘占用,等于它所有分区下所有part目录大小之和。正因为数据是分散在多个part里的,你没法简单“ls”一个目录拿到整张表的大小,必须调用系统表来聚合。

1.2 system.parts表结构里最关键的几个字段

system.parts是ClickHouse内置的系统表,每一行对应一个part。注意,不是一张表一行,而是一个part一行。如果一张表有100个分区、每个分区3个part,那这张表在system.parts里就有300行记录。

核心字段我按用途分类整理如下:

  • 定位字段:database(库名)、table(表名)、partition_id(分区ID)、part_type(part类型,如Wide、Compact)
  • 状态字段:active(1表示当前使用的part,0表示已经废弃等合并的旧part)、state(part状态,比如Committed、Outdated)
  • 容量字段:rows(part内的行数)、bytes_on_disk(part在磁盘上的实际字节数)、data_compressed_bytes(压缩后数据字节)、data_uncompressed_bytes(未压缩数据字节)
  • 区段信息:min_block_number、max_block_number(part内数据块编号区间),这块在解读part目录名时会提到
  • 时间字段:modification_time、min_time、max_time,对应分区键的取值区间

提示:bytes_on_disk是整个part目录的物理大小,包含了校验文件、标记文件、默认压缩算法下的数据文件,是最接近真实磁盘占用的指标。我们做容量统计时,默认用它。

1.3 和information_schema.tables对比,差别在哪

information_schema.tables是标准SQL里定义的元数据视图,ClickHouse也有提供,但total_bytes字段很多情况下并不返回真实值。这跟ClickHouse的分布式架构有关:一张分布式表的数据散落在多个分片、多个part目录,标准元数据视图不维护逐part的物理统计,所以经常是0或者空。

反观system.parts,它是直接从存储层读取的物理信息,每个part目录的大小都在这里如实记录。所以我的经验是:所有跟容量相关的查询,一律走system.parts,不要绕远路。

2. 数据库容量和表大小的核心查询语法

这一节直接上干货。我日常用的查询基本就是下面这几种场景,从库维度、表维度到分区维度都能覆盖,读写分离执行即可。

2.1 查看整个数据库的容量

统计一个库所有表的总大小,最简单的写法:

SELECT database, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(rows) AS total_rows FROM system.parts WHERE (database = 'your_database') AND active = 1 GROUP BY database;

这里有几个细节要说明。

第一,active = 1这个过滤条件非常重要。如果不加,会把那些已经合并完、等待清理的旧part也算进来,导致统计结果明显偏大,甚至翻倍。我的习惯是,任何容量查询默认都带active = 1,除非你就是为了排查“磁盘为什么迟迟不释放”而专门去看废弃part。

第二,formatReadableSize是ClickHouse的格式化函数,输出结果类似12.34 GiB,比直接看字节数直观得多。如果要做后续告警判断,建议保留原始字节数,或者用formatReadableSize和自算MB两个字段同时输出。

第三,如果集群是多副本的,system.parts在每台节点上只记录本节点的part,所以单节点查询得到的是该节点存储引擎层面的容量,不是整个集群的。想查全集群总容量,得在每个节点执行后再汇总,或者用clusterAllReplicas类函数,具体后面实操部分再展开。

2.2 查看单张表的大小和行数

统计某张表的总容量,基本查询是:

SELECT table, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(rows) AS total_rows, round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS compression_ratio FROM system.parts WHERE (database = 'your_database') AND (table = 'your_table') AND active = 1 GROUP BY table;

这里额外加了一列压缩比。压缩比是个很值得关注的指标,它在数值上等于“未压缩字节数 / 压缩后字节数”,反映的是数据在ClickHouse里的压缩效果。通常情况下:

  • 日志类文本数据,压缩比在5~10之间都很正常
  • 指标类数值数据,压缩比在2~4之间比较常见
  • 如果压缩比低于2,你要么存了本来就很难压缩的数据(比如已加密、已压缩的图片字段),要么列设计上还有优化空间

这列数据在做成本评估时很有价值。比如你发现某张表的压缩比很低,说明存储成本偏高,可以考虑调整压缩算法、更换编码方式,或者对低基数字段用字典编码。

2.3 按分区统计大小,定位“膨胀”的分区

分区是MergeTree里最重要的逻辑隔离单位,容量排查时经常要看分区维度:

SELECT partition_id, count() AS part_count, formatReadableSize(sum(bytes_on_disk)) AS partition_size, sum(rows) AS rows FROM system.parts WHERE (database = 'your_database') AND (table = 'your_table') AND active = 1 GROUP BY partition_id ORDER BY sum(bytes_on_disk) DESC;

这个查询最大的用处是找“分区不均匀”的问题。举个真实例子,我有一次接到线上反馈,说某张日志表磁盘增长异常,我按分区一查,发现最近3天生成的分区每个都超过200GB,而更早的历史分区每个只有10GB左右。这时候就意识到,新增数据里可能混入了大量异常字段,或者某个业务方改了写入逻辑,导致日志内容膨胀。

如果不做分区维度的统计,光看全表大小,你只能看到总量在涨,却很难定位到具体是哪一天、哪个分区出了问题。

2.4 用system.tables做快速元数据统计

除了system.parts,system.tables里也维护了一份表级别的元数据,其中total_bytes在部分场景下可用。它的底层实现其实也是汇总parts信息,但胜在查询更轻量,适合快速逛一遍集群里所有表的大小:

SELECT database, table, formatReadableSize(total_bytes) AS disk_size, total_rows FROM system.tables WHERE database = 'your_database' ORDER BY total_bytes DESC;

需要提醒的是,system.tables的total_bytes是否准确,和system.parts的统计口径不完全一致。在我见过的版本里,它多数是可靠的,但遇到表刚被DROP或TRUNCATE后元数据刷新不及时的情况,数值可能滞后。所以我偏向于把它当“快速预览”,正式做容量报告时还是以system.parts为准。

3. part命名规则与实际运维,读懂part目录名

维护ClickHouse时间长了,你会越来越在意part目录名。为什么要单独讲命名?因为从part名字里能解读出这个part属于哪个分区、覆盖哪个数据块区间、是第几次合并产生的,这对排查容量问题和合并异常非常有帮助。

3.1 part目录名的标准格式

一个MergeTree表的part目录名长这样:

202406_0_168_3

用下划线分割成四段,含义依次是:

  • 第一段:分区ID。比如按日期分区,这里通常是202406这样的年月字符串;如果是元组分区,则可能变成(202406,1)之类的编码形式
  • 第二段:min_block_number,当前part内数据块编号的最小值
  • 第三段:max_block_number,当前part内数据块编号的最大值
  • 第四段:合并代数,表示这个part经历了多少次合并。初始插入的part这一位通常是0,每次合并后会递增

举个例子,202406_0_168_3表示的是202406分区下的一个part,它覆盖了数据块编号0到168,已经合并过3次。

3.2 从system.parts逆向解读part信息

对应关系在system.parts中都能找到。查询part基本信息:

SELECT partition_id, name AS part_name, min_block_number, max_block_number, level, rows, bytes_on_disk, active FROM system.parts WHERE (database = 'your_database') AND (table = 'your_table') ORDER BY min_block_number LIMIT 20;

这个查询在平时排查时很常用。你可以观察到哪些part长期处于level=0没有合并,哪些part的min_block_number和max_block_number之间存在巨大空洞。这些信息比单纯看大小更能反映写入与合并的健康状态。

3.3 part命名在容量治理中的两个实践点

第一,判断合并是否卡住。正常情况下,一个分区内part数量应该维持在一个较低水平,后台会不断把小part合并成大part。如果你发现某分区的part数量一直上涨,而且新part的level一直是0,旧part长期处于outdated状态但又不被清理,那很可能是合并线程被限制或者磁盘空间不足导致合并失败。这时候容量统计反而要特别小心,因为bytes_on_disk会把新旧part都算进去,磁盘占用虚高。

第二,手动触发合并后的反馈。执行OPTIMIZE TABLE ... FINAL之后,观察part目录名的变化是最直观的反馈方式。合并成功后,max_block_number会变大,level会递增,多个旧part会变成 inactive。如果执行完OPTIMIZE后part名没变化,说明没有可合并的数据,或者表已经处于最紧凑状态。

4. 实操脚本与监控落地,一键统计集群容量

SQL单独写不算本事,真正好用的是把SQL封装成脚本,在故障排查和日常巡检时能够一键执行。这一节分享我实际在用的几个脚本和落地思路。

4.1 一次性统计全库所有表的大小

要在ClickHouse客户端里查一个库下所有表的大小,可以把前面单表查询的SQL去掉table过滤条件,并加上按表分组的聚合:

SELECT table, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(rows) AS total_rows, count() AS part_count FROM system.parts WHERE (database = 'your_database') AND active = 1 GROUP BY table ORDER BY sum(bytes_on_disk) DESC;

如果库里表特别多,这个查询也能跑得很快,因为system.parts本来就是内存表,扫描速度足够。

想要跨库全集群汇总,可以在clickhouse-client里直接跑:

SELECT database, formatReadableSize(sum(bytes_on_disk)) AS disk_size FROM system.parts WHERE active = 1 GROUP BY database ORDER BY sum(bytes_on_disk) DESC;

在分布式集群环境下,每台节点各自执行上述查询,得到的就是该节点本地的统计。如果只想看某个分片的情况,直接连对应节点查询即可。

4.2 定时巡检与告警阈值

我通常会让脚本每小时跑一次,把结果写入一张带日期的统计表,然后配置告警。比如:

  • 某个database的单日新增容量超过过去30天日均的5倍,触发扩容提醒
  • 某张核心表total_rows增长异常,触发排查
  • 某分区的part_count超过50,触发合并健康检查

简单实现方式是在ClickHouse里建一张统计结果表,然后用INSERT INTO ... SELECT把上面的查询结果定期写入:

CREATE TABLE default.capacity_statistics ( stat_date Date, database String, table String, disk_size_bytes UInt64, total_rows UInt64, part_count UInt64 ) ENGINE = MergeTree() ORDER BY (stat_date, database, table);

然后在外部用cron或调度平台定时执行:

INSERT INTO default.capacity_statistics SELECT today(), database, table, sum(bytes_on_disk), sum(rows), count() FROM system.parts WHERE active = 1 GROUP BY database, table;

之后查增长趋势就很方便。比如看某张表最近7天的容量变化:

SELECT stat_date, formatReadableSize(disk_size_bytes) AS disk_size FROM default.capacity_statistics WHERE (table = 'your_table') AND (stat_date >= today() - 7) ORDER BY stat_date;

配合Grafana或者自建监控面板,一张表近30天的容量曲线就出来了,容量规划不再靠拍脑袋。

4.3 容量异常后的清理策略

容量排查的最后一步是治理。如果发现某分区数据特别大且确认不需要了,优先用分区裁剪删除:

ALTER TABLE your_table DROP PARTITION '202405';

这条命令会删除整个202405分区的所有数据,并且会真正释放磁盘。注意,DROP PARTITION的耗时取决于分区大小,执行期间对查询有一定影响,建议在低峰期操作。

如果是历史数据自动过期,直接配置TTL:

ALTER TABLE your_table MODIFY TTL event_date + INTERVAL 90 DAY;

ClickHouse会在后台自动清理过期分区。我自己更喜欢TTL,因为不用人为干预,但要注意TTL的清理是“懒删除”的,它只删除part目录中的数据,是否立即释放磁盘取决于后台任务调度,不过最终会释放。

另外要提一个很容易记得的误解:TRUNCATE TABLE清空表后,磁盘空间不会立刻下降,这在第5部分会展开。

5. 常见问题与排查技巧实录,sysytem.parts使用避坑

这个部分是我最想写的。这些坑不是从文档里看来的,全是生产环境里一个个踩出来的。知道了它们,你至少能少熬几个大夜。

5.1 active=0导致磁盘统计虚高

有一次我排查磁盘告警,发现某节点磁盘使用率一直卡在98%,但是按业务表逐个查大小,怎么看都只占用了60%。后来查了system.parts里active=0的记录,才发现有大量废part没被物理删除。

这是因为我在业务低峰期执行了多次ALTER TABLE ... DELETE,每次删除都会旧part标记为 inactive,但不会立即删除物理文件。后台清理线程要等合并和下线流程跑完才动手。所以磁盘被一堆旧part占着,而你常规统计时往往只查active的part。

解决这个问题没有银弹,只能等后台清理,或者手动触发:

SYSTEM DROP MARKED PARTS;

我用这个命令的频率不高,但在测试环境做数据清理演练时很有效。生产环境执行前建议先在活动量小的节点上试跑一次,确认对查询无影响再全面执行。

5.2 查询结果为零,多半是权限或引擎问题

system.parts查询返回0行记录,常见原因有三类。

第一,当前用户没有读取系统表的权限。很多公司会给不同业务线分配独立的账号,默认权限并不包含访问所有系统表。我用system.parts之前习惯先确认账号角色,或者直接用管理员账号跑巡检。

第二,表是分布式表而不是本地表。system.parts不统计分布式表的逻辑容量,它统计的是本地MergeTree表。你要查的是distributed_logs这种引擎,实际容量应该去查它的本地表logs。这是特别常见的坑。

第三,表使用了非MergeTree引擎。system.parts只面向MergeTree家族表,如果是Log、Memory这类引擎,parts里根本没有对应记录,查出来当然为空。

5.3 TRUNCATE后容量不释放,别急着怀疑统计口径

TRUNCATE TABLE之后我去查system.parts,经常能看到active=0的part还躺在那里。有同事会误以为表没清干净,或者统计口径有问题,其实这是ClickHouse的设计。

TRUNCATE操作是标记part不可见,但物理删除是异步的。想要释放空间,比较稳妥的做法是:

TRUNCATE TABLE your_table; SYSTEM DROP MARKED PARTS;

或者干脆用DROP TABLE,这个会立即释放目录空间。

5.4 system.parts查询太慢怎么办

正常执行一个带WHERE (database = 'xx')的统计查询,毫秒级就能出来。如果你发现查询特别慢,先看是不是把条件写成了WHERE database = 'default' OR table = 'xxx'这种全表扫描的写法。

在超大集群里,system.parts里的记录数量可能是百万级甚至更多。强烈建议查询时始终带上database和table的精确过滤条件,这能极大缩小扫描范围。另外,如果只是日常巡检,可以只在凌晨低峰期跑,避免和业务查询抢CPU。

5.5 关于part的生命周期与增量统计

还有一个经验:如果做增量容量统计,不要只依赖sum(bytes_on_disk) - sum(bytes_on_disk)来算单日新增,因为合并会不断重写文件,导致part大小短期波动。更可靠的方式是:每天固定时间快照bytes_on_disk,做环比差值;同时把sum(rows)的差值作为参考。当两个差值方向不一致时,优先以行数变化为准判断业务增长,以磁盘变化为辅判断存储效率。

6. 一些关于 system.parts 的补充技巧与扩展思路

system.parts除了算容量之外,其实还可以做不少事情。我个人常用来做三件额外的事:确认分区键设置是否合理、核对备份恢复后的表结构是否健康、以及诊断数据倾斜。

6.1 用parts验证分区键设计是否合理

分区键选得好不好,从part数量就能看出来。如果分区粒度过细,比如按小时分区但每小时数据量很少,会产生大量小part,每个part里rows只有几千,bytes_on_disk也小得可怜,但是part数量很多,合并压力大,查询性能反而下降。

判断小part是否过多的SQL:

SELECT partition_id, count() AS part_cnt, min(rows) AS min_rows, max(rows) AS max_rows, round(avg(rows)) AS avg_rows FROM system.parts WHERE (database = 'your_database') AND (table = 'your_table') AND active = 1 GROUP BY partition_id ORDER BY part_cnt DESC;

如果某个分区的part_cnt特别大,且avg_rows很小,说明分区键粒度可能过细。这时候要么调整分区键,比如从小时改成天,要么考虑用OPTIMIZE TABLE ... FINAL强制合并。

6.2 从parts信息中发现数据倾斜

多副本集群里,如果每个分片节点上的part总大小差异巨大,说明数据路由可能倾斜。依次在每台节点上执行:

SELECT hostName() AS host, formatReadableSize(sum(bytes_on_disk)) AS disk_size FROM system.parts WHERE (database = 'your_database') AND (table = 'your_table') AND active = 1 GROUP BY host;

对比多节点的输出,如果某个节点明显大于其他节点,大概率是分片键选择不佳,写入热点全打到了一个节点上。这个检查我每次在做容量评估时都会顺手跑一遍,对后续扩容和分片键调整非常有参考价值。

6.3 集群扩容前的容量预判

在做磁盘扩容时,我习惯先用下面这个SQL预估未来30天每个节点需要的空间:

SELECT database, table, formatReadableSize(sum(bytes_on_disk)) AS disk_size, sum(bytes_on_disk) / sum(rows) AS bytes_per_row FROM system.parts WHERE active = 1 GROUP BY database, table ORDER BY bytes_per_row DESC;

bytes_per_row表示每行数据平均占用的磁盘字节数。结合业务方给出的预期日增行数,就能算出未来一个月的存储需求,比单纯看当前总量靠谱得多。

7. 最后分享一下我对system.parts的整体使用心得

用system.parts查容量这件事,看起来只是几条SQL的事,但背后的价值远不止“看到大小”这么简单。它是观察ClickHouse存储层健康状况的一扇窗口:part是否正常合并、分区是否合理、数据是否倾斜、压缩是否有效,都能从这张表里读出来。

我在实际运维中踩过几次大坑之后,总结出几条铁律:

  • 所有容量统计默认带active = 1,查废弃part时再单独放开
  • 统计口径以bytes_on_disk为主,行数和压缩比作为辅助判断
  • 跨节点容量汇总时按节点分别执行,不要试图在单节点上拿到全集群准确值
  • 磁盘告警不只看总量,还要看part数量和合并状态,否则会被“虚高”或“延迟释放”误导

如果你刚开始接触ClickHouse的容量管理,不用急着写一堆复杂的监控脚本,先把system.parts里的字段含义弄清楚,把前两节的SQL跑明白,你就能解决日常80%的容量问题。剩余的20%,靠的是对part生命周期和合并机制的深入理解——这部分只能靠实战慢慢积累。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 7:35:19

光电二极管与X射线探测器前端低噪声TIA设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 7:34:46

电控工程师简历突围:10个可验证开源项目实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 7:34:06

AD9361 HDL工程生成:用ADI TCL脚本在Vivado中高效搭建FPGA设计

做AD9361相关的板子也有些年了,这次要在Vivado里用ADI官方TCL脚本从头生成AD9361的HDL工程,本来以为就是跑个脚本的事,结果版本、路径、IP核升级这些坑一个个冒出来。折腾完回头一看,整个流程其实非常有规律,只要把原理…

作者头像 李华
网站建设 2026/10/2 7:33:16

归并排序从原理到实战:分治思想、复杂度分析与应用场景

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 7:32:09

QQ聊天记录转Word文档:数据库解析、脚本转换与交付验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 7:29:07

ADS2020交叉耦合VCO仿真全流程:从起振到相位噪声的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华