ClickHouse引擎介绍

1、TinyLog

最简单的一种引擎,每一列保存为一个文件,里面的内容是压缩过的,不支持索引
这种引擎没有并发控制,所以,当你需要在读,又在写时,读会出错。并发写,内容都会坏掉。

应用场景 :

a. 基本上就是那种只写一次
b. 然后就是只读的场景。
c. 不适用于处理量大的数据,官方推荐,使用这种引擎的表最多 100 万行的数据

drop table if exists test.tinylog;
create table test.tinylog (a UInt16, b UInt16) ENGINE = TinyLog;
insert into test.tinylog(a,b) values (7,13);

此时/var/lib/clickhouse/data/test/tinylog保存数据的目录结构:

├── a.bin
├── b.bin
└── sizes.json

a.bin 和 b.bin 是压缩过的对应的列的数据, sizes.json 中记录了每个 *.bin 文件的大小

2、Log

这种引擎跟 TinyLog 基本一致
它的改进点,是加了一个 __marks.mrk 文件,里面记录了每个数据块的偏移
这样做的一个用处,就是可以准确地切分读的范围,从而使用并发读取成为可能
但是,它是不能支持并发写的,一个写操作会阻塞其它读写操作
Log 不支持索引,同时因为有一个 __marks.mrk 的冗余数据,所以在写入数据时,一旦出现问题,这个表就废了

**应用场景:

同 TinyLog 差不多,它适用的场景也是那种写一次之后,后面就是只读的场景,临时数据用它保存也可以

drop table if exists test.log;
create table test.log (a UInt16, b UInt16) ENGINE = Log;
insert into test.log(a,b) values (7,13);

此时/var/lib/clickhouse/data/test/log保存数据的目录结构:

├── __marks.mrk
├── a.bin
├── b.bin
└── sizes.json

3、Memory

内存引擎,数据以未压缩的原始形式直接保存在内存当中,服务器重启数据就会消失
可以并行读,读写互斥锁的时间也非常短
不支持索引,简单查询下有非常非常高的性能表现

应用场景:

a. 进行测试
b. 在需要非常高的性能,同时数据量又不太大(上限大概 1 亿行)的场景

4、Merge

一个工具引擎,本身不保存数据,只用于把指定库中的指定多个表链在一起。 这样,读取操作可以并发执行,同时也可以利用原表的索引,但是,此引擎不支持写操作指定引擎的同时,需要指定要链接的库及表,库名可以使用一个表达式,表名可以使用正则表达式指定

create table test.tinylog1 (id UInt16, name String) ENGINE=TinyLog;
create table test.tinylog2 (id UInt16, name String) ENGINE=TinyLog;
create table test.tinylog3 (id UInt16, name String) ENGINE=TinyLog;

insert into test.tinylog1(id, name) values (1, 'tinylog1');
insert into test.tinylog2(id, name) values (2, 'tinylog2');
insert into test.tinylog3(id, name) values (3, 'tinylog3');

use test;
create table test.merge (id UInt16, name String) ENGINE=Merge(currentDatabase(), '^tinylog[0-9]+');
select _table,* from test.merge order by id desc

5、Distributed

与 Merge 类似, Distributed 也是通过一个逻辑表,去访问各个物理表,设置引擎时的样子是:

Distributed(remote_group, database, table [, sharding_key])

其中:

remote_group /etc/clickhouse-server/config.xml中remote_servers参数
database 是各服务器中的库名
table 是表名
sharding_key 是一个寻址表达式,可以是一个列名,也可以是像 rand() 之类的函数调用,它与 remote_servers 中的 weight 共同作用,决定在 写 时往哪个 shard 写

配置文件中的 remote_servers

<remote_servers>
   <log>
       <shard>
           <weight>1</weight>
           <internal_replication>false</internal_replication>
           <replica>
               <host>172.17.0.3</host>
               <port>9000</port>
           </replica>
       </shard>
       <shard>
           <weight>2</weight>
           <internal_replication>false</internal_replication>
           <replica>
               <host>172.17.0.4</host>
               <port>9000</port>
           </replica>
       </shard>
   </log>
</remote_servers>
  • log是某个 shard 组的名字,就是上面的 remote_group 的值
  • shard 是固定标签
  • weight 是权重,前面说的 sharding_key 与这个有关。
    简单来说,上面的配置,理论上来看:
    第一个 shard “被选中”的概率是 1 / (1 + 2) ,第二个是 2 / (1 + 2) ,这很容易理解。但是, sharding_key 的工作情况,是按实际数字的“命中区间”算的,即第一个的区间是 [0, 1) 的周期,第二个区间是 [1, 1+2) 的周期。比如把 sharding_key 设置成 id ,当 id=0 或 id=3 时,一定是写入到第一个 shard 中,如果把 sharding_key 设置成 rand() ,那系统会对应地自己作一般化转换吧,这种时候就是一种概率场景了。
  • internal_replication是定义针对多个 replica 时的写入行为的。
    如果为 false ,则会往所有的 replica 中写入数据,但是并不保证数据写入的一致性,所以这种情况时间一长,各 replica 的数据很可能出现差异。如果为 true ,则只会往第一个可写的 replica 中写入数据(剩下的事“物理表”自己处理)。
  • replica 就是定义各个冗余副本的,选项有 host , port , user , password 等

看一个实际的例子,我们先在两台机器上创建好物理表并插入一些测试数据:

create table test.tinylog_d1(id UInt16, name String) ENGINE=TinyLog;
insert into test.tinylog_d1(id, name) values (1, 'Distributed record 1');
insert into test.tinylog_d1(id, name) values (2, 'Distributed record 2');

在其中一台创建逻辑表:


create table test.tinylog_d (id UInt16, name String) ENGINE=Distributed(log, test,tinylog_d1 , id);

-- 插入数据到逻辑表,观察数据分发情况
insert into test.tinylog_d(id, name) values (0, 'main');
insert into test.tinylog_d(id, name) values (1, 'main');
insert into test.tinylog_d(id, name) values (2, 'main');

select name,sum(id),count(id) from test.tinylog_d group by name;

注:逻辑表中的写入操作是异步的,会先缓存在本机的文件系统上,并且,对于物理表的不可访问状态,并没有严格控制,所以写入失败丢数据的情况是可能发生的

Null

空引擎,写入的任何数据都会被忽略,读取的结果一定是空。

但是注意,虽然数据本身不会被存储,但是结构上的和数据格式上的约束还是跟普通表一样是存在的,同时,你也可以在这个引擎上创建视图

Buffer

  1. Buffer 引擎,像是Memory 存储的一个上层应用似的(磁盘上也是没有相应目录的)
  2. 它的行为是一个缓冲区,写入的数据先被放在缓冲区,达到一个阈值后,这些数据会自动被写到指定的另一个表中
  3. 和Memory 一样,有很多的限制,比如没有索引
  4. Buffer 是接在其它表前面的一层,对它的读操作,也会自动应用到后面表,但是因为前面说到的限制的原因,一般我们读数据,就直接从源表读就好了,缓冲区的这点数据延迟,只要配置得当,影响不大的
  5. Buffer 后面也可以不接任何表,这样的话,当数据达到阈值,就会被丢弃掉

一些特点:

  • 如果一次写入的数据太大或太多,超过了 max 条件,则会直接写入源表。
  • 删源表或改源表的时候,建议 Buffer 表删了重建。
  • “友好重启”时, Buffer 数据会先落到源表,“暴力重启”, Buffer 表中的数据会丢失。
  • 即使使用了 Buffer ,多次的小数据写入,对比一次大数据写入,也 慢得多 (几千行与百万行的差距)
-- 创建源表
create table test.mergetree (sdt  Date, id UInt16, name String, point UInt16) ENGINE=MergeTree(sdt, (id, name), 10);
-- 创建 Buffer表
-- Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)
create table test.mergetree_buffer as test.mergetree ENGINE=Buffer(test, mergetree, 16, 3, 20, 2, 10, 1, 10000);

insert into test.mergetree (sdt, id, name, point) values ('2017-07-10', 1, 'a', 20);
insert into test.mergetree_buffer (sdt, id, name, point) values ('2017-07-10', 1, 'b', 10);
select * from test.mergetree;
select '------';
select * from test.mergetree_buffer;
  • database数据库

  • table 源表,这里除了字符串常量,也可以使用变量的。

  • num_layer 是类似“分区”的概念,每个分区的后面的 min / max 是独立计算的,官方推荐的值是 16 。

  • min / max 这组配置荐,就是设置阈值的,分别是 时间(秒),行数,空间(字节)。

    阈值的规则: 是“所有的 min 条件都满足, 或 至少一个 max 条件满足”。

如果按上面我们的建表来说,所有的 min 条件就是:过了 3秒,2条数据,1 Byte。一个 max 条件是:20秒,或 10 条数据,或有 10K

Set

Set 这个引擎有点特殊,因为它只用在 IN 操作符右侧,你不能对它 select

create table test.set(id UInt16, name String) ENGINE=Set;
insert into test.set(id, name) values (1, 'hello');
-- select 1 where (1, 'hello') in test.set; -- 默认UInt8 需要手动进行类型转换
select 1 where (toUInt16(1), 'hello') in test.set;

注: Set 引擎表,是全内存运行的,但是相关数据会落到磁盘上保存,启动时会加载到内存中。所以,意外中断或暴力重启,是可能产生数据丢失问题的

MergeTree

这个引擎是 ClickHouse 的重头戏,它支持一个日期和一组主键的两层式索引,还可以实时更新数据。同时,索引的粒度可以自定义,外加直接支持采样功能

MergeTree(EventDate, (CounterID, EventDate), 8192)
MergeTree(EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID)), 8192)
  • EventDate一个日期的列名
  • intHash32(UserID) 采样表达式
  • (CounterID, EventDate) 主键组(里面除了列名,也支持表达式),也可以是一个表达式
  • 8192 主键索引的粒度
drop table if exists test.mergetree1;
create table test.mergetree1 (sdt  Date, id UInt16, name String, cnt UInt16) ENGINE=MergeTree(sdt, (id, name), 10);

-- 日期的格式,好像必须是 yyyy-mm-dd
insert into test.mergetree1(sdt, id, name, cnt) values ('2018-06-01', 1, 'aaa', 10);
insert into test.mergetree1(sdt, id, name, cnt) values ('2018-06-02', 4, 'bbb', 10);
insert into test.mergetree1(sdt, id, name, cnt) values ('2018-06-03', 5, 'ccc', 11);

结构

├── 20180601_20180601_1_1_0
│   ├── checksums.txt
│   ├── columns.txt
│   ├── id.bin
│   ├── id.mrk
│   ├── name.bin
│   ├── name.mrk
│   ├── cnt.bin
│   ├── cnt.mrk 
│   ├── cnt.idx
│   ├── primary.idx
│   ├── sdt.bin
│   └── sdt.mrk -- 保存一下块偏移量
├── 20180602_20180602_2_2_0
│   └── ...
├── 20180603_20180603_3_3_0
│   └── ...
├── format_version.txt
└── detached

ReplacingMergeTree

  1. 在 MergeTree 的基础上,添加了“处理重复数据”的功能=>实时数据场景
  2. 相比 MergeTree ,ReplacingMergeTree 在最后加一个”版本列”,它跟时间列配合一起,用以区分哪条数据是”新的”,并把旧的丢掉(这个过程是在 merge 时处理,不是数据写入时就处理了的,平时重复的数据还是保存着的,并且查也是跟平常一样会查出来的)
  3. 主键列组用于区分重复的行
-- 版本列 允许的类型是, UInt 一族的整数,或 Date 或 DateTime
create table test.replacingmergetree (sdt  Date, id UInt16, name String, cnt UInt16) ENGINE=ReplacingMergeTree(sdt, (name), 10, cnt);

insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-10', 1, 'a', 20);
insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-10', 1, 'a', 30);
insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-11', 1, 'a', 20);
insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-11', 1, 'a', 30);
insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-11', 1, 'a', 10);

select * from test.replacingmergetree;

-- 如果记录未执行merge,可以手动触发一下 merge 行为
optimize table test.replacingmergetree;

SummingMergeTree

  1. SummingMergeTree 就是在 merge 阶段把数据sum求和
  2. sum求和的列可以指定,不可加的未指定列,会取一个最先出现的值
create table test.summingmergetree (sdt Date, name String, a UInt16, b UInt16) ENGINE=SummingMergeTree(sdt, (sdt, name), 8192, (a));

insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-10', 'a', 1, 20);
insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-10', 'b', 2, 11);
insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-11', 'b', 3, 18);
insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-11', 'b', 3, 82);
insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-11', 'a', 3, 11);
insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-12', 'c', 1, 35);

-- 手动触发一下 merge 行为
optimize table test.summingmergetree;

select * from test.summingmergetree;

注: 可加列不能是主键中的列,并且如果某行数据可加列都是 null ,则这行会被删除

AggregatingMergeTree

AggregatingMergeTree 是在 MergeTree 基础之上,针对聚合函数结果,作增量计算优化的一个设计,它会在 merge 时,针对主键预处理聚合的数据应用于AggregatingMergeTree 上的聚合函数除了普通的 sum, uniq等,还有 sumState , uniqState ,及 sumMerge , uniqMerge 这两组

聚合数据的预计算是一种“空间换时间”的权衡,并且是以减少维度为代价的

CREATE TABLE test.amt_basic_tab(date Date,D1 String,D2 String,D3 String,M1 UInt16) ENGINE MergeTree() PARTITION BY date ORDER BY (D1,D2,D3);


insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '甲', 'a', '1', 1);
insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '甲', 'a', '1', 1);
insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '甲', 'b', '2', 1);
insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '乙', 'b', '3', 1);
insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '丙', 'b', '2', 1);
insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '丙', 'c', '1', 1);
insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '丁', 'c', '2', 1);
insert into test.amt_basic_tab (date, D1, D2, D3, M1) values ('2017-07-10', '丁', 'a', '1', 1);

-- 创建一个AggregatingMergeTree的物化视图
create materialized view amt_tab_view ENGINE = AggregatingMergeTree() PARTITION BY date ORDER BY (D2,D3) as select date,D2, D3, uniqState(D1) as uv from amt_basic_tab group by date,D2,D3;

使用场景

可以使用AggregatingMergeTree表来做增量数据统计聚合,包括物化视图的数据聚合。

CollapsingMergeTree

是专门为 OLAP 场景下,一种“变通”存数做法而设计的,在数据是不能改,更不能删的前提下,通过“运算”的方式,去抹掉旧数据的影响,把旧数据“减”去即可,从而解决”最终状态”类的问题,比如 当前有多少人在线?

“以加代删”的增量存储方式,带来了聚合计算方便的好处,代价却是存储空间的翻倍,并且,对于只关心最新状态的场景,中间数据都是无用的

CollapsingMergeTree 在创建时与 MergeTree 基本一样,除了最后多了一个参数,需要指定 Sign 位(必须是 Int8 类型)

create table test.collapsingmergetree(sign Int8, sdt Date, name String, cnt UInt16) ENGINE=CollapsingMergeTree(sdt, (sdt, name), 8192, sign);

文章作者: 凌云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 凌云 !
  目录