ClickHouse基础介绍

1.1 ClickHouse的由来

1 为什么叫 ClickHouse?
在这里插入图片描述

2 yandex是一家怎样的公司?

​ 欧洲最大的互联网公司之一,俄罗斯第一搜索引擎。

​ 起初ClickHouse设计目标是服务yandex.Metrica产品。Metrica是一款Web流量分析工具,基于前方的探针采集用户行为数据,然后进行数据分析。而在采集数据过程中,一次页面click,会产生一个event —— 基于页面点击事件流,面向数据仓库OLAP数据库。

1.2 ClickHouse的特点

1.完整的DBMS

​ DDL:可以动态的创建、修改或者删除数据库、表和视图,而无须重启服务;

​ DML:可以动态的查询、插入、修改、或者删除数据(性能差);

​ 权限控制:可以按照用户粒度设置数据库或者表的操作权限,保证数据的安全性;

​ 数据备份与恢复:提高数据备份导出与导入恢复机制;

​ 分布式管理:提供集群模式,能够自动管理多个数据库节点。

2.完全列式存储

​ 假设一张数据表A中字段A1~A50,100行数据。

   SELECT A1,A2,A3,A4,A5 FROM A;

​ 按行查找:数据库先追行扫描,获取每行数据的所有50个字段,再从每一行的数据中返回A1~A5这五个字段
在这里插入图片描述

​ 按列查找:有效的减少了查询时所需扫描的数据量;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pNPZzyjk-1615027305502)(E:\DB组件\内部培训\image\column.webp)]

​ 压缩前:abcdefghi_bcdefghi;

​ 压缩后:abcdefghi_(9,8)。

​ (9,8)表示如果从下划线开始向前移动9个字节,会匹配到8个字节长度的重复项,即bcdefghi

减少数据扫描范围,减少I / O量,适合做聚合计算,便于压缩(降低存储压力),传输越快

3.多样化的数据库引擎和表引擎

数据库引擎:

​ Ordinary 默认引擎
​ Dictionary 字典引擎 此类数据库会自动为所有数据字典创建他们的数据表。
​ Memory 内存引擎 用于存放临时数据,此类数据库下的数据表只会停留在内存,重启数据丢失。
​ Lazy 日志引擎 此类数据库下只能使用log系列的表引擎。
​ Mysql Mysql引擎 此类数据库会自动拉取远端Mysql中的数据,并创建Mysql表引擎的数据表。

表引擎(即表的类型):

​ - MergeTree 用于海量数据分析,支持数据分区、存储有序、支持索引、数据TTL等。
​ - ReplacingMergeTree 为了解决MergeTree相同主键无法去重的问题。
​ - SummingMergeTree 通过SummingMergeTree来支持对主键列进行预先聚合。
​ - AggregatingMergeTree 预先聚合引擎的一种,用于提升聚合计算的性能。
​ - CollapsingMergeTree 实现了CollapsingMergeTree来消除ReplacingMergeTree的功能限制。
​ - VersionedCollapsingMergeTree 为了解决CollapsingMergeTree乱序写入情况下无法正常折叠问题
​ - GraphiteMergeTree 用来对Graphite数据进行瘦身及汇总。

4.向量化执行引擎

​ 消除程序中循环的优化

grep -q sse4_2 /proc/cpuinfo && echo "SSE 4.2 supported" || echo "SSE 4.2 not supported"

5.SQL查询

​ 完全使用SQL作为查询语言 ( 支持GROUP BY、ORDER BY、JOIN、IN等)

6.多主架构

​ 采用Multi-Master主从架构,节点角色对等,客户端访问任意节点得到效果相同,规避了单点故障。

7.数据分片与分布式查询

​ 数据分片是将数据进行横向切分,解决存储和查询瓶颈的有效手段。ClickHouse支持分片,而分片则依赖集群。每个集群由1到多个分片组成,而每个分片则对应了ClickHouse的1个服务节点。分片的数量上限取决于节点数量 ( 1个分片只能对应1个服务节点 )。

​ ClickHouse提供了本地表 ( Local Table ) 与分布式表 ( Distributed Table ) 的概念。一张本地表等同于一份数据的分片。而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。

1.3 为什么ClickHouse如此之快?

​ 0.c++,c语言和硬件交互优势

​ 1.采用列式存储

​ 2.使用了向量化引擎

​ 3.软件架构设计采用自底向上方式。 追求自底向上、追求极致的设计思路

​ 4.方便实时的数据结构 MergeTree

​ 5.硬件

在这里插入图片描述

​ 6.算法

​ 对于常量,使用了Volnitsky算法;对于非常量,使用CPU的向量化执行SIMD,暴力优化;正则匹配使用re2和hyperscan算法。

​ 7. 特殊优化

​ 针对同一场景不同状况,选择使用不同的实现方式。
​ 例如去重计数uniqCombined函数,会根据数据量的不同选择不同的算法:
​ 当数据量较小的时候,会选择Array保存;
​ 当数据量中等的时候,会选择HashSet;
​ 当数据量很大的时候,会使用HyperLogLog算法
​ 对于数据结构比较清晰的场景,会通过代码生成技术实现循环展开,以减少循环次数。

​ 8.版本发布

​ 基本每个月都能发布一个版本,更新频繁时一周一个版本。意味着拥有一个持续验证,持续改进的机制。

​ 9.官方测试

​ • 单个节点 • 133字段 • 1000万、1亿和10亿数据 • 43条SQL的基准测试

请添加图片描述

​ 横扫主流,ClickHouse的平均响应速度是Vertica的2.63倍、InfiniDB的17倍、MonetDB的27倍、Hive的126倍、MySQL的429倍以及Greenplum的10倍。

分析型DBMS的性能比较:https://clickhouse.tech/benchmark/dbms/

1.4 ClickHouse新特性

1.4.1 原生Explain查询支持

​ 在clickhouse 20.6.3版本之前要查看SQL语句的执行计划需要设置日志级别为trace才能可以看到。

clickhouse-client --version
clickhouse-client -q 'select count(*) from datasets.hits_v1'
clickhouse-client --send_logs_level=trace <<< 'select * from datasets.hits_v1' >/dev/null

在这里插入图片描述

​ 在20.6.3版本引入了原生的执行计划的语法。 ------事前分析,语句并没有真正去执行

EXPLAIN [AST | SYNTAX | PLAN | PIPELINE] [setting = value, ...] SELECT ... [FORMAT ...]

AST				用于查看语法树。

SYNTAX			用于优化语法。

PLAN			用于查看执行计划,默认值。

PIPELINE		用于查看 PIPELINE 计划。

header			打印计划中各个步骤的 head 说明,默认关闭,默认值0。

description		打印计划中各个步骤的描述,默认开启,默认值1。

actions			打印计划中各个步骤的详细信息,默认关闭,默认值0explain plan select * from datasets.hits_v1;
explain SYNTAX select * from datasets.hits_v1;
explain PIPELINE select * from datasets.hits_v1;

在这里插入图片描述

1.4.2 基于RBAC的权限体系

ClickHouse server version 20.5 新增RBAC的语法:

GRANT [ON CLUSTER cluster_name] privilege [(column_name [,...])]  [,...]

ON {db.table|db.*|*.*|table|*} TO {user|role|CURRENT_USER} [,...] [WITH GRANT OPTION]

vim /etc/clickhouse-server/config.xml

<!-- 新增配置文件 --->
<access_control_path>/var/lib/clickhouse/access/</access_control_path>

如何基于RBAC权限创建用户?

1.授权

ll /var/lib/clickhouse/access/
-rw-r----- 1 clickhouse clickhouse 1 Feb 23 10:02 quotas.list
-rw-r----- 1 clickhouse clickhouse 1 Feb 23 10:02 roles.list
-rw-r----- 1 clickhouse clickhouse 1 Feb 23 10:02 row_policies.list
-rw-r----- 1 clickhouse clickhouse 1 Feb 23 10:02 settings_profiles.list
-rw-r----- 1 clickhouse clickhouse 1 Feb 23 10:02 users.list

chmod -R 775 /var/lib/clickhouse/access/

ll /var/lib/clickhouse/access/
-rwxrwxr-x 1 clickhouse clickhouse 1 Feb 23 10:02 quotas.list
-rwxrwxr-x 1 clickhouse clickhouse 1 Feb 23 10:02 roles.list
-rwxrwxr-x 1 clickhouse clickhouse 1 Feb 23 10:02 row_policies.list
-rwxrwxr-x 1 clickhouse clickhouse 1 Feb 23 10:02 settings_profiles.list
-rwxrwxr-x 1 clickhouse clickhouse 1 Feb 23 10:02 users.list

2.修改配置

vim /etc/clickhouse-server/users.xml

<profiles>
  <default>
          <max_memory_usage>10000000000</max_memory_usage>
            <use_uncompressed_cache>0</use_uncompressed_cache>
            <load_balancing>random</load_balancing>
        </default>
        <readonly>
            <readonly>0</readonly> 					<!-- 默认为1 需要修改为0 -->
        </readonly>
    </profiles>

<access_management>1</access_management>			<!-- 默认是注释掉的,打开 --->

3.登陆并创建用户 (使用默认提供的default账号,默认拥有所有的权限)

# clickhouse-client -m 
ClickHouse client version 20.8.3.18.

CREATE USER root IDENTIFIED WITH PLAINTEXT_PASSWORD BY 'root';
set allow_introspection_functions=1;				--启用/禁用内省功能以进行查询概要分析
GRANT ALL ON *.* TO root WITH GRANT OPTION;

验证:
show create user root;

┌─CREATE USER root────────────────────────────────────┐
│ CREATE USER root IDENTIFIED WITH plaintext_password │
└─────────────────────────────────────────────────────┘

show grants for root;

┌─GRANTS FOR root────────────────────────────┐
│ GRANT ALL ON *.* TO root WITH GRANT OPTION │
└────────────────────────────────────────────┘

select name,storage,auth_type,host_ip from system.users where name in ('root','default');

┌─name────┬─storage─────────┬─auth_type──────────┬─host_ip──┐
│ default │ users.xml       │ plaintext_password │ ['::/0'] │
│ root    │ local directory │ plaintext_password │ ['::/0'] │
└─────────┴─────────────────┴────────────────────┴──────────┘

select user_name,role_name,access_type,database,table,grant_option from system.grants where user_name in ('root','default');

┌─user_name─┬─role_name─┬─access_type─┬─database─┬─table─┬─grant_option─┐
│ default   │ ᴺᵁᴸᴸ      │ ALL         │ ᴺᵁᴸᴸ     │ ᴺᵁᴸᴸ  │            1 │
│ root      │ ᴺᵁᴸᴸ      │ ALL         │ ᴺᵁᴸᴸ     │ ᴺᵁᴸᴸ  │            1 │
└───────────┴───────────┴─────────────┴──────────┴───────┴──────────────┘

删除角色:

DROP ROLE [IF EXISTS] name [,...] [ON CLUSTER cluster_name]

1.4.3 FINAL 查询并行优化

​ ClickHouse的ReplacingMergeTree在没有MergeTree(最终一致性)之前去重,同分区依然还是有重复数据。

SELECT * FROM hits_100m_obfuscated FINAL WHERE EventDate = '2013-07-15' limit 100;

优化前:串行运行(单线程运行)查询。

在这里插入图片描述

优化后:并行运行查询,利用8个线程对6个分区进行去重、合并为1个分区。

在这里插入图片描述

1.4.4 原生Binlog同步支持

​ ClickHouse 20.8将新增 MaterializeMySQL引擎 ,可通过binlog日志实时物化mysql数据,提升数仓的查询性能和数据同步的时效性;原有mysql中承担的数据分析工作可交由clickhouse去做,这么做可显著降低线上mysql的负载,从此OLTP与OLAP业务实现完美融合。

​ MaterializeMySQL database engine 支持的情况:

​ 1.支持mysql 库级别的数据同步,暂不支持表级别的。

​ 2.MySQL 库映射到clickhouse中自动创建为ReplacingMergeTree 引擎的表。

​ 3.支持全量和增量同步,首次创建数据库引擎时进行一次全量复制,之后通过监控binlog变化进行增量数据同步。

​ 4.支持的操作:insert,update,delete,alter,create,drop,truncate等大部分DDL操作。

​ 5.支持的MySQL复制为GTID复制。

在这里插入图片描述

​ 原生Binlog同步支持原理图

搭建流程:

--添加mysql配置文件
vim /etc/my.cnf

server_id							= 66
binlog_format                       = ROW
log_bin								= /data/3306/binlog/mysql-bin
gtid-mode							= on
enforce-gtid-consistency       		= 1			# 设置为主从强一致性
log-slave-updates                   = 1			# 记录日志

--查询mysql版本信息
select version() ;
+------------+
| version()  |
+------------+
| 5.7.28-log |
+------------+

--创建测试库、表
create database clickhouse_test;
use clickhouse_test;

CREATE TABLE `scene` (
   `id` int NOT NULL AUTO_INCREMENT,
   `code` int NOT NULL,
   `title` text DEFAULT NULL,
   `updatetime` datetime DEFAULT NULL,
   PRIMARY KEY (`id`),   			## 主键要设置为not null,否则ClickHouse同步会报错
   KEY `idx_code` (`code`)   		## 索引键也要设置为not null,否则ClickHouse同步会报错
 ) ENGINE=InnoDB default charset=Latin1;

show tables;

--插入数据
INSERT INTO scene(code, title, updatetime) VALUES(1001,'aaa',NOW());
INSERT INTO scene(code, title, updatetime) VALUES(1002,'bbb',NOW());
INSERT INTO scene(code, title, updatetime) VALUES(1003,'ccc',NOW());
INSERT INTO scene(code, title, updatetime) VALUES(1004,'ddd',NOW());
commit;


--查询ClickHouse版本信息
SELECT version()						
┌─version()─┐
│ 20.8.3.18 │
└───────────┘

SET allow_experimental_database_materialize_mysql = 1
--该功能目前还处于实验阶段,在使用之前需要开启
select * from system.settings where name ='allow_experimental_database_materialize_mysql';

--创建一个复制管道
CREATE DATABASE clickhouse_mysql
ENGINE = MaterializeMySQL('127.0.0.1:3306', 'clickhouse_test', 'root', 'xxxxxxx')

SHOW DATABASES;
USE clickhouse_mysql;
SHOW TABLES;

SELECT * FROM scene;
┌─id─┬─code─┬─title─┬──────────updatetime─┐
│  11001 │ aaa   │ 2021-02-23 15:18:18 │
│  21002 │ bbb   │ 2021-02-23 15:18:23 │
│  31003 │ ccc   │ 2021-02-23 15:18:29 │
│  41004 │ ddd   │ 2021-02-23 15:18:34 │
└────┴──────┴───────┴─────────────────────┘


--尝试更新mysql表中数据,ClickHouse数据变化:_sign = 1 , _version ++
mysql> update scene set title='abc' where code=1001;
mysql> select * from scene;
+----+------+-------+---------------------+
| id | code | title | updatetime          |
+----+------+-------+---------------------+
|  1 | 1001 | abc   | 2021-02-23 15:18:18 |
|  2 | 1002 | bbb   | 2021-02-23 15:18:23 |
|  3 | 1003 | ccc   | 2021-02-23 15:18:29 |
|  4 | 1004 | ddd   | 2021-02-23 15:18:34 |
+----+------+-------+---------------------+

SELECT * FROM scene
┌─id─┬─code─┬─title─┬──────────updatetime─┐
│  11001 │ abc   │ 2021-02-23 15:18:18|  2 | 1002 | bbb   | 2021-02-23 15:18:23 |31003 │ ccc   │ 2021-02-23 15:18:29 │
│  41004 │ ddd   │ 2021-02-23 15:18:34 │
└────┴──────┴───────┴─────────────────────┘

SELECT *,_version,_sign FROM scene
┌─id─┬─code─┬─title─┬──────────updatetime─┬─_version─┬─_sign─┐
│  11001 │ aaa   │ 2021-02-23 15:18:1811 │
│  21002 │ bbb   │ 2021-02-23 15:18:2311 │
│  31003 │ ccc   │ 2021-02-23 15:18:2911 │
│  41004 │ ddd   │ 2021-02-23 15:18:3411 │
└────┴──────┴───────┴─────────────────────┴──────────┴───────┘
┌─id─┬─code─┬─title─┬──────────updatetime─┬─_version─┬─_sign─┐
│  11001 │ abc   │ 2021-02-23 15:18:1821 │
└────┴──────┴───────┴─────────────────────┴──────────┴───────┘


--尝试删除mysql表中数据,ClickHouse数据变化: _sign = -1 , _version ++
mysql> delete from scene where code=1002;
mysql> select * from scene;
+----+------+-------+---------------------+
| id | code | title | updatetime          |
+----+------+-------+---------------------+
|  1 | 1001 | abc   | 2021-02-23 15:18:18 |
|  3 | 1003 | ccc   | 2021-02-23 15:18:29 |
|  4 | 1004 | ddd   | 2021-02-23 15:18:34 |
+----+------+-------+---------------------+

SELECT * FROM scene
┌─id─┬─code─┬─title─┬──────────updatetime─┐
│  11001 │ abc   │ 2021-02-23 15:18:18 │
│  31003 │ ccc   │ 2021-02-23 15:18:29 │
│  41004 │ ddd   │ 2021-02-23 15:18:34 │
└────┴──────┴───────┴─────────────────────┘

SELECT  *,_version,_sign FROM scene
┌─id─┬─code─┬─title─┬──────────updatetime─┬─_version─┬─_sign─┐
│  11001 │ aaa   │ 2021-02-23 15:18:1811 │
│  21002 │ bbb   │ 2021-02-23 15:18:2311 │
│  31003 │ ccc   │ 2021-02-23 15:18:2911 │
│  41004 │ ddd   │ 2021-02-23 15:18:3411 │
└────┴──────┴───────┴─────────────────────┴──────────┴───────┘
┌─id─┬─code─┬─title─┬──────────updatetime─┬─_version─┬─_sign─┐
│  11001 │ abc   │ 2021-02-23 15:18:1821 │
└────┴──────┴───────┴─────────────────────┴──────────┴───────┘
┌─id─┬─code─┬─title─┬──────────updatetime─┬─_version─┬─_sign─┐
│  21002 │ bbb   │ 2021-02-23 15:18:233-1 │
└────┴──────┴───────┴─────────────────────┴──────────┴───────┘

-----------------------------------------------------------------------
ClickHouse 支持更新和删除,但是性能之差;MySQL修改、删除之后ClickHouse怎么做的?

SELECT * FROM scene;
等同于
select * from scene final where _sign = 1;

修改的数据用final去重;
删除的数据用_sign = 1 过滤;
-----------------------------------------------------------------------

--尝试追加mysql表中数据,ClickHouse数据变化:_sign = 1 , _version ++
mysql> INSERT INTO scene(code, title, updatetime) VALUES(1005,'eee',NOW());
mysql> INSERT INTO scene(code, title, updatetime) VALUES(1006,'fff',NOW());
mysql> INSERT INTO scene(code, title, updatetime) VALUES(1007,'ggg',NOW());
mysql> INSERT INTO scene(code, title, updatetime) VALUES(1008,'hhh',NOW());
mysql> select * from scene;
+----+------+-------+---------------------+
| id | code | title | updatetime          |
+----+------+-------+---------------------+
|  1 | 1001 | abc   | 2021-02-23 15:18:18 |
|  3 | 1003 | ccc   | 2021-02-23 15:18:29 |
|  4 | 1004 | ddd   | 2021-02-23 15:18:34 |
|  5 | 1005 | eee   | 2021-02-23 16:05:23 |
|  6 | 1006 | fff   | 2021-02-23 16:06:34 |
|  7 | 1007 | ggg   | 2021-02-23 16:06:34 |
|  8 | 1008 | hhh   | 2021-02-23 16:06:35 |
+----+------+-------+---------------------+

select * from scene;
┌─id─┬─code─┬─title─┬──────────updatetime─┐
│  11001 │ abc   │ 2021-02-23 15:18:18 │
│  31003 │ ccc   │ 2021-02-23 15:18:29 │
│  41004 │ ddd   │ 2021-02-23 15:18:34 │
│  51005 │ eee   │ 2021-02-23 16:05:23 │
│  61006 │ fff   │ 2021-02-23 16:06:34 │
│  71007 │ ggg   │ 2021-02-23 16:06:34 │
│  81008 │ hhh   │ 2021-02-23 16:06:35 │
└────┴──────┴───────┴─────────────────────┘
select *, _version,_sign  from scene;
┌─id─┬─code─┬─title─┬──────────updatetime─┬─_version─┬─_sign─┐
│  11001 │ abc   │ 2021-02-23 15:18:1821 │
│  21002 │ bbb   │ 2021-02-23 15:18:233-1 │
│  31003 │ ccc   │ 2021-02-23 15:18:2911 │
│  41004 │ ddd   │ 2021-02-23 15:18:3411 │
│  51005 │ eee   │ 2021-02-23 16:05:2341 │
│  61006 │ fff   │ 2021-02-23 16:06:3451 │
│  71007 │ ggg   │ 2021-02-23 16:06:3461 │
│  81008 │ hhh   │ 2021-02-23 16:06:3571 │
└────┴──────┴───────┴─────────────────────┴──────────┴───────┘


--在MySQL中执行删除表,ClickHouse也会删除表:
drop table scene

# 此时在clickhouse处会同步删除对应表,如果查询会报错
DB::Exception: Table scene_mms.scene doesn't exist.. 

--在mysql客户端新增一张表,clickhouse处也可以实时生成对应的数据表
--在mysql客户端添加列与删除列,clickhouse处也可以实时生成对应的列

​ MaterializeMySQL database engine 不支持的情况:

​ 1.MySQL中修改表名,ClickHouse不会同步,且查询报错;

​ 2.修改列名称也是不支持的,如果该这种情况,需要删除通道重建;

1.4.5 WAL预写日志

​ 介绍 WAL 之前,先重温一下 MergeTree 最基本的合并过程。

MergeTree的高频写问题?

CREATE TABLE test
(
    id UInt8,
    name String,
    age UInt8,
    shijian Date)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(shijian)
ORDER BY id;

insert into test values(1001,'张三','18',now());
insert into test values(1002,'李四','28',now());
insert into test values(1003,'王五','38',now());

​ 此时,MergeTree 会生成 3 个分区目录:

在这里插入图片描述

​ 手动合并之后的分区目录

在这里插入图片描述

​ 对于 ClickHouse MergeTree 引擎,在数据的写入过程中,数据总会以数据片段的形式被写入磁盘,且数据片段不可修改。每一批次的写入(每执行一次INSERT)MergeTree都会按照分区规则在磁盘上生成一个全新的分区目录(part),为了避免片段过多,在未来的某一时刻,属于相同分区的数据片段会被合并成一个全新的分区目录,这种数据分区往复合并的特点正是合并树的名称由来。其中如果有多个客户端,每个客户端写入的数据量较少、次数较频繁的情况下,就会引发以下错误提示。

Too many parts (N). Merges areprocessing significantly slower than inserts.

​ WAL预写日志解决了这个问题,提高写入性能,在 ClickHouse 的新版本中,MergeTree 多了这么几个参数:

	M(SettingUInt64, min_bytes_for_wide_part, 0, xxxxxxxx, 0) \
	M(SettingUInt64, min_rows_for_wide_part, 0, xxxxxxxxx, 0) \
	M(SettingUInt64, min_bytes_for_compact_part, 0, xxxxx, 0) \
	M(SettingUInt64, min_rows_for_compact_part, 0, xxxxxx, 0) \
	M(SettingBool, in_memory_parts_enable_wal, true, xxxx, 0) \
	M(SettingUInt64, write_ahead_log_max_bytes, 1024 * 1024 * 1024, xxxx, 0) \

​ 其中 in_memory_parts_enable_wal 默认为 true,这说明预写日志默认就是开启状态的。

CREATE TABLE default.test1
(
    id UInt8,
    name String,
    age UInt8,
    shijian Date
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(shijian)
ORDER BY id
SETTINGS min_rows_for_compact_part = 2, index_granularity = 8192;

​ min_rows_for_compact_part = 2 表示数据首先会被写到内存和 WAL中,当触发 Merge 的时候,如果数据大于 2 行,就直接把合并后的分区写到磁盘。

insert into test1 values(1001,'张三','18',now());
insert into test1 values(1002,'李四','28',now());
insert into test1 values(1003,'王五','38',now());

​ 写入之后还没有触发 Merge 动作,磁盘目录情况:

在这里插入图片描述

clickhouse-client -m

	optimize table test1;

在这里插入图片描述

​ 在此之前,MergeTree 只有一种 wide 布局,也就是每个列字段都拥有一组独立的文件,例如下图所示:

在这里插入图片描述

​ 由于现在添加了wal新特性,致使MergeTree的分区布局也得到了扩展,插入过程中,数据首先进入内存,满足阈值之后,会将内存中的数据刷到磁盘。在这里插入图片描述

CREATE TABLE default.test2
(
    id UInt8,
    name String,
    age UInt8,
    shijian Date
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(shijian)
ORDER BY id
SETTINGS min_rows_for_compact_part = 2, min_rows_for_wide_part = 10, index_granularity = 8192;
insert into test2 values(1001,'张三','18',now());
insert into test2 values(1002,'李四','28',now());
insert into test2 values(1003,'王五','38',now());
optimize table test2;

在这里插入图片描述

​ 所有的的数据写到了同一个 data.bin 文件中,所有列的标记文件也都写到了同一个.mark文件。当列字段很多,数据又很少的时候,可以考虑使用这种布局模式的分区。

二、MergeTree原理解析

2.1 MergeTree创建方式

​ MergeTree在写入数据时,数据总会以数据片段的形式写入磁盘,且数据片段不可修改。为了避免片段较多,clickhouse通过后台进程,定期合并这些数据片段,属于相同分区的数据片段会被合成一个新的片段。

创建语法:

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 			
			分区键:表示表数据会以何种标准进行分区;默认all分区。
    		分区方式:单列、元组形式使用多列或者使用列表达式。
    		合理使用数据分区,可以有效减少查询时数据文件的扫描范围。

ORDER BY	
			排序键:用于指定在一个数据片段内,数据以何种标准排序;默认情况和主键相同。
			排序方式:单列、元组形式使用多列。ORDER BY (counterID,EventDate)为例,在单个数据片段中,数据首先以counterID排序,相同的counterID,在按照EventDate排序。

PAIMARY KEY
			主键:会按照主键字段生成一级索引,用于加速表查询;默认情况下,主键个ORDER BY相同。

SAMPLE BY
			抽样表达式:用于声明数据以何种标砖进行采样。

SETTINGS:index_granularity 
			index_granularity对于MergeTree表示索引粒度,默认值8192.(每隔8192行数据生成一条索引)

SETTINGS:index_granularity_bytes
			19.11前:clickhouse只支持固定大小的索引间隔,由index_granularity控制,默认8192。
			在新版本:自适应间隔大小。根据每一批次写入数据体量大小,动态划分间隔大小。数据体量由index_granularity_bytes控制,默认10M(10*1024*1024),设置为0不启动自适应功能。

SETTINGS:enable_mixed_granularity_parts
			是否开启自适应索引间隔,默认开启

SETTINGS:merge_with_ttl_timeout			数据TTL功能
			
SETTINGS:storage_policy					多路径存储策略

2.2 MergeTree存储结构

​ MergeTree表引擎中数据拥有物理存储,数据会按分区目录的形式保存到磁盘中。

tree /var/lib/clickhouse/data/default/test

├── 202102_1_3_1				分区目录
│   ├── age.bin					数据文件。使用压缩格式存储(默认LZ4),存储某一列数据
│   ├── age.mrk2				标记文件。保存.bin文件中数据的偏移量,用于建立primary.idx和[column].bin文件之间的映射
│   ├── id.bin
│   ├── id.mrk2
│   ├── name.bin
│   ├── name.mrk2
│   ├── shijian.bin
│   └── shijian.mrk2
│   ├── checksums.txt			校验文件,保存余下各类文件的size大小及size的哈希值,校验数据完整性
│   ├── columns.txt				列信息文件。
│   ├── count.txt				计数文件。明文记录当前数据分区目录下的数据总行数
│   ├── minmax_shijian.idx		分区键的索引文件,记录当前分区下分区字段对应原始数据的最小和最大值
│   ├── partition.dat			分区键,保存分区表达式最终生成的值
│   ├── primary.idx				一级索引文件,二进制格式存储。一张MergeTree()表只能声明一次一级索引(primary key或者order  by)
├── detached
└── format_version.txt

2.3 数据分区

2.3.1 数据分区目录命名规则

举例说明:

202005_1_3_1				此目录直观来看,采用时间年月作为分区ID,分三批次插入到同一分区,并且三次插入完成之后的某个时刻进行了一次数据合并。

202005		PartitionID		分区目录ID

1			MinBlockNum		最小数据块编号 (默认和MaxBlockNum从1开始)

3			MaxBlockNum		最大数据块编号 (发生合并时取合并时的最大数据块编号)

1			Level			合并的层级,某个分区被合并过的次数或者这个分区的年龄。(每次合并自增+1)

2.3.2 数据分区合并过程

​ MergeTree分区目录创建:数据写入的过程中创建;创建之后在写入数据或者合并,目录也会变化。也就是说:一张表没有任何数据,那不会有任何分区目录存在。

MergeTree分区目录合并过程:

​ 伴随每次写入数据(insert),MergeTree都会生成一批新的分区目录(即使不同批次写入的数据属于相同分区,也会生成不同的分区目录)。在写入后的某个时刻,ClickHouse会通过后台任务再将属于相同分区的多个目录合并成一个新目录。已经存在的旧分区并不会立即删除,而是在之后的某个时刻通过后台任务删除(默认8分钟)。

新目录名称的合并规则:

​ MinBlockNum:取同一分区内所有目录中最小的MinBlockNum值。

​ MaxBlockNum:取同一分区内所有目录中最大的MaxBlockNum值。

​ Level:取同一分区内最大Level值并加1。

举例说明:

create table test(id UInt32,name String,age UInt8,shijian DateTime) engine = MergeTree() PARTITION BY toYYYYMM(shijian) ORDER BY id

insert into test values (1,'张三',18,'2020-12-08')					t1时刻

insert into test values (2,'李四',19,'2020-12-08')					t2时刻
	
insert into test values (3,'王五',22,'2021-01-03')					t3时刻

insert into test values (2,'李四',19,now())							t4时刻

SELECT now()

┌───────────────now()─┐
│ 2020-12-08 11:36:42 │
└─────────────────────┘


按照上述规则未合并时的目录:
PARTITIONID 		202012
MinBlockNum			1
MaxBlockNmu			1
		对于新建分区,它们的值一样(来源表内全局自增的BlockNum),初始值为1,每次新建目录累计加1。	
level				0				
    

    	202012_1_1_0												t1时刻的目录
    
    	202012_2_2_0												t2时刻的目录
    
    	202101_3_3_0												t3时刻的目录
    
    	202012_4_4_0												t4时刻的目录

按照上述规则合并时的目录:

假设在t2~t3时刻之间发生了合并,那么此时只有一个目录:202012_1_2_1

假设在t3~t4时刻之间发生了合并,那么此时肯有两个目录:202012_1_2_1,202101_3_3_0

假设在t4时刻之后发生了合并,那么此时也肯定有两个目录:202012_1_4_2,202101_3_3_0

注意:
	在创建完成之后的某个时刻进行合并,必须是相同分区才会合并,生成新的分区,同时将旧分区目录状态设置为非激活,然后在默认8分钟之后,删除非激活状态的分区目录。

​ 视频演示:

2.4 一级索引

MergerTree 指定主键方式:

​ 1.PRIMARY KEY MergerTree会根据index_granularity间隔(默认8192行)为数据表生成一级索引保存在primary.idx文件中,根据主键排序

​ 2.ORDER BY .bin 文件按完全相同PRIMARY KEY的规则排序

2.4.1 索引粒度

​ 数据以index_granularity的粒度(默认固定索引粒度8192)被标记成多个小空间,其中每个空间最多8192行数据。这段空间的具体区间就是MarkRange,并且通过start和end表示具体的范围。

在这里插入图片描述

2.4.2 稀疏索引

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

​ 稠密索引:每一行索引标记对应一行具体的数据记录

​ 稀疏索引:每一行索引标记对应一段具体的数据记录

​ 两者比较:

​ a 稀疏索引占用的索引存储空间比较小,但是查找时间较长; 数据量大场景,利用primary.idx内的索引数据常驻内存

​ b 稠密索引查找时间较短,索引存储空间较大。 数据量小场景

在这里插入图片描述

2.4.3 索引数据生成规则

​ 由于是稀疏索引,所以MergeTree需要间隔index_granularity行数据才会生成一条索引记录,其索引值会依据声明的主键字段获取。图所示效果使用年月分区(PARTITION BY toYYYYMM(EventDate)),所以2014年3月份的数据最终会被划分到同一个分区目录内。如果使用CounterID作为主键(ORDER BY CounterID),则每间隔8192行数据就会取一次CounterID的值作为索引值,索引数据最终会被写入primary.idx文件进行保存。

preview

​ 例如第0行CounterID取值57,第8192行CounterID取值1635,而第16384行CounterID取值3266,最终索引数据将会是5716353266。

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

preview

2.4.4 索引查询过程

​ 索引查询其实就是两个数值区间的交集判断:

​ 1.一个区间是由基于主键的查询条件转换而来的条件区间;

​ 2.一个区间是MarkRange对应的数值区间。MarkRange与索引编号对应,使用start和end表示具体的范围。通过start及end对应的索引编号取值,即能得到它所对应的数值区间。

​ 索引查询过程:

​ 这里假设索引粒度(index_grandularity)为3,即每3条数据生成一条索引记录。

img

索引范围:前后相邻的两个索引的值构成。

根据主键的查询条件,确定索引范围。
(1)id in (‘A02’, ‘A08’), 转化为索引范围区间[A01, A04] 和 [A07, A10], 对应索引标记0和2中查询数据。。
(2)id = ‘A04’, 在索引范围[A01, A04]和[A04, A07]区间查询数据,对应索引标记0和1。
(3)id > ‘A11’, 在索引范围[A10, +inf]区间查询数据,对应所有值大于3的索引标记。
(4)id like ‘A0%’, 在索引范围[A01, A04]、[A04,A07]和[A07, A10]区间查询数据,对应索 引标记为0、1和2。

2.5 数据存储

​ MergeTree中,数据按列存储。具体到每个列字段,每个列字段都拥有一个与之对应的.bin数据文件。

img

​ MergeTree在数据具体写入过程中,会按照索引粒度,按批次获取数据并进行处理。如下图:

preview

​ 多对一 1.单个批次数据SIZE < 64KB:则继续获取下一批数据,直至累积到SIZE>=64KB时,生成下一个压缩数据块;

​ 一对一 2.单个批次数据64KB<=SIZE<=1MB:直接生成下一个压缩数据块

​ 一对多 3.单个批次数据SIZE>1MB:首先按照1MB大小截断并生成下一个数据块。剩余数据继续按照大小判断执行。

总结:一个.bin文件由1至多个压缩数据块组成,每个压缩块大小在64KB~1MB之间。多个压缩块之间,按顺序写入首尾相接。

2.6 数据标记

​ 主要衔接一级索引和数据文件之间建立关联。数据标记和索引区间都有index_granularity粒度来间隔,有助于通过索引区间的下标编号找到对应的数据标记。数据标记文件和.bin文件也一一对应,每个列文件都对应一个.mrk的数据标记文件。

在这里插入图片描述

2.7 数据标记的工作方式

​ 在MergeTree读取数据时,必须通过标记数据的位置信息找到所需要的数据。查找过程大致分为读取压缩数据块和读取数据两个步骤。

举例说明:
	javaenable字段数据类型UInt8,每行数值占用1字节,使用默认的index_granularity粒度为8192,所以一个索引片段的数据大小恰好是8192B。
	压缩块大小为64KB~1MB,根据压缩数据块生成规则,size <64KB,继续写入写入下一批数据,直至>=64KB,生成下一个压缩数据块。
	在此例子中,每8行标记数据对应1个压缩数据块(8192B=8KB,64KB/8KB=8),如图,标记文件中的8行数据的压缩文件偏移量是一致的,因为这8行标记都指向了同一个压缩数据块。但是这8行标记文件解压缩数据块中的偏移量,从0 开始,按照8192B累加,当达到65536(64KB),则置0,生成下一个数据块。

在这里插入图片描述

2.8 对于分区、索引、标记和压缩数据的协同总结

2.8.1 写入过程

​ 1.生成分区目录(伴随每一次insert操作,生成一个新的分区目录);

​ 2.在后续的某个时刻,合并相同分区的目录;

​ 3.按照index_granularity索引粒度,分别生成primary.idx索引文件、二级索引、每一列字段的.mrk数据标记和.bin压缩数据文件。

​ 4.生成的索引和标记区间一一对应,标记区间与压缩块区间由于压缩数据块大小,生成一对一,一对多,多对一的三种关系。

preview

2.8.2 查询过程

​ 1.minmax.idx (分区索引)

​ 2.primary.idx (一级索引)

​ 3.skp_idx.idx (二级索引)

​ 4…mrk (标记文件)

​ 5…bin (数据压缩文件)

preview

​ 查询语句中没有where条件,1,2,3步骤不走;先扫描所有分区目录,及目录内索引段的最大区间,MergeTree借住数据标记,多线程的形式读取多个压缩块。

2.8.3 数据标记与压缩数据块的对应关系

压缩块的划分:

​ 索引粒度(index_granularity)的大小,及压缩块的三种规则决定数据块的大小在64KB~1MB。

​ 而一个索引间隔的数据,产生一行数据标记。

多对一:多个数据标记对应一个数据压缩块。一个index_granularity的未压缩SIZE<64KB

​ 假设JavaEnable字段的数据类型为UInt8,所以每行数据占用1字节。数据表的index_granularity粒度为8192,所以每一个索引片段大小正是8192B。按照数据压缩块规则,8192B<64KB,当等于64KB压缩为下一个数据块。(64KB/8192B=8,也就是8行数据为一个数据压缩块)

preview

一对一:一个数据标记对应一个数据压缩块。一个index_granularity的未压缩64KB<= SIZE <= 1MB

​ 假设URLHash字段数据类型UInt64,大小为8B,则一个默认间隔的数据大小为8*8192=65536B,正好是64KB。此时的标记数据和压缩数据是一对一的关系。

一对多:一个数据标记对应多个数据压缩块。一个index_granularity的未压缩SIZE> 1MB。

详情了解更多欢迎关注公众号
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45320660/article/details/114451454