clickhouse-数据库引擎

1、数据库引擎和表引擎

数据库引擎默认是Ordinary,在这种数据库下面的表可以是任意类型引擎。
生产环境中常用的表引擎是MergeTree系列,也是官方主推的引擎。
MergeTree是基础引擎,有主键索引、数据分区、数据副本、数据采样、删除和修改等功能,
ReplacingMergeTree有了去重功能,
SummingMergeTree有了汇总求和功能,
AggregatingMergeTree有聚合功能,
CollapsingMergeTree有折叠删除功能,
VersionedCollapsingMergeTree有版本折叠功能,
GraphiteMergeTree有压缩汇总功能。
在这些的基础上还可以叠加Replicated和Distributed。Integration系列用于集成外部的数据源,常用的有HADOOP,MySQL。

2、MaterializeMySQL

2.1、基本概述

MySQL 的用户群体很大,为了能够增强数据的实时性,很多解决方案会利用 binlog 将数据写入到 ClickHouse。为了能够监听 binlog 事件,我们需要用到类似 canal 这样的第三方中间件,这无疑增加了系统的复杂度。

ClickHouse 20.8.2.3 版本新增加了 MaterializeMySQL 的 database 引擎,该 database 能映 射 到 MySQL 中 的 某 个 database , 并 自 动 在 ClickHouse 中 创 建 对 应 的ReplacingMergeTree。ClickHouse 服务做为 MySQL 副本,读取 Binlog 并执行 DDL 和 DML 请求,实现了基于 MySQL Binlog 机制的业务数据库实时同步功能。

2.2、特点

MaterializeMySQL 同时支持全量和增量同步,在 database 创建之初会全量同步MySQL 中的表和数据,之后则会通过 binlog 进行增量同步;

MaterializeMySQL database 为其所创建的每张 ReplacingMergeTree 自动增加了_sign 和 _version 字段。

其中,_version 用作 ReplacingMergeTree 的 ver 版本参数,每当监听到 insert、update 和 delete 事件时,在 databse 内全局自增。而 _sign 则用于标记是否被删除,取值 1 或者 -1。

目前 MaterializeMySQL 支持如下几种 binlog 事件:

MYSQL_WRITE_ROWS_EVENT:_sign = 1,_version ++;
MYSQL_DELETE_ROWS_EVENT:_sign = -1,_version ++;
MYSQL_UPDATE_ROWS_EVENT:新数据 _sign = 1;
MYSQL_QUERY_EVENT: 支持 CREATE TABLE 、DROP TABLE 、RENAME TABLE 等。

2.3、使用细则

DDL 查询

MySQL DDL 查询被转换成相应的 ClickHouse DDL 查询(ALTER, CREATE, DROP, RENAME)。如果 ClickHouse 不能解析某些 DDL 查询,该查询将被忽略。

数据复制

MaterializeMySQL 不支持直接插入、删除和更新查询,而是将 DDL 语句进行相应转换:

MySQL INSERT 查询被转换为 INSERT with _sign=1;
MySQL DELETE 查询被转换为 INSERT with _sign=-1;
MySQL UPDATE 查询被转换成 INSERT with _sign=1 和 INSERT with _sign=-1。

SELECT 查询

如果在 SELECT 查询中没有指定_version,则使用 FINAL 修饰符,返回_version 的最大值对应的数据,即最新版本的数据;
如果在 SELECT 查询中没有指定_sign,则默认使用 WHERE _sign=1,即返回未删除状态(_sign=1)的数据。

索引转换

ClickHouse 数据库表会自动将 MySQL 主键和索引子句转换为ORDER BY 元组;
ClickHouse 只有一个物理顺序,由 ORDER BY 子句决定。如果需要创建新的物理顺序,请使用物化视图。

2.4、案例实操

MySQL 开启 binlog 和 GTID 模式,确保 MySQL 开启了 binlog 功能 ,且格式为 ROW 打开/etc/my.cnf,在[mysqld]下添加:

复制代码
server-id=1
log-bin=mysql-bin
binlog_format=ROW

开启 GTID 模式 ,如果如果 clickhouse 使用的是 20.8 prestable 之后发布的版本,那么 MySQL 还需要配置开启 GTID 模式, 这种方式在 mysql 主从模式下可以确保数据同步的一致性(主从切换时)。

复制代码
gtid-mode=on
enforce-gtid-consistency=1 # 设置为主从强一致性
log-slave-updates=1 # 记录日志

GTID 是 MySQL 复制增强版,从 MySQL 5.6 版本开始支持,目前已经是 MySQL 主流复制模式。它为每个 event 分配一个全局唯一 ID 和序号,我们可以不用关心 MySQL 集群主从拓扑结构,直接告知 MySQL 这个 GTID 即可。

重启 MySQL:

复制代码
sudo systemctl restart mysqld

准备 MySQL 表和数据

复制代码
CREATE DATABASE testck;
CREATE TABLE `testck`.`t_organization` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`code` int NOT NULL,
	`name` text DEFAULT NULL,
	`updatetime` datetime DEFAULT NULL,
	PRIMARY KEY (`id`),
	UNIQUE KEY (`code`)
) ENGINE=InnoDB;

INSERT INTO testck.t_organization (code,name,updatetime) VALUES(1000,'Realinsight',NOW());
INSERT INTO testck.t_organization (code,name,updatetime) VALUES(1001, 'Realindex',NOW());
INSERT INTO testck.t_organization (code,name,updatetime) VALUES(1002,'EDT',NOW());

CREATE TABLE `testck`.`t_user` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`code` int,
	PRIMARY KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO testck.t_user (code) VALUES(1);

开启 ClickHouse 物化引擎

复制代码
set allow_experimental_database_materialize_mysql=1;

创建复制管道

ClickHouse 中创建MaterializeMySQL 数据库

CREATE DATABASE test_binlog ENGINE = MaterializeMySQL('hadoop1:3306','testck','root','000000');
其中 4 个参数分别是 MySQL 地址、databse、username 和 password。

查看 ClickHouse 的数据:

复制代码
use test_binlog;
show tables;
select * from t_organization;
select * from t_user;

修改数据,在 MySQL 中修改数据:

复制代码
update t_organization set name = CONCAT(name,'-v1') where id = 1

查看 clickhouse 日志可以看到 binlog 监听事件,查询clickhouse:

复制代码
select * from t_organization;

删除数据,MySQL 删除数据:

复制代码
DELETE FROM t_organization where id = 2;

ClicKHouse,日志有 DeleteRows 的 binlog 监听事件,查看数据:

复制代码
select * from t_organization;

在刚才的查询中增加 _sign 和 _version 虚拟字段:select *,_sign,_version from t_organization order by _sign desc,_version desc;

在查询时,对于已经被删除的数据,_sign=-1,ClickHouse 会自动重写 SQL,将 _sign =-1 的数据过滤掉;

对于修改的数据,则自动重写 SQL,为其增加 FINAL 修饰符。

复制代码
select * from t_organization
等同于
select * from t_organization final where _sign = 1

删除表

在 mysql 执行删除表:drop table t_user; 此时在 clickhouse 处会同步删除对应表,如果查询会报错:

复制代码
show tables;
select * from t_user;
DB::Exception: Table scene_mms.scene doesn't exist.. 

mysql 新建表,clickhouse 可以查询到:

复制代码
CREATE TABLE `testck`.`t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` int,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO testck.t_user (code) VALUES(1);

#ClickHouse 查询
show tables;
select * from t_user;

3、表引擎

表引擎
表引擎的使用
表引擎决定了如何存储表的数据。包括:

数据的存储方式和位置,写到哪里以及从哪里读取数据;
支持哪些查询以及如何支持;
并发数据访问;
索引的使用(如果存在);
是否可以执行多线程请求;
数据复制参数;
表引擎的使用方式就是必须显式在创建表时定义该表使用的引擎,以及引擎使用的相关参数。

引擎的名称大小写敏感。

3.1、TinyLog

以列文件的形式保存在磁盘上,不支持索引,没有并发控制。一般保存少量数据的小表,生产环境上作用有限。可以用于平时练习测试用。

3.2、Memory

内存引擎:数据以未压缩的原始形式直接保存在内存当中,服务器重启数据就会消失。读写操作不会相互阻塞,不支持索引。简单查询下有非常非常高的性能表现(超过10G/s)。一般用到它的地方不多,除了用来测试,就是在需要非常高的性能,同时数据量又不太大(上限大概1亿行)的场景

3.3、MergeTree

ClickHouse中最强大的表引擎当属MergeTree(合并树)引擎及该系列(MergeTree)中的其他引擎,支持索引和分区,地位可以相当于innodb之于Mysql。

基本sql,建表语句:

复制代码
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
    ...
    INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
    INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MergeTree()
ORDER BY expr
[PARTITION BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
[SETTINGS name=value, ...]

PARTITION BY:分区键,用于指定数据以何种标准进行分区。分区键可以是单个列字段、元组形式的多个列字段、列表达式。如果不声明分区键,则ClickHouse会生成一个名为all的分区。合理使用数据分区,可以有效减少查询时数据文件的扫描范围。
ORDER BY 决定了每个分区中数据的排序规则;主键必须是order by字段的前缀字段;在ReplactingmergeTree中,order by相同的被认为是重复的数据;在SummingMergeTree中作为聚合的维度列;
PRIMARY KEY 决定了一级索引(primary.idx),默认情况下,主键与排序键(ORDER BY)相同,所以通常使用ORDER BY代为指定主键。一般情况下,在单个数据片段内,数据与一级索引以相同的规则升序排序。与其他数据库不同,MergeTree主键允许存在重复数据;
SAMPLE BY:抽样表达式,用于声明数据以何种标准进行采样。抽样表达式需要配合SAMPLE子查询使用;
SETTINGS:index_granularity:索引粒度,默认值8192。也就是说,默认情况下每隔8192行数据才生成一条索引;
SETTINGS:index_granularity_bytes:在19.11版本之前,ClickHouse只支持固定大小的索引间隔(index_granularity)。在新版本中增加了自适应间隔大小的特性,即根据每一批次写入数据的体量大小,动态划分间隔大小。而数据的体量大小,由index_granularity_bytes参数控制,默认10M;
SETTINGS:enable_mixed_granularity_parts:设置是否开启自适应索引间隔的功能,默认开启。

例子:

复制代码
create table t_order_mt(
    id UInt32,
    sku_id String,
    total_amount Decimal(16,2),
    create_time Datetime
) engine = MergeTree
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id,sku_id);

插入数据:

复制代码
insert into t_order_mt values
(101,'sku_001',1000.00,'2020-06-01 12:00:00') ,
(102,'sku_002',2000.00,'2020-06-01 11:00:00'),
(102,'sku_004',2500.00,'2020-06-01 12:00:00'),
(102,'sku_002',2000.00,'2020-06-01 13:00:00'),
(102,'sku_002',12000.00,'2020-06-01 13:00:00'),
(102,'sku_002',600.00,'2020-06-02 12:00:00');

查询数据:

1)根据日期分区,2020-06-01、2020-06-02共两个分区;

2)主键可重复;

3)分区内根据id和sku_id排序。

分区(可选)

分区的目的主要是降低扫描的范围,优化查询速度,如果不填,只会使用一个分区。分区后,面对涉及跨分区的查询统计,ClickHouse会以分区为单位并行处理。

文件结构

在前面安装时,就介绍过,配置文件中表明了默认的数据存储位置是/var/lib/clickhouse,因此结构如下:

里面有两个文件夹很重要:metadatadata。metadata保存了数据库的元数据,每个库的每个表都会记录表结构信息:

复制代码
ATTACH TABLE _ UUID 'c51df0c7-bae7-4abb-b8a8-d2a5523cdb26'
(
    `id` UInt32,
    `sku_id` String,
    `total_amount` Decimal(16, 2),
    `create_time` DateTime
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(create_time)
PRIMARY KEY id
ORDER BY (id, sku_id)
SETTINGS index_granularity = 8192

基本上和建表语句差不多。

data保存了每个库的表数据:

20200601_1_1_0、20200602_2_2_0共两个分区目录。

分区目录命名格式:PartitionId_MinBlockNum_MaxBlockNum_Level ,分表代表分区值最小分区块编号 、最大分区块编号、合并层级。而每一个分区目录下都包含如下文件:

复制代码
-rw-r-----. 1 clickhouse root 259 8月  29 03:02 checksums.txt
-rw-r-----. 1 clickhouse root 118 8月  29 03:02 columns.txt
-rw-r-----. 1 clickhouse root   1 8月  29 03:02 count.txt
-rw-r-----. 1 clickhouse root 189 8月  29 03:02 data.bin
-rw-r-----. 1 clickhouse root 144 8月  29 03:02 data.mrk3
-rw-r-----. 1 clickhouse root  10 8月  29 03:02 default_compression_codec.txt
-rw-r-----. 1 clickhouse root   8 8月  29 03:02 minmax_create_time.idx
-rw-r-----. 1 clickhouse root   4 8月  29 03:02 partition.dat
-rw-r-----. 1 clickhouse root   8 8月  29 03:02 primary.idx

data.bin:数据文件;(其实每一个列都会有一个bin文件)
data.mrk3:标记文件,标记文件在idx索引文件和bin数据文件之间起到了桥梁作用;(每一个列都会有一个mrk文件)
count.txt:有几条数据;
default_compression_codec.txt:默认压缩格式;
columns.txt:列的信息;
primary.idx:主键索引文件;
partition.dat与minmax_[Column].idx:如果使用了分区键,则会额外生成这2个文件,均使用二进制存储。partition.dat保存当前分区下分区表达式最终生成的值;minmax索引用于记录当前分区下分区字段对应原始数据的最小值和最大值。以t_order_mt的20200601分区为例,partition.dat中的值为20200601,minmax索引中保存的值为2020-06-01 12:00:002020-06-01 13:00:00

分区命名

PartitionId:数据分区规则由分区ID决定,分区ID由partition by分区键决定。根据分区键字段类型,ID生成规则可分为:

未定义分区键:没有定义partition by,默认生成一个目录名为all的数据分区,所有数据均存放在all目录下;
整型分区键:分区键为整型,直接用该整型值的字符串形式作为分区ID;
日期类分区键:分区键为日期类型,或者可以转换为日期类型;
其他类型分区键:String、Float类型等,通过128位的Hash算法取其Hash值作为分区ID。

MinBlockNum:最小分区块编号,自增类型,从1开始向上递增。每产生一个新的目录分区就向上递增一个数字;

MaxBlockNum:最大分区块编号,新创建的分区MinBlockNum等于MaxBlockNum的编号;

Level:合并的层级,被合并的次数。合并次数越多,层级值越大。对于每一个新创建的分区目录而言,其初始值均为0。以分区为单位,如果相同分区发生合并动作,则在相应分区内计数+1。

分区合并
任何一个批次的数据写入都会产生一个临时分区,也就是写入一个新的分区目录,不会纳入任何一个已有的分区。写入后的某个时刻(大概10-15分钟后),ClickHouse会自动执行合并操作(等不及也可以手动通过optimize执行),把临时分区的数据,合并到已有分区中,已经存在的旧分区目录并不会立即被删除,而是在之后的某个时刻通过后台任务被删除(默认8分钟)。

MinBlockNum:取同一分区内所有目录中最小的MinBlockNum值
MaxBlockNum:取同一分区内所有目录中最大的MaxBlockNum值
Level:取同一分区内最大Level值+1

合并过程:

手动合并:

复制代码
optimize table xxxx final;

在如上的基础上,在插入数据:

复制代码
insert into t_order_mt values
(101,'sku_001',1000.00,'2020-06-01 12:00:00') ,
(102,'sku_002',2000.00,'2020-06-01 11:00:00'),
(102,'sku_004',2500.00,'2020-06-01 12:00:00'),
(102,'sku_002',2000.00,'2020-06-01 13:00:00'),
(102,'sku_002',12000.00,'2020-06-01 13:00:00'),
(102,'sku_002',600.00,'2020-06-02 12:00:00');

查看数据并没有纳入任何分区,而是新产生了一个分区:

在看数据文件:

也多加了两个分区文件。手动optimize之后:

复制代码
optimize table t_order_mt final;

再查询:

可以看到一个PartitionId的数据就已经合并成功了。而也会新产生两个文件代表新的分区数据。

主键(可选)
ClickHouse中的主键,和其他数据库不太一样,**它只提供了数据的一级索引,但是却不是唯一约束。**这就意味着是可以存在相同primary key的数据。

主键的设定主要依据是查询语句中的where条件。根据条件通过对主键进行某种形式的二分查找,能够定位到对应的index granularity,避免了全表扫描。

index granularity:直接翻译的话就是索引粒度,指在稀疏索引中两个相邻索引对应数据的间隔。ClickHouse中的MergeTree默认是8192。官方不建议修改这个值,除非该列存在大量重复值,比如在一个分区中几万行才有一个不同数据。

稀疏索引的好处就是可以用很少的索引数据,定位更多的数据,代价就是只能定位到索引粒度的第一行,然后再进行进行一点扫描。

order by(必须)
order by设定了分区内的数据按照哪些字段顺序进行有序保存。order by是MergeTree中唯一一个必填项,甚至比primary key还重要,因为当用户不设置主键的情况,很多处理会依照order by的字段进行处理。

要求:主键必须是order by字段的前缀字段。

比如order by字段是(id,sku_id),那么主键必须是id或者(id,sku_id)。

一级索引

MergeTree的主键使用PRIMARY KEY定义,待主键定义之后,MergeTree会依据index_granularity间隔(默认8192行),为数据表生成一级索引并保存至primary.idx文件内,索引数据按照PRIMARY KEY排序。相比使用PRIMARY KEY定义,更为常见的是通过ORDER BY指代主键。在此种情形下,PRIMARY KEY与ORDER BY定义相同,所以索引(primary.idx)和数据(.bin)会按照完全相同的规则排序。

稀疏索引
primary.idx文件内的一级索引采用稀疏索引实现

稀疏索引与稠密索引的区别:

在稠密索引中每一行索引标记都会对应到一行具体的数据记录;而在稀疏索引中,每一行索引标记对应的是一段数据,而不是一行。由于稀疏索引占用空间小,所以primary.idx内的索引数据常驻内存。

索引粒度

index_granularity表示索引的粒度,默认8192

数据以index_granularity的粒度被标记成多个小的区间,其中每个区别最多index_granularity行数据。MergeTree使用MarkRange表示一个具体的区间,并通过start和end表示其具体的范围

索引数据的生成规则
由于是稀疏索引,所以MergeTree需要间隔index_granularity行数据才会生成一条索引记录,其索引值会依据声明的主键字段获取

使用CounterID作为主键

该表使用年月分区(PARTITION BY toYYYYMM(EventDate)),所以2014年3月份的数据最终会被划分到同一个分区目录内。如果使用CounterID作为主键(ORDER BY CounterID),则每间隔8192行数据就会取一次CounterID的值作为索引值,索引数据最终会被写入primary.idx文件进行保存

例如第0(8192 ∗ 0)行CounterID取值57,第8192(8192 ∗ 1)行CounterID取值1635,而第16384(8192 ∗ 2)行CounterID取值3266,最终索引数据将会是5716353266

使用CounterID和EventDate作为主键

如果使用多个主键,例如ORDER BY(CounterID,EventDate),则每间隔8192行可以同时取CounterID与EventDate两列的值作为索引值,如上图所示。

索引的查询过程

生成查询条件区间,首先,将查询条件转换为条件区间,例如下面的例子:

复制代码
WHERE ID = 'A003'
['A003', 'A003']

WHERE ID > 'A000'
('A000', +inf)

WHERE ID < 'A188'
(-inf, 'A188')

WHERE ID LIKE 'A006%'
['A006', 'A007')

递归交集判断

以递归的形式,依次对MarkRange的数值区间与条件区间做交集判断。从最大区间[A000, +inf)开始:如果不存在交集,则直接通过剪枝算法优化此整段MarkRange;
如果存在交集,且MarkRange步长大于8(end-start),则将此区间进一步拆分成8个子区间(merge_tree_coarse_index_granularity指定,默认值为8),并重复此规则,继续做递归交集判断;如果存在交集,且MarkRange不可再分解(步长小于8),则记录MarkRange并返回。合并MarkRange区间,将最终匹配的MarkRange聚在一起,合并它们的范围:

MergeTree通过递归的形式持续向下拆分区间,最终将MarkRange定位到最细的粒度,以帮助在后续读取数据的时候,能够最小化扫描数据的范围。以上图为例,当查询条件WHERE ID='A003'的时候,最终只需要读取[A000, A003]和[A003, A006]两个区间的数据,它们对应MarkRange(start:0,end:2)范围,而其他无用的区间都被裁剪掉了。因为MarkRange转换的数值区间是闭区间,所以会额外匹配到临近的一个区间。

二级索引

目前在ClickHouse的官网上二级索引的功能在v20.1.2.4之前是被标注为实验性的,在这个版本之后默认是开启的

**老版本使用二级索引前需要增加设置:**是否允许使用实验性的二级索引(v20.1.2.4开始,这个参数已被删除,默认开启)

复制代码
set allow_experimental_data_skipping_indices=1;

如果在建表语句中声明了跳数索引,则会额外生成相应的索引与标记文件(skp_idx_[Column].idx与skp_idx_[Column].mrk)。

创建测试表:

复制代码
create table t_order_mt2(
id UInt32,
sku_id String,
total_amount Decimal(16,2),
create_time Datetime,
INDEX a total_amount TYPE minmax GRANULARITY 5
) engine =MergeTree
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id, sku_id);

INDEX a total_amount TYPE minmax GRANULARITY 5:

a表示索引名称;

total_amount表示索引字段;
TYPE表示索引类型:
minmax 存储指定一段数据内的最小和最大极值(如果表达式是 tuple ,则存储 tuple 中每个元素的极值),这些信息用于跳过数据块,类似主键。
set(max_rows) 存储指定表达式的不重复值(不超过 max_rows 个,max_rows=0 则表示『无限制』)。这些信息可用于检查数据块是否满足 WHERE 条件。
ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed) 存储一个包含数据块中所有 n元短语(ngram) 的 布隆过滤器。只可用在字符串上。 可用于优化 equals , like 和 in 表达式的性能。
n -- 短语长度。
size_of_bloom_filter_in_bytes -- 布隆过滤器大小,字节为单位。(因为压缩得好,可以指定比较大的值,如 256 或 512)。
number_of_hash_functions -- 布隆过滤器中使用的哈希函数的个数。
random_seed -- 哈希函数的随机种子。
tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed) 跟 ngrambf_v1 类似,但是存储的是token而不是ngrams。Token是由非字母数字的符号分割的序列。
bloom_filter(bloom_filter([false_positive]) -- 为指定的列存储布隆过滤器
GRANULARITY 5 是设定二级索引对于一级索引粒度的粒度。
minmax索引的聚合信息是在一个index_granularity区间内数据的最小和最大值。以下图为例,假设index_granularity=8192且granularity=3,则数据会按照index_granularity划分为n等份,MergeTree从第0段分区开始,依次获取聚合信息。当获取到第3个分区时(granularity=3),则汇总并会生成第一行minmax索引(前3段minmax汇总后取值为[1, 9]):

minmax索引的聚合信息是在一个index_granularity区间内数据的最小和最大值。以下图为例,假设index_granularity=8192且granularity=3,则数据会按照index_granularity划分为n等份,MergeTree从第0段分区开始,依次获取聚合信息。当获取到第3个分区时(granularity=3),则汇总并会生成第一行minmax索引(前3段minmax汇总后取值为[1, 9]):

插入数据:

复制代码
insert into t_order_mt2 values
(101,'sku_001',1000.00,'2020-06-01 12:00:00') ,
(102,'sku_002',2000.00,'2020-06-01 11:00:00'),
(102,'sku_004',2500.00,'2020-06-01 12:00:00'),
(102,'sku_002',2000.00,'2020-06-01 13:00:00'),
(102,'sku_002',12000.00,'2020-06-01 13:00:00'),
(102,'sku_002',600.00,'2020-06-02 12:00:00');

对比效果:

复制代码
clickhouse-client --send_logs_level=trace <<< 
'select * from t_order_mt2 where total_amount > toDecimal32(900., 2)';

日志中可以看到二级索引能够为非主键字段的查询发挥作用,分区下文件skp_idx_a.idxskp_idx_a.mrk3为跳数索引文件:

复制代码
checksums.txt  count.txt  data.mrk3      minmax_create_time.idx  primary.idx    skp_idx_a.mrk3
columns.txt    data.bin   default_compression_codec.txt  partition.dat           skp_idx_a.idx

数据TTL

MergeTree提供了可以管理数据表或者列的生命周期的功能。

列级TTL

当列中的值过期时, ClickHouse会将它们替换成该列数据类型的默认值。如果数据片段中列的所有值均已过期,则ClickHouse 会从文件系统中的数据片段中删除此列。TTL的列必须是日期类型且不能为主键。

复制代码
create table t_order_mt3(
  id UInt32,
  sku_id String,
  total_amount Decimal(16,2) TTL create_time+interval 10 SECOND,
  create_time Datetime 
) engine =MergeTree
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id, sku_id);

插入数据(请根据实际时间修改数据,注意是linux的时间):

复制代码
insert into t_order_mt3 values
(106,'sku_001',1000.00,'2022-08-29 04:14:20'),
(107,'sku_002',2000.00,'2022-08-29 04:14:20'),
(110,'sku_003',600.00,'2022-08-29 04:14:20');

手动合并,查看效果:到期后,指定的字段数据归0:

复制代码
optimize table t_order_mt3 final;
select * from t_order_mt3;

表级TTL

表可以设置一个用于移除过期行的表达式,以及多个用于在磁盘或卷上自动转移数据片段的表达式。当表中的行过期时,ClickHouse 会删除所有对应的行。对于数据片段的转移特性,必须所有的行都满足转移条件。

下面的这条语句是数据会在create_time之后10秒丢失:

复制代码
alter table t_order_mt3 MODIFY TTL create_time + INTERVAL 10 SECOND;

[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]

TTL 规则的类型紧跟在每个 TTL 表达式后面,它会影响满足表达式时(到达指定时间时)应当执行的操作:

DELETE - 删除过期的行(默认操作);
TO DISK 'aaa' - 将数据片段移动到磁盘 aaa;
TO VOLUME 'bbb' - 将数据片段移动到卷 bbb.
GROUP BY - 聚合过期的行
涉及判断的字段必须是Date或者Datetime类型,推荐使用分区的日期字段

能够使用的时间周期:

SECOND
MINUTE
HOUR
DAY
WEEK
MONTH
QUARTER
YEAR

数据存储,各列独立存储
在MergeTree中,数据按列存储。而具体到每个列字段,数据也是独立存储的,每个列字段都拥有一个与之对应的.bin数据文件。数据文件以分区目录的形式被组织存放,所以在.bin文件中只会保存当前分区片段内的这一部分数据。按列独立存储的设计优势显而易见:

可以更好地进行数据压缩(相同类型的数据放在一起,对压缩更加友好);

能够最小化数据扫描的范围。

数据是经过压缩的,目前支持LZ4、ZSTD、Multiple和Delta几种算法,默认使用LZ4算法;其次,数据会事先依照ORDER BY的声明排序;最后,数据是以压缩数据块的形式被组织并写入.bin文件中的

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

MergeTree在数据具体的写入过程中,会按照索引粒度(默认情况下,每次取8192行),按批次获取数据并进行处理。如果把一批数据的未压缩大小设为size,则整个写入过程遵循以下规则:

单个批次数据size < 64KB:如果单个批次数据小于64KB,则继续获取下一批数据,直至累积到size >= 64KB时,生成下一个压缩数据块;
单个批次数据64KB <= size <= 1MB:如果单个批次数据大小恰好在64KB与1MB之间,则直接生成下一个压缩数据块;
单个批次数据size > 1MB:如果单个批次数据直接超过1MB,则首先按照1MB大小截断并生成下一个压缩数据块。剩余数据继续依照上述规则执行。此时,会出现一个批次数据生成多个压缩数据块的情况。

一个.bin文件是由1至多个压缩数据块组成的,每个压缩块大小在64KB~1MB之间。多个压缩数据块之间,按照写入顺序首尾相接,紧密地排列在一起。

数据标记

数据标记的生成规则

从上图中可以发现,数据标记和索引区间是对齐的,均按照index_granularity的粒度间隔。

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

一行标记数据使用一个元组表示,元组内包含两个整型数值的偏移量信息。它们分别表示此段数据区间内,在对应的.bin压缩文件中,压缩数据块的起始偏移量;以及将该数据压缩块解压后,其未压缩数据的起始偏移量:

每一行标记数据都表示了一个片段的数据(默认8192行)在.bin压缩文件中的读取位置信息。标记数据与一级索引数据不同,它并不能常驻内存,而是使用LRU(最近最少使用)缓存策略加快其取用速度。

数据标记的工作方式

下图为为hits_v1测试表的JavaEnable字段及其标记数据与压缩数据的对应关系:

JavaEnable字段的数据类型为UInt8,所以每行数值占用1字节。而hits_v1数据表的index_granularity粒度为8192,所以一个索引片段的数据大小恰好是8192B。按照压缩数据块的生成规则,如果单个批次数据小于64KB,则继续获取下一批数据,直至累积到size >= 64KB时,生成下一个压缩数据块。因此在JavaEnable的标记文件中,每8行标记数据对应1个压缩数据块(1B*8192=8192B,64KB=65536B,65536/8192=8)。所以,从上图能够看到,其左侧的标记数据中,8行数据的压缩文件偏移量都是相同的,因为这8行标记都指向了同一个压缩数据块。而在这8行的标记数据中,它们的解压缩数据块中的偏移量,则依次按照8192B(每行数据1B,每一个批次8192行数据)累加,当累加达到65536(64KB)时则置0。因为根据规则,此时会生成下一个压缩数据块。

1)读取压缩数据块

上下相邻的两个压缩文件中的起始偏移量,构成了与获取当前标记对应的压缩数据块的偏移量区间。由当前标记数据开始,向下寻找,直到找到不同的压缩文件偏移量为止。此时得到的一组偏移量区间即时压缩数据块在.bin文件中的偏移量。如上图所示,读取右侧.bin文件中[0, 12016](8+12000+8=12016)字节数据,就能获得第0个压缩数据块。

压缩数据块被整个加载到内存之后,会进行解压,在这之后就进入具体数据的读取环节了。

2)读取数据

在读取解压后的数据时,MergeTree并不需要一次性扫描整段解压数据,它可以根据需要,以index_granularity的粒度加载特定的一小段

上下相邻两个解压缩数据块中的起始偏移量,构成了与获取当前标记对应的数据的偏移区间。通过这个区间能够在它的压缩块被解压之后,依照偏移量按需读取数据,如上图所示,通过[0, 8192]能够读取压缩数据块0中的第一个数据片段。

数据标记与压缩数据块的对应关系
由于压缩数据块的划分,与一个间隔(index_granularity)内的数据大小相关,每个压缩数据块的体积都被严格控制在64KB~1MB。而一个间隔(index_granularity)的数据,又只会产生一行数据标记。那么根据一个间隔内数据的实际字节大小,数据标记和压缩数据块之间会产生三种不同的对应关系:

1)多对一

多个数据标记对应一个压缩数据块,当一个间隔(index_granularity)内的数据未压缩大小size小于64KB时,会出现这种对应关系

以hits_v1测试表的JavaEnable字段为例。JavaEnable数据类型为UInt8,大小为1B,则一个间隔内数据大小为8192B。所以在此种情形下,每8个数据标记会对应同一个压缩数据块

2)一对一

一个数据标记对应一个压缩数据块,当一个间隔(index_granularity)内的数据未压缩大小size大于等于64KB且小于等于1MB时,会出现这种对应关系。

以hits_v1测试表的URLHash字段为例。URLHash数据类型为UInt64,大小为8B,则一个间隔内数据大小为65536B,恰好等于64KB。所以在此种情形下,数据标记与压缩数据块是一对一的关系。


3)一对多

一个数据标记对应多个压缩数据块,当一个间隔(index_granularity)内的数据未压缩大小size直接大于1MB时,会出现这种对应关系。

以hits_v1测试表的URL字段为例。URL数据类型为String,大小根据实际内容而定。

小节
写入过程
数据写入的第一步是生成分区目录,伴随着每一批数据的写入,都会生成一个新的分区目录。在后续的某一时刻,属于相同分区的目录会依照规则合并到一起;接着,按照index_granularity索引粒度,会分别生成primary.idx一级索引(如果声明了二级索引,还会创建二级索引文件)、每一个列字段的.mrk数据标记和.bin压缩数据文件:

从分区目录201403_1_34_3能够得知,该分区数据共分34批写入,期间发生过3次合并。在数据写入的过程中,依据index_granularity的粒度,依次为每个区间的数据生成索引、标记和压缩数据块。

查询过程
在最理想的情况下,MergeTree首先可以依次借助分区索引、一级索引和二级索引,将数据扫描范围缩至最小。然后再借助数据标记,将需要解压与计算的数据范围缩至最小

如果一条查询语句没有指定任何WHERE条件,或是指定了WHERE条件,但条件没有匹配到任何索引(分区索引、一级索引和二级索引),那么MergeTree就不能预先减小数据范围。在后续进行数据查询时,它会扫描所有分区目录,以及目录内索引段的最大区间。虽然不能减少数据范围,但是MergeTree仍然能够借助数据标记,以多线程的形式同时读取多个压缩数据块,以提升性能。

3.4、ReplacingMergeTree

ReplacingMergeTree是MergeTree的一个变种,它存储特性完全继承MergeTree,只是多了一个去重的功能,去重时机。只有同一批插入(新版本)或合并分区时才会进行去重。合并会在未知的时间在后台进行,所以你无法预先作出计划。有一些数据可能仍未被处理;

1)去重范围。如果表经过了分区,去重只会在分区内部进行去重,不能执行跨分区的去重;

2)实际上是使用order by字段作为唯一键进行去重;

3)认定重复的数据保留,版本字段值最大的;

4)如果版本字段相同则按插入顺序保留最后一笔

所以ReplacingMergeTree能力有限,ReplacingMergeTree适用于在后台清除重复的数据以节省空间,但是它不保证没有重复的数据出现。

复制代码
create table t_order_rmt(
    id UInt32,
    sku_id String,
    total_amount Decimal(16,2) ,
    create_time Datetime 
) engine =ReplacingMergeTree(create_time)
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id, sku_id);

ReplacingMergeTree()填入的参数为版本字段,重复数据保留版本字段值最大的。如果不填版本字段,默认按照插入顺序保留最后一条

插入数据:

复制代码
insert into t_order_rmt values
(101,'sku_001',1000.00,'2020-06-01 12:00:00') ,
(102,'sku_002',2000.00,'2020-06-01 11:00:00'),
(102,'sku_004',2500.00,'2020-06-01 12:00:00'),
(102,'sku_002',2000.00,'2020-06-01 13:00:00'),
(102,'sku_002',12000.00,'2020-06-01 13:00:00'),
(102,'sku_002',600.00,'2020-06-02 12:00:00');

执行查询操作:

复制代码
select * from t_order_rmt;

3.5、SummingMergeTree

对于不查询明细,只关心以维度进行汇总聚合结果的场景。如果只使用普通的MergeTree的话,无论是存储空间的开销,还是查询时临时聚合的开销都比较大

ClickHouse为了这种场景,提供了一种能够预聚合的引擎SummingMergeTree。

以SummingMergeTree()中指定的列作为汇总数据列;
可以填写多列必须数字列,如果不填,以所有非维度列(除了order by的列之外)且为数字列的字段为汇总数据列;
以order by的列为准,作为维度列;
其他的列按插入顺序保留第一行;
不在一个分区的数据不会被聚合;
只有在同一批次插入(新版本)或分片合并时才会进行聚合

复制代码
create table t_order_smt(
    id UInt32,
    sku_id String,
    total_amount Decimal(16,2) ,
    create_time Datetime 
) engine =SummingMergeTree(total_amount)
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id,sku_id );

插入数据:

复制代码
insert into t_order_smt values
(101,'sku_001',1000.00,'2020-06-01 12:00:00'),
(102,'sku_002',2000.00,'2020-06-01 11:00:00'),
(102,'sku_004',2500.00,'2020-06-01 12:00:00'),
(102,'sku_002',2000.00,'2020-06-01 13:00:00'),
(102,'sku_002',12000.00,'2020-06-01 13:00:00'),
(102,'sku_002',600.00,'2020-06-02 12:00:00');

执行查询操作:

相关推荐
=蜗牛=5 天前
Docker 简单部署 ClickHouse 超详细图文步骤
clickhouse·docker·容器·部署·图文
狼与自由6 天前
clickhouse log引擎
clickhouse
狼与自由7 天前
clickhouse AggregatingMergeTree
clickhouse
狼与自由7 天前
clickhouse ReplacingMergeTree
android·clickhouse
狼与自由8 天前
clickhouse中的分区
clickhouse
狼与自由8 天前
clickhouse 查询
clickhouse
狼与自由8 天前
clickhouse mergeTree
clickhouse
狼与自由9 天前
clickhouse建表
clickhouse
简简单单就是我_hehe9 天前
clickhouse内置函数和关键词总结
clickhouse
狼与自由9 天前
clickhouse引擎
clickhouse·c#·linq