bigywater.tech
ClickHouse 内部原理 · 9

Sparse Primary Index:ClickHouse 为什么索引不是每行一个

建两张200万行的表,一张用默认的index_granularity=8192,一张调到128,看稀疏索引的大小和点查询代价是怎么权衡的。

1/64粒度→17倍索引
granularity决定索引大小
8192→128行
同一次点查询读取量随粒度精确等比缩小
64倍
两次read_rows的比例,跟粒度比例精确对应

做的事:同样200万行,两种索引粒度

MySQL 的 B+树索引是"每一行都能在索引里精确定位";ClickHouse 的主键索引不是这样——它默认每 8192 行才记一条索引项,这就是"稀疏"的意思。

建表,唯一区别是index_granularityCREATE TABLE demo.sparse_default (id UInt64, v Float64) ENGINE=MergeTree ORDER BY id -- 默认 index_granularity=8192 CREATE TABLE demo.sparse_fine (id UInt64, v Float64) ENGINE=MergeTree ORDER BY id SETTINGS index_granularity=128 各插入200万行 (numbers(2000000))

验证:索引本身的大小差了多少

直接查 system.parts 里索引占用的字节数。

结果(marks_bytes:索引标记文件大小)sparse_default (granularity=8192): 781 字节 / 940 字节(2个part) sparse_fine (granularity=128): 13407 字节 / 16746 字节(2个part)

granularity 从8192降到128,是缩小到原来的 1/64,索引变大了约 17倍。默认粒度下,200万行只需要约 2000000/8192≈244 条索引项,小到可以整个常驻内存;如果真的做成"每行一条索引"(相当于粒度=1),索引项数量会是这里的 8192 倍,对于百亿行级别的表,那样的索引会大到没法放进内存——这正是 ClickHouse 选择"稀疏"索引的原因:用不追求精确定位换来索引本身足够小、能常驻内存。

验证:稀疏索引的代价——一次点查询到底读了多少行

对两张表分别查同一个id,看read_rows。

结果SELECT v FROM demo.sparse_default WHERE id = 1234567 → read_rows = 8192 SELECT v FROM demo.sparse_fine WHERE id = 1234567 → read_rows = 128

两次查询都只要 1 行结果,但默认粒度读了 8192 行,细粒度只读了 128 行——正好是 8192 / 128 = 64 倍,跟 granularity 的比例分毫不差。这就是稀疏索引的代价:索引只能告诉你"这个id落在哪一个8192行的颗粒里",具体是颗粒里的第几行,要真的把这整个颗粒读出来再线性找。granularity 越小,点查询浪费的读取越少,但索引本身越大;默认的8192是ClickHouse在"索引大小"和"点查询精度"之间选的一个折中点,而且这个折中本来就是为"大批量范围扫描"场景设计的,不是为"单行精确查找"设计的。

这也是为什么 ClickHouse 从不建议拿它做高频单行查找——稀疏索引天生要为每次点查询多读出一整个颗粒的数据,颗粒越大代价越高,这跟 MySQL 的 B+树索引"一次查找只碰几个数据页"是完全不同的设计取舍。

分步演示:稀疏索引的权衡

每一步都是本文上面建表、查询system.parts和query_log得到的原始结果。

索引粒度决定了索引大小和点查询代价的权衡
这一步新验证的粒度

    本文用到的工具

    • 本机 ClickHouse 26.9.1,200万行 MergeTree 表,对比 index_granularity=8192 与 128
    • system.parts:核对 marks_bytes / primary_key_bytes_in_memory
    • system.query_log:对比同一条点查询在不同粒度下的read_rows
    京ICP备2026067502号-1京公网安备11010502063487号