bigywater.tech
ClickHouse 内部原理 · 2

行式存储和列式存储到底差在哪里

建一张 50 列的宽表,用 system.query_log 里的 read_bytes 数字,看列存到底是怎么让“只查几个字段”变得比“查整行”便宜很多倍的。

320倍
查1列 vs 查全部50列的读取量差距
线性
read_bytes 随列数增长的规律
6.4倍
SELECT * 比只算length()多读的数据量

做的事:一张 50 列的宽表,2 万行

建一张只有 id + 50 个 String 列的表,每个字符串约 45 字节,插入 2 万行,再用不同的查询去读它。

CREATE TABLE demo.wide (id UInt64, c1 String, ..., c50 String) ENGINE=MergeTree ORDER BY id插入 20000 行,每行 50 个字符串列 SELECT count() FROM demo.wide → 20000

行式数据库(比如 MySQL 的 InnoDB)会把每一行的 51 个字段紧挨着存在一起;ClickHouse 这种列式数据库,是把同一列的 2 万个值单独存成一段连续数据,51 个列对应 51 段独立的物理存储。这个差异平时看不出来,一旦"只查一部分列"就会立刻体现在读取的数据量上。

验证:只查 1 列 vs 查全部 50 列

用 system.query_log 里记录的 read_bytes,看两条查询到底读了多少数据。

SELECT sum(length(c1)) FROM demo.wide → read_bytes = 156.25 KiB SELECT * FROM demo.wide FORMAT Null → read_bytes = 49.04 MiB

同一张表,只查 1 列只读了 156.25 KiB,查全部 50 列读了 49.04 MiB——差了 约 320 倍。行式存储做不到这一点:因为一行的 51 个字段物理上挨在一起,哪怕你只要其中 1 个字段,也得把这一行完整读出来再从里面抠出那 1 个字段;列存则可以直接跳过其它 50 列对应的磁盘文件,压根不去碰它们。

这就是"分析型查询"最常见的访问模式——一张表几十上百个字段,但一次统计通常只用到其中三五个。列存让"用几个字段就只读几个字段对应的数据"变成了可行的事,而不是理论上的优化。

验证:读取量是不是跟列数成正比

从 1 列加到 50 列,逐步测 read_bytes,看是不是线性增长。

SELECT sum(length(c1)+...+length(cN)) FROM demo.wide, read_bytes:N=1 列 → 156.25 KiB N=3 列 → 468.75 KiB (≈3.0倍) N=10 列 → 1.53 MiB (≈9.8倍) N=25 列 → 3.81 MiB (≈24.9倍) N=50 列 → 7.63 MiB (≈48.8倍)

数字几乎是完美的线性关系:查 3 列约等于查 1 列的 3 倍,查 50 列约等于 49 倍。列存把"读取量"和"查询涉及的列数"绑定在一起,而不是绑定在"表有多少列"或"表有多少行的原始大小"上。

意外发现:length() 读到的比 SELECT * 还要少

同样是摸全部 50 列,为什么 sum(length(...)) 只读 7.63 MiB,SELECT * 却要读 49.04 MiB?

原因在 String 列的存储结构:ClickHouse 把一列 String 拆成两段——一段是每个字符串的结束位置偏移量(offsets,定长、很小),另一段才是所有字符串真正的字节内容(chars,不定长、很大)。length() 只需要相邻两个 offset 相减就能算出字符串长度,完全不需要碰 chars 那一大段内容;而 SELECT * 要把每个字符串原样吐出来,必须把 chars 完整读出来。

同一份数据、同一批列,"只要长度"和"要完整内容"在列存里是两种不同代价的操作——这在行式存储里是做不到的,因为字符串的偏移量和内容在磁盘上物理挨在一起,没法只读一半。

分步演示:读取量是怎么随列数线性增长的

每一步都是本文上面查询 system.query_log 得到的原始 read_bytes。

列存的读取量随查询涉及的列数线性增长
这一步新验证的列数

    本文用到的工具

    • 本机 ClickHouse 26.9.1 单节点,50 列 MergeTree 宽表,2万行
    • system.query_log:记录每条查询的 read_rows / read_bytes
    • SYSTEM FLUSH LOGS:强制把 query_log 缓冲区落盘再查询
    京ICP备2026067502号-1京公网安备11010502063487号