clickhouse扫盲

786 阅读20分钟

1. 背景

ClickHouse(下简称CH)是由俄罗斯搜索引擎公司Yandex研发的开源列存数据库,主要用于数据分析OLAP1,由于其在海量数据检索优异的性能,近年来倍受业界关注且发展迅速。 CH给人最直观的印象就是检索速度"极快",接下来就从存储层的架构来剖析一下它是如何做到在PB级的OLAP场景下还能做到准确且快速的结果返回。

2. CH的核心特性

image.png

  1. CH拥有完备的数据管理功能,可以成为一个DBMS
  2. 列存的设计在面对大数据量的检索时,可以减少数据扫描范围,提升扫描速率
  3. 基于列存的设计进行数据压缩效率更高,相同数据类型和现实语义的字段拥有最高的压缩比,从而减小数据体量,提升传输速率,减轻网络带宽及IO的压力
  4. 利用CPU的SIMD指令实现数据级并行来实现向量化执行
  5. 作为一个列存数据库,使用关系模型描述数据并提供了传统db的概念(database,table和view等等), 完全使用SQL作为查询语言,提供标准协议的SQL查询接口
  6. SIMD不适合较多分支判断的场景,CH采用多线程技术来实现线程级并行;采用分布式技术支持数据层面的分布式,实现计算查询的下推等操作;
  7. 不同于HBase和ES之类的分布式系统,CH不采用Master-Slave主从架构,采用Multi-Master多主架构,每个节点角色对等,不再区分管控、数据和计算节点,这样可以有效避免单点故障、集群无主的问题,故障恢复快

3. 表引擎分类

CH一共提供四大种表引擎,用户可以根据数据的存取方式、并发读写支持、index支持、query种类支持以及主从复制来进行选择。

表引擎类型特征
MergeTree适用于大数据量(PB级)查询分析
Log适用于小表数据分析,主要用于快速写入小表(一百万行左右的表)然后全部读出的场景
存储在磁盘;追加写模式;支持并发读写锁;不支持修改操作;不支持索引;写数据不具备原子性
Integeration外表数据集成,主要用于导入外部数据,或者在CH中直接操作外部数据源
Special特殊表引擎,譬如分布式表Distributed表提供分布式查询路由,合并表Merge来提供并行读取其他表中的数据进行整合等

4. MergeTree表引擎

MergeTree家族之于CH就好比Innodb之于MySQL,是官方的主推引擎,支持几乎所有的CH核心功能。

4.1 LSM Tree和Merge Tree

LSM Tree 当下数据库的存储层最流行的数据结构就是B+ Tree和Log-Structured Merge-Tree(LSM-Tree),几乎所有的NoSQL数据库(HBase, InfluxDB等)存储层都是使用的LSM Tree的变种。 image.png 对照上图简述一下LSM Tree的写入流程:

  1. 先写入到WAL(Write-Ahead Log),用作故障恢复
  2. WAL写完成,写入到内存的MemTable,当超过阈值会触发Minor Compaction写入到磁盘的SSTable上
  3. 每个Level的SSTable体积到达对应的阈值会触发Major Compaction合并成更大的SSTable(L0的SSTable会有重叠key,但在Major Compaction过程中进行了去重因此大于Level0之后都不会出现重复key)

Merge Tree CH的Merge Tree相较于LSM Tree核心思想都是相同的:顺序组织存储文件,不断将小的文件merge成大文件,LSM Tree是对SSTable进行merge,MergeTree是对Data part进行merge。 但Merge Tree少了「log-structured」关键字,从官方的文档上可以看出两者的区别就在MergeTree没有MemTable和log的概念,即它不会在内存中按照append-only的方式写入log来构建memTable,而是直接写入文件系统。 因此MergeTree就更适合数据按照批写入而不是按行写入,即写入的频率不宜过快。

image.png

MergeTree在刚开始的时候是没有设计WAL机制的。不过随着发展,在 #10697中通过引入in-memory part的概念(对应memTable的概念)支持了WAL,从而来支持MergeTree引擎更好地处理频繁的小批量数据写入。 具体的实现是通过如下参数进行控制:

  • in_memory_parts_enable_wal:in-memory part是否开启WAL
  • min_rows_for_compact_part:如果单次写入的数据行数小于此阈值,就会写入in-memory part和WAL,在下一次merge的时候才进行flush,否则直接flush到磁盘
  • min_bytes_for_compact_part:同上,根据单次写入数据大小判断写入内存还是磁盘

4.2 MergeTree存储结构

对照建表语句来阐述一下CH存储层重要概念,并介绍MergeTree是如何组织文件的: CREATE TABLE log_store (  timestamp Date,  userID String,  stream String,  level UInt32,  INDEX stream_minmax (stream) type minmax granularity 3 -- 二级索引 ) ENGINE = MergeTree() -- 表引擎 PARTITION BY timestamp -- 分区键 ORDER BY (timestamp, userID, stream) -- 排序键 PRIMARY KEY (timestamp, userID) -- 主键,不指定则为排序键 SETTINGS index_granularity = 8192 -- 索引粒度

INSERT INTO log_store(timestamp, userID, stream, level) VALUES('2021-10-28', 'w', 's1', 1); INSERT INTO log_store(timestamp, userID, stream, level) VALUES('2021-10-27', 'w', 's1', 2), ('2021-10-27', 'z', 's2', 3); INSERT INTO log_store(timestamp, userID, stream, level) VALUES('2021-10-29', 'w', 's1', 2), ('2021-10-29', 'z', 's2', 3), ('2021-10-29', 'z', 's3', 4); INSERT INTO log_store(timestamp, userID, stream, level) VALUES('2021-11-11', 'w', 's1', 2), ('2021-11-11', 'z', 's2', 3), ('2021-11-11', 'y', 's3', 4); 插入了四组不同timestamp(也是partition)的记录,查看log_store表的目录如下 image.png

每一个MergeTree分区目录的格式如下图: image.png 由PartitionID_MinBlockNum_MaxBlockNum_Level四个部分组成:

  • PartitionID:DDL中指定的PARTITION字段
  • MinBlockNum/MaxBlockNum:最大/小数据块编号,单张表内的_全局_自增变量,初始创建时Max = Min
  • Level:合并层级,记录分区目录合并的次数,合并规则为去两个合并分区的最大Level+1

再看一下分区目录下文件结构:

$> tree 20211027_3_3_0/

20211027_3_3_0 ├── checksums.txt # 校验文件 ├── columns.txt # 列信息 ├── count.txt # 数据条数 ├── data.bin # 压缩后的列数据
├── data.mrk3 # 列字段标记信息 ├── default_compression_codec.txt # 压缩方式 ├── minmax_timestamp.idx # 分区索引文件 ├── partition.dat ├── primary.idx # 一级索引 ├── skp_idx_stream_minmax.idx2 # 二级索引 └── skp_idx_stream_minmax.mrk3 # 二级索引标记文件

0 directories, 11 files 这里先引入几个重要的概念: Partition

  • 按照指定的列对本地数据进行逻辑分区,每个partition相互独立,逻辑上无关联
  • 每个partition由多个partitionID相同的data part组成

Shard

  • 在partition之上进一步对数据进行横向切分,并分配到不同的CH节点上去
  • 实现存储空间的可扩展性,通过增加shard来扩容
  • 支持分布式查询

image.png Data Part

  • 用来进行物理分区,每个data part存储着压缩后的数据文件,元数据,各种索引文件和校验文件
  • Data part生成之后就是immutable的,生成和销毁都与写入和merge有关
  • 在磁盘上有两种存储方式:随着数据量的增加,最终都会由Wide方式来存储数据。
    • Compact方式:所有列数据都存在一个文件data.bin,适用于存储大小不超过10M的data part;
    • Wide方式:每一列都有单独的.mrk3和.bin文件存储该列的标记信息和数据;

Granule

  • 每个data part逻辑上划分为多个Granule,每个Granule包含若干行记录,是CH在内存中进行数据扫描的单位
  • 写入数据时通过index_granularity控制单个Granule存储的行数,index_granularity_byte控制单个Granule的大小
  • 每个Granule对应这.mrk文件中的一个mrk

Block

  • Block作为CH进行磁盘IO的基本单位和文件压缩/解压缩的基本单位,可以理解为.bin文件即由若干个Block有序组成
  • 每个Block(结构如下)都由header文件若干个Granule组成,大小范围由min_compress_block_size(default 64KB)和max_compress_block_size(default 1M)决定,假设写入单个Granule数据未压缩大小为size,Block的生成规则如下:
    1. size<64KB: 继续获取下一批数据累积到min再生成下一个Block,多对一
    2. 64KB<=size<=1M:直接生成一个Block,一对一
    3. size>=1M: 按照1M进行截断,剩余数据递归执行Block的生成规则,因此会出现一批数据生成多个Block的情况,一对多

image.png

mrk 由上面定义可知,Block既不是定data size也不是定行数的,Granule也不是一个定长的逻辑。因此需要通过.mrk数据标记文件来记录一级索引和数据的映射关系来快速定位到目标Granule。 .mrk文件由若干个mark object组成,每个mark object对应一个Granule,记录3个信息:

  • row_count: 每个Granule的行数
  • offset_in_compressedfile_: Granule所在的Block在.bin文件中的offset
  • offset_in_decompressed_block: Granule在解压缩后的Block中的offset

从查询的角度来看,

  1. 在进行查询的时候通过主键索引按照MarkRange来找出目标Granule
  2. 接着通过mrk文件就可以快速地定位到目标Block,即先定位到.bin中的目标Block
  3. 然后对目标Block进行解压,按照offset取到Block中存放着目标数据的Granule
  4. 最后取Granule中的数据,这样就可以从而避免了每次都全量加载.bin文件

Data part(以20211027_3_3_0为例)的组成如下图所示: image.png

4.3 索引分类

4.3.1 分区索引

Data part粒度的索引,存储这整个分区键的min/max值,用于分区裁剪,直接裁剪掉不符合条件的data part,有效的减少范围查询的读放大问题。

4.3.2 一级索引

image.png

  • 建表时由PRIMARY_KEY显示指定,由ORDER_BY隐式指定,用来减少查询时数据扫描的范围
  • 对应data part中的primary.idx文件,采用稀疏索引实现,占用空间小,因此其常驻内存,取速度很快稀疏索引 vs 稠密索引稠密索引类似于map结构,一行索引对应一条记录;稀疏索引一行索引则对应一段数据。稀疏索引在数据量大的时候优势非常明显:以 index_granularity=8192 为例子,MergeTree只需要12208行索引标记就可以为1亿行数据记录提供索引。
  • 按照index_granulatiry参数来进行进行索引文件的划分,MergeTree每隔index_granulrity行数据生成一条索引记录,同时还会按照相同的粒度组织.mrk和.bin文件
  • MarkRange来表示一个具体Granule区间,使用start和end表示具体的范围
  • 索引记录的内容是每个Granule首条记录的主键字段,存储非常紧凑,并采用位读取取代标志位或者状态码来极致的节省存储空间

4.3.3 二级索引

  • 也称跳数索引,由数据的聚合信息构建而成,也是为了减少查询时的数据扫描范围
  • INDEX index_name expr TYPE index_type(...) GRANULARITY granularity,会生成额外的skp_idx_[Column].idx和skp_idx_[Column].mrk文件
  • granularity是跳过的index_granularity粒度,如下图所示:

image.png

  • 跳数索引一共有4种:取极大/小值的minmax,无重集合set,数据短语布隆过滤器ngrambf_v1以及其变种tokenbf_v1
索引类型声明方式说明
minmaxINDEX a ID TYPE minmax GRANULARITY N记录N个index_granularity区间中的极大和极小值,作用类似data part的minmax区间,可以快速跳过不符合的MarkRange
setINDEX b (length(ID) * 10) TYPE set(max_rows) GRANULARITY N记录每个granule中max_rows条计算表达式的唯一值
ngrambf_v1INDEX c (ID, Code) TYPE ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed)四个参数分别为token长度,过滤器的大小,过滤器中使用Hash函数的个数,random seed
tokenbf_v1INDEX d ID tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed)会自动按照非字符的、数字的字符串进行token切割,因此没有参数n

5. MergeTree查询

MergeTree的查询过程是一个不断缩小数据范围的过程,在最理想的情况下,MergTree可以借助分区索引,一级索引和二级索引来将数据扫描范围缩至最小,然后借助数据标记文件,将需要解压和计算的数据范围缩至最小。

5.1 索引工作原理

索引记录的内容是每个Granule首条记录的主键字段,假设有一份数据,共1920行记录,主键为String类型,取值从A0000开始,后续依次为A0001,A0002.....A1919。 MergeTree索引粒度index_granularity = 30,则primary.idx的物理存储格式如下: A0000A0030A0060A0090A0120.......A1830A1860A1890 按照索引数据,这份数据会被划分成1920/30=64个小的MarkRange,整个MarkRange的范围为[A0000, +INF),结构如下图: image.png 主键索引的工作流程可以分为如下几步:

  1. 生成查询区间:将查询条件转化为条件区间,如WHERE id='A1024'转化为['A1024', 'A1024'],WHERE id<'A0087'转化为(-INF, 'A0087'),WHERE id LIKE 'A1234%'转化为['A1234', 'A1235')
  2. 递归交集判断:从最大的区间[A0000, +INF),依次对MarkRange的数值区间与条件区间做交集判断:
    • 不存在交集,则通过减值算法优化此整段MarkRange
    • 存在交集
      • 判断区间步长(end-start)不小于参数merge_tree_coarse_index_granularity(default=8),进一步按照coarse粒度进行步长划分,递归进行交集判断
      • 不可再分则进行区间匹配,记录满足条件的MarkRange并返回
  3. 合并最终匹配到的MarkRange范围

image.png

5.2 查询流程

在最理想情况下,每一步查询都能命中索引,通过索引进行串的执行流程如下: image.png

  1. 首先通过分区文件minmax_[Column].idx过滤data part
  2. 接着通过一级索引primary.idx过滤mark range
  3. 然后通过二级索引skp_idx_[Column]_[Agg_State].idx2进一步过滤mark range
  4. 最后通过标记文件*.mrk迅速定位到数据在哪个Block的Granule中,执行扫描、合并并返回

上述步骤中数据扫描若没有能够命中索引,那MergeTree就会进行全量的扫描

5.3 数据扫描

MergeTree提供三种数据扫描的方式:

  • Normal:MergeTree表最常用的模式,并行扫描多个data part,对每个data part扫描时可以借助标记文件并行扫多个Block
    • 并行扫描:在data part并行扫描基础之上,实现mark range data part粒度的扫描,共享mark range task pool问题避免数据存储出现的长尾问题
    • 数据Cache:主键索引和分区键索引在查询时加载到内存,mark文件和列存文件有对应的MarkRangeCache和UNcompressedCache,分别存储mark文件的binary内存和解压缩后的block数据
    • SIMD反序列化:用手写的sse指令加速分序列化
    • PreWhere过滤:CH的语法支持了额外的PreWhere过滤条件,它会先于Where条件进行判断。当用户在sql的filter条件中加上PreWhere过滤条件时,存储扫描会分两阶段进行,先读取PreWhere条件中依赖的列值,然后计算每一行是否符合条件。相当于在Mark Range的基础上进一步缩小扫描范围,PreWhere列扫描计算过后,ClickHouse会调整每个Mark对应的Granule中具体要扫描的行数,相当于可以丢弃Granule头尾的一部分行
  • Sorted:借助ORDER BY下推存储加速查询,充分利用data part内部数据是有序的特性实现查询结果的全局有序
  • Final:该模式对CollapsingMergeTree、SummingMergeTree等表引擎提供一个最终merge后的数据视图,主要思想是一边scan一边merge,而不用等数据merge成一个data part就可以看到数据结果

6. 存储层其他特性

6.1 TTL机制

在大多数的数据存储场景下,数据都是有时效性的,CH通过ttl机制来控制数据的生命周期:

  1. 列级别ttl:某一列部分数据过期,则用该列数据类型的默认值替换列中过期值,当某一列数据全部过期,CH会从data part中删除该列
  2. 表级别ttl:用来进行删除过期行,在存储介质之间迁移data part,压缩data part等表级别操作

CH在合并data part的过程中会删除过期数据,这是一个off-schedule的合并过程,意味着合并越频繁会消耗越多资源(通过merge_with_ttl_timeout控制),同时也意味着如果执行SELCT查询时CH后台正在执行合并就会查到过期数据,官方建议在SELCT之前先执行OPTIMIZE。

6.2 存储策略

CH对数据的存储介质策略也是将数据分为冷/热两类,热数据为较新更易被读取到的数据,通常存储在高性能磁盘中(如SSD)冷数据是读取频度很低的历史数据,通常存储在成本更低性能相对差一些的磁盘中(如HDD)。 在MergeTree中,data part是进行数据迁移的最小单位,即属于同一个data part的数据会存放在同一张磁盘上。CH通过system.storage_policies和system.disks来配置MergeTree引擎家族的存储策略,配置使用xml文件示例如下:

<storage_configuration>
  <disks>
        <disk_name_1> <!-- disk name -->
            <path>/mnt/fast_ssd/clickhouse/</path>
        </disk_name_1>
        <disk_name_2>
            <path>/mnt/hdd1/clickhouse/</path>
            <keep_free_space_bytes>10485760</keep_free_space_bytes>
        </disk_name_2>
        <disk_name_3>
            <path>/mnt/hdd2/clickhouse/</path>
            <keep_free_space_bytes>10485760</keep_free_space_bytes>
        </disk_name_3>
       ...
    </disks>
    <policies>
       # roud-robin on [disk1, disk2]
        <hdd_in_order> <!-- policy name -->
            <volumes>
                <single> <!-- volume name -->
                    <disk>disk1</disk>
                    <disk>disk2</disk>
                </single>
            </volumes>
        </hdd_in_order>
      # 数据超过10G 或 ssd使用量超过80% 将数据从ssd迁移到hdd
        <moving_from_ssd_to_hdd>
            <volumes>
                <hot>
                    <disk>fast_ssd</disk>
                    <max_data_part_size_bytes>1073741824</max_data_part_size_bytes>
                </hot>
                <cold>
                    <disk>disk1</disk>
                </cold>
            </volumes>
            <move_factor>0.2</move_factor>
        </moving_from_ssd_to_hdd>
    </policies>
   ...
</storage_configuration>

7. Doris索引构建技术

从OLAP db的选型及index技术进行对比,一下主要分析Doris(Palo)相关内容。 Presto, Druid, Kylin, HBase Doris(Palo)是我厂基于MPP架构的交互式SQL数据仓库,主要用于解决近实时的报表和多维分析。 Doris底层按照列组织存储文件segment,segment文件格式如下图: image.png 文件包括:

  • magic code:8bytes,用于识别文件格式和版本
  • data region:存储各列的数据信息,按需分page加载
  • index region:各列的index数据统一存储在index region,按照列粒度进行加载
  • Footer:
    • FileFooterPB:定义文件的元数据信息
    • 4个字节的footer pb内容的checksum
    • 4个字节的FileFooterPB消息长度,用于读取FileFooterPB
    • 8个字节的MAGIC CODE,之所以在末位存储,是方便不同的场景进行文件类型的识别

Column中的数据按照page的方式进行组织,page是编码和压缩的基本单位,按照ch的概念来类比:segment就是data part,page就是block。

7.1 FileFooterPB数据结构

包含了列的meta信息、索引的meta信息,Segment的short key索引信息、总行数 image.png

列的meta信息

  • ColumnId:当前列在schema中的序号
  • UniqueId:全局唯一的id
  • Type:列的类型信息
  • Length:列的长度信息
  • Encoding:编码格式
  • Compression:压缩格式
  • Dict PagePointer:字典信息

列索引的meta信息

  • OrdinalIndex:存放列的稀疏索引meta信息。
  • ZoneMapIndex:存放ZoneMap索引的meta信息,内容包括了最大值、最小值、是否有空值、是否没有非空值。SegmentZoneMap存放了全局的ZoneMap信息,PageZoneMaps则存放了每个页面的统计信息。
  • BitMapIndex:存放BitMap索引的meta信息,内容包括了BitMap类型,字典数据BitMap数据。
  • BloomFilterIndex:存放了BloomFilter索引信息。

为了防止索引本身数据量过大,ZoneMapIndex、BitMapIndex、BloomFilterIndex采用了两级的Page管理。对应了IndexColumnMeta的结构,当一个Page能够放下时,当前Page直接存放索引数据,即采用1级结构;当一个Page无法放下时,索引数据写入新的Page中,Root Page存储数据Page的地址信息。

7.2 列存储结构

Column中的数据按照page的方式进行组织,page是编码和压缩的基本单位,每个page大小一般为64KB,其存储的位置和大小由ordinal index管理。按照ch的概念来类比:segment就是data part,page就是block。

7.2.1 data page存储结构

DataPage主要为Data部分、Page Footer两个部分。 Data部分存放了当前Page的列的数据。当允许存在Null值时,对空值单独存放了Null值的Bitmap,由RLE格式编码通过bool类型记录Null值的行号。 image.png Page Footer包含了Page类型Type、UncompressedSize未压缩时的数据大小、FirstOrdinal当前Page第一行的RowId、NumValues为当前Page的行数、NullMapSize对应了NullBitmap的大小。

7.2.1 数据压缩

针对不同的字段类型采用了不同的编码。默认情况下,针对不同类型采用的对应关系如下:

数据类型压缩方式
TINYINT/SMALLINT/INT/BIGINT/LARGEINTBIT_SHUFFLE
FLOAT/DOUBLE/DECIMALBIT_SHUFFLE
CHAR/VARCHARDICT
BOOLRLE
DATE/DATETIMEBIT_SHUFFLE
HLL/OBJECTPLAIN

默认采用LZ4F格式对数据进行压缩。

7.3 索引类型

7.3.1 Ordinal index

Ordinal Index索引提供了通过行号来查找Column Data Page数据页的物理地址。Ordinal Index能够将按列存储数据按行对齐,可以理解为一级索引。其他索引查找数据时,都要通过Ordinal Index查找数据Page的位置。因此,这里先介绍Ordinal Index索引。 在一个segment中,数据始终按照key(AGGREGATE KEY、UNIQ KEY 和 DUPLICATE KEY)排序顺序进行存储,即key的排序决定了数据存储的物理结构。确定了列数据的物理结构顺序,在写入数据时,Column Data Page是由Ordinal index进行管理,Ordinal index记录了每个Column Data Page的位置offset、大小size和第一个数据项行号信息,即Ordinal。这样每个列具有按行信息进行快速扫描的能力。Ordinal index类似CH的primary index,采用的都是稀疏索引结构提升查询效率。

Ordinal index元信息存储在SegmentFooterPB中的每个列的OrdinalIndexMeta中。具体结构如下图所示: image.png 在OrdinalIndexMeta中存放了索引数据对应的root page地址,这里做了一些优化,当数据仅有一个page时,这里的地址可以直接指向唯一的数据page;当一个page放不下时,指向OrdinalIndex类型的二级结构索引page,索引数据中每个数据项对应了Column Data Page offset位置、size大小和ordinal行号信息。其中Ordinal index索引粒度与page粒度一致,默认64KB。

7.3.2 Short Key index

Short Key Index前缀索引,是在key(AGGREGATE KEY、UNIQ KEY 和 DUPLICATE KEY)排序的基础上,实现的一种根据给定前缀列,快速查询数据的索引方式。这里Short Key Index索引也采用了稀疏索引结构,在数据写入过程中,每隔一定行数,会生成一个索引项。这个行数为索引粒度默认为1024行,可配置。相对于的CH中的skip index的实现和功能也是类似的。 image.png 其中,KeyBytes中存放了索引项数据,OffsetBytes存放了索引项在KeyBytes中的偏移。

7.3.3 ZoneMap index

ZoneMap索引存储结构如下图所示: image.png 在SegmentFootPB结构中,每一列索引元数据ColumnIndexMeta中存放了当前列的ZoneMapIndex索引数据信息。ZoneMapIndex有两个部分,SegmentZoneMap和PageZoneMaps。SegmentZoneMap存放了当前Segment全局的ZoneMap索引信息,PageZoneMaps存放了每个Data Page的ZoneMap索引信息。 PageZoneMaps对应了索引数据存放的Page信息IndexedColumnMeta结构,目前实现上没有进行压缩,编码方式也为Plain。IndexedColumnMeta中的OrdinalIndexPage指向索引数据root page的偏移和大小,这里同样做了优化二级Page优化,当仅有一个DataPage时,OrdinalIndexMeta直接指向这个DataPage;有多个DataPage时,OrdinalIndexMeta先指向OrdinalIndexPage,OrdinalIndexPage是一个二级Page结构,里面的数据项为索引数据DataPage的地址偏移offset,大小Size和ordinal信息。

7.3.4 BloomFilter index

当一些字段不能利用Short Key Index并且字段存在区分度比较大时,Doris提供了BloomFilter索引。BloomFilterIndex信息存放了生产的Hash策略、Hash算法和BloomFilter过对应的数据Page信息。BloomFilter索引数据对应数据Page的存放与ZoneMapIndex类似,做了二级Page的优化。 image.png

7.3.5 BitMap index

在数据查询时,对于区分度不大,列的基数比较小的数据列,可以采用BitMap index来加速数据的查询。比如,性别,婚姻,地理信息等。结构如下: image.png BitmapIndex包含了三部分,BitMap的类型、字典信息DictColumn、位图索引数据信息BitMapColumn。其中DictColumn、BitMapColumn都对应IndexedColumnMeta结构,分别存放了字典数据和索引数据的Page地址offset、大小size。

7.4 索引查询流程

在查询一个Segment中的数据时,根据执行的查询条件,会对首先根据字段加索引的情况对数据进行过滤。然后在进行读取数据,整体的查询流程如下: image.png

  1. 首先,会按照Segment的行数构建一个row_bitmap,表示记录哪些数据需要进行读取,没有使用任何索引的情况下,需要读取所有数据。
  2. 当查询条件中按前缀索引规则使用到了key时,会先进行ShortKey Index的过滤,可以在ShortKey Index中匹配到的ordinal行号范围,合入到row_bitmap中。
  3. 当查询条件中列字段存在BitMap Index索引时,会按照BitMap索引直接查出符合条件的ordinal行号,与row_bitmap求交过滤。这里的过滤是精确的,之后去掉该查询条件,这个字段就不会再进行后面索引的过滤。
  4. 当查询条件中列字段存在BloomFilter索引并且条件为等值(eq,in,is)时,会按BloomFilter索引过滤,这里会遍历所有索引,过滤每一个Page的BloomFilter,找出查询条件能命中的所有Page。将索引信息中的ordinal行号范围与row_bitmap求交过滤。
  5. 当查询条件中列字段存在ZoneMap索引时,会按ZoneMap索引过滤,这里同样会遍历所有索引,找出查询条件能与ZoneMap有交集的所有Page。将索引信息中的ordinal行号范围与row_bitmap求交过滤。
  6. 生成好row_bitmap之后,批量通过每个Column的OrdinalIndex找到到具体的Data Page。
  7. 批量读取每一列的Column Data Page的数据。在读取时,对于有null值的page,根据null值位图判断当前行是否是null,如果为null进行直接填充即可。

总结

本次分享主要分享了CH的存储层架构及索引技术,在此基础上对Doris的相关技术也进行了分析 。 CH通过实现MergeTree系列表引擎来实现大数据量下的快速查询,CH对比其他OLAP存储的设计相对精简,给人整体的感觉在设计之初并不是为了天然支持分布式的大数据处理,而更像一个高性能的单机分析性数据库,在索引的设计上采用分区索引、多级索引来优化查询效率,但整体的感觉上中规中矩,没有直观的让人从存储层上看出它「快」的原因。可能CH在代码层的优化很细节,亦或者是得益于其向量化执行,Compaction流程、运行时代码生成技术优势等。后续我将就CH的向量化执行进行调研,详细剖析一下它的实现细节。 目前,CH的GitHub拥有2W+ Star,1.7K issues,可以说是顶级活跃度的开源项目了,相较于其他的OLAP产品譬如Presto,Druid等,CH更加偏向于一个完备的OLAP解决方案,再加上在各种数据分析场景下优异的表现,得到业界的广泛的认可和应用。

参考

  1. IBM OLAP definition www.ibm.com/cloud/learn…
  2. OLAP vs OLTP www.ibm.com/cloud/blog/…
  3. 表引擎分类 clickhouse.com/docs/en/eng…
  4. LSM-Tree cloud.tencent.com/developer/a…
  5. Polymorphic parts (compact format) github.com/ClickHouse/…
  6. Polymorphic parts (in-memory format) github.com/ClickHouse/…
  7. ch的partition和shard区别 www.jianshu.com/p/178a01e0a…
  8. Sharding tables ClickHouse cloud.yandex.com/en/docs/man…
  9. ClickHouse MergeTree变得更像LSM Tree www.jianshu.com/p/f3881fae4…
  10. OLAP选型 mp.weixin.qq.com/s/JTCGcMeg7…
  11. WeChat CH实战 mp.weixin.qq.com/s/Hc3p2_Yx1…
  12. Doris存储文件格式 doris.incubator.apache.org/master/zh-C…
  13. Doris存储层设计 mp.weixin.qq.com/s?__biz=Mzg…
  14. RLE en.wikipedia.org/wiki/Run-le…