目录
一、概述
1.1 存储结构的整体介绍
1.2 存储结构的设计目标
二、存储文件格式
2.1 存储目录结构
编辑
2.2 Segment v2文件结构
3.1 列的meta信息
3.2 列索引的meta信息
四、前缀索引(Short Key Index)
4.1 功能介绍
4.2 索引生成
4.3 索引的底层存储结构
4.3.2 Short Key Page
4.4 查询过滤
4.5 应用案例
五、Ordinal lndex (—级索引)
5.1 功能介绍
5.2 索引生成
5.3 索引的底层结构
5.4 查询过滤
六、ZoneMap索引
6.1 功能介绍
6.2 索引生成
6.3 索引的底层结构
6.4 查询过滤
6.5 应用案例
七、Bitmap索引
7.1 功能介绍
7.2 索引生成
7.3 索引的底层结构
7.4 查询过滤
7.5 应用案例
八、 Bloom filter索引
8.1 功能介绍
8.2 索引生成
8.3 索引的底层结构
8.4 查询过滤
8.5 应用案例
Doris是基于MPP架构的交互式SQL数据仓库,主要用于解决近实时的报表统计和多维分析。Doris高效导入、查询离不开其存储结构的设计。通过阅读Doris BE模块代码,分析Doris BE模块存储层的实现原理,主要包括Doris列存的设计、索引设计、数据读写流程、Compaction流程、Tablet数据分片和Rowset版本管理等功能。
该篇文章主要介绍了Segment V2版本的存储层结构,包括内置的前缀索引(Short Key Index)、Ordinal 索引、ZoneMap索引以及用户手动创建的Bitmap索引和 Bloom filter索引。
- 批量导入,少量更新
- 绝大多数的读请求
- 宽表场景,读取大量行,少量列
segment文件既是存储文件的最小单元,也是索引文件生成的文件存储区域。segment文件的目录结构和segment文件本身的构成:
对存储数据的路径管理是通过be.conf中的参数storage_root_path配置的。在storage目录下的data目录中,存放着以分桶 id为命名的目录,下一层级是tablet id命名的tablet目录,在tablet目录下面就是Segment文件。Segment文件可以有多个,一般按照大小进行分割,默认为256MB。该文件命名是以{rowset_id}_{segment_id}.dat结构的dat文件。
总结:分区是逻辑上的概念,只记录在表的元数据中,每个分区的数据会按照分桶键进行hash分桶,表中的数据经过分区分桶后,就会形成一个个的tablet,尽量均匀的分布在集群的所有BE中。 tablet是StarRocks中数据均衡的最小单位,默认的三副本是指同一个 tablet会在集群中保留三份,每个tablet之间的数据没有交集,在物理上独立存储。集群的副本修复或磁盘均衡,均是以tablet为单位移动或者克隆的。且每次的数据导入、更新或者删除,本质上也是对一个个tablet中的数据进行操作。
一个tablet中包含若干连续的rowset,而rowset是逻辑概念,代表tablet中一次数据变更的数据集合(数据变更包括了数据新增,更新或删除等),rowset按版本信息进行记录,每次变更就会生成一个个版本。一个rowset可能会包含多个segment,segment可以认为是rowset中的数据分段。执行数据导入时,每完成写入一个segment就会增加一个文件块对应。Segment文件可以有多个,一般按照大小进行分割,默认为256MB 。
Segment整体的文件格式分为data region 数据区域、index region索引区域、footer三个部分,如下图所示:
Footer信息段在文件的尾部,存储了文件的整体结构,包括数据域的位置,索引域的位置等信息,其中有SegmentFooterPB,CheckSum,Length,MAGIC CODE4个部分。
SegmentFooterPB的数据结构如下:
SegmentFooterPB采用了PB格式进行存储,主要包含了列的meta信息(Column Meta)、索引的meta信息(Column Index Meta),Segment的short key索引信息、总行数num_rows
- Columnld:当前列在模式中的序号
- Uniqueld:全局唯一的id
- Type:列的类型信息
- Length:列的长度信息
- Encoding:列的编码格式
- Compression:列的压缩格式
- Dict PagePointer:列的字典信息
由于Doris底层数据是按照排序键排序后存储的,而Short Key Index前缀索引,是在key (duplicate key、aggregate key、unique key、primary key)排序的基础上,实现的一种根据给定一定数量(不超过3列,不超过36个字节,遇到字符串会自动截断)前缀列,每间隔一定行数(1024),生成的一个索引项 (稀疏索引)。当查询的过滤条件命中前缀索引时,就能快速定位到数据存储所在的比较精确地址。
前缀索引采用了稀疏索引结构,在数据写入过程中,每隔一定行数,会生成一个索引项。这个行数为索引粒度默认为1024行。
Segment文件的footer中保存了Short Key Page在Segment中的offset和size大小,以便数据读取时能够正确的从Segment文件中加载出前缀索引数据。
KeyBytes中存放了索引项数据,OffsetBytes存放了索引项在KeyBytes中的偏移。
数据查询时会打开Segment文件,从Segment文件的footer中获取Short Key Page的offset和大小,然后从Segment文件中读取Short Key Page保存的索引,从中解析出每一条的前缀索引项。
ps: 因为前缀索引是稀疏索引,只能粗粒度的定位出key可能存在的范围,然后使用二分查找算法去精确定位定位key所在的行。
Short Key Index前缀索引采用了前36个字节,作为这行数据的前缀索引。当遇到varchar类型时,前缀索引会直接截断。前缀字段的数量不超过 3 个,前缀索引项的最大长度为 36 字节。
#创建明细表,在 duplicate key 中指定排序列为 uid 和 name。说明建表时如何指定排序列,以及其前缀索引的组成。
create table user_access (
uid int,
name varchar(64),
age int,
phone varchar(16),
last_access datetime,
credits double
)
duplicate key(uid, name)
distributed by hash (uid, name);
# 由于前缀索引项的最大长度为36字节,超过部分会被截断,因此该表的前缀索引项为 uid (4 字节) + name (只取前 32 字节),前缀字段为 uid 和 name。
select * from user_access where uid = 1 and name = 'xx'
上述案例中:当查询条件是前缀索引的前缀时,可以极大的加快查询速度,代码如下:
select * from user_access where uid = 1 and name = 'xx'
# 上述代码的查询性能远高于下面的代码
select * from user_access where phone = '150003212317'
Doris底层采用列存的方式来存储数据,每一列数据会被分为多个Data Page数据页,见下图:
数据刷写时,会为每一个 Data Page(每列的每一个数据块,大小一般是64kb)生成一条Ordinal lndex索引,该索引保存Data Page在Segment文件中的offset、Data Page的大小以及Data Page的起始行号。而所有Data Page数据页的索引项会保存在一个Ordinal lndex Page中,而Segment文件中的footer会保存Ordinal lndex Page在Segment文件中的offset、Ordinal lndex Page的大小。综上,首先通过Segment文件的footer找到Ordinal lndex Page,然后通过Ordinal lndex Page中的索引项找到Data Page数据页
Ordinal lndex索引提供了通过行号来查找Column Data Page数据页的物理地址,Ordinal lndex索引能够将按照列存储的数据按行对齐,可以理解为一级索引。因此,其他类型的索引在查找数据的时候,都要借助Ordinal lndex(一级索引)查找 Data Page数据页的物理地址。
在一个segment文件中,数据始终按照key排序存储,数据写入的过程中,每一列的data page会由Ordinal Index管理,他会记录每一列对应的data page的offset,size大小,和该data page的第一个数据的行号信息。这样在查询的时候,就能通过 Ordinal lndex索引够快速定位到对应的data page的物理地址。
ps: column的data数据按照page为单位分块存储,每个page大小一般为64kb。
数据查询时,会加载每一列的Ordinal 索引数据,通过Segment footer中记录的Ordinal索引的Meta信息判断当前列是否存在Ordinal Index Page,即判断当前列是否有多个Data Page。
如果当前列存在Ordinal Index Page,则从Segment footer中获取Ordinal Index Page在Segment中的offset和Ordinal Index Page的大小,然后从Segment文件中读取Ordinal Index Page数据,然后通过Ordinal lndex Page中的索引项找到当前列的每一个Data Page的信息,包括Data Page在Data Page在Segment中的offset以及Data Page的大小。
Doris会为Segment文件中的一列数据(key 列)自动添加ZoneMap索引,注意:当表的模型为dupulcate时,会所有字段开启zonemap索引。
ZoneMap索引存储了Segment和每个列对应每个Page的统计信息。Doris可以根据这些统计信息,快速判断这些数据块是否可以过滤掉,从而减少扫描数据量,提升查询速度。统计信息包括了Min最大值、Max最小值、HashNull空值、HasNotNull不全为空的信息。
待补充!
待补充!
待补充!
在数据查询涉及到范围条件过滤时,会按照ZoneMap统计信息选取扫描的数据范围。
#创建明细表,排序键是uid, name
create table user_access (
uid int,
name varchar(64),
age int,
phone varchar(16),
last_access datetime,
credits double
)
duplicate key(uid, name)
distributed by hash (uid, name);
# 上述代码会对udi,name字段创建 ZoneMap索引,对字段uid进行过滤
select * from user_access where uid >5 --范围条件过滤
Doris支持对低基数列创建Bitmap位图索引来加速数据查询。高基数列:例如UserID,低基数列:例如性别,婚姻状态等。
Bitmap位图索引创建时需要通过 create index 进行创建。Bitmap的索引是整个Segment中的Column字段的索引,而不是为每个Page单独生成一份。在写入数据时,会维护一个map结构,去记录下每个key值对应的行号,并采用Roaring位图对rowid进行编码。生成索引数据时,首先写入字典数据,即将map结构的key值写入到DictColumn中。然后,key对应Roaring编码的rowid(value值)以字节方式将数据写入到BitMapColumn。
总结而言,Bitmap索引由两部分组成:
如下图:column列取值为:[x, x, y, y, y, z, y, x, z, x],一共有10行。则该列数据对应的Bitmap索引,其中有序字典为{x, y, z};x, y, z字典值对应的位图分别是:
数据刷写时,会给用户指定的列创建Bitmap索引。向列中每添加一个值,都会更新当前列的Bitmap索引。从Bitmap索引的有序字典中查找添加的值是否已经存在,如果本次添加的值在Bitmap索引的有序字典中已经存在,则直接更新该字典值对应的Roaring位图,如果本次添加的值在Bitmap索引的有序字典中不存在,则将该值添加到有序字典,并为该字典值创建Roaring位图。此外null值也会有单独的Roaring位图。Bitmap索引的字典数据和Roaring位图数据分开存储。
待补充!
待补充!
#创建明细表,对字段city 设置位图索引
create table user_access (
uid int,
name varchar(64),
age int,
city varchar(16),
index city_index(city )
)
duplicate key(uid, name)
distributed by hash (uid, name);
#建表后使用create index 创建Bitmap索引
create index city_index on user_access (city);
#由于cty的取值比较少,建立数据字典和位图后,通过扫描位图便可以快速查找出匹配行。
select * from user_access where city in ('北京','上海');
注意事项:
- Bitmap索引适用于可使用等值条件 (
=
) 查询或 [not] in 范围查询的列(低基数的列)- 主键表和明细表中所有列都可以创建 Bitmap 索引;聚合表和更新表中,只有维度列(即 Key 列)支持创建 bitmap 索引。
- 支持为如下类型的列创建 Bitmap 索引:
- 日期类型:date、datetime。
- 数值类型:tinyint、smallint、int、bitgint、largeint、decimal 和 boolean。
- 字符串类型:char、string 和 varchar。
Doris支持用户对适用于高基数列(取值区分度比较大的字段)添加Bloom Filter(布隆过滤器)索引,Bloom filter索引可以快速判断表的数据文件中是否可能包含要查询的数据,如果不包含就跳过,从而减少扫描的数据量。 ps:高基数列:例如UserID,低基数列:例如性别,婚姻状态等。
Bloom filter布隆过滤器实际上是由一个超长的二进制位数组和一系列的哈希函数组成的,通常应用在需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合,因为Bloom filter布隆过滤器有一定的误判率~
例如:下图中 m=18, k=3 (m是该Bit数组的大小,k是Hash函数的个数),集合中已有的 x、y、z 三个元素通过3种不同的哈希函数散列到Bit 数组中。当判断元素w是否在该集合存在时,通过3 种Hash函数计算之后因为有一个比特为0,因此得出元素w不在该集合中。
总结:布隆过滤器是怎么判断某个元素是否在集合中呢?
元素经过哈希函数得到所有的偏移位置,若这些位置全都为1,则说明这个元素大概率是在这个集合中,若有一个不为1,则判断这个元素一定不在这个集合中。
数据刷写时,会给每一个Data Page数据块(大小一般是64kb)创建一条Bloom Filter索引项。
数据查询时会加载列的Bloom Filter索引数据,并解析出每一个Data Page的Bloom Filter索引项
#创建明细表,对字段name 设置BloomFilter索引
create table user_access (
uid int,
name varchar(64),
age int,
phone varchar(16),
last_access datetime,
credits double
)
duplicate key(uid, name)
distributed by hash (uid, name)
properties("bloom_filter_columns" = "name");
select * from user_access where name = 'xx';
#由于name的区分度较大,为了提升sql的查询性能,对name数据增加了BloomFilter索引
#properties("bloom_filter_columns" = "name");
注意事项:
- 主键表和明细表中所有列都可以创建 bloom filter 索引;聚合表和更新表中,只有维度列(即 key 列)支持创建 bloom filter索引。
- 支持为如下类型的列创建 bloom filter 索引:
- 数值类型:smallint、int、bigint 和 largeint。
- 字符串类型:char、string 和 varchar。
- 日期类型:date、datetime。
在创建表的时候,可以指定一个列或者多个列(一般来说前三列)作为这个表的排序键(Sort Key),当数据导入时,数据会按照排序键的定义,按照顺序存储在磁盘空间上,当查询根据这些排序字段进行查询时,就能够根据已经排好序的数据,快速定位到你要查询的对应数据集所对应的磁盘地址,在scan阶段就能够大面积减少无关数据,加速查询。
排序键的具体介绍可以见文章:
StarRocks表设计——排序键和数据模型-CSDN博客文章浏览阅读528次,点赞15次,收藏9次。2.2 StarRocks表设计——排序键和数据模型https://blog.csdn.net/SHWAITME/article/details/136136900?spm=1001.2014.3001.5501
由于Doris底层数据是按照排序键排序后存储的,而Short Key Index前缀索引,是在key (duplicate key、aggregate key、unique key、primary key)排序的基础上,实现的一种根据给定一定数量(不超过3列,不超过36个字节,遇到字符串会自动截断)前缀列,每间隔一定行数(1024),生成的一个索引项 (稀疏索引)。当查询的过滤条件命中前缀索引时,就能快速定位到数据存储所在的比较精确地址。
Ordinal lndex索引提供了通过行号来查找Column Data Page数据页的物理地址,Ordinal lndex索引能够将按照列存储的数据按行对齐,可以理解为一级索引。因此,其他类型的索引在查找数据的时候,都要借助Ordinal lndex(一级索引)查找 Data Page数据页的物理地址。
在一个segment文件中,数据始终按照key排序存储,数据写入的过程中,每一列的data page会由Ordinal Index管理,他会记录每一列对应的data page的offset,size大小,和该data page的第一个数据的行号信息。这样在查询的时候,就能通过 Ordinal lndex索引够快速定位到对应的data page的物理地址。
Doris会为Segment文件中的一列数据(key 列)自动添加ZoneMap索引,注意:当表的模型为dupulcate时,会所有字段开启zonemap索引。
ZoneMap索引存储了Segment和每个列对应每个Page的统计信息。Doris可以根据这些统计信息,快速判断这些数据块是否可以过滤掉,从而减少扫描数据量,提升查询速度。统计信息包括了Min最大值、Max最小值、HashNull空值、HasNotNull不全为空的信息。
Doris支持对低基数列创建Bitmap位图索引来加速数据查询。高基数列:例如UserID,低基数列:例如性别,婚姻状态等。
Bitmap位图索引创建时需要通过 create index 进行创建。Bitmap的索引是整个Segment中的Column字段的索引,而不是为每个Page单独生成一份。在写入数据时,会维护一个map结构,去记录下每个key值对应的行号,并采用Roaring位图对rowid进行编码。生成索引数据时,首先写入字典数据,即将map结构的key值写入到DictColumn中。然后,key对应Roaring编码的rowid(value值)以字节方式将数据写入到BitMapColumn。
Doris支持用户对适用于高基数列(取值区分度比较大的字段)添加Bloom Filter(布隆过滤器)索引,Bloom filter索引主要用于快速判断某列中是否存在某个值。BloomFilter判定该列中不存在指定的值,如果确定不存在,就不会读取这个数据文件;如果索引判定该列中存在指定的值,也有可能这个值实际上不会存在,这时,会读取数据文件来进一步确认。
ps:高基数列:例如UserID,低基数列:例如性别,婚姻状态等。
参考文章:
聊聊分布式 SQL 数据库Doris(七)-腾讯云开发者社区-腾讯云
【Doris】Doris存储层设计介绍1——存储结构设计解析_doris 存储原理-CSDN博客
深度解析|Apache Doris 索引机制解析
索引概述 - Apache Doris
索引 | StarRocks