【AI】 Clickhouse MergeTree基本原理
ClickHouse 的 MergeTree 引擎系列是其最核心、最强大的存储引擎,为 ClickHouse 提供了卓越的列式存储性能、高效的数据插入和近乎实时的查询能力。理解其原理对于高效使用 ClickHouse 至关重要。
核心原理围绕着 分片(Partition)、排序(Ordering)、稀疏索引(Sparse Indexing)、标记文件(Mark Files)和异步合并(Asynchronous Merges) 这几个关键概念:
-
分区(Partitioning):
- 目的:加速查询,通过分区裁剪(Partition Pruning)减少数据扫描范围。
- 机制 :数据表在磁盘上的物理存储按分区键(
PARTITION BY
表达式)被分割成不同的子目录 。例如,按日期PARTITION BY toYYYYMM(date)
。 - 物理结构 :每个分区对应磁盘上的一个独立目录(如
202308
,202309
)。查询时,WHERE 条件如date >= '2023-09-01' AND date < '2023-10-01'
,可以快速定位到202309
分区目录,忽略其他分区数据。 - 注意 :分区粒度需谨慎。分区过多 (如按小时分区)会导致小文件过多,增加元数据负担,降低合并效率;分区过少(如单分区)则失去分区裁剪的优势。
-
数据排序(Ordering):
- 目的:高效定位数据范围,支持快速范围查询、ORDER BY、GROUP BY 以及数据压缩(相似数据邻近存储压缩率更高)。
- 机制 :在每个分区的内部,数据行严格地 按照排序键(ORDER BY 键或 PRIMARY KEY)定义的顺序存储在磁盘上。
- 物理结构 :每个列字段(
.bin
文件)内部的数据都是按照排序键有序排列的。 - 影响:排序键的设计是查询性能的关键。将最常用于过滤(WHERE)、分组(GROUP BY)和排序(ORDER BY)的列放在前面。
-
稀疏主键索引(Sparse Primary Index):
-
目的 :在大数据集中快速定位数据块位置,避免全扫描,同时极小化索引存储开销。
-
机制:
-
索引粒度(index_granularity) :默认 8192 行。索引并不指向每一行,而是指向一个数据块 的起始位置(一个数据块包含
index_granularity
行)。 -
索引文件(.idx) :存储每个索引粒度对应的数据块中,排序键第一个字段的最小值 (或前几个字段的布隆过滤器等)。想象一下电话簿的索引页只列出每 N页的第一个名字。
-
工作流程:
- 查询条件如
WHERE user_id = 123
。 - 在稀疏索引文件 (
.idx
) 中查找user_id = 123
可能位于哪些数据块(查找满足条件的最小和最大边界数据块)。 - 读取这些候选数据块对应的
.bin
文件偏移信息(来自.mrk
文件)。 - 从磁盘只读取这些特定的数据块 (通常包含多个
index_granularity
行)到内存。 - 在内存中对这些加载进来的完整数据块进行精确扫描,找到
user_id = 123
的实际行。
- 查询条件如
-
-
优势 :索引文件非常小(只有行数/
index_granularity
个条目),可以常驻内存。即使表有 PB 级数据,索引通常也只有几 MB 到几百 MB。 -
局限性 :索引只包含第一个字段的最小值。对于复合键
(a, b, c)
,索引只记录每个数据块中a
的最小值。高效查找b
或c
需要a
的等值查询或使用辅助索引(如跳数索引)。点查询WHERE user_id = 123
效率极高,因为只需加载少数数据块;但如果查询条件是低基数字段(如status = 1
,满足条件的行分布在大量数据块中),效率会下降。
-
-
标记文件(Mark Files, .mrk):
-
目的:连接稀疏索引(.idx)和真实列数据文件(.bin),提供从逻辑索引位置到物理磁盘偏移量的精确映射。
-
机制:
-
对于每个列的
.bin
文件,都有一个对应的.mrk
文件。 -
.mrk
文件中的每个"标记"对应索引粒度中的一个条目(.idx
文件中的一个索引条目)。 -
每个"标记"包含两部分信息:
- 块在
.bin
文件中的偏移量 :用于定位到包含目标行的压缩数据块的开头。 - 解压后块内的行偏移量:压缩块会被完整读入内存解压。该偏移量指明目标行在这个解压块内的具体起始位置。
- 块在
-
-
工作流程 :索引查找定位到候选的索引条目后,通过索引条目在
.mrk
文件中的位置,找到对应的标记,读出.bin
文件偏移量和块内行偏移量,然后精准读取所需的数据部分。
-
-
异步数据合并(Asynchronous Merges):
-
问题来源 :数据是分批次(称为 "部分(part)" 或 "片段(fragment)" )插入的(每次 INSERT 生成一个新的 part)。物理上,每个 part 是位于磁盘上的一个独立的目录,包含其分区内的数据块、索引、标记等。每个 part 内部按排序键有序,但不同的 part 之间可能存在重叠(同一个用户数据分散在多个 part 里)。
-
目的:
- 减少文件数量,提高查询效率(避免打开太多文件)。
- 将小的部分聚合成大的、更连续有序的部分,优化顺序读取性能。
- 强制执行数据排序(合并后分区内部完全有序)。
- 对于 Aggregating/Summing/Collapsing 等变体引擎,执行聚合或折叠操作。
- 物理删除被标记删除的数据(通过 ALTER ... DELETE)或过期数据(通过 TTL)。
-
机制:
- 后台线程池(
background_pool_size
)负责自动检测和调度合并任务。 - 合并规则:系统会选择多个相邻且位于同一分区内的、较小的部分(基于大小、创建时间等)。
- 合并过程:读取选定部分的已排序数据,重新归并排序 (对于主引擎),或应用聚合/折叠逻辑(对于变体引擎),写入一个新的、更大的有序部分目录。
- 提交新部分:新部分写入完成并通过校验后,被"提交"到活动数据集。
- 清理旧部分:原始的被合并的旧部分目录会被标记为"非活动"状态(通过文件系统重命名),稍后被异步安全删除。
- 后台线程池(
-
特点:
- 完全异步:INSERT 操作本身非常快,因为只需写入一个新的小部分。合并操作在后台不阻塞查询和插入。
- 无事务性保证:SELECT 在旧部分被删除前,仍然能看到旧数据(直到合并完成提交)。合并过程通常很快(秒到分钟级别)。
- 可调优:合并策略(大小、时间触发)、线程数等可以配置。
- "最终有序和聚合":对于主引擎,数据在分区内达到最终有序是在合并完成后;对于变体引擎,聚合或折叠逻辑也是在合并时最终生效。
-
数据写入流程简化:
-
用户执行
INSERT INTO table ...
-
ClickHouse 接收数据行。
-
按分区键计算分区:决定数据属于哪个分区目录。
-
按排序键对数据排序:在内存中对这批数据进行排序。
-
生成新部分:
- 在对应分区目录下创建一个新的子目录(例如
all_1_1_0
)。 - 将排序后的数据按列写入
.bin
文件(可选压缩)。 - 生成基于该部分数据的稀疏主键索引 (
.idx
)。 - 生成连接索引和数据的标记文件 (
.mrk
)。 - 写入其他元数据(如校验和)。
- 在对应分区目录下创建一个新的子目录(例如
-
通知系统该新部分已就绪,可被查询。
-
后台合并线程会在适当时机检测并合并这些小的部分。
变种引擎的原理增强:
- ReplacingMergeTree:在合并时,根据排序键去重(只保留最后插入的或指定版本号的)。
- SummingMergeTree/AggregatingMergeTree:在合并时,对指定的数值列进行预聚合(SUM/COUNT 等),或应用聚合函数(如 uniqState, sumState)。
- CollapsingMergeTree/VersionedCollapsingMergeTree :在合并时,根据特定标记列(
sign
或version
+sign
)折叠/删除成对出现的行。 - GraphiteMergeTree:专为存储 Graphite 监控数据设计,在合并时做高级聚合(降采样)。
关键优势总结:
- 高效写入:小部分插入非常快(内存排序写文件),不阻塞。
- 高效查询:分区裁剪、稀疏索引数据块定位、顺序IO读取、列式压缩、向量化执行。
- 存储优化:列式压缩、后台合并减少碎片、清理过期/删除数据。
- 灵活可扩展:通过不同的变种引擎支持不同场景(去重、聚合、折叠等)。
注意事项:
- 最终一致性:数据插入立即可见,但聚合、去重、折叠的效果需要等待合并后才能最终体现。
- 删除是异步的:通过 ALTER ... DELETE 标记数据,实际物理删除发生在后续合并。
- TTL 删除也是异步的,同样依赖合并。
- 需要合理设计分区键 、排序键 (主键索引)和索引粒度。
理解 MergeTree 的这些核心原理(分区、排序、稀疏索引、标记、异步合并)对于设计高性能的 ClickHouse 表、诊断性能问题以及合理配置系统至关重要。它完美体现了 ClickHouse "空间换时间"和"利用硬件特性(顺序IO)"的设计哲学。