看完这篇,你就知道怎么选用ClickHouse表引擎了

在ClickHouse中,表引擎决定了数据如何读取和存储、支持哪些查询以及怎么支持、是否并行数据访问、是否支持索引、是否支持多线程、是否支持数据复制。
本文将通过实际案例一步一步讲述ClickHouse的表引擎,跟着本文步骤一步一步来做吧。

ClickHouse表引擎总共分为四个系列,即:Log系列,MergeTree系列,Integration系列和Special系列,其中还包含两种特殊的引擎Replicated和Distributed。

一、MergeTree系列

1. MergeTree

MergeTree系列是适用于高负载任务的最通用和功能最强大的表引擎,也是官方主推的存储引擎,支持几乎所有ClickHouse核心功能。

MergeTree表引擎主要用于海量数据分析,支持数据分区、存储有序、主键索引、稀疏索引、数据TTL等。

MergeTree虽然有主键索引,但是其主要作用是加速查询,并不具有唯一约束语义。

假如有如下建表语句:主键为(id, create_time),按照id, create_time排序,数据按照 create_time分区,并保留最近3个月

 1CREATE TABLE test_merge_tree (
2  id UInt32,
3  name String default null,
4  create_time Date
5ENGINE = MergeTree()
6  PARTITION BY create_time
7  ORDER BY  (id, create_time)
8  PRIMARY KEY (id, create_time)
9  TTL create_time + INTERVAL 3 MONTH
10  SETTINGS index_granularity=8192;

写入几条数据:这里写入几条,其中有几条是主键重复的数据

1insert into test_merge_tree values(1'xiaoming','2024-03-05');
2insert into test_merge_tree values(1'xiaohua','2024-03-05');
3insert into test_merge_tree values(2'xiaomao','2024-03-06');
4insert into test_merge_tree values(2'xiaogou','2024-03-06');

查询一下表数据:

 1select * from test_merge_tree;
2------------------------------
3┌─id─┬─name─────┬─create_time─┐
4│  1 │ xiaoming │  2024-03-05 │
5└────┴──────────┴─────────────┘
6┌─id─┬─name────┬─create_time─┐
7│  1 │ xiaohua │  2024-03-05 │
8└────┴─────────┴─────────────┘
9┌─id─┬─name────┬─create_time─┐
10│  2 │ xiaogou │  2024-03-06 │
11└────┴─────────┴─────────────┘
12┌─id─┬─name────┬─create_time─┐
13│  2 │ xiaomao │  2024-03-06 │
14└────┴─────────┴─────────────┘

可以看到:数据并没有按照主键去重。

由于MergeTree采用类似LSM tree的结构,很多存储层处理逻辑直到Compaction期间才会发生,所以我们手动触发一下compaction。

1optimize table test_merge_tree final;

再来查询一下数据:

 1select * from test_merge_tree;
2------------------------------
3┌─id─┬─name────┬─create_time─┐
4│  2 │ xiaomao │  2024-03-06 │
5│  2 │ xiaogou │  2024-03-06 │
6└────┴─────────┴─────────────┘
7┌─id─┬─name─────┬─create_time─┐
8│  1 │ xiaoming │  2024-03-05 │
9│  1 │ xiaohua  │  2024-03-05 │
10└────┴──────────┴─────────────┘

我们发现仍然有4条数据。所以,可以得出结论:MergeTree虽然有主键索引,但是其主要作用是加速查询,而不具有主键唯一约束的作用

2. ReplacingMergeTree

为了解决上述提到的MergeTree相同主键无法去重的问题,ClickHouse提供了ReplacingMergeTree引擎,专门用来做去重。

首先来建一张ReplacingMergeTree引擎的表:

 1CREATE TABLE test_replacing_merge_tree (
2  id UInt32,
3  name String default null,
4  create_time Date
5ENGINE = ReplacingMergeTree()
6  PARTITION BY create_time
7  ORDER BY  (id, create_time)
8  PRIMARY KEY (id, create_time)
9  TTL create_time + INTERVAL 3 MONTH
10  SETTINGS index_granularity=8192;

写入几条数据:

1insert into test_replacing_merge_tree values(1'xiaoming','2024-03-05');
2insert into test_replacing_merge_tree values(1'xiaohua','2024-03-05');
3insert into test_replacing_merge_tree values(2'xiaomao','2024-03-06');
4insert into test_replacing_merge_tree values(2'xiaogou','2024-03-06');

查询一下表数据:

 1select * from test_replacing_merge_tree;
2-----------------------------------
3┌─id─┬─name─────┬─create_time─┐
4│  1 │ xiaoming │  2024-03-05 │
5└────┴──────────┴─────────────┘
6┌─id─┬─name────┬─create_time─┐
7│  2 │ xiaogou │  2024-03-06 │
8└────┴─────────┴─────────────┘
9┌─id─┬─name────┬─create_time─┐
10│  1 │ xiaohua │  2024-03-05 │
11└────┴─────────┴─────────────┘
12┌─id─┬─name────┬─create_time─┐
13│  2 │ xiaomao │  2024-03-06 │
14└────┴─────────┴─────────────┘

可以看到,数据仍然没有去重,我们再来手动触发一下Compaction:

1optimize table test_replacing_merge_tree final;

再次查询一下数据:

1select * from test_replacing_merge_tree;
2-------------------------------------
3┌─id─┬─name────┬─create_time─┐
4│  1 │ xiaohua │  2024-03-05 │
5└────┴─────────┴─────────────┘
6┌─id─┬─name────┬─create_time─┐
7│  2 │ xiaogou │  2024-03-06 │
8└────┴─────────┴─────────────┘

这次我们发现确实去重了。但是需要注意以下限制:

① 在没有彻底optimize之前,可能无法达到主键去重的效果,比如部分数据已经被去重,而另外一部分数据仍旧有主键重复;

② 在分布式场景下,相同primary key的数据可能被sharding到不同节点上,不同shard间可能无法去重

③ optimize是后台动作,无法预测在什么时候进行Compaction

④ 数据量大的时候,手动执行optimize在要消耗大量时间

所以,ReplacingMergeTree可以用于数据最终去重,但是无法解决我们查询过程中数据重复的问题。

3. CollapsingMergeTree

对于以上ReplacingMergeTree的问题,ClickHouse提供了CollapsingMergeTree来消除这个限制。

该引擎要求在建表语句中指定一个标记列Sign,这个sign字段名也可以自定义,不需要必须是sign。后台Compaction时会将主键相同、Sign相反的行进行折叠。Sign=1的行称之为状态行,Sign=-1的行称之为取消行。

要新增状态时,写入一行状态行;需要删除状态时,则写入一行取消行。在后台Compaction时,状态行与取消行会自动做折叠(删除)处理。而尚未进行Compaction的数据,状态行与取消行同时存在。

因此为了能够达到主键折叠(删除)的目的,需要业务层进行以下改造处理:

1① 执行删除操作需要写入取消行,而取消行中需要包含与原始状态行一样的数据(Sign列除外)。所以在应用层需要记录原始状态行的值,或者在执行删除操作前先查询数据库获取原始状态行;
2
3② 由于后台Compaction时机无法预测,在发起查询时,状态行和取消行可能尚未被折叠;另外,ClickHouse无法保证primary key相同的行落在同一个节点上,不在同一节点上的数据无法折叠。因此在进行count(*)、sum(col)等聚合计算时,可能会存在数据冗余的情况。为了获得正确结果,业务层需要改写SQL,将`count()、sum(col)`分别改写为`sum(Sign)、sum(col * Sign)`。

还是用一个例子来说明吧,首先建一张CollapsingMergeTree引擎的表:

 1CREATE TABLE test_collapsing_merge_tree (
2  id UInt32,
3  num_of_apple UInt32 default null,
4  create_time Date,
5  sign Int8
6ENGINE = CollapsingMergeTree(sign)
7  PARTITION BY create_time
8  ORDER BY  (id, create_time)
9  PRIMARY KEY (id, create_time)
10  TTL create_time + INTERVAL 3 MONTH
11  SETTINGS index_granularity=8192;

写入几行数据:

1-- 插入状态行,注意sign一列的值为1
2insert into test_collapsing_merge_tree values(110,'2024-03-05',1);
3-- 插入取消行,注意sign一列的值为-1
4insert into test_collapsing_merge_tree values(110,'2024-03-05',-1);
5-- 插入一行主键相同的新状态行
6insert into test_collapsing_merge_tree values(120,'2024-03-05',1);

查询一下:

 1select * from test_collapsing_merge_tree;
2---------------------------------------
3┌─id─┬─num_of_apple─┬─create_time─┬─sign─┐
4│  1 │           10 │  2024-03-05 │    1 │
5└────┴──────────────┴─────────────┴──────┘
6┌─id─┬─num_of_apple─┬─create_time─┬─sign─┐
7│  1 │           20 │  2024-03-05 │    1 │
8└────┴──────────────┴─────────────┴──────┘
9┌─id─┬─num_of_apple─┬─create_time─┬─sign─┐
10│  1 │           10 │  2024-03-05 │   -1 │
11└────┴──────────────┴─────────────┴──────┘

可以看到未Compaction之前,状态行与取消行共存。所以我们需要改写一下sql:

 1SELECT
2    id,
3    sum(num_of_apple * signAS num_of_apple
4FROM test_collapsing_merge_tree
5GROUP BY id
6HAVING sum(sign) > 0;
7-----------------------
8┌─id─┬─num_of_apple─┐
9│  1 │           20 │
10└────┴──────────────┘

可以看到,这样可以得到正确的结果。我们手动触发一下Compaction看看:

1optimize table test_collapsing_merge_tree final;

再来查询一下数据:

1select * from test_collapsing_merge_tree;
2----------------------------------
3┌─id─┬─num_of_apple─┬─create_time─┬─sign─┐
4│  1 │           20 │  2024-03-05 │    1 │
5└────┴──────────────┴─────────────┴──────┘

可以看到状态行、取消行已经被折叠,只剩下最新的一行状态行。

CollapsingMergeTree虽然解决了主键相同的数据即时删除的问题,但是状态持续变化且多线程并行写入情况下,状态行与取消行位置可能乱序,导致无法正常折叠。比如下面的例子:

首先还是建一个跟上面一样的表结构:

 1CREATE TABLE test_collapsing_merge_tree_no_order (
2  id UInt32,
3  num_of_apple UInt32 default null,
4  create_time Date,
5  sign Int8
6ENGINE = CollapsingMergeTree(sign)
7  PARTITION BY create_time
8  ORDER BY  (id, create_time)
9  PRIMARY KEY (id, create_time)
10  TTL create_time + INTERVAL 3 MONTH
11  SETTINGS index_granularity=8192;

然后写入几条数据:

1-- 先插入取消行,注意sign一列的值为-1
2insert into test_collapsing_merge_tree_no_order values(110,'2024-03-05',-1);
3-- 再插入状态行,注意sign一列的值为1
4insert into test_collapsing_merge_tree_no_order values(110,'2024-03-05',1);

手动触发一下Compaction:

1optimize table test_collapsing_merge_tree_no_order final;

查询一下:

1┌─id─┬─num_of_apple─┬─create_time─┬─sign─┐
2│  1 │           10 │  2024-03-05 │   -1 │
3│  1 │           10 │  2024-03-05 │    1 │
4└────┴──────────────┴─────────────┴──────┘

可以看到即使Compaction之后也无法进行主键折叠,状态行和取消行的数据仍旧都存在。

4. VersionedCollapsingMergeTree

为了解决CollapsingMergeTree乱序写入情况下无法正常折叠问题,VersionedCollapsingMergeTree表引擎在建表语句中新增了一列version,用于在乱序情况下记录状态行与取消行的对应关系。主键相同,且version相同、sign相反的行,在Compaction时会被删除。

与CollapsingMergeTree类似, 为了获得正确结果,业务层需要改写SQL,将count()、sum(col)分别改写为sum(Sign)、sum(col * Sign)

首先还是来建一张VersionedCollapsingMergeTree引擎表:

 1CREATE TABLE test_versioned_collapsing_merge_tree (
2  id UInt32,
3  num_of_apple UInt32 default null,
4  create_time Date,
5  sign Int8,
6  version UInt8
7ENGINE = VersionedCollapsingMergeTree(sign,version)
8  PARTITION BY create_time
9  ORDER BY  (id, create_time)
10  PRIMARY KEY (id, create_time)
11  TTL create_time + INTERVAL 3 MONTH
12  SETTINGS index_granularity=8192;

写入几条数据:

1-- 先插入取消行,注意sign一列的值为-1,version=1
2insert into test_versioned_collapsing_merge_tree values(110,'2024-03-05',-1,1);
3-- 再插入状态行,注意sign一列的值为1,version=1
4insert into test_versioned_collapsing_merge_tree values(110,'2024-03-05',1,1);
5-- 再插入一行主键相同的新状态行,sign=1,version=2
6insert into test_versioned_collapsing_merge_tree values(120,'2024-03-05',1,2);

为了获取正确的结果,仍然需要改写sql:

1SELECT
2    id,
3    sum(num_of_apple * signAS num_of_apple
4FROM test_versioned_collapsing_merge_tree
5GROUP BY id
6HAVING sum(sign) > 0;

手动触发一下Compaction:

1optimize table test_versioned_collapsing_merge_tree final;

再次查询一下:

1select * from test_versioned_collapsing_merge_tree;
2----------------------------------
3┌─id─┬─num_of_apple─┬─create_time─┬─sign─┬─version─┐
4│  1 │           20 │  2024-03-05 │    1 │       2 │
5└────┴──────────────┴─────────────┴──────┴─────────┘

可以看到即使取消行与状态行乱序写入,仍然可以被正确折叠。

5. SummingMergeTree

ClickHouse提供了SummingMergeTree来支持对主键列进行预先聚合。

在后台Compaction时,会将主键相同的多行进行sum求和,然后使用一行数据替换,从而降低存储空间占用,提升聚合计算性能。

首先建个表吧:

1CREATE TABLE summtt
2(
3    key UInt32,
4    value UInt32
5)
6ENGINE = SummingMergeTree()
7ORDER BY key

插入几条数据:

1-- 插入数据
2INSERT INTO summtt Values(1,1),(1,3),(2,2)

查询一下数据:

1select * from summtt;
2--------------------
3┌─key─┬─value─┐
4│   1 │     4 │
5│   2 │     2 │
6└─────┴───────┘

可以看到数据已经被预先聚合,如果没有聚合,需要手动触发一下compaction。

6. AggregatingMergeTree

SummingMergeTree对非主键列进行sum聚合,而ClickHouse也提供另一种引擎:AggregatingMergeTree,他可以指定各种聚合函数。

AggregatingMergeTree的语法比较复杂,需要结合物化视图或ClickHouse的特殊数据类型AggregateFunction一起使用。在insert和select时,也有独特的写法和要求:写入时需要使用-State语法,查询时使用-Merge语法。

示例一:配合物化视图使用

先建一张明细表:

1CREATE TABLE visits
2(
3    UserID UInt64,
4    CounterID UInt8,
5    StartDate Date,
6    Sign Int8
7)
8ENGINE = CollapsingMergeTree(Sign)
9ORDER BY UserID;

插入几条数据:

1INSERT INTO visits VALUES(00'2024-03-06'1);
2INSERT INTO visits VALUES(11'2024-03-07'1);

建立物化视图,对明细表进行预先聚合:

1CREATE MATERIALIZED VIEW visits_agg_view
2ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMM(StartDate) ORDER BY (CounterID, StartDate)
3AS SELECT
4    CounterID,
5    StartDate,
6    sumState(Sign)    AS Visits,
7    uniqState(UserID) AS Users
8FROM visits
9GROUP BY CounterID, StartDate;

查询物化视图:

1SELECT
2    StartDate,
3    sumMerge(Visits) AS Visits,
4    uniqMerge(UsersAS Users
5FROM visits_agg_view
6GROUP BY StartDate
7ORDER BY StartDate;

示例二:配合AggregateFunction使用

先一张明细表:

1CREATE TABLE detail_table
2(   CounterID UInt8,
3    StartDate Date,
4    UserID UInt64
5ENGINE = MergeTree() 
6PARTITION BY toYYYYMM(StartDate) 
7ORDER BY (CounterID, StartDate);

写入几条数据:

1INSERT INTO detail_table VALUES(1'2024-03-05'1);
2INSERT INTO detail_table VALUES(2'2024-03-04'1);

建一张聚合表:

1CREATE TABLE agg_table
2(   CounterID UInt8,
3    StartDate Date,
4    UserID AggregateFunction(uniq, UInt64)
5ENGINE = AggregatingMergeTree() 
6PARTITION BY toYYYYMM(StartDate) 
7ORDER BY (CounterID, StartDate);

导入明细表数据到聚合表中:

1INSERT INTO agg_table
2select CounterID, StartDate, uniqState(UserID)
3from detail_table
4group by CounterID, StartDate

从聚合表中查询数据:

1SELECT uniqMerge(UserID) AS state 
2FROM agg_table 
3GROUP BY CounterID, StartDate;

二、 Integration系列

这种引擎主要用于将外部数据导入到ClickHouse中,或者在ClickHouse中直接操作外部数据源。这里举一些常见的例子:

1. Hive

 1CREATE TABLE test.test_orc
2(
3    `f_tinyint` Int8,
4    `f_smallint` Int16,
5    `f_int` Int32,
6    `f_integer` Int32,
7    `f_bigint` Int64,
8    `f_float` Float32,
9    `f_double` Float64,
10    `f_decimal` Float64,
11    `f_timestamp` DateTime,
12    `f_date` Date,
13    `f_string` String,
14    `f_varchar` String,
15    `f_bool` Bool,
16    `f_binary` String,
17    `f_array_int` Array(Int32),
18    `f_array_string` Array(String),
19    `f_array_float` Array(Float32),
20    `f_array_array_int` Array(Array(Int32)),
21    `f_array_array_string` Array(Array(String)),
22    `f_array_array_float` Array(Array(Float32)),
23    `day` String
24)
25ENGINE = Hive('thrift://localhost:9083''test''test_orc')
26PARTITION BY day

2. Jdbc

1CREATE TABLE jdbc_table
2(
3    `int_id` Int32,
4    `int_nullable` Nullable(Int32),
5    `float` Float32,
6    `float_nullable` Nullable(Float32)
7)
8ENGINE JDBC('jdbc:mysql://localhost:3306/?user=root&password=root''test''test')

3. Hdfs

1CREATE TABLE hdfs_engine_table 
2(
3  name Stringvalue UInt32
4
5ENGINE=HDFS('hdfs://hdfs1:9000/other_storage''TSV')

4. Kafka

 1-- 创建kafka表
2CREATE TABLE source
3(
4    `ts` DateTime, 
5    `tag` String
6    `message` String
7)
8ENGINE = Kafka()
9SETTINGS kafka_broker_list = '10.10.80.70:6667,10.10.80.71:6667,10.10.80.72:6667'
10         kafka_topic_list = 'tag',
11         kafka_group_name = 'clickhouse'
12         kafka_format = 'JSONEachRow',
13         kafka_skip_broken_messages = 1,
14         kafka_num_consumers = 2
15
16-- 创建ClickHouse表
17CREATE TABLE target
18(
19    `ts` DateTime, 
20    `tag` String
21)
22ENGINE = MergeTree()
23PARTITION BY toYYYYMM(ts)
24ORDER BY tag
25
26-- 将kafka数据导入ClickHouse
27CREATE MATERIALIZED VIEW source_mv TO target 
28AS SELECT ts, tag FROM source;

三、Log系列

Log系列表引擎功能相对简单,主要用于快速写入小表,一次性读出的场景。

Log系列包括以下几种引擎:

① TinyLog

不支持并发读取数据文件,格式简单,适合用来做临时表。

② StripLog

支持并发读取数据文件。将所有列存储在同一个大文件中,减少了文件个数。

③ Log

支持并发读取数据文件。每个列会单独存储在一个独立文件中。

他们的共同点是:

① 数据被顺序追加到磁盘上

② 不支持delete、update

③ 不支持index

④ 不支持原子性写

⑤ insert会阻塞select操作

四、Special系列

Special系列的表引擎,是为了特定场景而定制的。比如Memory,Buffer,Dictionary,File,URL等等。

五、总结

ClickHouse提供了丰富多样的表引擎供我们使用。本文主要讲述了ClickHouse的表引擎,同时对于MergeTree系列表引擎进行了详细对比和示例。
比如Log系列用来做小表数据分析、MergeTree系列用来做大数据量分析,而Integration系列用于外部数据集成,除了这些表引擎之外,ClickHouse还提供了Replicated、Distributed等高级表引擎,以提供更好的可靠性保证和分布式能力。


往期推荐

Flink 自定义各种UDF函数实践

Doris Catalog入门学习

用代码实例讲解Flink WaterMark

Flink 多维实时分析的真实案例


请使用浏览器的分享功能分享到微信等