简介
1、由Yandex开源的高性能OLAP数据库
2、采用列式存储结构,拥有高效的数据压缩能力
3、通过多核并行处理以及向量执行引擎提升查询能力
4、多样化的表引擎用于支撑不同的应用场景
5、支持多线程和分布式处理
ClickHouse性能
100million条数据量下,ClickHouse的单表聚合查询性能非常高,是Greenplum(x2)集群的16倍,是PostgreSQL的10倍,是Mysql的833倍。
适用场景
• 商业智能
• 电信行业数据存储统计
• 新浪微博用户行为记录分析
• 电子商务用户分析
• 金融等领域
不适用的场景
• 不支持事务
• 不擅长根据主键按行粒度进行查询(虽然支持)
• 不擅长按行删除数据(虽然支持)
• 不擅长按行更新数据(虽然支持)
ClickHouse采用列式存储,其格式相对于行存储格式来说,对行粒度查询进行处理稍显劣势。
核心特性
完备的DBMS功能
• DDL:可以动态的创建、修改或删除数据库、表和视图
• DML:可以动态查询、插入、修改或删除数据
• 权限控制:可以按照用户粒度设置数据库或表的操作权限,保证数据安全
• 数据备份与恢复,提供了数据的导入和导出恢复机制
• 分布式管理:提供集群模式,能够自动管理多个数据库节点
列式存储和数据压缩
• 列式存储有效减少查询时扫描数据量
• 列式存储比行存储的另一个优势是数据压缩的友好性
• 同一个字段拥有相同的数据类型和实现语义,数据重复项的可能性更高
• 默认采用LZ4压缩算法(速度较快,但压缩率较低),以及ZSTD压缩算法(速度较慢,压缩率较高)
• ClickHouse的LZ4算法在Yandex的生产环境数据压缩比能达到8:1
• TPCH数据(无索引),ckdb压缩率达到52%,rocksdb达到61%
行式存储是按行读取,比如一个表中有很多列,读取其中三列,就需要对所有列进行一个扫描;列式存储只需要对所需要的三列进行读取,就有效的减少了查询时候的扫描数据量。
列式存储是一个字段作为一组数据进行一个存储。
向量化执行引擎
• 利用寄存器硬件层面的特性,为上层的程序性能带来了指数级的提升
• SIMD指令,单条指令操作多条数据,通过数据并行以提高数据的并行操作。它的原理是在CPU寄存器层面实现数据的并行操作。

通过数据的并行来提高数据的查询能力和处理能力。
多样化表引擎
• 为了避免平庸,ClickHouse拥有和Mysql类似的表引擎设计,它拥有4大类30多种表引擎
• 每一种表引擎都拥有各自的特点,用户可以根据实际业务场景的要求,选择合适的表引擎使用

• ReplacingMergeTree
主要用于相同主键进行合并处理比如主键重复的数据有多条,可以在内部合并,最终保留一条
多线程和分布式
• 多线程
如果说向量化执行是通过数据级并行的方式提升性能,那么多线程就是通过线程级并行的方式实现性能的提升,默认CPU核数的一半
• 分布式
为了利用分布式设计,ClickHouse在数据存取,即支持分区(纵向扩展、利用多线程原理),也支持分片(横向扩展,利用分布式原理,由replica组成)
另外ClickHouse提供了本地表和分布式表的概念,本地表相当于数据分片,分布式表是本地表的访问代理,其本身不存储任何数据

ClickHouse架构设计
多主架构
区别于Master-Slave主从架构,ClickHouse采用了Multi-Master多主架构,集群中的每个节点角色对等,客户端访问任意一个节点都能得到相同的效果
多主架构具有很多优势,不用区分多主控节点,数据节点和计算节点,集群中的所有节点功能相同,客户端访问每一个节点都能得到相同的效果

MergeTree原理解析

ClickHouse核心模块--Column&Field
• Column与Field是ClickHouse数据最基础的映射单元
• 内存中的每一列数据由一个Column对象表示。Column对象分为接口和实现两部分,在IColunn接口对象中,定义了对数据进行各种关系运算的方法
• 在大多数场合,ClickHouse都会以整列的方式操作数据。如果需要操作单个具体的数值,则需要使用Field对象,Field对象代表一个单值。与Column对象的泛化设计思路不同,Field对象使用了聚合的设计模式。
• 在Field对象内部聚合了Null、UInt64、String和Array等13种数据类型及相应的处理逻辑。
ClickHouse核心模块--DataType&Block
• 数据的序列化核反序列化工作由DataType负责。IDataType接口定义了许多正反序列化的方式,它们成对出现
• ClickHouse内部的数据操作是面向Block对象进行的,并且采用流的形式
• Block对象可以看作数据表的子集。Block对象是由Column、DataType及列名称组成的三元组,仅通过Block对象能完成一系列的数据操作
• Block流操作有两组顶层接口,IBlockInputStream负责数据的读取和关系运算;IBlockOutputStream负责将数据输出到下一环节。IBlockInputStream接口有众多实现类,这些实现类大致可以分为三类,第一类用于处理数据定义的DDL操作;第二类用于处理关系运算的各种操作;第三类是与表引擎呼应,每一种表引擎都拥有与之对应的。
ClickHouse核心模块--Parser&Interpreter
• Parser分析器可以将一条SQL语句以递归下降的方式解析成AST语法树的形式。不同的SQL语句,会经由不同的Parser实现类解析
• Interpreter解释器负责解释AST,并进一步创建查询的执行Processor,起到串联整个查询过程的作用,它会根据解释器的类型,聚合它所需要的资源
ClickHouse核心模块--Functions&Aggregate Functions
• ClickHouse主要提供两类函数,普通函数核聚合函数
• 普通函数由IFunction接口定义,拥有数十钟函数实现,普通函数是没有状态的,而是采用向量化的方式直接作用于一整列数据
• 聚合函数由AggregateFunction接口定义,聚合函数是有状态的,以COUNT聚合函数为例,其AggregateFunctionCount的状态使用整型Unit64记录,聚合函数的状态支持序列化核反序列化。所以能够在分布式节点之间进行传输,以实现增量计算。
MergeTree表引擎家族

MergeTree创建方式

• PARTITION BY 选填,分区键
• ORDER BY 必填,排序键
• PRIMARY KEY 选填,表示主键,声明之后会依次按照主键字段生成一级索引,用于加速表查询。如果不指定,那么主键默认和排序键相同。MergeTree允许主键有重复数据
• SAMPLE KEY 选填,抽样表达式。用于声明数据以何种标准进行采样,其用于配合SAMPLE子查询使用
• INDEX 选填,二级索引,也叫做跳数索引
• SETTINGS 选填,用于指定一些额外的参数,例index_granularity(索引粒度),min_compress_block_size(最小的压缩数据块大小)
MergeTree存储结构

• columns.txt
列信息文件,使用文本文件存储,用于保存分区下的列字段信息
• primay.idx
索引文件,用于存储稀疏索引
• [column].bin
数据文件,使用压缩格式存储,默认使用LZ4压缩格式,用于存储某一列数据
• [column].mrk
列字段标记,保存了bin文件中的数据偏移量信息,MergeTree通过标记文件建立了primay.idx稀疏索引与bin数据文件的映射关系
• [column].mrk2
如果用了自适应大小的索引间隔,则标记文件会以.mrk2命名
• partition.dat
用于保存当前分区表达式最终生成值
• minmax_[column].idx
用于记录当前分区字段对应原始数据的最小值和最大值
• skip_idx_[column]
二级索引生成文件
数据分区
![]

数据是以分区目录的形式进行组织的,每个分区的数据独立分开存储;横向切分是分片;纵向切分是分区。
数据分区合并

t0时刻,有三批数据写入。
第一批数据是 2021-05-01,因分区键是年-月,则会得到分区目录202105_1_1_0
202105表示年月的分区id,第一个1表示最小的blockNum,第二个1表示最大的blockNum,第一批数据,maxBlockNum=minBlockNum=blockNum=1,最后一个0表示合并的次数,此时还未发生合并所以是level=0
第二批数据是2021-05-02,则会得到202105_2_2_0,因blockNum=2=minBlockNum=maxBlockNum,还未合并所以level是0
第三批数据是2021-06-02,则会得到202105_3_3_0,因blockNum=3=minBlockNum=maxBlockNum,还未合并所以level是0
t1时刻,202105的分区发生合并
202105_1_1_0和202105_2_2_0合并得到202105_1_2_1
分区id不变还是202105
最小blockNum合并结果为1,因为一个是1一个是2,取最小值即为1
最大blockNum合并结果为2,因为一个是1一个是2,取最大值即为2
level结果是1,因为一个是0,另外一个也是0,取两者的最大值再加1即为1
所以合并之后的分区目录为202105_1_2_1
合并之后,老的分区目录则会处于非激活状态,不对外提供服务,默认8分钟之后,异步被清理。
一级索引

• primary.idx文件内的一级索引(主键索引)采用稀疏索引实现
• 稀疏索引占用的索引存储空间较小。数据量大的场景可以利用primary.idx内的索引数据常驻内存,加快查询速度
• 默认索引粒度大小为8192
• 每隔一个索引粒度会取该粒度范围内的第一个主键值作为索引保存到primay.idx文件中
二级索引(跳数索引)
二级索引由数据块按粒度分割后,各部分数据聚合信息构成

索引a表示:粒度范围内price*size即总价的最小值和最大值
• minmax 存储指定表达式的极值
• set(max_rows) 存储指定表达式的不重复值,max_rows表示重复值的个数限制,比如max_rows=10表示只有10个不同的值可重复;0表示无限制
• ngrambf_v1 存储一个包含数据块中所有ngram的布隆过滤器,用于字符串的equals、like、in过滤。ngram是统计语言模型的算法,用于分词
• tokenbf_v1 跟ngrambf_v1类似,但是它不是用ngrams进行分词,而是使用token,token是非字母数字的符号分割的序列比如分号;
• bloom_filter 指定列存储的布隆过滤器
数据压缩-- 压缩数据块

• bin压缩文件是由多个压缩数据块组成的,而每个压缩数据块的头信息则会基于CompressionMethod_CompressedSize_uncompressedSize公式生成
• 压缩方法包含:LZ4、ZSTD、Multiple、Delta多种算法
数据压缩-- 压缩方式

• 单个间隔数据不超过64KB,则累积到64KB生成下一个压缩块
• 单个间隔数据大于64KB,不超过1MB,则直接生成下一个压缩块
• 单个间隔数据大于1MB,则直接生成多个压缩快
数据标记

.mrk标记文件为一级索引和数据文件之间建立关联,主要保存两个信息
• 一级索引对应的编号信息
• bin压缩数据块的起始偏移量和解压缩块的起始偏移量
每个索引粒度内取第一条重新写入。每个索引值都会有一个下标。第一个索引粒度内,使用第一条数据保存到索引池,下标编号为0,对应start0~start1区间。索引文件中保存了编号信息,通过编号信息找到压缩数据的起始偏移量和解压缩数据的起始偏移量
MergeTree写入的过程

每一批数据写入到数据目录里去,有三种不同的压缩方式,随着压缩文件的生成也伴随着一级索引和标记文件的构建,最终使得压缩文件、标记、索引一一对应。
MergeTree的读取过程

通过查询语句的filter过滤条件,根据分区索引找到唯一满足的分区目录,进入到分区目录,会根据一级索引来进行过滤,排除掉不符合的一些索引信息,保留索引2和索引3,然后根据二级索引排除掉索引2,那么就只剩索引3这个一级索引,数据标记能够为一级索引和数据文件进行关联,找到对应的压缩块,然后解压缩,然后根据标记中的起始偏移量找到对应的数据,这就是MergeTree的读取过程。
如果没有过滤条件则会通过多线程的操作对这些分区目录并行的进行读取,加速查询过程。




