建两张200万行的表,一张用默认的index_granularity=8192,一张调到128,看稀疏索引的大小和点查询代价是怎么权衡的。
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在"索引大小"和"点查询精度"之间选的一个折中点,而且这个折中本来就是为"大批量范围扫描"场景设计的,不是为"单行精确查找"设计的。
每一步都是本文上面建表、查询system.parts和query_log得到的原始结果。
system.parts:核对 marks_bytes / primary_key_bytes_in_memorysystem.query_log:对比同一条点查询在不同粒度下的read_rows