Six thousand words of painstaking in-depth summary, to reveal why ClickHouse queries so fast!

Detailed explanation and architecture principle of ClickHouse table engine

1. ClickHouse design ideas and core technical features

1.1 ClickHouse all-knowing and all-solution

ClickHouse is a columnar database management system (DBMS) for online analytics (OLAP ) . It comes from Yandex, a Russian local search engine company listed on Nasdaq in 2011. It was born to serve Yandex.Metrica, Yandex's own web traffic analysis product. Later, it evolved and gradually formed into the current ClickHouse. The full name is : Click Stream, Data WareHouse

ClickHouse official website: clickhouse.tech/, it has ROLAP, online real-time query, complete DBMS function support, columnar storage, does not require any data preprocessing, supports batch update, has very complete SQL support and functions, supports high availability, Does not rely on Hadoop's complex ecology, out-of-the-box, and many other features.

In the case of 100 million data sets, the average response speed of ClickHouse is 2.63 times that of Vertica, 17 times that of InfiniDB, 27 times that of MonetDB, 126 times that of Hive, 429 times that of MySQL and 10 times that of Greenplum. Detailed test results can be found at: clickhouse.tech/benchmark/d…

ClickHouse is very suitable for the field of business intelligence (that is, what we call BI field), in addition, it can also be widely used in advertising traffic, Web, App traffic, telecommunications, finance, e-commerce, information security, online games , Internet of Things and many other fields.

ClickHouse is an open source columnar database that has attracted much attention in recent years, mainly used in the field of data analysis (OLAP). At present, the domestic community is hot, and various major factories have followed up on large-scale use:

  • Toutiao uses ClickHouse to analyze user behavior internally. There are thousands of ClickHouse nodes in total, with a maximum of 1200 nodes in a single cluster, with a total data volume of dozens of PB, and the daily increase of raw data is about 300TB.
  • Tencent internally uses ClickHouse for game data analysis, and has established a complete monitoring operation and maintenance system for it.
  • Ctrip began to access the trial in July 2018, and currently 80% of its business is running on ClickHouse. Every day, the data increment is more than one billion, and nearly one million query requests are made.
  • Kuaishou is also using ClickHouse internally, with a total storage of about 10PB, adding 200TB every day, and 90% of the queries are less than 3S.

ClickHouse Disadvantages:

  1. No full transaction support;
  2. Sparse indexing makes ClickHouse not good at query requirements of fine-grained or key-value type data;
  3. Lack of high-frequency, low-latency modification or deletion of existing data. Can only be used to delete or modify data in bulk;
  4. Not good at join operation;

1.2 Analysis of ClickHouse Design Ideas

So how should a storage engine specially designed for OLAP analysis be designed?

How to query and analyze a large amount of data in massive data? The core requirement is to achieve high-performance and low-latency query and analysis functions in massive data sets. Some common solutions and methods are as follows:

  1. Data sorting
  2. Data partition sharding + distributed query
  3. Columnar storage + unification of field types
  4. Column clipping
  5. Pre-aggregation (search engine: enter a keyword, the search engine finds all URLs corresponding to this keyword according to the keyword to the database: these URLs are calculated in advance)
  6. Take advantage of CPU features: vectorization engine, the operating system must support;
  7. 主键索引 + 二级索引 + 位图索引 + 布隆索引 等等各种索引技术
  8. 支持近似计算, pv 一个电商平台的 sku 总数;
  9. 定制引擎:多样化的存储引擎满足不同场景的特定需要;
  10. 多样化算法选择:Volnitsky高效字符串搜索算法 和 HyperLogLog基于概率高效去重算法;

总结一下:单条记录的增删改查操作,通过数据横向切割,做到数据操作的快速定位,在海量数据分析中,一般就是针对大量行数据少列做分析,既然并不是全部列,那么把数据做纵向切分把表中的数据按照列来单独存储,那么在做分析的时候,同样可以快速把待查询分析的数据总量降低到原来表的 1/n,同样提高效率。而且对于常用的聚合逻辑的结果,也可以提前算出来缓存起来用来提供效率,这就是预聚合技术。

提到预聚合,大家会想到 Kylin, Kylin 是一个把预聚合技术发挥到极致的一个 OLAP 技术,但是 Kylin 也有它的缺点:

  1. 预聚合只支持固定的分析场景,无法满足自定义分析场景,所以预聚合只能作为一种可选方案 ;
  2. 维度组合爆炸会导致数据膨胀,这样会造成不必要的计算和存储开销。无必要的维度组合的计算就属于浪费资源 ;
  3. 大概率数据都是增量生成,预聚合不能进行数据更新。所以会产生大量的重算。

2 ClickHouse 引擎详解

ClickHouse 是一个 OLAP 类型的分析型数据库,也有库和表的概念,而且库和表还都提供了不同类型的引擎。所以关于 ClickHouse 的底层引擎,其实可以分为 数据库引擎表引擎 两种。在此,我们重点讲解表引擎。

2.1 ClickHouse 库引擎介绍

关于库引擎,简单总结一下。ClickHouse 也支持在创建库的时候,指定库引擎,目前支持 5 种,分别是:Ordinary,Dictionary, Memory, Lazy, MySQL,其实 Ordinary 是默认库引擎,在此类型库引擎下,可以使用任意类型的表引擎。

  1. Ordinary引擎: 默认引擎,如果不指定数据库引擎创建的就是 Ordinary 数据库;

  2. Dictionary引擎: 此数据库会自动为所有数据字典创建表 ;

  3. Memory引擎: 所有数据只会保存在内存中,服务重启数据消失,该数据库引擎只能够创建 Memory 引擎表 ;

  4. MySQL引擎: 改引擎会自动拉取远端 MySQL 中的数据,并在该库下创建 MySQL 表引擎的数据表;

  5. Lazy延时引擎: 在距最近一次访问间隔 expiration_time_in_seconds 时间段内,将表保存在内存中,仅适用于 Log 引擎表;

2.2 ClickHouse 表引擎介绍

表引擎在 ClickHouse 中的作用十分关键,直接决定了数据如何存储和读取、是否支持并发读写、是否支持 index、支持的 query 种类、是否支持主备复制等。

  1. 数据的存储方式和位置,写到哪里以及从哪里读取数据
  2. 支持哪些查询以及如何支持。
  3. 并发数据访问。
  4. 索引的使用(如果存在)。
  5. 是否可以执行多线程请求。
  6. 数据复制参数。

具体可看官网:clickhouse.tech/docs/zh/eng…

ClickHouse 的表引擎提供了四个系列(Log、MergeTree、Integration、Special)大约 28 种表引擎,各有各的用途。比如 Log 系列用来做小表数据分 析,MergeTree 系列用来做大数据量分析,而 Integration 系列则多用于外表数据集成。Log、Special、Integration 系列的表引擎相对来说,应用场景有限,功能简单,应用特殊用途,MergeTree 系列表引擎又和两种特殊表引擎(Replicated,Distributed)正交形成多种具备不同功能的 MergeTree 表引擎。

image-20220715121004181

MergeTree 作为家族中最基础的表引擎,提供了主键索引、数据分区、数据副本和数据采样等基本能力,而家族中其他的表引擎则在 MergeTree 的基础 之上各有所长:

image-20220715121740290

2.3 MergeTree 引擎工作机制详解

MergeTree 系列是官方主推的存储引擎,支持几乎所有 ClickHouse 核心功能,该系列中,常用的表引擎有:MergeTreeReplacingMergeTreeCollapsingMergeTreeVersionedCollapsingMergeTreeSummingMergeTreeAggregatingMergeTree 等。学习好 MergeTree 表引擎的工作机 制,是应用好 ClickHouse 的最基本基础。

关于 MergeTree 表引擎类型:

  • 原生 MergeTree 表引擎主要用于海量数据分析,支持数据分区、存储有序、主键索引、稀疏索引、数据 TTL 等。MergeTree 支持所有ClickHouse SQL 语法,但是有些功能与 MySQL 并不一致,比如在 MergeTree 中主键并不用于去重。
  • 为了解决 MergeTree 相同主键无法去重的问题,ClickHouse 提供了 ReplacingMergeTree 引擎,用来做去重。ReplacingMergeTree 确保数据最终被去重,但是无法保证查询过程中主键不重复。因为相同主键的数据可能被 shard 到不同的节点,但是 compaction 只能在一个节点中进行,而且 optimize 的时机也不确定。
  • 解决删除场景,CollapsingMergeTree 引擎要求在建表语句中指定一个标记列 Sign(插入的时候指定为 1,删除的时候指定为 -1),后台 Compaction 时会将主键相同、Sign 相反的行进行折叠,也即删除。来消除 ReplacingMergeTree 的限制。
  • 为了解决 CollapsingMergeTree 乱序写入情况下无法正常折叠问题,VersionedCollapsingMergeTree 表引擎在建表语句中新增了一 列 Version,用于在乱序情况下记录状态行与取消行的对应关系。主键相同,且 Version 相同、Sign 相反的行,在 Compaction 时会被删除。
  • 解决聚合场景,ClickHouse 通过 SummingMergeTree 来支持对主键列进行预先聚合。在后台 Compaction 时,会将主键相同的多行进行 sum 求 和,然后使用一行数据取而代之,从而大幅度降低存储空间占用,提升聚合计算性能。同理还有 AggregatingMergeTree用来预聚合平均值。

MergeTree 的建表语法:

CREATE TABLE [IF NOT EXISTS] [db_name.]table_name ( name1 [type] [DEFAULT|MATERIALIZED|ALIAS expr], name2 [type] [DEFAUErEMAMLERLALLIZED|ALIAS expr], 省略...
) ENGINE = MergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[SETTINGS name=value, 省略...]

介绍一下其中的几个关键选项:

  1. PARTITION BY: 分区键。指定表数据以何种标准进行分区。分区键既可以是单个列字段,也可以通过元组的形式使用多个列字段,同时它也支持使用列表达 式。选填。如果没有执行分区字段,则所有数据,都在一个分区里面: all;
  2. ORDER BY: 排序键,用于指定在一个数据片段内,数据以何种标准排序。默认情况下主键(PRIMARY KEY)与排序键相同。必填。
  3. PRIMARY KEY: 主键。声明后会依照主键字段生成一级索引。默认情况下,主键与排序键(ORDER BY)相同,所以通常直接使用 ORDER BY 代为指定主键。
  4. SETTINGS: index_granularity 选项表示索引的粒度,默认值为 8192。MergeTree 索引在默认情况下,每间隔 8192 行数据才生成一条索引。选填。
  5. SAMPLE BY:抽样表达式,用于声明数据以何种标准进行采样。选填。

注意 settings 中的重要参数:

  1. index_granularity 默认是 8192 = 1024 * 8, 推荐不修改
  2. index_granularity_bytes 默认 10M,需要通过 enable_mixed_granularity_parts 来开启
  3. enable_mixed_granularity_parts 默认开启自适应索引粒度
  4. merge_with_ttl_timeout 提供数据 TTL 功能;

这里要特别声明,MergeTree 主键索引是稀疏索引!

  • 稠密索引: 一条数据创建一条索引;
  • 稀疏索引: 一段数据创建一条索引;

3. ClickHouse 工作原理

本章节将会介绍 MergeTree 表引擎的内部工作细节! 最终就是告诉大家为什么clickhouse做查询分析那么快?

ClickHouse 从 OLAP 场景需求出发,定制开发了一套全新的高效列式存储引擎,并且实现了数据有序存储、主键索引、稀疏索引、数据 Sharding、数据 Partitioning、TTL、主备复制等丰富功能。这些功能共同为 ClickHouse 极速的分析性能奠定了基础。

  • clickhouse 数据分区
  • clickhouse 列式存储
  • clickhouse 一级索引 主键索引
  • clickhouse 二级索引 跳数索引
  • clickhouse 数据压缩
  • clickhouse 数据标记

3.1 数据分区

关于表分区目录结构: MergeTree 表的分区目录物理结构,该表有 a,b,date,name 四个字段。 当创建好了这张表之后,那么一次批量插入,就可能形成多个分区,其实每个分区,就是表存储目录中的一个子文件夹

假设用一个 文件夹来存储这张表的所有数据,为了提高效率,可以考虑把 表的所有数据,按照某个维度,分割成多个子文件夹,假设以日期字段为例, 查询不同的月份,到表文件夹的不同子文件夹中寻找即可。

引出两个问题:

  • 分区文件夹的命名规则是什么?合并规则是什么?
  • 分区文件夹的文件列表,各是什么作用,什么含义?

image-20220715154810044

关于这些文件的解释:

  1. 分区目录:20190710_20190711_1_5_1,一个分区可能会有多个不同的目录,该目录下存储该分区的数据及其他各种形式的数据。后台会执行合并,把相同分 区的多个目录合并到一个分区。
  2. checksums.txt:校验文件。使用二进制格式存储。它保存了余下各类文件(primary.idx、count.txt等)的 size 大小及 size 的哈希值,用于快速校 验文件的完整性和正确性。
  3. columns.txt:列信息文件,使用明文格式存储。用于保存此数据分区下的列字段信息 。
  4. count.txt:计数文件,使用明文格式存储。用于记录当前数据分区目录下数据的总行数。
  5. primary.idx:一级索引文件,主键索引文件。
  6. xxx.bin:数据文件,使用压缩格式存储,默认为 LZ4 压缩格式,用于存储某一列的数据,每一列都对应一个该文件,如列 date 为 date.bin。
  7. xxx.mrk:列字段标记文件。
  8. xxx.mrk2:列字段标记文件,如果使用了自适应大小的索引间隔,则标记文件以 .mrk2 命名,否则以 .mrk 命名。它建立 primary.idx 稀疏索引与 xxx.bin 数据文件之间的映射关系,先通过主键索引找到数据的偏移量,然后去 xxx.bin 数据文件中找到真实数据
  9. 还有二级索引 和 分区键相关信息文件,跳数索引文件等等

关于表分区命名规则:分区的命名规则:PartitionID_MinBlockNum_MaxBlockNum_Level

image-20220715155029257

该 blocknum 在该表内全局累加,每次创建一个新的分区目录的时候,就会累加 1。Level 是分区被合并过的次数计数,合并一次则加1。

insert into table values(201905 zhangsan), (201906  lisi), (201906  wangwu);            201905_1_1_0     201906_2_2_0
insert into table values(201905 zhangsan), (201906  lisi), (201906  wangwu);            201905_3_3_0     201906_4_4_0
201905_1_1_0 + 201905_3_3_0 = 201905_1_3_1
201905_3_5_2 + 201905_6_9_3 = 201905_3_9_4

关于分区的合并规则:

image-20220715155130800

3.2 列式存储

对于 OLAP 技术来说,一般都是这对大量行少量列做聚合分析,所以列式存储技术基本可以说是 OLAP 必用的技术方案。列式存储相比于行式存储,列式存储在分析场景下有着许多优良的特性。

  • 分析场景中往往需要读大量行但是少数几个列。在行存模式下,数据按行连续存储,所有列的数据都存储在一个block中,不参与计算的列在IO时也 要全部读出,读取操作被严重放大。而列存模式下,只需要读取参与计算的列即可,极大的减低了IO cost,加速了查询。
  • 同一列中的数据属于同一类型,压缩效果显著,压缩比高。列存往往有着高达十倍甚至更高的压缩比,节省了大量的存储空间,降低了存储成本。
  • 更高的压缩比意味着更小的data size,从磁盘中读取相应数据耗时更短。
  • 自由的压缩算法选择。不同列的数据具有不同的数据类型,适用的压缩算法也就不尽相同。可以针对不同列类型,选择最合适的压缩算法。
  • 高压缩比,意味着同等大小的内存能够存放更多数据,系统cache效果更好。

image-20220715155337255

3.3 一级索引

关于一级索引: MergeTree 的主键使用 PRIMARY KEY 定义,待主键定义之后,MergeTree 会依据index_granularity 间隔(默认 8192 行),为数 据表生成一级索引并保存至 primary.idx 文件内。一级索引是稀疏索引,意思就是说:每一段数据生成一条索引记录,而不是每一条数据都生成索引, 如果是每一条数据都生成索引,则是稠密索引。稀疏索引的好处,就是少量的索引标记,就能记录大量的数据区间位置信息,比如不到 24414 条标记信 息,就能为 2E 条数据提供索引(算法:200000000 / 8192)。在 ClickHouse 中,一级索引常驻内存。总的来说: 一级索引和标记文件一一对齐,两个 索引标记之间的数据,就是一个数据区间,在数据文件中,这个数据区间的所有数据,生成一个压缩数据块。

image-20220715160316279

需要注意的是: ClickHouse 的主键索引与 MySQL 等数据库不同,它并不用于去重,即便 primary key 相同的行,也可以同时存在于数据库中。要想实现去重效果,需要结合具体的表引擎 ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree 实现。这个在之前的表引擎介绍中讲过。

3.4 二级索引

关于二级索引: 又称之为跳数索引。目的和一级索引一样,是为了减少待搜寻的数据的范围。跳数索引的默认是关闭的,需要通过参数来开启,索引生成粒度由 granularity 控制,如果生成了二级索引,则会在分区目录下生成额外的:skp_idx_[Column].idx skp_idx_[Column].mrk 文件。跳数索引的生成规则:按照特定规则每隔 granularity 个 index_granularity 条数据,就会 生成一条跳数索引。比如 minmax 跳数索引,生成的是:granularity 个 index_granularity 条数据内的最大值最小值生成一条索引,如果将来需要针对 构建二级索引的这个字段求最大值最小值,则可以帮助提高效率。跳数索引一共支持四种类型:minmax(最大最小)、set(去重集合)、 ngrambf_v1(ngram 分词布隆索引) 和 tokenbf_v1(标点符号分词布隆索引),一张数据表支持同时声明多个跳数索引。比如:

GRANULARITY = 你在创建二级索引索引的指定的

INDEX_GRANULARITY = 8192 构建一条主键索引

GRANULARITY * INDEX_GRANULARITY 构建一条 二级索引

CREATE TABLE skip_test(
    ID String,
    URL String,
    Code String,
    EventTime Date,
    INDEX a ID TYPE minmax GRANULARITY 5,
    INDEX b (length(ID) * 8) TYPE set(2) GRANULARITY 5,
    INDEX c (ID, Code) TYPE ngrambf_v1(3, 256, 2, O) GRANULARITY 5,
    INDEX d ID TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5
) ENGINE= MergeTree()
    order by id;

关于跳数索引支持的多种类型的区别:

  1. minmax:以 index_granularity 为单位,存储指定表达式计算后的 min、max 值;在等值和范围查询中能够帮助快速跳过不满足要求的块,减少 IO。
  2. set(max_rows):以 index granularity 为单位,存储指定表达式的 distinct value 集合,用于快速判断等值查询是否命中该块,减少 IO。
  3. ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed):将 string 进行 ngram 分词后,构建 bloom filter,能够优化 等值、like、in 等查询条件.
  4. tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed):与 ngrambf_v1 类似,区别是不使用 ngram 进行分词,而是通过标点符号进行词语分割。
  5. bloom_filter([false_positive]):对指定列构建 bloom filter,用于加速 等值、like、in 等查询条件的执行。

image-20220715162152757

3.5 数据压缩

关于数据压缩: ClickHouse 的数据存储文件 column.bin 中存储是一列的数据,由于一列是相同类型的数据,所以方便高效压缩。在进行压缩的时候,请 注意:一个压缩数据块由头信息和压缩数据两部分组成,头信息固定使用 9 位字节表示,具体由 1 个 UInt8(1字节)整型和 2 个 UInt32(4字节)整型 组成,分别代表使用的压缩算法类型、压缩后的数据大小和压缩前的数据大小。每个压缩数据块的体积,按照其压缩前的数据字节大小,都被严格控制在 64KB~1MB,其上下限分别由 min_compress_block_size(默认65536=64KB)与 max_compress_block_size(默认1048576=1M)参数指定。具体压 缩规则:

原理的说法: 每 8192 条记录,其实就是一条一级索引、一个索引区间压缩成一个数据块。

1、单个批次数据 size < 64KB:如果单个批次数据小于 64KB,则继续获取下一批数据,直至累积到size >= 64KB时,生成下一个压缩数据块。如果平均每条 记录小于8byte,多个数据批次压缩成一个数据块

2、单个批次数据 64KB <= size <=1MB:如果单个批次数据大小恰好在 64KB 与 1MB 之间,则直接生成下一个压缩数据块。

3、单个批次数据 size > 1MB:如果单个批次数据直接超过 1MB,则首先按照 1MB 大小截断并生成下一个压缩数据块。剩余数据继续依照上述规则执行。此时, 会出现一个批次数据生成多个压缩数据块的情况。如果平均每条记录的大小超过 128byte,则会把当前这一个批次的数据压缩成多个数据块。

image-20220715162545793

总结:

  • 在一个 xxx.bin 字段存储文件中,并不是一个压缩块对应到一条一级索引,而是每 8192 条数据,构建一条一级索引。
  • 一个 [Column].bin 其实是由一个个的压缩数据块组成的。每个压缩块的大小在: 64kb - 1M 之间。

注意:一个压缩数据块由头信息和压缩数据两部分组成。头信息固定使用9位字节表示,具体由1个UInt8(1字节)整型和 2 个 UInt32(4字节)整型组成,分别代表使用的压缩算法类型、压缩后的数据大小和压缩前的数据大小;

我们来看看 column.bin 数据文件的组成:

image-20220715162941774

3.6 数据标记

关于数据标记:数据标记文件也与 .bin 文件一一对应,一级索引和数据之间的桥梁。即每一个列字段 [Column].bin 文件都有一个与之对应的 [Column].mrk2 数据标记文件,用于记录数据在 .bin 文件中的偏移量信息。一行标记数据使用一个元组表示,元组内包含两个整型数值的偏移量信息。 它们分别表示在此段数据区间内,在对应的 .bin 压缩文件中,压缩数据块的起始偏移量;以及将该数据压缩块解压后,其未压缩数据的起始偏移量。每 一行标记数据都表示了一个片段的数据(默认8192行)在 .bin 压缩文件中的读取位置信息。标记数据与一级索引数据不同,它并不能常驻内存,而是使 用 LRU(最近最少使用)缓存策略加快其取用速度。

总结数据读取流程: 先根据一级索引,找到标记文件中的对应数据压缩块信息(压缩块在 .bin 文件中的起始偏移量和未压缩之前该条数据的是偏移量) 然后从 .bin 文件中,把压缩块加载到内存,解压缩之后,执行读取。

数据标记文件也与 .bin 文件一一对应。即每一个列字段 [Column].bin 文件都有一个与之对应的 [Column].mrk 数据标记文件,用于记录数据在 .bin 文 件中的偏移量信息。

标记文件的最重要的作用,就是建立了主键索引到数据文件的数据的映射! MergeTree 具体是如何定位压缩数据块并读取数据的呢?

  • 第一步:根据标记文件中的信息,找到对应的压缩数据块,读取压缩数据块,执行解压缩;
  • Step 2: From the decompressed data block, load the data into the memory with the granularity of index_granularity, and execute the query until the result data is found;

4 ClickHouse core query process

The essence of data query can be seen as a process of continuously reducing the scope of data. In the most ideal case, MergeTree can firstly use partition index, primary index and secondary index in turn to reduce the data scanning range to a minimum. Then, with the help of data markers, the range of data that needs to be decompressed and calculated is reduced to a minimum.

select name from student where date = 201905;

image-20220715163500301

  1. designated partition
  2. Specify field (xxx.bin)
  3. Locate the record in the marked file (name.mrk2) according to the primary index (primary.idx)
  4. Scan the mark file of the corresponding field to obtain two offset information (the current data to be searched, the compressed data block in the .bin data file, the offset of the compressed data block in the .bin file, the compressed data After the block is decompressed, the data to be found is at the offset of the current compressed data block)
  5. Locate a compressed data cache in the .bin file according to the first offset
  6. Read data into memory to perform decompression
  7. Find the corresponding data in the memory decompressed data according to the second offset

There is only one core principle to improve the efficiency of data query: whoever does the auxiliary action can quickly help us to quickly reduce the range of data to be searched. to a single server for processing. Generally speaking, this server processes this request very quickly!

Guess you like

Origin juejin.im/post/7120519057761107999