Clickhouse 系列 - 第四章 - 索引

cfcz48

在第三节中,已经向读者介绍了 clickhouse 在处理数据时按照 block 为单位进行压缩,之后写入磁盘数据文件中。

在第三节中,已经向读者介绍了 clickhouse 在处理数据时按照 block 为单位进行压缩,之后写入磁盘数据文件中。这样可以减少数据量的大小减少磁盘 io 时间。但是,如果没有索引,则意味着每次查询时都需要读取所有的数据,即使通过压缩已经降低了 6.2 倍的数据量,这依然要花费很多的磁盘 IO。此时索引就出现了,可以再次帮助我们减少查询时需要读取的数据量。

在介绍 clickhouse 的索引之前,我们先回顾一下关系型数据库 MySQL 中常用的索引技术——B + 树。B + 树算法超出本文内容,在这里不做深入讨论,我们主要分析下 MySQL 使用 B + 树的目的和 B + 树的本质。其实,B + 树本质是一颗 N 叉树,其叶子节点就是有序排列的索引值,因此在查询时可以根据这棵树快速定位到数据所在,而且由于其有序,可以适应范围查找。下图展示了一颗 B + 树。

B + 树示意图

了解了 B + 树的本质之后,读者可以试着回答一个问题:clickhouse 是否有必要使用 B + 树进行索引?为什么?

如果您的答案是不需要,那么说明您已经对 clickhouse 和 MySQL 存储引擎都了解地比较深入了。如果您的答案是需要或者不确定,那么也不用着急,下面就会详细说明原因。

这个问题的答案就是不需要,原因在于 clickhouse 的存储引擎和 MySQL 的存储引擎设计上的不同。MySQL 由于要支持事务,使用 MVCC 的事务控制机制,因此会出现一个问题:数据的插入顺序和索引的排序不同。例如我对 age 列做索引,我的插入顺序为 44,21,20,33,19。这组数据在 B + 树中需要被重新排列为 19,20,21,33,44。这就是数据的插入顺序和索引排序不同的情况。而在关系型数据库中数据的插入顺序就是存储引擎写入数据文件的顺序。

在 clickhouse 的存储引擎中,在第三章和番外篇中已经详细介绍了 LSM 机制,clickhouse 的存储引擎通过 LSM 机制保证数据的按照主键的顺序写入存储引擎,也就是说即使插入顺序为 44,21,20,33,19。这组数据在经过 clickhouse 的 LSM 机制后写入数据文件的顺序就是 19,20,21,33,44。这意味着 clickhouse 在存储数据时已经有序存储了,因此不需要再次使用 B + 树进行索引了。

一级索引

综上,其实 clickhouse 的一级索引非常简单,只需要记录每一个 block 第一个值即可。例如一组一亿行的数据,主键范围从 1~100,000,000。存储到 clickhouse 后按照 8192 行为一个 block,那么一共有 12208 个 block。索引为 1,8193,16635…… 在查询时只需要就可以根据值确定到需要读取哪几个 block 了。例如我需要查询 id>500 and id <12258 的数据,那就只需要读取第 0 块和第 1 块 block 即可。

在 clickhouse 的数据存储文件中,一级索引存在于 primary.idx 中。一级索引的本质是存储了每个 block 中数据的最小值,从而为确定需要查询的数据确定好其所在的 block。它简历了数据到 block 的映射关系。简单来说,给定一个数据,通过一级索引能够快速查询到这个数据所在的 block。从而避免查询一个数据需要遍历整个数据集。

标记

上面一部分已经给读者介绍了什么是一级索引。但一级索引并不能单独实现快速查找的目标。或者说,一级索引只实现了数据到 block 的映射。但还存在一个问题,我即使已经知道我的数据存储在了第一个 block,那我如何定位到这个 block 的位置呢?这个问题的答案就需要通过标记文件来实现。换句话说,标记文件存储了 block 到文件偏移量的映射。

这个问题其实也不难理解,我们知道一个 block 是 8192 行数据组成的。如果每个 block 的大小都是 64M。那么找到第 N 个 block 的地址就很容易,通过 N*64m 即可,但关键 block 经过压缩后是无法保证其大小的,也就是说每个 block 的大小是不同的。那么就必须将每个 block 的起始位置存储下来,方便查找。

在 clickhouse 的实际处理中,每行标记有 3 个字段组成 blockid, 数据文件中的偏移量,压缩后块中偏移量。每个字段是 8 字节,因此每个标记一共 24 字节。通过 blockid 可以 N*24B 快速定位到位置。

其实有 blockid 和数据文件中偏移量,就已经可以快读定位了,那么 clickhouse 标记文件中的第三项压缩后块中偏移量是用来做说明的呢?这个问题的答案其实和 clickhouse 对于压缩块的处理有关。第三章已经提过,clickhouse 按照每 8192 行生成一个 block。但如果 8192 行的数据依然很小怎么办?例如 UInt8 类型,一行数据只有 8 位 1 字节。即使 8192 行数据也仅仅只有 8192Byte=8KB。这个数据量用来压缩还是显得太小。因此,clickhouse 在处理时,会按照每个压缩块最小 64KB,最大 1M 的规则进行处理。即一个 block 数据总和小于 64K 时,会继续取下一个 block。因此会出现多个 block 出现在一个压缩块中。标记文件中的第三个参数就是用来处理这种情况的。

因此假设一个标记文件的内容如下:

000
108192
2016384
1120160
标记文件内容 读取第 2 块 block 时,根据 2*24Byte=48,定位到(2,0,16384)这一行,然后根据 0,从 bin 文件中读取偏移量 = 0 的数据块,经过解压缩后再根据 16384 读取解压缩后从 16384 开始的数据,即可找到对应的原始数据。 总结 -- clickhouse 的索引由于其存储引擎的设计,可以做的非常简单。主要有一级索引和标记组成。一级索引实现数据到 block 的映射,标记实现 block 到文件偏移量的实现。另外,由于一级索引非常小,1 亿条数据只需要 1 万多行的索引,因此一级索引可以常驻内存,加速查找。 同时,clickhouse 还提供了二级索引,不过二级索引比较简单,且不是必须的,对整体性能影响也不大,因此我会在番外篇中介绍二级索引。 至此,clickhouse 的存储引擎的设计已经更新完毕,下一期我将通过一个完整的例子串联起整个 clickhouse 的存储引擎的工作原理。并且对之前写得部分进行优化,尤其是增加大量的图片来向读者更清晰地介绍概念。 相信通过这几节的内容,大家对 clickhouse 的设计哲学也有了一定的认识。这边我又要提出一个新的疑问,读者可以事先思考一下:按照本文目前的介绍来看,clickhouse 的加速强在设计上,那么为什么 clickhouse 要使用 C++ 语言编写?使用 JAVA 能否实现同样的性能?为什么?这个问题将会引出下一部分的核心内容:clickhouse 的计算引擎。让我们在下一部分重逢,开始一段更有挑战的旅程。
阅读 1.3k
6 声望
5 粉丝
0 条评论
你知道吗?

6 声望
5 粉丝
文章目录
宣传栏