详解 ClickHouse 中的 MergeTree 引擎工作原理-2
第一部分我们介绍了 MergeTree 的一些基本概念,像是为即将出现在舞台上的演员做了一个简单的介绍,而这一部分我们将会在通过精彩的表演展示每个演员的特征以及演员之间的绝妙配合。
本文所有的说明基于如下语句创建的表 hits
CREATE TABLE hits
(
`UserID` UInt32,
`URL` String,
`EventTime` DateTime
)
ENGINE = MergeTree
PRIMARY KEY (UserID, URL)
ORDER BY (UserID, URL, EventTime)
SETTINGS index_granularity = 8192, index_granularity_bytes = 0;由第一部分可知,ClickHouse 中的物理存储文件主要包含索引文件、bin 文件和 Mark 文件,下面会详细分析这些文件的结构以及在一次查询中的作用。
Bin 文件#
在 ClickHouse 中,每次写操作插入的一批数据按照主键列(以及排序键的附加列)的字典序从小到大排序,并且被划分在多个逻辑上的 Granule 中。在 hits 表中会首先按照 UserID 排序,然后是 URL,最后是 EventTime,而且表是按照 Wide 格式存储的,所以每个列都有对应的 Bin 文件。逻辑上数据存储的示意图如下

在实际存储的时候,一个或者多个 Granule 将会按照配置的压缩算法压缩为一个 Block,默认的压缩算法为**LZ4**,最后多个压缩好的 Block 会被存储在 Bin 文件中落盘,Bin 文件的物理结构示意图如下

Primary 主键索引文件#
第一部分介绍过,Primary 索引文件中存储的是每个 Granule 中的第一条数据,包含了索引声明中的列,示例表 hit 的索引文件的构成如下图所示

Primary 索引文件包含的是未经压缩的数组,里面的数据也是按照由大到小的顺序进行排序的,所以 ClickHouse 可以在查询的时候使用二分查找法来定位主键的位置,同时它在 ClickHouse 运行的时候会被加载进内存中,以提供更快的处理速度。
因为索引中每条记录都代表着一个 Granule 的第一条记录,故可以通过找到一条记录中主键在索引中的位置快速定位到这条记录所在的 Granule,从而快速查找到这条记录的所有数据,这个过程是 ClickHouse 查询执行的第一阶段 - Granule Selection
Mark 文件#
从上文中 Granule Selection 阶段可知,如果要查询一条记录,通过主键索引可以大大减少需要遍历的 Granule 数量,可以在这个阶段定位到此条记录的 Granule。
为了进一步读取 Granule 中的数据找到所需要的记录,需要将此 Granule 读入处理引擎中进行处理,而由上文可知,Granule 是被存储在某个 Bin 文件中某个 Compressed Block 内的,那么如何定位到这个物理文件的位置,依旧是需要解决的问题,这就是查询执行的第二阶段 - Data Reading
在 ClickHouse 中,Granule 的物理位置存储在 Mark 文件中,跟 Bin 文件类似,Wide 格式下,每一列都有一个单独的 Mark 文件,下图展示了示例表 hit 的 Mark 文件结构

Mark 文件数据格式跟主键索引文件类似,也是未压缩的数组,索引同样从 0 开始,其中 Mark 文件中每条数据都对应着主键索引文件的一条数据,也是对应着一个 Granule 的信息。如上图所示,Mark 文件的每条数据包含两部分 block_offset 和 granule_offset,作用如下图

- block_offset 该数据指明包含了此 Granule 数据的压缩 Block 在 Bin 文件中的偏移量,用于定位 Block 的位置
- granule_offset 该数据指明了该 Granule 在解压缩后的 Block 中的位置,用于定位 Granule 在 Block 中的位置
有了 Mark 文件,我们就能够从 Bin 文件中定位到 Granule 的物理位置,就可以读取这个物理文件得到所需要的数据,完成整个查询过程。
总结#
本文在上一部分的基础上进一步剖析各个文件的内容以及他们在一次查询中的作用,为我们了解存储引擎 MergeTree 提供了更加深入的视角,也可以从中窥见 ClickHouse 查询的执行原理和过程。