理解
.bin
文件中只会保存当前分区片段内的这一部分数据列式存储的优势
存储方式
压缩数据组成
一个压缩数据块由头信息和压缩数据两部分组成,bin压缩文件是由多个压缩数据块组成的,而每个压缩数据块的头信息则是基于CompressionMethod_CompressedSize_UncompressedSize公式生成的
压缩数据块的体积
数据写入过程
数据压缩的优势
定义
理解数据标记
.bin
文件和数据标记文件是一一对应的
[Column].bin
文件都有一个 [Column].mrk
数据标记文件与之对应,用于记录数据在 .bin
文件中的偏移量信息对于上图的解释:
每一行标记数据都标记了一个片段的数据(默认 8192 行)在 .bin 压缩文件中的读取位置信息,因为 Age 占 1 字节,所以每次读取 8912 行相当于每次读取 8192 个字节,因此 “未压缩数据的起始偏移量” 就是 0、8192、16384、24576、…。但是需要注意图中的 57344,它表示第 8 批未压缩数据的起始偏移量,因为此时已经达到了 64KB,所以会生成一个压缩数据块,于是接下来读取第 9 批未压缩数据的时候就会对应新的压缩数据块,因此起始偏移量会重置为 0,而不是 65536。
我们这里是 Age 字段为例,至于其它列也是同理,只不过由于每一行字符串的长度不同,所以我们很难计算每次读 8192 行的话会读多少个字节,但它们的原理是很明显都是一样的。
然后是 “压缩数据块的起始偏移量”,因为读了 8 批才生成了第一个压缩数据块,因此前 8 行都是 0。然后由于第一个压缩数据块的大小是 276,因此第 9 行、即索引为 8 的位置,存储的值就是 276,表示第二个压缩数据块的起始偏移量
MergeTree 在读取数据时,必须通过标记文件中的偏移量才能找到所需要的数据,因此整个查找过程可以分为读取压缩数据块和读取数据两个步骤
.bin
文件,而是可以根据需要只加载特定的压缩数据块,而这向特性则要借助 .mrk
文件中所保存的偏移量信息由于压缩数据块的划分,与一个间隔(index_granularity)内的数据大小相关,每个压缩数据块的体积都被严格控制在64KB~1MB。而一个间隔(index_granularity)的数据,又只会产生一行数据标记。那么根据一个间隔内数据的实际字节大小,数据标记和压缩数据块之间会产生三种不同的对应关系。
分区、索引、标记和压缩数据,就类似于 MergeTree 的一套组合拳,使用恰当的话威力无穷。那么在依次介绍了各自的特点之后,现在将它们聚在一起总结一下
生成分区目录,并在后续进行合并相同分区目
生成一级索引、压缩数据文件、数据标记文件
MergeTree表在写入数据时的分区目录、索引、标记和压缩数据的生成图解
从分区目录 202006_1_34_3 能够得知,该分区数据总共分 34 批写入,期间发生过 3 次合并。
在数据写入的过程中,依据 index_granularity 的粒度,依次为每个区间的数据生成索引、标记和压缩数据块。其中索引和标记区间是对齐的,而标记与压缩块则是根据区间大小的不同,会生成多对一、一对一、一对多的关系
本质
过程简述
查询数据图解
重点掌握
1、掌握Clickhouse的MergeTree的数据读写流程
2、掌握Clickhouse中MergeTree的6个变种表引擎的使用场景
3、掌握Clickhouse的数据查询的操作语法理解内容
1、Clickhouse的MergeTree之列式存储与数据压缩 -24
2、Clickhouse的MergeTree之数据标记的生成与使用方式 -25~26
3、Clickhouse的MergeTreeFamily之多路径存储 -30
4、Clickhouse的外部存储引擎 -36~39
5、Clickhouse的内存类型引擎 -40~42
6、Clickhouse的日志类型引擎 -43~45
19.15版本之前
19.15版本开始
存储策略
配置方式
<storage_configuration>
<disks>
<disk_name_a>
<path>/chbase/datapath><!—磁盘路径 -->
<keep_free_space_bytes>1073741824keep_free_space_bytes>
disk_name_a>
<disk_name_b>
<path>… path>
<keep_free_space_bytes>...keep_free_space_bytes>
disk_name_b>
disks>
<policies>
<default_jbod>
<volumes>
<jbod> <!—自定义名称 磁盘组 -->
<disk>disk_name_adisk>
<disk>disk_name_bdisk>
jbod>
volumes>
default_jbod>
policies>
storage_configuration>
产生背景
以分区为单位删除重复数据
操作方式
ENGINE = ReplacingMergeTree(ver)
--排序键ORDER BY所声明的表达式是后续作为判断数据是否重复的依据。
--如果没有设置ver版本号,则保留同一组重复数据中的最后一行。
CREATE TABLE replace_table(
id String,
code String,
create_time DateTime
)ENGINE = ReplacingMergeTree()
PARTITION BY toYYYYMM(create_time)
ORDER BY (id,code)
PRIMARY KEY id
-- 2021-05_1_1_0
insert into replace_table select number%10 ,'code', '2021-05-01' from
numbers(20);
-- 2021-05_2_2_0
insert into replace_table select number%10 ,'code', '2021-05-03' from
numbers(20);
-- 2021-05_1_2_1
optimize table replace_table final;
-- 2021-06_3_3_0
insert into replace_table select number%10 ,'code', '2021-06-01' from
numbers(20);
-- 2021-07_4_4_0
insert into replace_table select number%10 ,'code', '2021-07-01' from
numbers(20);
-- 2021-07_5_5_0
insert into replace_table select number%10 ,'code', '2021-07-03' from
numbers(20);
-- 2021-05_1_2_2
-- 2021-06_3_3_1
optimize table replace_table final;
select * from replace_table;
--基于id字段去重,并且使用create_time字段作为版本号
--删除重复数据的时候,会保留同一组数据内create_time时间最大的那一行
CREATE TABLE replace_table_v(
id String,
code String,
create_time DateTime
)ENGINE = ReplacingMergeTree(create_time)
PARTITION BY toYYYYMM(create_time)
ORDER BY id
产生背景
原始解决方案 :通过GROUP BY聚合查询,并利用SUM聚合函数汇总结果
SummingMergeTree就是为了应对这类查询场景而生的。
操作方式
CREATE TABLE summing_table(
id String,
city String,
v1 UInt32,
v2 Float64,
create_time DateTime
)ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(create_time)
ORDER BY (id, city)
PRIMARY KEY id
insert into summing_table select '1','上海',1,1.0,'2021-05-01' from
numbers(100);
insert into summing_table select '2','广州',1,1.0,'2021-05-01' from
numbers(100);
insert into summing_table select '3','武汉',1,1.0,'2021-05-01' from
numbers(100);
insert into summing_table select '1','上海',1,1.0,'2021-05-01' from
numbers(100);
optimize table summing_table final;
select * from summing_table;
--用ORBER BY排序键作为聚合数据的条件Key
--只有在合并分区的时候才会触发汇总的逻辑
--以数据分区为单位来聚合数据。当分区合并时,同一数据分区内聚合Key相同的数据会被合并汇
总,而不同分区之间的数据则不会被汇总。
--如果在定义引擎时指定了columns汇总列,则SUM汇总这些列字段;如果未指定,则聚合所有非
主键的数值类型字段。
--在进行数据汇总时,因为分区内的数据已经基于ORBER BY排序,所以能够找到相邻且拥有相同
聚合Key的数据。
--在汇总数据时,同一分区内,相同聚合Key的多行数据会合并成一行。其中,汇总字段会进行
SUM计算;对于那些非汇总字段,则会使用第一行数据的取值。
数据立方体
操作方式
--GROUP BY id,city
--UNIQ(code), SUM(value)
CREATE TABLE agg_table(
id String,
city String,
code AggregateFunction(uniq,String),
value AggregateFunction(sum,UInt32),
create_time DateTime
)ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(create_time)
ORDER BY (id,city)
PRIMARY KEY id
INSERT INTO TABLE agg_table SELECT
'A000','wuhan',uniqState('code1'),sumState(toUInt32(100)),'2019-08-10 17:00:00'
AggregatingMergeTree更为常见的应用方式是结合物化视图使用(结合kafka),将它作为物化视图的表引擎
CREATE TABLE agg_table_basic(
id String,
city String,
code String,
value UInt32
)ENGINE = MergeTree()
PARTITION BY city
ORDER BY (id,city)
CREATE MATERIALIZED VIEW agg_view
ENGINE = AggregatingMergeTree()
PARTITION BY city
ORDER BY (id,city)
AS SELECT
id,
city,
uniqState(code) AS code,
sumState(value) AS value
FROM agg_table_basic
GROUP BY id, city
INSERT INTO TABLE agg_table_basic VALUES
('A000','wuhan','code1',100),('A000','wuhan','code2',200),
('A000','zhuhai', 'code1',200)
SELECT id, sumMerge(value), uniqMerge(code) FROM agg_view GROUP BY id,city
产生背景
数据的“删除”
操作方式
ENGINE = CollapsingMergeTree(sign)
--sign用于指定一个Int8类型的标志位字段。
CREATE TABLE collpase_table(
id String,
code Int32,
create_time DateTime,
sign Int8
)ENGINE = CollapsingMergeTree(sign)
PARTITION BY toYYYYMM(create_time)
ORDER BY id
--修改前的源数据, 它需要被修改
INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1)
--镜像数据, ORDER BY字段与源数据相同(其他字段可以不同),sign取反为-1,它会和源数据折叠
INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',-1)
--修改后的数据 ,sign为1
INSERT INTO TABLE collpase_table VALUES('A000',120,'2019-02-20 00:00:00',1)
--修改前的源数据, 它需要被删除
INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1)
--镜像数据, ORDER BY字段与源数据相同, sign取反为-1, 它会和源数据折叠
INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',-1)
折叠规则
特点
折叠数据并不是实时触发的,和所有其他的MergeTree变种表引擎一样,这项特性也只有在分区合并的时候才会体现。
所以在分区合并之前,用户还是会看到旧的数据。解决这个问题的方式有两种
SELECT id,SUM(code),COUNT(code),AVG(code),uniq(code)
FROM collpase_table
GROUP BY id
SELECT id,SUM(code * sign),COUNT(code * sign),AVG(code *
sign),uniq(code * sign)
FROM collpase_table
GROUP BY id
HAVING SUM(sign) > 0
只有相同分区内的数据才有可能被折叠
CollapsingMergeTree对于写入数据的顺序有着严格要求。
理解
操作方式
ENGINE = VersionedCollapsingMergeTree(sign,ver)
CREATE TABLE ver_collpase_table(
id String,
code Int32,
create_time DateTime,
sign Int8,
ver UInt8
)ENGINE = VersionedCollapsingMergeTree(sign,ver)
PARTITION BY toYYYYMM(create_time)
ORDER BY id
--删除
INSERT INTO TABLE ver_collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1,1)
INSERT INTO TABLE ver_collpase_table VALUES('A000',102,'2019-02-2000:00:00',1,1)
--修改
INSERT INTO TABLE ver_collpase_table VALUES('A001',101,'2019-02-20 00:00:00',-1,1)
INSERT INTO TABLE ver_collpase_table VALUES('A001',102,'2019-02-20 00:00:00',1,1)
INSERT INTO TABLE ver_collpase_table VALUES('A001',103,'2019-02-20 00:00:00',1,2)
MergeTree表引擎向下派生出6个变种表引擎
--在HDFS上创建用于存放文件的目录
hadoop fs -mkdir /clickhouse
--在HDFS上给ClickHouse用户授权
hadoop fs -chown -R clickhouse:clickhouse /clickhouse
--hdfs_uri表示HDFS的文件存储路径,format表示文件格式
ENGINE = HDFS(hdfs_uri,format)
CREATE TABLE hdfs_table1(
id UInt32,
code String,
name String
)ENGINE = HDFS('hdfs://node01:8020/clickhouse/hdfs_table1','CSV');
INSERT INTO hdfs_table1 SELECT
number,concat('code',toString(number)),concat('n',toString(number)) FROM numbers(5)
--其他方式,只能读取
ENGINE =HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table2/*','CSV')
ENGINE =HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table2/organization_{1..3}.csv','CSV')
ENGINE =HDFS('hdfs://hdp1.nauu.com:8020/clickhouse/hdfs_table2/organization_?.cs v','CSV')
MySQL表引擎可以与MySQL数据库中的数据表建立映射,并通过SQL向其发起远程查询,包括SELECT和INSERT
ENGINE = MySQL('host:port', 'database', 'table', 'user', 'password'[,replace_query, 'on_duplicate_clause'])
操作方式
CREATE TABLE mysql_dept(
deptno UInt32,
dname String,
loc String
)ENGINE = MySQL('192.168.88.101:3306', 'scott', 'dept', 'root','123456');
SELECT * FROM mysql_dept
INSERT INTO TABLE mysql_dept VALUES (50,'干饭部','207')
--目前MySQL表引擎不支持任何UPDATE和DELETE操作
这里可以在navicat中查看插入的数据已经更新到了MySQL中
目前ClickHouse还不支持恰好一次(Exactly once)的语义,因为这需要应用端与Kafka深度配合才能实现
常见参数
ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'host:port,... ',
kafka_topic_list = 'topic1,topic2,...',
kafka_group_name = 'group_name',
kafka_format = 'data_format'[,]
[kafka_row_delimiter = 'delimiter_symbol']
[kafka_schema = '']
[kafka_num_consumers = N]
[kafka_skip_broken_messages = N]
[kafka_commit_every_batch = N]
参数详解
操作方式
CREATE TABLE kafka_table(
id UInt32,
code String,
name String
) ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'node01:9092',
kafka_topic_list = 'topic_clickhouse',
kafka_group_name = 'clickhouse',
kafka_format = 'TabSeparated',
kafka_skip_broken_messages = 10;
--使用Java代码产生数据
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
YjxKafkaUtil.sendMsg("topic_clickhouse", i +"\tcode\tname");
Thread.sleep(1000);
}
}
出现的问题
解决方案
CREATE TABLE kafka_queue(
id UInt32,
code String,
name String
) ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'node01:9092',
kafka_topic_list = 'topic_clickhouse',
kafka_group_name = 'clickhouse',
kafka_format = 'TabSeparated',
kafka_skip_broken_messages = 10;
CREATE TABLE kafka_view (
id UInt32,
code String,
name String
) ENGINE = MergeTree()
ORDER BY id;
CREATE MATERIALIZED VIEW consumer TO kafka_view
AS SELECT id,code,name FROM kafka_queue;
理解
操作方式
--自动创建
CREATE TABLE file_table (
name String,
value Int32
) ENGINE = File("CSV")
INSERT INTO file_table VALUES ('one', 1), ('two', 2), ('three', 3)
--插入数据 自动创建
# /var/lib/clickhouse/data/yjxxt/file_table///data.CSV
--手动创建
//创建表目录
# mkdir /chbase/data/default/file_table1
//创建数据文件
# mv /chbase/data/default/file_table/data.CSV/chbase/data/default/file_table1
ATTACH TABLE file_table1(
name String,
value UInt32
)ENGINE = File(CSV)
INSERT INTO file_table1 VALUES ('four', 4), ('five', 5)
理解
特点
操作方式
CREATE TABLE memory_1 (
id UInt64
)ENGINE = Memory()
特点
Set表引擎的存储结构的两个组成部分
操作方式
CREATE TABLE set_1 (
id UInt8
)ENGINE = Set();
INSERT INTO TABLE set_1 SELECT number FROM numbers(10)
SELECT arrayJoin([1, 2, 3]) AS a WHERE a IN set_1
理解
ENGINE = Join(join_strictness, join_type, key1[, key2, ...])
操作方式
CREATE TABLE join_tb1(
id UInt8,
name String,
time Datetime
) ENGINE = Log
INSERT INTO TABLE join_tb1 VALUES (1,'ClickHouse','2019-05-01 12:00:00'),(2,'Spark', '2019-05-01 12:30:00')
CREATE TABLE id_join_tb1(
id UInt8,
price UInt32,
time Datetime
) ENGINE = Join(ANY, LEFT, id)
INSERT INTO TABLE id_join_tb1 VALUES (1,100,'2019-05-01 11:55:00'),(1,105,'2019-05-01 11:10:00')
在日常运转的过程中,数据查询也是ClickHouse的主要工作之一。ClickHouse完全使用SQL作为查询语言,能够以SELECT查询语句的形式从数据库中选取数据,这也是它具备流行潜质的重要原因。虽然ClickHouse拥有优秀的查询性能,但是我们也不能滥用查询,掌握ClickHouse所支持的各种查询子句,并选择合理的查询形式是很有必要的。使用不恰当的SQL语句进行查询不仅会带来低性能,还可能导致不可预知的系统错误
ClickHouse对于SQL语句的解析是大小写敏感的,这意味着SELECT a和SELECT A表示的语义是不相同的
目前支持的查询字句如下
[WITH expr |(subquery)]
SELECT [DISTINCT] expr
[FROM [db.]table | (subquery) | table_function] [FINAL] [SAMPLE expr]
[[LEFT] ARRAY JOIN]
[GLOBAL] [ALL|ANY|ASOF] [INNER | CROSS | [LEFT|RIGHT|FULL [OUTER]] ]
JOIN (subquery)|table ON|USING columns_list
[PREWHERE expr]
[WHERE expr]
[GROUP BY expr] [WITH ROLLUP|CUBE|TOTALS]
[HAVING expr]
[ORDER BY expr]
[LIMIT [n[,m]]
[UNION ALL]
[INTO OUTFILE filename]
[FORMAT format]
[LIMIT [offset] n BY columns]