ClickHouse - 04

5、数据库引擎


ClickHouse 中支持在创建数据库时指定引擎,目前比较常用的两种引擎为默认引擎和 MySQL 数据库引擎。

5.1、Ordinary


Ordinary 是 ClickHouse 中默认引擎,在这种数据库引擎下可以使用任意表引擎。

:) CREATE DATABASE test ENGINE = Ordinary;

# 查看建库信息
:) SHOW CREATE DATABASE test;
┌─statement──────────────────────────────┐
│ CREATE DATABASE test ENGINE = Ordinary │
└────────────────────────────────────────┘

5.2、MySQL


MySQL 引擎用于将远程 MySQL 服务器中数据库映射到 ClickHouse 中,并允许对表进行 INSERT 插入和 SELECT 查询,方便在 ClickHouse 与 MySQL 之间进行数据交换。但不会将 MySQL 的数据同步到 ClickHouse 中,仅将 MySQL 的表映射为 ClickHouse 表,在 MySQL 中进行 CRUD 操作,可以同时映射到 ClickHouse 中。

MySQL数据库引擎会将对其的查询转换为 MySQL 语法并发送到 MySQL 服务器,因此可以执行诸如 SHOW TABLES 或 SHOW CREATE TABLE 之类的操作,但是不允许创建表、修改表、删除数据、重命名操作。建库语法如下:

CREATE DATABASE [IF NOT EXISTS] {database_name} [ON CLUSTER cluster]
    ENGINE = MySQL('{host}:{port}', ['{database}' | database], '{user}', '{password}');

在 ClickHouse 中使用 MySQL 引擎建库,将 MySQL 库中数据映射到 ClickHouse 中,MySQL 库中表字段类型与 ClickHouse 表字段类型的映射如下:

MySQL ClickHouse
UNSIGNED TINYINT UInt8
TINYINT Int8
UNSIGNED SMALLINT UInt16
SMALLINT Int16
UNSIGNED INT | UNSIGNED MEDIUMINT UInt32
INT | MEDIUMINT Int32
UNSIGNED BIGINT UInt64
BIGINT Int64
FLOAT Float32
DOUBLE Float64
DATE Date
DATETIME, TIMESTAMP DateTime
BINARY FixedString

如果 MySQL 中字短为允许为空,则 ClickHouse 中将映射为 Nullable 类型。

6、表引擎


表引擎在 ClickHouse 中的作用十分关键,直接决定了数据如何存储和读取、是否支持并发读写、是否支持 Index 索引、支持的 Query 种类、是否支持主备复制等。

ClickHouse 提供了大约28种表引擎,比如 Log 系列用来做小表数据分析,MergeTree 系列用来做大数据量分析,而 Integration 系列则多用于外表数据集成,以及复制表 Replicated 系列,分布式表 Distributed 等,纷繁复杂。

ClickHouse 表引擎一共分为四个系列,分别是 Log系列、MergeTree系列、Integration系列、Special系列。其中包含了两种特殊的表引擎 Replicated、Distributed,功能上与其他表引擎正交,根据场景组合使用。

6.1、Log 系列


Log 系列表引擎功能相对简单,主要用于快速写入小表(1百万行左右的表),然后全部读出的场景,即一次写入,多次查询。Log 系列表引擎包含:TinyLog、StripeLog、Log三种引擎。

Log 表引擎的共性

  • 数据被顺序 append 写入本地磁盘。
  • 不支持 deleteupdate 修改数据。
  • 不支持 index 索引。
  • 不支持原子性写。如果某些操作(异常的服务器关闭)中断了写操作,则可能会获得带有损坏数据的表。
  • insert 会阻塞 select 操作。当向表中写入数据时,针对这张表的查询会被阻塞,直至写入动作结束。

Log 表引擎区别

  • TinyLog:不支持并发读取数据文件,查询性能较差;格式简单,适合用来暂存中间数据。
  • StripLog:支持并发读取数据文件,查询性能比 TinyLog 好;将所有列存储在同一个大文件中,减少了文件个数。
  • Log:支持并发读取数据文件,查询性能比 TinyLog 好;每个列会单独存储在一个独立文件中。

TinyLog

TinyLog 是 Log 系列引擎中功能简单、性能较低的引擎。它的存储结构由数据文件和元数据两部分组成。其中,数据文件按列独立存储,每一个列字段对应一个文件。

由于 TinyLog 数据存储不分块,所以不支持并发数据读取,该引擎适合一次写入,多次读取的场景,对于处理小批量中间表的数据可以使用该引擎,这种引擎会有大量小文件,性能会低。

:) CREATE TABLE t_tinylog(id UInt8, name String, age UInt8) engine = TinyLog;
:) INSERT INTO t_tinylog VALUES (1, 'zs', 18), (2, 'ls', 19), (3, 'ww', 20);
:) SELECT * FROM t_tinylog;
┌─id─┬─name─┬─age─┐
│  1 │ zs   │  18 │
│  2 │ ls   │  19 │
│  3 │ ww   │  20 │
└────┴──────┴─────┘

# TinyLog 引擎不支持删除数据 删除数据将引发异常
:) ALTER TABLE t_tinylog DELETE WHERE id = 1;
Code: 48. DB::Exception: Received from localhost:9000. DB::Exception: Mutations are not supported by storage TinyLog.

TinyLog 引擎表中的每个列单独对应一个 bin 文件,同时还有一个 sizes.json 文件存储元数据,记录了每个 bin 文件中数据大小。

_> cd /var/lib/clickhouse/data/{database}/{table} && ll
-rw-r----- 1 clickhouse clickhouse 29 Mar 22 00:50 age.bin
-rw-r----- 1 clickhouse clickhouse 29 Mar 22 00:50 id.bin
-rw-r----- 1 clickhouse clickhouse 48 Mar 22 00:50 name.bin
-rw-r----- 1 clickhouse clickhouse 90 Mar 22 00:50 sizes.json

StripeLog

相比 TinyLog 而言,StripeLog 数据存储会划分块,每次插入对应一个数据块,拥有更高的查询性能(拥有 .mrk 标记文件,支持并行查询)。StripeLog 引擎将所有列存储在一个文件中,使用了更少的文件描述符。对每一次 insert 请求,ClickHouse 将数据块追加在表文件的末尾,逐列写入。StripeLog 引擎不支持 ALTER UPDATE 和 ALTER DELETE 操作。

:) CREATE TABLE t_stripelog(id UInt8, name String, age UInt8) engine = StripeLog;

# 多次插入会将数据插入不同数据块中
:) INSERT INTO t_stripelog VALUES (1, 'zs', 18);
:) INSERT INTO t_stripelog VALUES (2, 'ls', 19), (3, 'ww', 20);

:) SELECT * FROM t_stripelog;
┌─id─┬─name─┬─age─┐
│  1 │ zs   │  18 │
└────┴──────┴─────┘
┌─id─┬─name─┬─age─┐
│  2 │ ls   │  19 │
│  3 │ ww   │  20 │
└────┴──────┴─────┘

StripeLog 引擎表文件如下:

_> cd /var/lib/clickhouse/data/{database}/{table} && ll
-rw-r----- 1 clickhouse clickhouse 291 Mar 22 01:02 data.bin
-rw-r----- 1 clickhouse clickhouse 150 Mar 22 01:02 index.mrk
-rw-r----- 1 clickhouse clickhouse  69 Mar 22 01:02 sizes.json
  • data.bin:数据文件,所有列字段都写入该文件中。

  • index.mrk:数据标记文件,保存了数据在 data.bin 文件中的位置信息,即每个插入数据列的 offset 信息。

    利用数据标记能够使用多个线程,并行读取 data.bin 压缩数据,提升查询性能。

  • sizes.json:元数据文件,记录了 data.binindex.mrk 大小信息。

Log

Log 引擎表适用于临时数据,一次性写入、测试场景。Log 引擎结合了 TinyLog 表引擎和 StripeLog 表引擎的长处,是 Log 系列引擎中性能最高的表引擎。

Log 表引擎会将每一列都存在一个文件中,对于每一次的 insert 操作,会生成数据块,经测试,数据块个数与当前节点的 CPU 核心数一致。

:) CREATE TABLE t_log(id UInt8, name String, age UInt8) engine = Log;

:) INSERT INTO t_log VALUES (1,'zs',18);
:) INSERT INTO t_log VALUES (2,'ls',19);
:) INSERT INTO t_log VALUES (3,'ww',20);
:) INSERT INTO t_log VALUES (4,'ml',21);
:) INSERT INTO t_log VALUES (5,'tq',22);

:) select * from t_log;
┌─id─┬─name─┬─age─┐
│  1 │ zs   │  18 │
│  2 │ ls   │  19 │
└────┴──────┴─────┘
┌─id─┬─name─┬─age─┐
│  3 │ ww   │  20 │
│  4 │ ml   │  21 │
│  5 │ tq   │  22 │
└────┴──────┴─────┘

Log 引擎表文件如下:

_> cd /var/lib/clickhouse/data/{database}/{table} && ll
-rw-r----- 1 clickhouse clickhouse  48 Mar 22 02:07 __marks.mrk
-rw-r----- 1 clickhouse clickhouse 135 Mar 22 02:07 age.bin
-rw-r----- 1 clickhouse clickhouse 135 Mar 22 02:07 id.bin
-rw-r----- 1 clickhouse clickhouse 145 Mar 22 02:07 name.bin
-rw-r----- 1 clickhouse clickhouse 123 Mar 22 02:07 sizes.json
  • __marks.mrk:数据标记,保存了每个列文件中的数据位置信息,利用数据标记能够使用多个线程,并行读取 data.bin 压缩数据,提升查询性能。
  • sizes.json:记录了 bin__mark.mrk 大小信息。

6.2、Special 系列


Memory

Memory 表引擎直接将数据保存在内存中,ClickHouse 中的 Memory 表引擎具有以下特点:

  • Memory 引擎以未压缩的形式将数据存储在 RAM 中,数据完全以读取时获得的形式存储。
  • 并发数据访问是同步的,锁范围小,读写操作不会相互阻塞。
  • 不支持索引。
  • 查询是并行化的,在简单查询上达到最大速率,超过10GB/秒,在相对较少的行(最多约100,000,000行)上有高性能的查询。
  • 没有磁盘读取,不需要解压缩或反序列化数据,速度更快,在某些情况下,与 MergeTree 引擎的性能几乎一样高。
  • 重新启动服务器时,表存在,但是表中数据全部清空。
  • Memory 引擎多用于测试。
:) CREATE TABLE t_memory(id UInt8, name String, age UInt8) engine = Memory;
:) INSERT INTO t_memory VALUES (1, 'zs', 18), (2, 'ls', 19), (3, 'ww', 20);
:) SELECT * FROM t_memory;
┌─id─┬─name─┬─age─┐
│ 1  │  zs  │  18 │
│ 2  │  ls  │  19 │
│ 3  │  ww  │  20 │
└────┴──────┴─────┘

# 重启 ClickHouse 服务
_> systemctl restart clickhouse-server
_> clickhouse-client -m
:) SELECT * FROM t_memory;
0 rows in set. Elapsed: 0.003 sec.

Merge

Merge 引擎本身不存储数据,但可用于同时从任意多个其他的表中读取数据,这里需要多个表的结构相同,并且创建的 Merge 引擎表的结构也需要和这些表结构相同才能读取。读是自动并行的,不支持写入。读取时,被真正读取到数据的表如果设置了索引,索引也会被使用。

Merge 引擎参数:数据库名和用于匹配表名的正则表达式。

注意:当选择需要读取的表时,会匹配正则表达式匹配命中的表,如果当前 Merge 表的名称也符合正则表达式匹配规则,Merge 表本身会自动排除,以避免进入递归死循环,当然也可以创建两个相互无限递归读取对方数据的 Merge 表,但这并没有什么意义。

注意:Merge 引擎表不会在对应的库路径下产生对应的目录结构。

:) CREATE TABLE m_t1 (id UInt8, name String, age UInt8) engine = TinyLog;
:) INSERT INTO m_t1 VALUES (1, 'zs', 18), (2, 'ls', 19);

:) CREATE TABLE m_t2 (id UInt8, name String, age UInt8) engine = TinyLog;
:) INSERT INTO m_t2 VALUES (3, 'ww', 20), (4, 'ml', 21);

:) CREATE TABLE m_t3 (id UInt8, name String, age UInt8) engine = TinyLog;
:) INSERT INTO m_t3 VALUES (5, 'tq', 22), (6, 'zb', 23);

# Merge 语法:Merge(${database}, '${regex}')
:) CREATE TABLE t_merge (id UInt8, name String, age UInt8) engine = Merge(test, '^m_t');

:) SELECT * FROM t_merge;
┌─id─┬─name─┬─age─┐
│  1 │ zs   │  18 │
│  2 │ ls   │  19 │
└────┴──────┴─────┘
┌─id─┬─name─┬─age─┐
│  3 │ ww   │  20 │
│  4 │ ml   │  21 │
└────┴──────┴─────┘
┌─id─┬─name─┬─age─┐
│  5 │ tq   │  22 │
│  6 │ zb   │  23 │
└────┴──────┴─────┘
6 rows in set. Elapsed: 0.006 sec.

Distributed

Distributed 是 ClickHouse 中分布式引擎,使用分布式引擎声明的表才可以在其他节点访问与操作。

Distributed 引擎和 Merge 引擎类似,本身不存放数据,功能是在不同节点把多张相同结构的物理表合并为一张逻辑表。

分布式表引擎语法:Distributed(cluster_name, database_name, table_name[, sharding_key])

  • cluster_name:映射 metrika.xml 配置文件中 clickhouse_remote_servers 标签下配置集群名称。
  • database_name:数据库名称。
  • table_name:表名称。
  • sharding_key:可选项,用于分片的 KEY 值,在数据写入的过程中,分布式表会依据分片 KEY 的规则,将数据分布到各个节点的本地表。

注意:创建分布式表是读时检查机制,对创建分布式表和本地表的顺序并没有强制要求。

#在node1、node2、node3节点上启动ClickHouse 服务
_> ssh node01 'systemctl start clickhouse-server'
_> ssh node02 'systemctl start clickhouse-server'
_> ssh node03 'systemctl start clickhouse-server'

_> ssh node01
_> clickhouse-client
:) CREATE TABLE test_local (id UInt8, name String) engine= TinyLog;
:) INSERT INTO test_local VALUES (1, 'zs'), (2, 'ls');

_> ssh node02
_> clickhouse-client
:) CREATE TABLE test_local (id UInt8, name String) engine= TinyLog;
:) INSERT INTO test_local VALUES (3, 'ww'), (4, 'ml');

_> ssh node03
_> clickhouse-client
:) CREATE TABLE test_local (id UInt8, name String) engine= TinyLog;
:) INSERT INTO test_local VALUES (5, 'tq'), (6, 'zb');

# 在 node01 节点创建分布式表 t_distributed 表引擎使用 Distributed 引擎
_> ssh node01
_> clickhouse-client
:) CREATE TABLE t_distributed(id UInt8, name String) engine = Distributed(clickhouse_cluster, default, test_local, id);
# 以上分布式表 t_distributed 只存在于 node01 节点 ClickHouse 中

:) SELECT * FROM t_distributed;
┌─id─┬─name─┐
│  1 │ zs   │
│  2 │ ls   │
└────┴──────┘
┌─id─┬─name─┐
│  5 │ tq   │
│  6 │ zb   │
└────┴──────┘
┌─id─┬─name─┐
│  3 │ ww   │
│  4 │ ml   │
└────┴──────┘
6 rows in set. Elapsed: 0.010 sec. 

# 向分布式表中插入数据 将分布式存储在不同节点
:) INSERT INTO t_distributed VALUES (7, 'dzs'), (8, 'dls'), (9, 'dww'), (10, 'dml'), (11, 'dtq'), (12, 'dzb');

_> ssh node01
:) SELECT * FROM test_local;
┌─id─┬─name─┐
│  1 │ zs   │
│  2 │ ls   │
│  9 │ dww  │
│ 12 │ dzb  │
└────┴──────┘
4 rows in set. Elapsed: 0.004 sec. 

_> ssh node02
:) SELECT * FROM test_local;
┌─id─┬─name─┐
│  3 │ ww   │
│  4 │ ml   │
│  7 │ dzs  │
│ 10 │ dml  │
└────┴──────┘
4 rows in set. Elapsed: 0.004 sec. 

_> ssh node03
:) SELECT * FROM test_local;
┌─id─┬─name─┐
│  5 │ tq   │
│  6 │ zb   │
│  8 │ dls  │
│ 11 │ dtq  │
└────┴──────┘
4 rows in set. Elapsed: 0.005 sec. 

以上在 node01 节点创建的分布式表 t_distributed 虽然数据是分布式存储在每个 ClickHouse 集群节点,但只能在 node1 节点查询 t_distributed 表,其他 ClickHouse 节点查询不到此分布式表。如需每台 ClickHouse 节点上都能访问分布式表,可以指定集群,创建该表:

# 创建分布式表 t_cluster ,引擎使用Distributed 引擎
:) CREATE TABLE t_cluster ON cluster clickhouse_cluster (id UInt8, name String) engine =  Distributed(clickhouse_cluster, default, test_local, id);
┌─host───┬─port─┬─status─┬─error─┬─num_hosts_remaining─┬─num_hosts_active─┐
│ node03 │ 9000 │      0 │       │                   2 │                0 │
│ node02 │ 9000 │      0 │       │                   1 │                0 │
│ node01 │ 9000 │      0 │       │                   0 │                0 │
└────────┴──────┴────────┴───────┴─────────────────────┴──────────────────┘
3 rows in set. Elapsed: 0.152 sec. 

上面的语句中使用了 ON CLUSTER 分布式 DDL,意味着在集群的每个分片节点,都会创建一张 Distributed 表,这样便可以从其中任意一端发起对所有分片的读、写请求。

6.3、MergeTree 系列表引擎


在所有的表引擎中,最为核心的当属 MergeTree 系列表引擎,这些表引擎拥有最为强大的性能和最广泛的使用场合。对于非 MergeTree 系列的其他引擎而言,主要用于特殊用途,场景相对有限。而 MergeTree 系列表引擎是官方主推的存储引擎,有主键索引、数据分区、数据副本、数据采样、删除和修改等功能,支持几乎所有 ClickHouse 核心功能。

MergeTree 系列表引擎包含:MergeTree、ReplacingMergeTree、SummingMergeTree(汇总求和功能)、AggregatingMergeTree(聚合功能)、CollapsingMergeTree(折叠删除功能)、VersionedCollapsingMergeTree(版本折叠功能)引擎,在这些的基础上还可以叠加 Replicated 和 Distributed。

6.3.1、MergeTree


MergeTree 在写入一批数据时,数据总会以数据片段的形式写入磁盘,且数据片段在磁盘上不可修改。为了避免片段过多,ClickHouse 会通过后台线程,定期合并这些数据片段,属于相同分区的数据片段会被合成一个新的片段。这种数据片段往复合并的特点,也正是合并树名称的由来。

MergeTree 作为家族系列最基础的表引擎,主要有以下特点:

  • 存储的数据按照主键排序:创建稀疏索引加快数据查询速度。
  • 支持数据分区,可以通过 PARTITION BY 语句指定分区字段。
  • 支持数据副本。
  • 支持数据采样。

建表语句

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, ...]
  • ENGINE:ENGINE = MergeTree(),MergeTree 引擎没有参数。
  • ORDER BY:必选项,排序字段。比如 ORDER BY (Col1, Col2),值得注意的是,如果没有使用 PRIMARY KEY 显式的指定主键 ORDER BY 排序字段自动作为主键。如果不需要排序,则可以使用 ORDER BY tuple() 语法,创建的表也就不包含主键。这种情况下,ClickHouse 会按照插入的顺序存储数据。
  • PARTITION BY:可选项,分区字段,例如要按月分区,可以使用表达式 toYYYYMM(date_column),分区名格式为 YYYYMM
  • PRIMARY KEY:可选项,指定主键,如果排序字段与主键不一致,可以单独指定主键字段,否则默认主键是排序字段。另外,如果指定了 PRIMARY KEY 与排序字段不一致,要保证 PRIMARY KEY 指定的主键是 ORDER BY 指定字段的前缀,这种强制约束保障了即便在两者定义不同的情况下,主键仍然是排序键的前缀,不会出现索引与数据顺序混乱的问题。大部分情况下不需要专门指定一个 PRIMARY KEY 子句。注意:在 MergeTree 中主键并不用于去重,而是用于索引,加快查询速度。
    • ORDER BY (A,B,C) PRIMARY KEY A:允许。
    • ORDER BY (A,B,C) PRIMARY KEY B:禁止:DB::Exception: Primary key must be a prefix of the sorting key。
  • SAMPLE BY:可选项,采样字段。如果指定了该字段,那么主键中也必须包含该字段。
  • TTL:可选项,数据存活时间。在 MergeTree 中,可以为某个列字段或整张表设置 TTL。当时间到达时,如果是列字段级别的 TTL,则会删除这一列的数据;如果是表级别的 TTL,则会删除整张表的数据。
  • SETTINGS:可选项。额外的参数配置。

数据分区

:) CREATE TABLE t_mt(
:-]      id          UInt8,
:-]      name        String,
:-]      age         UInt8,
:-]      birthday    Date,
:-]      location    String
:-] ) engine = MergeTree()
:-] ORDER BY (id,age)
:-] PARTITION by toYYYYMM(birthday);

:) INSERT INTO t_mt VALUES (1, 'zs', 18, '2021-06-01', 'sh'), (2, 'ls', 19, '2021-02-10', 'bj'), (3, 'ww', 12, '2021-06-01', 'tj'), (1, 'ml', 10, '2021-06-18', 'sh'), (5, 'tq', 22, '2021-02-09', 'gz');

:) SELECT * FROM t_mt;
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  2 │ ls   │  19 │ 2021-02-10 │ bj       │
│  5 │ tq   │  22 │ 2021-02-09 │ gz       │
└────┴──────┴─────┴────────────┴──────────┘
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  1 │ ml   │  10 │ 2021-06-18 │ sh       │
│  1 │ zs   │  18 │ 2021-06-01 │ sh       │
│  3 │ ww   │  12 │ 2021-06-01 │ tj       │
└────┴──────┴─────┴────────────┴──────────┘
5 rows in set. Elapsed: 0.020 sec. 

继续插入数据:

:) INSERT INTO t_mt VALUES (1, 'zb', 11, '2021-06-08', 'bj'), (2, 'lj', 19, '2021-02-10', 'tj'), (3, 'zs', 12, '2021-07-01', 'bj');
:) SELECT * FROM t_mt;
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  2 │ ls   │  19 │ 2021-02-10 │ bj       │
│  5 │ tq   │  22 │ 2021-02-09 │ gz       │
└────┴──────┴─────┴────────────┴──────────┘
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  2 │ lj   │  19 │ 2021-02-10 │ tj       │
└────┴──────┴─────┴────────────┴──────────┘
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  1 │ ml   │  10 │ 2021-06-18 │ sh       │
│  1 │ zs   │  18 │ 2021-06-01 │ sh       │
│  3 │ ww   │  12 │ 2021-06-01 │ tj       │
└────┴──────┴─────┴────────────┴──────────┘
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  1 │ zb   │  11 │ 2021-06-08 │ bj       │
└────┴──────┴─────┴────────────┴──────────┘
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  3 │ zs   │  12 │ 2021-07-01 │ bj       │
└────┴──────┴─────┴────────────┴──────────┘
8 rows in set. Elapsed: 0.005 sec. 

可以看到新插入的数据生成了新数据块,实际上这里在底层对应新的分区文件片段,MergeTree 引擎会在插入数据15分钟左右,将同一个分区的各个分区文件片段合并成一整个分区文件。也可以手动执行 OPTIMIZE 语句手动触发合并。

:) OPTIMIZE TABLE t_mt PARTITION '202102';
:) OPTIMIZE TABLE t_mt PARTITION '202106'; 

:) SELECT * FROM t_mt;
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  2 │ ls   │  19 │ 2021-02-10 │ bj       │
│  2 │ lj   │  19 │ 2021-02-10 │ tj       │
│  5 │ tq   │  22 │ 2021-02-09 │ gz       │
└────┴──────┴─────┴────────────┴──────────┘
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  1 │ ml   │  10 │ 2021-06-18 │ sh       │
│  1 │ zb   │  11 │ 2021-06-08 │ bj       │
│  1 │ zs   │  18 │ 2021-06-01 │ sh       │
│  3 │ ww   │  12 │ 2021-06-01 │ tj       │
└────┴──────┴─────┴────────────┴──────────┘
┌─id─┬─name─┬─age─┬───birthday─┬─location─┐
│  3 │ zs   │  12 │ 2021-07-01 │ bj       │
└────┴──────┴─────┴────────────┴──────────┘
8 rows in set. Elapsed: 0.004 sec. 
  • OPTIMIZE TABLE t_mt PARTITION 'value':合并指定分区,如果有多个分区,需要执行多次。
  • OPTIMIZE TABLE t_mt:合并一个分区,如果有多个分区,需要执行多次。
  • OPTIMIZE TABLE t_mt FINAL:合并所有分区。

数据文件目录解析

在创建表,并插入数据后,表目录如下:

_> cd /var/lib/clickhouse/data/{database}/{table} && ll
drwxr-x--- 2 clickhouse clickhouse 305 Mar 22 05:25 202102_2_2_0
drwxr-x--- 2 clickhouse clickhouse 305 Mar 22 05:25 202106_1_1_0
drwxr-x--- 2 clickhouse clickhouse   6 Mar 22 05:24 detached
-rw-r----- 1 clickhouse clickhouse   1 Mar 22 05:24 format_version.txt

202102_2_2_0202106_1_1_0 为分区目录。该分区目录可在 system.parts 表中查询:

:) SELECT table, partition, name, active FROM system.parts WHERE table = 't_mt';
┌─table─┬─partition─┬─name─────────┬─active─┐
│ t_mt  │ 202102    │ 202102_2_2_0 │      1 │
│ t_mt  │ 202106    │ 202106_1_1_0 │      1 │
└───────┴───────────┴──────────────┴────────┘
2 rows in set. Elapsed: 0.004 sec. 
  • table:归属表。

  • partition:分区名称。

  • name:对应磁盘数据所在的分区目录片段,格式为 {分区名称}_{数据块最小编号}_{数据块最大编号}_{合并次数}

  • active:分区片段状态:1代表激活状态,0代表非激活状态。

    非激活片段是那些在合并到较大片段之后剩余的源数据片段,损坏的数据片段也表示为非活动状态。非激活片段会在合并后的10分钟左右被删除。

分区片段目录结构如下

_> cd /var/lib/clickhouse/data/{database}/{partition} && ll
-rw-r----- 1 clickhouse clickhouse  28 Mar 22 05:25 id.bin
-rw-r----- 1 clickhouse clickhouse  48 Mar 22 05:25 id.mrk2
-rw-r----- 1 clickhouse clickhouse  32 Mar 22 05:25 name.bin
-rw-r----- 1 clickhouse clickhouse  48 Mar 22 05:25 name.mrk2
-rw-r----- 1 clickhouse clickhouse  28 Mar 22 05:25 age.bin
-rw-r----- 1 clickhouse clickhouse  48 Mar 22 05:25 age.mrk2
-rw-r----- 1 clickhouse clickhouse  30 Mar 22 05:25 birthday.bin
-rw-r----- 1 clickhouse clickhouse  48 Mar 22 05:25 birthday.mrk2
-rw-r----- 1 clickhouse clickhouse  32 Mar 22 05:25 location.bin
-rw-r----- 1 clickhouse clickhouse  48 Mar 22 05:25 location.mrk2
-rw-r----- 1 clickhouse clickhouse   4 Mar 22 05:25 minmax_birthday.idx
-rw-r----- 1 clickhouse clickhouse   4 Mar 22 05:25 primary.idx
-rw-r----- 1 clickhouse clickhouse   4 Mar 22 05:25 partition.dat
-rw-r----- 1 clickhouse clickhouse 493 Mar 22 05:25 checksums.txt
-rw-r----- 1 clickhouse clickhouse 108 Mar 22 05:25 columns.txt
-rw-r----- 1 clickhouse clickhouse   1 Mar 22 05:25 count.txt
  • checksums.txt:校验和文件,使用二进制格式存储,保存余下各类文件(primary. idxcount.txt等)的 size 大小及 size 哈希值,用于快速校验文件的完整性和正确性。
  • columns.txt:存储当前分区所有列信息。使用明文格式存储。
  • count.txt:计数文件,使用明文格式存储。用于记录当前数据分区目录下数据的总行数。
  • primary.idx:一级索引文件,使用二进制格式存储。用于存放稀疏索引,一张 MergeTree 表只能声明一次一级索引,即通过 ORDER BY 或者 PRIMARY KEY 指定字段。借助稀疏索引,在数据查询的时能够排除主键条件范围之外的数据文件,从而有效减少数据扫描范围,加速查询速度。
  • *.bin:数据文件,使用压缩格式存储,默认为 LZ4 压缩格式,用于存储某一列的数据。
  • *.mrk2:列字段标记文件,使用二进制格式存储。标记文件中保存了 bin 文件中数据的偏移量信息。
  • partition.dat & minmax_[Column].idx:如果指定了分区键,则会额外生成 partition.datminmax 索引文件,它们均使用二进制格式存储。partition.dat 用于保存当前分区下分区表达式最终生成的值,即分区字段值;而 minmax 索引用于记录当前分区下分区字段对应原始数据的最小和最大值。

ClickHouse MergeTree 引擎表支持分区,索引,修改,并发查询数据,当查询 MergeTree 表数据时,首先通过 primary.idx 文件获取对应索引,根据索引找到 [column].mrk2 文件获取对应的数据块偏移量,之后再根据偏移量从 [column].bin 文件中读取块数据。

分区合并规则

为表设置分区可以在查询过程中跳过不需要的数据目录,提升查询效率。在 ClickHouse 中并不是所有的表都支持分区,目前只有 MergeTree 家族系列的表引擎才支持数据分区。

向 MergeTree 分区表中插入数据时,每次都会生成对应的分区片段,不会立刻合并相同分区的数据,需要等待15分钟左右,ClickHouse 会自动合并相同的分区片段,并删除合并之前的源数据片段,当然也可以手动执行 OPTIMIZE 语句手动触发合并分区表中的分区片段。

分区合并策略.png

表设置分区字段时,分区健不仅可以指定为时间列,也可以是表中任意列或者列的表达式。如果按照字符串字段来进行分区,在底层 /var/lib/clickhouse/data/{database}/ 目录下对应的表中的分区片段名称是使用字符串的 hashcode + 编码 的形式来命名。

6.3.2、ReplacingMergeTree


MergeTree 不能对相同主键的数据进行去重,ClickHouse 提供了 ReplacingMergeTree 引擎,可以针对同分区内相同主键的数据进行去重,它能够在合并分区时删除重复的数据。值得注意的是,ReplacingMergeTree 只是在一定程度上解决了数据重复问题,由于自动分区合并机制在后台定时执行,所以并不能完全保障数据不重复 。

ReplacingMergeTree 适用于在后台清除重复的数据以节省空间。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = ReplacingMergeTree([ver])
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
  • ver:可选参数,指定列的版本,可以是 UInt*DateDateTime 类型的字段作为版本号。该参数决定了数据去重的方式。当没有指定 ver 时,保留最后插入的数据,也就是最新的数据。如果指定了具体的 ver 列,则保留最大版本数据。

注意事项

  • 数据重复判断:ReplacingMergeTree 在去除重复数据时,是以 ORDER BY 排序键为基准的,而不是 PRIMARY KEY
  • 删除重复数据时机:在执行分区合并时,会触发删除重复数据。OPTIMIZE 合并操作是在后台执行的,无法预测具体执行时间点,除非手动执行。
  • 不同分区的重复数据不会被去重:ReplacingMergeTree 是以分区为单位删除重复数据的。只有在相同的数据分区内重复的数据才可以被删除,而不同数据分区之间的重复数据依然不能被剔除。
  • 数据去重策略:如果没有设置 ver 版本号,则保留同一组重复数据中最新插入的数据;如果设置了 ver 版本号,则保留同一组重复数据中 ver 字段取值最大的一行。
  • OPTIMIZE 命令使用:一般在数据量比较大的情况,尽量不要使用该命令。因为在海量数据场景下,执行 OPTIMIZE 要消耗大量时间。

6.3.3、SummingMergeTree


该引擎继承了MergeTree引擎,当合并 SummingMergeTree 表的数据片段时,ClickHouse 会把所有具有相同主键的行合并为一行,该行包含了被合并的行中具有数值数据类型的列的汇总值,即如果存在重复的数据,会对对这些重复的数据进行合并成一条数据,类似于 GROUP BY 的效果,可以显著减少存储空间并加快数据查询速度。

如果只需要查询数据的汇总结果,不关心明细数据,并且数据的汇总条件是预先明确的,即 GROUP BY 的分组字段是确定的,可以使用该表引擎。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = SummingMergeTree([columns])
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
  • columns:可选参数。将要被汇总的列,或者多个列,多个列需要写在元组中 SummingMergeTree((columns_1, column_2))。所选的列必须是数值类型,并且不可位于主键中。如果未指定 columns,ClickHouse 会把所有不在主键中的数值类型的列都进行汇总。

注意事项

  • 合并规则:通过 ORBER BY 排序键作为聚合数据的条件 Key。即如果排序 Key 是相同的,则会合并成一条数据,并对指定的合并字段进行聚合。
  • 仅对分区内相同排序 Key 的数据行进行聚合合并:以数据分区为单位来聚合数据。当分区合并时,同一数据分区内聚合 Key 相同的数据会被合并汇总,而不同分区之间的数据则不会被汇总。
  • 未指定聚合字段聚合规则:如果未指定聚合字段,则会按照非主键的数值类型字段进行聚合。
  • 非汇总聚合字段合并规则:如果两行数据除了排序字段相同,其他的非聚合字段不相同,那么在聚合发生时,会保留最初的那条数据,新插入的数据对应的那个字段值会被舍弃。

6.3.4、AggregatingMergeTree


该表引擎继承自 MergeTree,可以使用 AggregatingMergeTree 表来做增量数据统计聚合。如果要按一组规则来合并减少行数,则使用 AggregatingMergeTree 是合适的。AggregatingMergeTree 是通过预先定义的聚合函数计算数据并通过二进制的格式存入表内。

与 SummingMergeTree 的区别在于:SummingMergeTree 对非主键列进行 sum 聚合,而 AggregatingMergeTree 则可以指定各种聚合函数。对某些字段需要进行聚合时,需要在创建表字段时指定成 AggregateFunction 类型。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
    aggName1 AggregateFunction(function, type),
    ...
) ENGINE = AggregatingMergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[TTL expr]
[SETTINGS name=value, ...]

对于 AggregateFunction 类型列字段,在进行数据的写入和查询时与其他的表引擎有很大区别,在写入数据时,需要调用 *State() 函数;而在查询数据时,则需要调用相应的 *Merge() 函数。

:) INSERT INTO table_name SELECT 1, 'value', ... , sumState(toDecimal32(value, 2));

-- 使用 *Merge() 函数查询 AggregatingMergeTree 表中数据 必须指定 GROUP BY 子句
:) SELECT name1, name2, sumMerge(aggName1) FROM table_name GROUP BY name1, name2;

合并规则:向表中插入排序字段相同的数据进行分区聚合时,数据按建表指定的聚合字段进行合并,其他的非聚合字段会保留最初的数据,新插入的数据对应的字段值将被舍弃。

通常情况下使用 AggregatingMergeTree 表引擎较为不便,更多情况下,将 AggregatingMergeTree 作为物化视图的表引擎与 MergeeTree 配合使用。

-- 创建表 t_merge_base 表,使用 MergeTree 引擎
:) CREATE TABLE t_merge_base(
:-]    id       UInt8,
:-]    name     String,
:-]    age      UInt8,
:-]    location String,
:-]    dept     String,
:-]    workdays UInt8,
:-]    salary   Decimal32(2)
:-] )
:-] ENGINE = MergeTree()
:-] ORDER BY (id, age)
:-] PRIMARY KEY id
:-] PARTITION BY location;

-- 创建物化视图 view_aggregating 使用 AggregatingMergeTree 引擎
:)  CREATE MATERIALIZED VIEW view_aggregating
:-] ENGINE = AggregatingMergeTree()
:-] ORDER BY id 
:-] AS SELECT id, name, sumState(salary) as total FROM t_merge_base GROUP BY id, name;


-- 向表 t_merge_base 中插入数据
:) INSERT INTO t_merge_base VALUES (1, 'zs', 18, 'bj', 'dsj', 24, 10000),
:-] (2, 'ls', 19, 'sh', 'java', 22, 8000),
:-] (3, 'ww', 20, 'bj', 'java', 26, 12000);

-- 查看 view_aggregating 物化视图数据
:) SELECT id, name, sumMerge(total) AS sum_total FROM view_aggregating GROUP BY id, name, total;
┌─id─┬─name─┬─sum_total─┐
│  2 │ ls   │   8000.00 │
│  1 │ zs   │  10000.00 │
│  3 │ ww   │  12000.00 │
└────┴──────┴───────────┘

-- 继续向表 t_merge_base 中插入排序键相同的数据
:) INSERT INTO t_merge_base VALUES (1, 'zsx', 18, 'bj', 'qd', 22, 5000);

-- 手动执行 OPTIMIZE 命令 合并物化视图 view_aggregating 相同分区数据
:) OPTIMIZE TABLE view_aggregating;

-- 查询视图 view_aggregating 数据
:) SELECT id, name, sumMerge(total) AS sum_total FROM view_aggregating GROUP BY id, name, total;
┌─id─┬─name─┬─sum_total─┐
│  2 │ ls   │   8000.00 │
│  1 │ zs   │  15000.00 │
│  3 │ ww   │  12000.00 │
└────┴──────┴───────────┘

注意:通过普通 MergeTree 表与 AggregatingMergeTree 物化视图结合使用,MergeTree 中存放原子数据,物化视图中存入聚合结果数据,可以提升数据查询效率。

SimpleAggregateFunction

SimpleAggregateFunction(name, types_of_arguments…) 数据类型存储字面值,而不将其存储为 AggregateFunction 类型,因此该类型列的插入和查询不需要通过*State()*Merge() 函数进行转换。SimpleAggregateFunction 支持以下聚合函数:

  • any:用于获取某列聚合结果的任意值,该函数的结果是不确定的。要获得确定的结果,可以使用 minmax 函数代替。
  • anyLast:用于获取某列聚合结果的最后一个值。结果与 any 函数一样不确定。
  • min:获取最小值。
  • max:获取最大值。
  • sum:获取汇总值。
  • sumWithOverflow:使用指定的类型存储聚合结果,如果聚合结果溢出,则根据具体类型的溢出状态存储最终结果,有一定风险。
  • groupBitAnd:被聚合的数据集合进行按位与运算,返回 UInt* 类型值。
  • groupBitOr:被聚合的数据集合进行按位或运算,返回 UInt* 类型值。
  • groupBitXor:被聚合的数据集合进行按位异或运算,返回 UInt* 类型值。
  • groupArray:语法:groupArray(x)groupArray(max_size)(x)。创建一个数组,将聚合后该列数据集合存储在该数组中,max_size 可指定数组最大宽度。如果设置为1,该函数等同于 any 函数。
  • groupUniqArray:语法:groupUniqArray(x)groupUniqArray(max_size)(x)。与 groupArray 类似,只是仅存储去重后值。
  • sumMap:语法:sumMap(key, value)sumMap(Tuple(key, value))。返回 Tuple(key, value) 数组,数组中数据按 key 分组聚合 value 值。
  • minMap:语法及结果形式上与 sumMap 一致,只是 value 仅存储最小值。
  • maxMap:语法及结果形式上与 sumMap 一致,只是 value 仅存储最大值。
  • argMin:语法:argMin(arg, val)argMin(Tuple(arg, val))。返回 arg 最小值数据行中 val 列的值。
  • argMax:语法:argMax(arg, val)argMax(Tuple(arg, val))。返回 arg 最大值数据行中 val 列的值。

6.3.5、CollapsingMergeTree


CollapsingMergeTree 通过以增代删的思路,支持行级数据修改和删除的表引擎。它通过定义一个 sign 标记位字段,记录数据行的状态。如果 sign 标记为1,则表示这是一行有效的数据;如果 sign 标记为-1,则表示这行数据需要被删除。当 CollapsingMergeTree 分区合并时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。

每次需要新增数据时,写入一行sign标记为1的数据;需要删除数据时,则写入一行sign标记为-1的数据。此外,只有相同分区内的数据才有可能被折叠。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
    sign Int8
) ENGINE = CollapsingMergeTree(sign)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

CollapsingMergeTree 对于写入数据的顺序有着严格要求,否则导致无法正常折叠。

数据折叠保留规则

同一分区内 ORDER BY 字段相同的数据存在多条,且 sign 值不同,数据保留规则如下:

  • 如果 sign = 1sign = -1 的行数相同并且最后一行数据 sign = 1,则保留第一行 sign = -1 的行和最后一行 sign = 1 的行。
  • 如果 sign = 1 的行比 sign = -1 的行多,则保留最后一条 sign = 1 的行。
  • 如果 sign = -1 的行比 sign = 1 的行多,则保留第一条 sign = -1 的行。
  • 其他情况,不保留数据。
-- 创建表 t_collapsing 使用CollapsingMergeTree
:) CREATE TABLE t_collapsing(
:-]     id          UInt8,
:-]     name        String,
:-]     loc         String,
:-]     login_time  UInt8,
:-]     total_dur   UInt8,
:-]     sign        Int8
:-] )
:-] ENGINE = CollapsingMergeTree(sign)
:-] ORDER BY (id, total_dur)
:-] PRIMARY KEY id
:-] PARTITION BY loc;

-- 向表 t_collapsing 中插入数据
:) INSERT INTO t_collapsing VALUES(1, 'zs', 'bj', 1, 30, 1), (2, 'ls', 'sh', 1, 40, 1);

-- 查看表 t_collapsing 中数据
:) SELECT * FROM t_collapsing;
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh  │          1 │        40 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  1 │ zs   │ bj  │          1 │        30 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘

-- 向表 t_collapsing 中继续插入一条数据,删除 id = 1 的数据
:) INSERT INTO t_collapsing VALUES(1, 'zs', 'bj', 1, 30, -1);

-- 查询表 t_collapsing_mt 中的数据 
:) SELECT * FROM t_collapsing;
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh  │          1 │        40 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  1 │ zs   │ bj  │          1 │        30 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  1 │ zs   │ bj  │          1 │        30 │   -1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘

-- 手动触发 OPTIMIZE 合并相同分区数据
:) OPTIMIZE TABLE t_collapsing FINAL;

-- sign = 1 和 sign = -1 的数据被抵消了
:) SELECT * FROM t_collapsing;
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh  │          1 │        40 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘


-- 插入以下两条数据
:) INSERT INTO t_collapsing VALUES(2, 'ls', 'sh', 1, 40, -1), (2, 'ls', 'sh', 2, 100, 1);

-- 查询表 t_collapsing 中数据
:) SELECT * FROM t_collapsing;
┌─id─┬─name─┬─loc──┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh.  │          2 │       100 │    1 │
└────┴──────┴──────┴────────────┴───────────┴──────┘
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh  │          1 │        40 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh  │          1 │        40 │   -1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘

-- 手动触发 OPTIMIZE 合并相同分区数据
:) OPTIMIZE TABLE t_collapsing FINAL;

-- 查询表 t_collapsing 中数据
:) SELECT * FROM t_collapsing;
┌─id─┬─name─┬─loc──┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh   │          2 │       100 │    1 │
└────┴──────┴──────┴────────────┴───────────┴──────┘

注意:当数据插入到表中的顺序标记如果不是 1, -1 这种顺序时,合并相同分区内的数据不能达到修改和更新效果。

:) INSERT INTO t_collapsing VALUES(1, 'zs', 'bj', 1, 30, -1), (1, 'zs', 'bj', 1, 30, 1), (2, 'ls', 'sh', 1, 40, 1);

-- 查询表 t_collapsing 中数据
:) SELECT * FROM t_collapsing;
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh  │          1 │        40 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  1 │ zs   │ bj  │          1 │        30 │   -1 │
│  1 │ zs   │ bj  │          1 │        30 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘

-- 手动执行 OPTIMIZE 命令 合并相同分区数据
:) OPTIMIZE TABLE t_collapsing;

-- 查询表 t_collapsing 表中的数据,数据没有变化
:) SELECT * FROM t_collapsing;
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  2 │ ls   │ sh  │          1 │        40 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘
┌─id─┬─name─┬─loc─┬─login_time─┬─total_dur─┬─sign─┐
│  1 │ zs   │ bj  │          1 │        30 │   -1 │
│  1 │ zs   │ bj  │          1 │        30 │    1 │
└────┴──────┴─────┴────────────┴───────────┴──────┘

如果数据的写入程序是单线程执行的,则能够较好地控制写入顺序;如果需要处理的数据量很大,数据的写入程序通常是多线程执行的,那么此时就不能保障数据的写入顺序了。在这种情况下,CollapsingMergeTree 的工作机制就会出现问题。但是可以通过 VersionedCollapsingMergeTree 的表引擎得到解决。

6.3.6、VersionedCollapsingMergeTree


CollapsingMergeTree 表引擎对于数据写入乱序的情况下,不能够实现数据折叠的效果。VersionedCollapsingMergeTree 表引擎的作用与 CollapsingMergeTree 完全相同,它们的不同之处在于,VersionedCollapsingMergeTree 对数据的写入顺序没有要求,在同一个分区内,任意顺序的数据都能够完成折叠操作。

VersionedCollapsingMergeTree 使用 version 列来实现乱序情况下的数据折叠,该引擎除了需要指定一个sign标识之外,还需要指定一个 UInt* 类型的 version 版本号。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
    sign Int8,
    version UInt8
) ENGINE = VersionedCollapsingMergeTree(sign, version)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

合并规则:无论 sign = -1 在前,还是 sign = -1 在前,版本相同的邻近数据会被折叠。

-- 创建表 t_version_collapsing 使用 VersionedCollapsingMergeTree 引擎
:) CREATE TABLE t_version_collapsing(
:-]     id           UInt8,
:-]     name         String,
:-]     loc          String,
:-]     login_times  UInt8,
:-]     total_dur    UInt8,
:-]     sign         Int8,
:-]     version      UInt8
:-] )
:-] ENGINE = VersionedCollapsingMergeTree(sign, version)
:-] ORDER BY (id, total_dur)
:-] PRIMARY KEY id 
:-] PARTITION BY loc;

-- 向表 t_version_collapsing 中插入数据
:) INSERT INTO TABLE t_version_collapsing VALUES(1, 'zs', 'bj', 1, 30, -1, 1), (2, 'ls', 'sh', 1, 40, 1, 2);

-- 查询表 t_version_collapsing_mt 中的数据
:) SELECT * FROM t_version_collapsing;
┌─id─┬─name─┬─loc─┬─login_times─┬─total_dur─┬─sign─┬─version─┐
│  2 │ ls   │ sh  │           1 │        40 │    1 │       2 │
└────┴──────┴─────┴─────────────┴───────────┴──────┴─────────┘
┌─id─┬─name─┬─loc─┬─login_times─┬─total_dur─┬─sign─┬─version─┐
│  1 │ zs   │ bj  │           1 │        30 │   -1 │       1 │
└────┴──────┴─────┴─────────────┴───────────┴──────┴─────────┘

-- 向表 t_version_collapsing 中插入以下数据 删除'zs'信息 更新'ls'信息
:) INSERT INTO TABLE t_version_collapsing VALUES(1, 'zs', 'bj', 1, 30, 1, 1), (2, 'ls', 'sh', 1, 40, -1, 2), (2, 'ls', 'sh', 2, 100, 1, 2);

-- 查询表 t_version_collapsing 中数据
:) SELECT * FROM t_version_collapsing;
┌─id─┬─name─┬─loc─┬─login_times─┬─total_dur─┬─sign─┬─version─┐
│  2 │ ls   │ sh  │           1 │        40 │    1 │       2 │
└────┴──────┴─────┴─────────────┴───────────┴──────┴─────────┘
┌─id─┬─name─┬─loc─┬─login_times─┬─total_dur─┬─sign─┬─version─┐
│  2 │ ls   │ sh  │           1 │        40 │   -1 │       2 │
│  2 │ ls   │ sh  │           2 │       100 │    1 │       2 │
└────┴──────┴─────┴─────────────┴───────────┴──────┴─────────┘
┌─id─┬─name─┬─loc─┬─login_times─┬─total_dur─┬─sign─┬─version─┐
│  1 │ zs   │ bj  │           1 │        30 │   -1 │       1 │
└────┴──────┴─────┴─────────────┴───────────┴──────┴─────────┘
┌─id─┬─name─┬─loc─┬─login_times─┬─total_dur─┬─sign─┬─version─┐
│  1 │ zs   │ bj  │           1 │        30 │    1 │       1 │
└────┴──────┴─────┴─────────────┴───────────┴──────┴─────────┘

-- 手动执行 OPTIMIZE 命令合并相同分区的数据
:) OPTIMIZE TABLE t_version_collapsing FINAL;

-- 查询表 t_version_collapsing 中数据
:) SELECT * FROM t_version_collapsing;
┌─id─┬─name─┬─loc─┬─login_times─┬─total_dur─┬─sign─┬─version─┐
│  2 │ ls   │ sh  │           2 │       100 │    1 │       2 │
└────┴──────┴─────┴─────────────┴───────────┴──────┴─────────┘

6.4、Integration 系列表引擎


ClickHouse 提供了许多与外部系统集成的方法,包括一些表引擎。这些表引擎与其他类型的表引擎类似,可以用于将外部数据导入到 ClickHouse 中,或者在ClickHouse 中直接操作外部数据源。

6.4.1、HDFS


HDFS 引擎支持 ClickHouse 直接读取 HDFS 中特定格式的数据文件,目前文件格式支持 Json、Csv 文件等,ClickHouse 通过 HDFS 引擎建立的表,不会在ClickHouse 中产生数据,读取的是 HDFS 中的数据,将 HDFS 中的数据映射成 ClickHouse 中的一张表,这样就可以使用 SQL 操作 HDFS 中的数据。

ClickHouse 并不能够删除 HDFS 上的数据,当我们在 ClickHouse 客户端中删除了对应的表,只是删除了表结构,HDFS 上的文件并没有被删除,这一点与 Hive的外部表十分相似。

ENGINE = HDFS(URI, format)

注意:URI 是 HDFS 文件路径,format 指定文件格式。

HDFS 文件路径中文件为多个时,URI 可以指定为 some_file_?

当数据映射是 HDFS 多个文件夹下数据时,可以指定 somepath/* 来指定 URI。

服务配置

由于 HDFS 配置 HA 模式,有集群名称,所以 URI 使用 HDFS 集群名称时,ClickHouse 无法识别,这时需要做以下配置:

  • 将 Hadoop 路径下 $HADOOP_HOME/etc/hadoop/hdfs-site.xml 配置文件复制到 /etc/clickhouse-server 目录中。
  • 修改 /etc/init.d/clickhouse-server 文件,加入一行:export LIBHDFS3_CONF=/etc/clickhouse-server/hdfs-site.xml
  • 重启 clickhouse-server 服务:systemctl restart clickhouse-server

当然,也可以不做以上配置,在写 HDFS URI 时,直接写成对应的节点+端口即可。

读写模式

ClickHouse HDFS 引擎表使用 wildcard URI 时,该表为只读,不允许写入。因为 ClickHouse 无法确认数据具体写入哪个文件。

-- t_hdfs_readonly 表仅可查询 不可修改
:) CREATE TABLE t_hdfs_readonly(id UInt8, name String, age UInt8) ENGINE = HDFS('hdfs://hadoop-cluster/ch/*.csv', 'CSV');
:) SELECT * FROM t_hdfs_readonly;
┌─id─┬─name─┬─age─┐
│  3 │  zs  │  21 │
│  4 │  ml  │  22 │
└────┴──────┴─────┘
┌─id─┬─name─┬─age─┐
│  1 │  zs  │  19 │
│  2 │  ls  │  20 │
└────┴──────┴─────┘

-- t_hdfs_readwrite 表可查询 也可写入数据
:) CREATE TABLE t_hdfs_readwrite(id UInt8, name String, age UInt8) ENGINE = HDFS('hdfs://mycluster/chdata.csv', 'CSV');
:) INSERT INTO t_hdfs_readwrite VALUES(5, 'tq', 23), (6, 'zb', 24);
:) SELECT * FROM t_hdfs_readwrite;
┌─id─┬─name─┬─age─┐
│  5 │  tq  │  23 │
│  6 │  zb  │  24 │
└────┴──────┴─────┘

6.4.2、MySQL


ClickHouse MySQL 数据库引擎可以将 MySQL 某个库下的表映射到 ClickHouse 中,使用 ClickHouse 对数据进行操作。ClickHouse 同样支持 MySQL 表引擎,即映射一张 MySQL 中的表到 ClickHouse 中,使用 ClickHouse 进行数据操作,与 MySQL 数据库引擎一样,这里映射的表只能做查询和插入操作,不支持删除和更新操作。

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],
    ...
) ENGINE = MySQL('host:port', 'database', 'table', 'user', 'password'[, replace_query, 'on_duplicate_clause']);
  • host:port:MySQL 服务器名称和端口。
  • database:MySQL 数据库。
  • table:映射的 MySQL 中的表。
  • user & password:MySQL 账户/密码。
  • replace_query:是否使用 REPLACE INTO 插入数据,默认为0,使用 INSERT INTO
  • on_duplicate_clause:默认不使用,可设置为 ON DUPLICATE KEY 后面的语句 ,设置该参数,需要 replace_query 设置为0。
:) CREATE TABLE t_mysql_engine (
:-]      id     UInt8,
:-]      name   String,
:-]      age    UInt8
:-] )
:-] ENGINE = MySQL('node2:3306', 'test', 't_ch', 'root', '123456', 0, 'UPDATE age = VALUES(age)');

6.4.3、Kafka


ClickHouse 中还可以创建表指定为 Kafka 为表引擎,这样创建出的表可以查询到 Kafka 中的流数据。对应创建的表不会将数据存入 ClickHouse 中,这张 Kafka 引擎表相当于 Consumer,消费 Kafka 中的数据,数据被查询之后,就不会再次被查询到。

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) 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_max_block_size = 0,]
    [kafka_skip_broken_messages = N,]
    [kafka_commit_every_batch = 0,]
    [kafka_thread_per_consumer = 0]
  • kafka_broker_list:以 , 分隔的 Kafka Broker 节点列表。
  • kafka_topic_list:Topic 列表。
  • kafka_group_name:Kafka 消费者组名称。
  • kafka_format:Kafka 中消息的格式,例如 JSONEachRow、CSV 等等,具体参照 ClickHouse Formats

在 ClickHouse 中创建的 Kafka 引擎表只是一个数据管道,当查询这张表时就是消费 Kafka 中的数据,数据被消费完成之后,不能再次被读取到。如果想将 Kafka 中的数据持久化到 ClickHouse 中,可以通过物化视图方式访问 Kafka 中的数据,可以通过以下三个步骤完成将 Kafka 中数据持久化到 ClickHouse 中:

  • 创建 Kafka 引擎表,消费 kafka 中的数据。
  • 再创建一张 ClickHouse 中普通引擎表,这张表面向终端用户查询使用。这里生产环境中经常创建 MergeTree 家族引擎表。
  • 创建物化视图,将 Kafka 引擎表数据实时同步到终端用户查询表中。
-- 在 ClickHouse 中创建 t_kafka_consumer 表,使用 Kafka 引擎
:) CREATE TABLE t_kafka_consumer (
:-]       id     UInt8,
:-]       name   String,
:-]       age    UInt8
:-] )
:-] ENGINE = Kafka()
:-] SETTINGS 
:-]     kafka_broker_list = 'node1:9092,node2:9092,node3:9092',
:-]     kafka_topic_list = 'ch-topic',
:-]     kafka_group_name = 'ch-consumer',
:-]     kafka_format = 'JSONEachRow';

-- 在 ClickHouse 中创建一张终端用户查询使用的表 使用 MergeTree 引擎
:) CREATE TABLE t_kafka_mt (
:-]       id      UInt8,
:-]       name    String,
:-]       age     UInt8
:-] ) ENGINE = MergeTree()
:-] ORDER BY id;

-- 创建物化视图 同步表 t_kafka_consumer 数据到 t_kafka_mt 中
:) CREATE MATERIALIZED VIEW  view_consumer TO t_kafka_mt 
:-] AS SELECT id, name, age FROM t_kafka_consumer;
-- 注意:物化视图在 ClickHouse 中也是存储数据的
-- CREATE MATERIALIZED VIEW  view_consumer TO t_kafka_mt 语句是将物化视图 view_consumer 中的数据存储到到对应的 t_kafka_mt 表中
-- 这样同步的目的是如果不想继续同步kafka中的数据 可以直接删除物化视图即可

-- 生产数据
_> kafka-console-producer.sh --broker-list node1:9092,node2:9092,node3:9092 --topic ch-topic
{"id":1,"name":"zs","age":18}
{"id":2,"name":"ls","age":19}
{"id":3,"name":"ww","age":20}
{"id":4,"name":"ml","age":21}
{"id":5,"name":"tq","age":22}


-- 查询表 t_kafka_mt 中数据 数据同步完成
:) SELECT * FROM t_kafka_mt;
┌─id─┬─name─┬─age─┐
│  1 │ zs   │  18 │
│  2 │ ls   │  19 │
│  3 │ ww   │  20 │
│  4 │ ml   │  21 │
│  5 │ tq   │  22 │
└────┴──────┴─────┘
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容