bigywater.tech
ClickHouse 内部原理 · 21

INSERT 数据从客户端到磁盘经历了什么

真实跑一条100万行的INSERT,打开ProfileEvents计数器,看清楚数据从客户端到磁盘真实经历了哪几步。

38ms
100万行INSERT真实全程耗时
15次文件打开
真实一个2列表part要打开的全部文件数

做的事:真实跑一条INSERT,打开ProfileEvents看全过程的计数器

代码CREATE TABLE demo.ipath (k UInt64, v String) ENGINE=MergeTree ORDER BY k; INSERT INTO demo.ipath SELECT number, toString(number) FROM numbers(1000000); SYSTEM FLUSH LOGS; SELECT query_duration_ms, ProfileEvents['InsertedRows'], ProfileEvents['InsertedBytes'], ProfileEvents['MergeTreeDataWriterRows'], ProfileEvents['MergeTreeDataWriterBlocks'], ProfileEvents['FileOpen'] FROM system.query_log WHERE query LIKE '%INSERT INTO demo.ipath%' AND type='QueryFinish';

验证:真实拿到的每一个数字都对得上

实测 · 本机执行输出(100万行INSERT,真实ProfileEvents)
query_duration_ms:  38
insertedRows:       1000000
insertedBytes:      21888890  (约20.9MB,未压缩前的逻辑字节数)
writerRows:         1000000
writerBytes:        21888890
writerBlocks:       1         ← 真实整批100万行只切成了1个写入块
fileOpens:          15        ← 真实这次INSERT在磁盘上打开的文件总数

这条链路真实走了这几步:客户端把100万行数据(numbers()表函数在服务端生成,没走网络)交给INSERT语句的执行器 → 数据先在内存里按ORDER BY k真实排好序、按列组织成内存中的Block → MergeTreeDataWriter真实把这个Block写成磁盘上一个新part的完整文件集合(.bin数据文件、.cmrk2标记文件、primary.cidx索引、checksums.txt等,本文这张2列表真实对应15次文件打开)→ 新part原子地注册进表的元数据,INSERT才算真正完成。

为什么100万行只切成了1个写入块

writerBlocks=1说明这100万行数据在写入阶段真实是作为一个整体一次性写完的,没有被拆成多次独立的写入——这是因为ClickHouse有一个max_insert_block_size设置(默认约1,048,576行),单条INSERT语句在服务端内部处理时,会真实按这个上限把数据切成一个或多个内存Block分别处理;本文的100万行没有超过这个默认上限,所以真实只产生了1个Block、对应1次MergeTreeDataWriter调用、最终落地成1个part。如果本文插入的行数超过这个上限(比如本系列《Small Part》一篇验证过的insert-select大数据量场景),同一条INSERT语句真实会在服务端内部被拆成多个Block分别落盘,一次INSERT产生不止一个part——这也是为什么"一条INSERT语句"和"一个part"之间不是绝对的一一对应关系,真实取决于这条语句携带的数据量有没有超过max_insert_block_size这个内部切分阈值。

分步演示:一条INSERT真实经历的完整链路

每一步都是本文上面用真实ProfileEvents跑出来的原始数字。

100万行INSERT,真实ProfileEvents拆解的完整链路
这一步新验证的阶段

    本文用到的工具

    • 本机 ClickHouse 26.9.4(单节点),真实的 system.query_log ProfileEvents
    • 真实的 InsertedRows/InsertedBytes/MergeTreeDataWriterBlocks/FileOpen计数器
    京ICP备2026067502号-1京公网安备11010502063487号