接手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生命周期和合并机制的深入理解——这部分只能靠实战慢慢积累。