bigywater.tech
ClickHouse 内部原理 · 6

Block:ClickHouse 为什么不逐行传递数据

用 Native 格式导出一份查询结果的原始字节,逐字节解码出 ClickHouse 处理器之间真正传递的最小单位——Block。

1份表头
整个Block只需要1份列名+类型信息
按列连续
同一列的值物理挨在一起
2列3行
本文解码的最小Block示例

做的事:把一个 Block 的原始字节流导出来看

ClickHouse 处理器之间传递数据的最小单位不是"一行",而是"一个 Block"(一批列 + 一批行)。用 Native 格式可以看到这个 Block 在内存/网络里的原始字节。

导出3行2列的Block,用xxd看原始字节clickhouse-client --query "SELECT id, v FROM demo.olap_batch WHERE id<=3 ORDER BY id FORMAT Native" | xxd 00000000: 0203 0269 6406 5549 6e74 3634 0100 0000 ...id.UInt64.... 00000010: 0000 0000 0200 0000 0000 0000 0300 0000 ................ 00000020: 0000 0000 0176 0746 6c6f 6174 3634 0000 .....v.Float64.. 00000030: 0000 0000 f83f 0000 0000 0000 1440 0000 .....?.......@.. 00000040: 0000 0000 0c40 .....@

这一段字节就是一个的 Block,逐字节拆开看:

字节含义
02这个 Block 有 2 列
03这个 Block 有 3 行
02 "id" 06 "UInt64"第1列:列名"id",类型"UInt64"
01 00...00 / 02 00...00 / 03 00...00id列的3个值:1、2、3,连续排列
01 "v" 07 "Float64"第2列:列名"v",类型"Float64"
f8 3f... / 14 40... / 0c 40...v列的3个值:1.5、5、3.5,连续排列

验证:一列的值挨在一起,不是一行的值挨在一起

从上面这份字节流能直接验证:id列的3个值(1,2,3)先连续排完,然后才轮到v列的3个值(1.5,5,3.5),而不是"第1行(1,1.5)、第2行(2,5)、第3行(3,3.5)"这样按行交替排列。

这就是一个 Block 的样子:一份表头(列名+类型,整个Block只有一份)+ 每一列各自连续的一段数据。表头信息(列名、类型字符串)在整个 Block 里只需要传一次,不用像逐行传递那样每一行都重复一遍字段名——这也是为什么 Block 越大,表头这类固定开销摊得越薄。

v 列里的 5 和 3.5 不是巧合——这条查询就是本系列第1篇里跑过 ALTER TABLE UPDATE 之后的同一张表:id=2 的偶数行被翻倍成了5,id=1 和 id=3 保持原样。这里只是换了个角度,用最底层的字节确认这些值是怎么被组织和传输的。

为什么处理器之间要传 Block,不是传一行

Pipeline 里的每个处理器(Transform)的输入输出,单位都是 Block。

如果每次只传 1 行,处理器每处理一次就要重新解析一遍"这一行有哪些列、什么类型",这份表头开销会被重复 2000 万次;而传一个装了几万行的 Block,表头只需要解析 1 次,之后就是对一整段连续内存做批量运算——这跟本系列第4篇里测出的"max_block_size=1 时耗时暴涨361倍"是同一个原因的两个角度:那一篇看到的是墙钟时间的暴涨,这一篇看到的是这个暴涨背后,物理上到底在传递什么样的数据结构。

一句话总结:Row 是逻辑上的一条记录,Block 才是 ClickHouse 在内存里流动、在处理器之间传递的物理单位——一份表头,加上每一列各自连续的一段数据。

分步演示:逐字节解码一个的Block

每一步都是本文上面导出的Native格式原始字节,按顺序拆解。

一个Block的字节,逐段解码
这一步新解码的部分

    本文用到的工具

    • FORMAT Native:ClickHouse 内部处理器之间使用的二进制格式
    • xxd:把原始字节流转成可读的十六进制
    京ICP备2026067502号-1京公网安备11010502063487号