文章目录
- Clickhouse 表引擎—MergeTree系列
- ClickHouse表引擎概览
- Log系列
- Integration系列
- Special系列
- MergeTree系列
- MergeTree
- ReplacingMergeTree
- CollapsingMergeTree
- VersionedCollapsingMergeTree
- SummingMergeTree
- AggregatingMergeTree
- 示例一:**配合物化视图使用**
- 示例二:**配合特殊数据类型AggregateFunction使用**
- 总结
Clickhouse 表引擎—MergeTree系列
表引擎是Clickhouse 的一大特色。可以说,表引擎决定了如何存储表的数据。包括:
- 数据的存储方式和位置,写到哪里以及从哪里读取数据。
- 支持哪些查询以及如何支持。
- 并发数据访问。
- 索引的使用(如果存在)。
- 是否可以执行多线程请求。
- 数据复制。
ClickHouse提供了大约28种表引擎,各有各的用途,比如有Lo
系列用来做小表数据分析,MergeTree
系列用来做大数据量分析,而Integration
系列则多用于外表数据集成。再考虑复制表Replicated
系列,分布式表Distributed
等,纷繁复杂,新用户上手选择时常常感到迷惑。
ClickHouse表引擎概览
下图是ClickHouse提供的所有表引擎汇总。
一共分为四个系列,分别是Log、MergeTree、Integration、Special。其中包含了两种特殊的表引擎Replicated、Distributed,功能上与其他表引擎正交,我们后续会单独写一篇文章来介绍。
Log系列
Log系列表引擎功能相对简单,主要用于快速写入小表(1百万行左右的表),然后全部读出的场景。
几种Log表引擎的共性是:
- 数据被顺序append写到磁盘上;
- 不支持delete、update;
- 不支持index;
- 不支持原子性写;
- insert会阻塞select操作。
它们彼此之间的区别是:
- TinyLog:不支持并发读取数据文件,查询性能较差;格式简单,适合用来暂存中间数据;
- StripLog:支持并发读取数据文件,查询性能比TinyLog好;将所有列存储在同一个大文件中,减少了文件个数;
- Log:支持并发读取数据文件,查询性能比TinyLog好;每个列会单独存储在一个独立文件中。
Integration系列
该系统表引擎主要用于将外部数据导入到ClickHouse中,或者在ClickHouse中直接操作外部数据源。
- Kafka:将Kafka Topic中的数据直接导入到ClickHouse;
- MySQL:将Mysql作为存储引擎,直接在ClickHouse中对MySQL表进行select等操作;
- JDBC/ODBC:通过指定jdbc、odbc连接串读取数据源;
- HDFS:直接读取HDFS上的特定格式的数据文件;
Special系列
Special系列的表引擎,大多是为了特定场景而定制的。这里也挑选几个简单介绍,不做详述。
- Memory:将数据存储在内存中,重启后会导致数据丢失。查询性能极好,适合于对于数据持久性没有要求的1亿一下的小表。在ClickHouse中,通常用来做临时表。
- Buffer:为目标表设置一个内存buffer,当buffer达到了一定条件之后会flush到磁盘。
- File:直接将本地文件作为数据存储;
- Null:写入数据被丢弃、读取数据为空;
MergeTree系列
Clickhouse 中最强大的表引擎当属 MergeTree
(合并树)引擎及该系列(*MergeTree
)中的其他引擎。
Log、Special、Integration主要用于特殊用途,场景相对有限。MergeTree系列才是官方主推的存储引擎,支持几乎所有ClickHouse核心功能。
MergeTree
系列的引擎被设计用于插入极大量的数据到一张表当中。数据可以以数据片段的形式一个接着一个的快速写入,数据片段在后台按照一定的规则进行合并。相比在插入时不断修改(重写)已存储的数据,这种策略会高效很多。
以下重点介绍MergeTree、ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree、SummingMergeTree、AggregatingMergeTree引擎。
MergeTree
MergeTree表引擎主要用于海量数据分析,支持数据分区、存储有序、主键索引、稀疏索引、数据TTL等。MergeTree支持所有ClickHouse SQL语法,但是有些功能与MySQL并不一致,比如在MergeTree中主键并不用于去重,以下通过示例说明。
如下建表DDL所示,test_tbl的主键为(id, create_time),并且按照主键进行存储排序,按照create_time进行数据分区,数据保留最近一个月。
CREATE TABLE test_tbl (
id UInt16,
create_time Date,
comment Nullable(String)
) ENGINE = MergeTree()
PARTITION BY create_time
ORDER BY (id, create_time)
PRIMARY KEY (id, create_time)
TTL create_time + INTERVAL 1 MONTH
SETTINGS index_granularity=8192;
写入数据:值得注意的是这里我们写入了几条primary key相同的数据。
insert into test_tbl values(0, '2019-12-12', null);
insert into test_tbl values(0, '2019-12-12', null);
insert into test_tbl values(1, '2019-12-13', null);
insert into test_tbl values(1, '2019-12-13', null);
insert into test_tbl values(2, '2019-12-14', null);
查询数据: 可以看到虽然主键id、create_time相同的数据只有3条数据,但是结果却有5行。
select count(*) from test_tbl;
┌─count()─┐
│ 5 │
└─────────┘
select * from test_tbl;
┌─id─┬─create_time─┬─comment─┐
│ 2 │ 2019-12-14 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 1 │ 2019-12-13 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 0 │ 2019-12-12 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 1 │ 2019-12-13 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 0 │ 2019-12-12 │ NULL │
└────┴─────────────┴─────────┘
查询结果来自clickhouse-client, 图中的分割表明数据来自不同的数据块,例如在其他客户端查询工具则无法展示此差异
由于MergeTree采用类似LSM tree的结构,很多存储层处理逻辑直到Compaction期间才会发生。因此强制后台compaction执行完毕,再次查询,发现仍旧有5条数据。
optimize table test_tbl final;
select count(*) from test_tbl;
┌─count()─┐
│ 5 │
└─────────┘
select * from test_tbl;
┌─id─┬─create_time─┬─comment─┐
│ 2 │ 2019-12-14 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 0 │ 2019-12-12 │ NULL │
│ 0 │ 2019-12-12 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 1 │ 2019-12-13 │ NULL │
│ 1 │ 2019-12-13 │ NULL │
└────┴─────────────┴─────────┘
结合以上示例可以看到,MergeTree虽然有主键索引,但是其主要作用是加速查询,而不是类似MySQL等数据库用来保持记录唯一。即便在Compaction完成后,主键相同的数据行也仍旧共同存在。
ReplacingMergeTree
为了解决MergeTree相同主键无法去重的问题,ClickHouse提供了ReplacingMergeTree引擎,用来做去重。也就是说该引擎和 MergeTree的不同之处在于它会删除排序键值相同的重复项。
需要注意的是
数据的去重只会在数据合并期间进行。合并会在后台一个不确定的时间进行,因此你无法预先作出计划。有一些数据可能仍未被处理。尽管你可以调用 OPTIMIZE
语句发起计划外的合并,但请不要依靠它,因为 OPTIMIZE
语句会引发对数据的大量读写。
因此,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*
, Date
或 DateTime
。可选参数。
在数据合并的时候,ReplacingMergeTree
从所有具有相同排序键的行中选择一行留下:
- 如果
ver
列未指定,保留最后一条。 - 如果
ver
列已指定,保留ver
值最大的版本。
示例如下:
-- 建表
CREATE TABLE test_tbl_replacing (
id UInt16,
create_time Date,
comment Nullable(String)
) ENGINE = ReplacingMergeTree()
PARTITION BY create_time
ORDER BY (id, create_time)
PRIMARY KEY (id, create_time)
TTL create_time + INTERVAL 1 MONTH
SETTINGS index_granularity=8192;
-- 写入主键重复的数据
insert into test_tbl_replacing values(0, '2019-12-12', null);
insert into test_tbl_replacing values(0, '2019-12-12', null);
insert into test_tbl_replacing values(1, '2019-12-13', null);
insert into test_tbl_replacing values(1, '2019-12-13', null);
insert into test_tbl_replacing values(2, '2019-12-14', null);
-- 查询,可以看到未compaction之前,主键重复的数据,仍旧存在。
select count(*) from test_tbl_replacing;
┌─count()─┐
│ 5 │
└─────────┘
select * from test_tbl_replacing;
┌─id─┬─create_time─┬─comment─┐
│ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 0 │ 2019-12-12 │ ᴺᵁᴸᴸ │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 1 │ 2019-12-13 │ ᴺᵁᴸᴸ │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 2 │ 2019-12-14 │ ᴺᵁᴸᴸ │
└────┴─────────────┴─────────┘
-- 强制后台compaction:
optimize table test_tbl_replacing final;
-- 再次查询:主键重复的数据已经消失。
select count(*) from test_tbl_replacing;
┌─count()─┐
│ 3 │
└─────────┘
select * from test_tbl_replacing;
┌─id─┬─create_time─┬─comment─┐
│ 2 │ 2019-12-14 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 1 │ 2019-12-13 │ NULL │
└────┴─────────────┴─────────┘
┌─id─┬─create_time─┬─comment─┐
│ 0 │ 2019-12-12 │ NULL │
└────┴─────────────┴─────────┘
虽然ReplacingMergeTree提供了主键去重的能力,但是仍旧有以下限制:
- 在没有彻底optimize之前,可能无法达到主键去重的效果,比如部分数据已经被去重,而另外一部分数据仍旧有主键重复;
- 在分布式场景下,相同primary key的数据可能被sharding到不同节点上,不同shard间可能无法去重;
- optimize是后台动作,无法预测具体执行时间点;
- 手动执行optimize在海量数据场景下要消耗大量时间,无法满足业务即时查询的需求;
因此ReplacingMergeTree更多被用于确保数据最终被去重,而无法保证查询过程中主键不重复。
CollapsingMergeTree
有这样的一个场景,为某个对象保存不断变化的数据这样一个情况, 似乎为一个对象保存一行记录并在其发生任何变化时更新记录是合乎逻辑的,但是更新操作对 DBMS 来说是昂贵且缓慢的,因为它需要重写存储中的数据。
如果你需要快速的写入数据,则更新操作是不可接受的,但是你可以按下面的描述顺序地更新一个对象的变化。
在写入行的时候使用特定的列 Sign
。如果 Sign = 1
则表示这一行是对象的状态,我们称之为状态行。如果 Sign = -1
则表示是对具有相同属性的状态行的取消,我们称之为取消行。
ClickHouse实现了CollapsingMergeTree来消除ReplacingMergeTree的限制。该引擎要求在建表语句中指定一个标记列Sign,后台Compaction时会将主键相同、Sign相反的行进行折叠,也即删除。
因此,该引擎可以显著的降低存储量并提高 SELECT
查询效率,而且还会减少需要存储的数据量
CollapsingMergeTree将行按照Sign的值分为两类:Sign=1的行称之为状态行,Sign=-1的行称之为取消行。
- 每次需要新增状态时,写入一行状态行
- 需要删除状态时,则写入一行取消行。
在后台Compaction时,状态行与取消行会自动做折叠(删除)处理。而尚未进行Compaction的数据,状态行与取消行同时存在。
因此为了能够达到主键折叠(删除)的目的,需要业务层进行适当改造:
- 执行删除操作需要写入取消行,而取消行中需要包含与原始状态行一样的数据(Sign列除外)。所以在应用层需要记录原始状态行的值,或者在执行删除操作前先查询数据库获取原始状态行;
- 由于后台Compaction时机无法预测,在发起查询时,状态行和取消行可能尚未被折叠;另外,ClickHouse无法保证primary key相同的行落在同一个节点上,不在同一节点上的数据无法折叠。因此在进行count(*)、sum(col)等聚合计算时,可能会存在数据冗余的情况。为了获得正确结果,业务层需要改写SQL,将
count()、sum(col)
分别改写为sum(Sign)、sum(col * Sign)
。
例如,我们想要计算用户在某个站点访问的页面页面数以及他们在那里停留的时间。
CREATE TABLE test_tbl_collapsing_merge
(
UserID UInt64,
PageViews UInt8,
Duration UInt8,
Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID;
在某个时候,我们将用户的活动状态写入下面这样的行。
-- 插入状态行,注意sign一列的值为1
INSERT INTO test_tbl_collapsing_merge VALUES (4324182021466249494, 5, 146, 1);
一段时间后,我们写入下面的两行来记录用户活动的变化,第一行是取消上一次的状态的行,注意sign一列的值为-1,其余值与状态行一致;
-- 插入一行取消行,用于抵消上述状态行。
INSERT INTO test_tbl_collapsing_merge VALUES (4324182021466249494, 5, 146, -1);
-- 并且插入一行主键相同的新状态行,用来将PageViews从5更新至6,将Duration从146更新为185.
INSERT INTO test_tbl_collapsing_merge VALUES (4324182021466249494, 6, 185, 1);
此时的数据情况
SELECT * FROM test_tbl_collapsing_merge;
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 5 │ 146 │ -1 │
│ 4324182021466249494 │ 6 │ 185 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
第一行取消了这个对象(用户)的状态。它需要复制被取消的状态行的所有除了 Sign
的属性。
第二行包含了当前的状态。因为我们只需要用户活动的最后状态,这些行
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 5 │ 146 │ -1 │
│ 4324182021466249494 │ 6 │ 185 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
可以在折叠对象的失效(老的)状态的时候被删除。CollapsingMergeTree
会在合并数据片段的时候做这件事。我们可以看到折叠其实就是删除
折叠不应该改变统计数据的结果。 变化逐渐地被折叠,因此最终几乎每个对象都只剩下了最后的状态。
Sign
是必须的因为合并算法不保证所有有相同主键的行都会在同一个结果数据片段中,甚至是在同一台物理服务器上。ClickHouse 用多线程来处理 SELECT
请求,所以它不能预测结果中行的顺序。如果要从 CollapsingMergeTree
表中获取完全折叠后的数据,则需要聚合。
要完成折叠,请使用 GROUP BY
子句和用于处理符号的聚合函数编写请求。例如,要计算数量,使用 sum(Sign)
而不是 count()
。要计算某物的总和,使用 sum(Sign * x)
而不是 sum(x)
,并添加 HAVING sum(Sign) > 0
子句。
聚合体 count
,sum
和 avg
可以用这种方式计算。如果一个对象至少有一个未被折叠的状态,则可以计算 uniq
聚合。min
和 max
聚合无法计算,因为 CollaspingMergeTree
不会保存折叠状态的值的历史记录。
如果你需要在不进行聚合的情况下获取数据(例如,要检查是否存在最新值与特定条件匹配的行),你可以在 FROM
从句中使用 FINAL
修饰符。这种方法显然是更低效的。
说白了就是你要为中间状态买单,除非你能确定所有的折叠都已经完成了
以下用示例说明:
-- 为了获取正确的sum值,需要改写SQL:
-- sum(PageViews) => sum(PageViews * Sign)、
-- sum(Duration) => sum(Duration * Sign)
SELECT
UserID,
sum(PageViews * Sign) AS PageViews,
sum(Duration * Sign) AS Duration
FROM
test_tbl_collapsing_merge
GROUP BY
UserID
HAVING
sum(Sign) > 0;
┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │ 6 │ 185 │
└─────────────────────┴───────────┴──────────┘
如果我们不需要聚合并想要强制进行折叠,我们可以在 FROM
从句中使用 FINAL
修饰语。
SELECT * FROM test_tbl_collapsing_merge final ;
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 6 │ 185 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
或者是我们强制Compaction
-- 强制后台Compaction
optimize table test_tbl_collapsing_merge final;
-- 再次查询,可以看到状态行、取消行已经被折叠,只剩下最新的一行状态行。
select * from test_tbl_collapsing_merge;
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 6 │ 185 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
这种查询数据的方法是非常低效的。不要在大表中使用它。
CollapsingMergeTree虽然解决了主键相同的数据即时删除的问题,但是状态持续变化且多线程并行写入情况下,状态行与取消行位置可能乱序,导致无法正常折叠。
如下面乱序插入例子所示:
-- 建表
CREATE TABLE test_tbl_collapsing_merge2
(
UserID UInt64,
PageViews UInt8,
Duration UInt8,
Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID;
-- 先插入取消行
INSERT INTO test_tbl_collapsing_merge2 VALUES (4324182021466249495, 5, 146, -1);
-- 后插入状态行
INSERT INTO test_tbl_collapsing_merge2 VALUES (4324182021466249495, 5, 146, 1);
-- 强制Compaction
optimize table test_tbl_collapsing_merge2 final;
-- 可以看到即便Compaction之后也无法进行主键折叠: 2行数据仍旧都存在。
select * from test_tbl_collapsing_merge2;
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249495 │ 5 │ 146 │ -1 │
│ 4324182021466249495 │ 5 │ 146 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
VersionedCollapsingMergeTree
为了解决CollapsingMergeTree乱序写入情况下无法正常折叠问题,VersionedCollapsingMergeTree表引擎在建表语句中新增了一列Version,用于在乱序情况下记录状态行与取消行的对应关系。主键相同,且Version相同、Sign相反的行,在Compaction时会被删除。
所以VersionedCollapsingMergeTree
用于相同的目的,但使用不同的折叠算法,允许以多个线程的任何顺序插入数据。 特别是, Version
列有助于正确折叠行,即使它们以错误的顺序插入。 相比之下, CollapsingMergeTree
只允许严格连续插入。
与CollapsingMergeTree类似, 为了获得正确结果,业务层需要改写SQL,将count()、sum(col)
分别改写为sum(Sign)、sum(col * Sign)
。
建表语句如下
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE = VersionedCollapsingMergeTree(sign, version)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
引擎参数
VersionedCollapsingMergeTree(sign, version)
-
sign
— 指定行类型的列名: 1
是一个 “state” 行, -1
是一个 “cancel” 行。 列数据类型应为 Int8
. -
version
— 指定对象状态版本的列名。列数据类型应为 UInt*
.
这里的示例和上面的一样,我们就看能不能CollapsingMergeTree
乱序的问题
-- 建表
CREATE TABLE test_tbl_version_collapsing_merge
(
UserID UInt64,
PageViews UInt8,
Duration UInt8,
Sign Int8,
Version UInt8
)
ENGINE = VersionedCollapsingMergeTree(Sign, Version)
ORDER BY UserID;
乱序行插入,可以参考CollapsingMergeTree 的例子
-- 先插入一行取消行,注意Signz=-1, Version=1
INSERT INTO test_tbl_version_collapsing_merge VALUES (4324182021466249494, 5, 146, -1, 1);
-- 后插入一行状态行,注意Sign=1, Version=1;
INSERT INTO test_tbl_version_collapsing_merge VALUES (4324182021466249494, 5, 146, 1, 1);
插入最后的结果行
-- 新的状态行注意Sign=1, Version=2,将PageViews从5更新至6,将Duration从146更新为185。
INSERT INTO test_tbl_version_collapsing_merge VALUES (4324182021466249494, 6, 185, 1, 2);
查看结果
-- 查询可以看到未compaction情况下,所有行都可见。
SELECT * FROM test_tbl_version_collapsing_merge;
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 5 │ 146 │ -1 │
│ 4324182021466249494 │ 6 │ 185 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
为了获取正确的sum值,需要改写SQL:
-- sum(PageViews) => sum(PageViews * Sign)、
-- sum(Duration) => sum(Duration * Sign)
SELECT
UserID,
sum(PageViews * Sign) AS PageViews,
sum(Duration * Sign) AS Duration
FROM test_tbl_version_collapsing_merge
GROUP BY
UserID
HAVING
sum(Sign) > 0;
┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │ 6 │ 185 │
└─────────────────────┴───────────┴──────────┘
看看能不能CollapsingMergeTree
乱序的问题
-- 强制后台Compaction
optimize table test_tbl_version_collapsing_merge final;
-- 再次查询,可以看到即便取消行与状态行位置乱序,仍旧可以被正确折叠。
select * from test_tbl_version_collapsing_merge;
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘
SummingMergeTree
ClickHouse通过SummingMergeTree来支持对主键列进行预聚合。ClickHouse 会把所有具有相同主键的行合并为一行,该行包含了被合并的行中具有数值数据类型的列的汇总值。如果主键的组合方式使得单个键值对应于大量的行,则可以显著的减少存储空间并加快数据查询的速度。
我们推荐将该引擎和 MergeTree
一起使用。例如,在准备做报告的时候,将完整的数据存储在 MergeTree
表中,并且使用 SummingMergeTree
来存储聚合数据。这种方法可以使你避免因为使用不正确的主键组合方式而丢失有价值的数据。
值得注意的是:
- ClickHouse只在后台Compaction时才会进行数据的预先聚合,而compaction的执行时机无法预测,所以可能存在部分数据已经被预先聚合、部分数据尚未被聚合的情况。因此,在执行聚合计算时,SQL中仍需要使用GROUP BY子句。
- 在预先聚合时,ClickHouse会对主键列之外的其他所有列进行预聚合。如果这些列是可聚合的(比如数值类型),则直接sum;如果不可聚合(比如String类型),则随机选择一个值。
- 通常建议将SummingMergeTree与MergeTree配合使用,使用MergeTree来存储具体明细,使用SummingMergeTree来存储预先聚合的结果加速查询。
建表语句如下
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, ...]
SummingMergeTree 的参数
columns
包含了将要被汇总的列的列名的元组。可选参数。 所选的列必须是数值类型,并且不可位于主键中。如果没有指定 columns
,ClickHouse 会把所有不在主键中的数值类型的列都进行汇总。
示例如下:
-- 建表
CREATE TABLE test_tbl_summary_merge
(
key UInt32,
value UInt32
)
ENGINE = SummingMergeTree()
ORDER BY key
-- 插入数据(不要在一个语句中插入三条数据可能无法复现)
INSERT INTO test_tbl_summary_merge Values(1,1);
INSERT INTO test_tbl_summary_merge Values(1,2);
INSERT INTO test_tbl_summary_merge Values(2,1);
-- compaction前查询,仍存在多行
select * from test_tbl_summary_merge;
┌─key─┬─value─┐
│ 1 │ 1 │
│ 1 │ 2 │
│ 2 │ 1 │
└─────┴───────┘
ClickHouse可能不会完整的汇总所有行,因此我们在查询中使用了聚合函数 sum
和 GROUP BY
子句。
-- 通过GROUP BY进行聚合计算
SELECT key, sum(value) FROM test_tbl_summary_merge GROUP BY key
┌─key─┬─sum(value)─┐
│ 2 │ 1 │
│ 1 │ 3 │
└─────┴────────────┘
手动触发合并
-- 强制compaction
optimize table test_tbl_summary_merge final;
-- compaction后查询,可以看到数据已经被预先聚合
select * from test_tbl_summary_merge;
┌─key─┬─value─┐
│ 1 │ 3 │
│ 2 │ 1 │
└─────┴───────┘
-- compaction后,仍旧需要通过GROUP BY进行聚合计算,因为可能有新的数据插入(除非你确认没有)
SELECT key, sum(value) FROM test_tbl_summary_merge GROUP BY key
┌─key─┬─sum(value)─┐
│ 2 │ 1 │
│ 1 │ 3 │
└─────┴────────────┘
我们可以将其看作是一定程度上的预聚合,而且聚合方式单一。
需要注意的是
- 列中数值类型的值会被汇总。这些列的集合在参数
columns
中被定义。 - 如果用于汇总的所有列中的值均为0,则该行会被删除。
- 如果列不在主键中且无法被汇总,则会在现有的值中任选一个。
- 主键所在的列中的值不会被汇总。
AggregatingMergeTree
AggregatingMergeTree也是预先聚合引擎的一种,用于提升聚合计算的性能。与SummingMergeTree的区别在于:SummingMergeTree对非主键列进行sum聚合,而AggregatingMergeTree则可以指定各种聚合函数。
AggregatingMergeTree的语法比较复杂,需要结合物化视图或ClickHouse的特殊数据类型AggregateFunction一起使用。在insert和select时,也有独特的写法和要求:写入时需要使用-State语法,查询时使用-Merge语法。
对于 SELECT
查询的结果, AggregateFunction
类型的值对 ClickHouse 的所有输出格式都实现了特定的二进制表示法。在进行数据转储时,例如使用 TabSeparated
格式进行 SELECT
查询,那么这些转储数据也能直接用 INSERT
语句导回
以下通过示例进行介绍。
示例一:配合物化视图使用
建立明细表
CREATE TABLE test_tbl_visits
(
UserID UInt64,
CounterID UInt8,
StartDate Date,
Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID;
对明细表建立物化视图,该物化视图对明细表进行预先聚合
注意:
预先聚合使用的函数分别为: sumState, uniqState。对应于写入语法<agg>-State
.
CREATE MATERIALIZED VIEW test_view_visits_agg
ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMM(StartDate) ORDER BY (CounterID, StartDate)
AS SELECT
CounterID,
StartDate,
sumState(Sign) AS Visits,
uniqState(UserID) AS Users
FROM test_tbl_visits
GROUP BY CounterID, StartDate;
插入明细数据
INSERT INTO test_tbl_visits VALUES(0, 0, '2019-11-11', 1);
INSERT INTO test_tbl_visits VALUES(1, 1, '2019-11-12', 1);
数据会同时插入到表和视图中,并且视图 test_view_visits_agg
会将里面的数据聚合。
要获取聚合数据,需要对物化视图进行最终的聚合操作
注意:使用的聚合函数为 sumMerge, uniqMerge。对应于查询语法<agg>-Merge
.
SELECT
StartDate,
sumMerge(Visits) AS Visits,
uniqMerge(Users) AS Users
FROM
test_view_visits_agg
GROUP BY
StartDate
ORDER BY
StartDate;
’
普通函数 sum, uniq不再可以使用
-- 如下SQL会报错: Illegal type AggregateFunction(sum, Int8) of argument
SELECT
StartDate,
sum(Visits),
uniq(Users)
FROM visits_agg_view
GROUP BY StartDate
ORDER BY StartDate;
示例二:配合特殊数据类型AggregateFunction使用
建立明细表
CREATE TABLE test_tbl_detail
( CounterID UInt8,
StartDate Date,
UserID UInt64
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(StartDate)
ORDER BY (CounterID, StartDate);
插入明细数据
INSERT INTO test_tbl_detail VALUES(0, '2019-11-11', 1);
INSERT INTO test_tbl_detail VALUES(1, '2019-11-12', 1);
INSERT INTO test_tbl_detail VALUES(1, '2019-11-12', 2);
建立预先聚合表,其中UserID一列的类型为:AggregateFunction(uniq, UInt64)
CREATE TABLE test_tbl_detail_agg
( CounterID UInt8,
StartDate Date,
UserID AggregateFunction(uniq, UInt64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(StartDate)
ORDER BY (CounterID, StartDate);
从明细表中读取数据,插入聚合表。
-- 注意:子查询中使用的聚合函数为 uniqState, 对应于写入语法<agg>-State
INSERT INTO test_tbl_detail_agg
select
CounterID, StartDate, uniqState(UserID)
from
test_tbl_detail
group by
CounterID, StartDate
不能使用普通insert语句向AggregatingMergeTree中插入数据。下面的SQL会报错:Cannot convert UInt64 to AggregateFunction(uniq, UInt64)
INSERT INTO test_tbl_detail_agg VALUES(1, '2019-11-12', 1);
从聚合表中查询。
-- 注意:select中使用的聚合函数为uniqMerge,对应于查询语法<agg>-Merge
SELECT
CounterID, StartDate,uniqMerge(UserID) AS state
FROM
test_tbl_detail_agg
GROUP
BY CounterID, StartDate;
’
总结
ClickHouse提供了丰富多样的表引擎,应对不同的业务需求。本文概览了ClickHouse的表引擎,同时对于MergeTree系列表引擎进行了详细对比和样例示范。
在这些表引擎之外,ClickHouse还提供了Replicated、Distributed等高级表引擎,我们会在后续进一步深度解读。
- MergeTree 不能做到去重
- ReplacingMergeTree 针对MergeTree 不能去重实现了去重的能力
-
CollapsingMergeTree
会异步的删除(折叠)这些除了特定列Sign
有1
和-1
的值以外,其余所有字段的值都相等的成对的行。没有成对的行会被保留。也就是提供了一种删除能力虽然是异步的,也就是最终一致性。需要注意的是前面两种都没有删除能力 - VersionedCollapsingMergeTree解决了CollapsingMergeTree顺序错误的情况下不能被正确折叠的问题
- SummingMergeTree提供了类似ReplacingMergeTree的能力,也就是去重的能力,只不过被去重的数据可以保留对数据的贡献,也就是聚合后去重。
- AggregatingMergeTree解决了SummingMergeTree单一的聚合方式的问题,可以提供更多的聚合方式