第 5 章 · MergeTree 核心原理

四个交互演示:① Part 目录结构 · ② 稀疏索引跳 Granule 动画 · ③ 后台 Merge 合并动画 · ④ Skip Index 类型对比

点一下虚拟目录,看看 Part 内部到底有什么

events_mt 表为例,每个 Part 目录下都有三类文件:元数据 / 主键索引 / 列数据(每列独立 .bin + .mrk2)。

← 左侧选一个文件查看

列式存储的精髓:每一列独立成文件。SELECT 只读到需要的 .bin,不碰别的列。
Part 目录名:{分区号}_{minBlock}_{maxBlock}_{level}。 比如 202401_5_9_1 表示"2024 年 1 月分区、已经合并过一轮(level=1)、块号区间 5~9"。

给个 user_id,看它怎么跳过 99% 的 Granule

下面模拟一张有 120 个 Granule(每个 8192 行 ≈ 百万行) 的 Part,按主键 user_id 排序。 输入一个 user_id,点「查询」,看稀疏索引怎么二分 + 只命中少数几个 Granule。

总 Granule 数
120
考察的 Granule
0
实际读取 Granule
0
节省行数
0
关键观察: 稀疏索引的"稀疏"在于"每 8192 行记一条",所以 1 亿行的表 primary.idx 只有 ~12000 条,可以常驻内存; 真正去磁盘读数据的 Granule 数通常只有 1~10 个。

插入产生小 Part,后台 Merge 把它们合并成大 Part

点「插入一批」模拟一次 INSERT,生成一个 level=0 的新 Part;点「触发 Merge」让 MergeSelector 把相近大小的 Part 合并到更高 level。

Level 0(刚插入)
Level 1(第 1 轮合并)
Level 2(第 2 轮合并)
Level 3+(老炼 Part)
活跃 Part
0
总行数
0
累计合并次数
0
最高 level
0
生产告警:活跃 Part 数超过 parts_to_throw_insert(默认 300)时,新的 INSERT 会被拒绝 —— 这就是著名的 Too many parts 错误。所以千万不要高频小批量写入!

同一份数据 + 同一个查询,4 种跳数索引的过滤效果

选择数据分布和查询类型,看哪种索引能跳得最干净。

口诀:有序列选 minmax、低基数选 set(N)、高基数等值选 bloom_filter、字符串关键词选 tokenbf_v1、短串/中文选 ngrambf_v1