MergeTree系列表引擎
ClickHouse中最核心的引擎当属MergeTree系列引擎,其中基础表引擎为MergeTree,常用的表引擎还有ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree和VersionedCollapsingMergeTree。每一种MergeTree的变种,在继承了基础MergeTree的能力后,还增加了它独有的特性。其名称中的“合并”二字奠定了所有类型MergeTree的基因,它们的所有特殊逻辑,都是在触发合并的过程中被激活的。接下来主要介绍它们各自的特点与使用方法。
1. ReplacingMergeTree
在MergeTree中,虽然有主键,但是它没有唯一键的约束。也就是说,写入数据的主键是可以重复的。但在某些场合下,用户不希望表中有重复数据,ReplacingMergeTree就是为此场景而设计,它可以在合并分区时,删除重复的数据条目。不过此方法仅是在“一定程度”上解决了重复数据的问题,稍后会对此作出解释。
创建测试表:
CREATE TABLE replace_table
(
`id` String,
`code` String,
`create_time` DateTime
)
ENGINE = ReplacingMergeTree
PARTITION BY toYYYYMM(create_time)
PRIMARY KEY id
ORDER BY (id, code)
这里ORDER BY 是去除重复数据的关键,排序键ORDER BY所声明的表达式是后续判断是否有重复数据的依据。在这个例子中,数据会基于id 和 code 两个字段做去重。
分2个批次插入数据:
INSERT INTO replace_table VALUES
('A001', 'C1', '2019-05-10 17:00:00'),
('A001', 'C2', '2019-05-14 17:00:00'),
('A001', 'C3', '2019-05-15 17:00:00')
INSERT INTO replace_table VALUES
('A001', 'C1', '2019-05-11 17:00:00'),
('A001', 'C100', '2019-05-12 17:00:00'),
('A001', 'C200', '2019-05-13 17:00:00')
# 查询数据:
SELECT *
FROM replace_table
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-11 17:00:00 │
│ A001 │ C100 │ 2019-05-12 17:00:00 │
│ A001 │ C200 │ 2019-05-13 17:00:00 │
└──────┴──────┴─────────────────────┘
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-10 17:00:00 │
│ A001 │ C2 │ 2019-05-14 17:00:00 │
│ A001 │ C3 │ 2019-05-15 17:00:00 │
└──────┴──────┴─────────────────────┘
可以看到在表中是存在重复的id与<id, code>,使用 OPTIMIZE 命令强制触发合并后再次查看数据条目:
SELECT *
FROM replace_table
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-11 17:00:00 │
│ A001 │ C100 │ 2019-05-12 17:00:00 │
│ A001 │ C2 │ 2019-05-14 17:00:00 │
│ A001 │ C200 │ 2019-05-13 17:00:00 │
│ A001 │ C3 │ 2019-05-15 17:00:00 │
└──────┴──────┴─────────────────────┘
可以看到<id, code> 重复的条目已经被删除了。在optimize强制触发合并后,会按照<id, code> 进行分组,并保留分组内最后一条条目(观察create_time日期字段)。
从测试结果可以知道,ReplacingMergeTree在去除重复数据时,是以ORDER BY排序键作为基准,而不是PRIMARY KEY。
前面提到ReplacingMergeTree仅在“一定程度上”解决了数据重复的问题,现在我们解释这点。首先,再插入一条数据:
INSERT INTO replace_table VALUES ('A001', 'C1', '2019-08-10 17:00:00')
执行 OPTIMIZE TABLE 后再查看数据:
OPTIMIZE TABLE replace_table FINAL
SELECT *
FROM replace_table
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-08-10 17:00:00 │
└──────┴──────┴─────────────────────┘
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-11 17:00:00 │
│ A001 │ C100 │ 2019-05-12 17:00:00 │
│ A001 │ C2 │ 2019-05-14 17:00:00 │
│ A001 │ C200 │ 2019-05-13 17:00:00 │
│ A001 │ C3 │ 2019-05-15 17:00:00 │
└──────┴──────┴─────────────────────┘
可以看到表中存在相同的<id, code> 行,但是并没有执行去重。这是因为ReplacingMergeTree是以分区为单位进行合并,并在此时删除重复数据。不同分区之间的重复数据无法进行删除。所以ReplacingMergeTree仅是在“一定程度上“解决了数据重复问题。
1.2. 版本号
ReplacingMergeTree中可以根据版本号来决定:在对重复数据进行去重时,保留哪条数据。
例如以下建表语句:
CREATE TABLE replace_table
(
`id` String,
`code` String,
`create_time` DateTime
)
ENGINE = ReplacingMergeTree(create_time)
PARTITION BY toYYYYMM(create_time)
ORDER BY id
可以看到在指定ReplacingMergeTree引擎时,传入了一个列字段create_time。这个表基于id字段进行去重,并且使用create_time作为版本号。
在使用create_time作为版本号后,在对数据进行去重时,会保留id重复的条目中,create_time时间最长的一行。
1.3. ReplacingMergeTree总结
ReplacingMergeTree的处理逻辑为:
- 使用ORDER BY 排序键作为判断数据是否重复的唯一键
- 只有在合并分区时才会触发删除重复数据的逻辑
- 仅有相同分区的数据会进行去重,无法跨分区进行去重
- 在进行去重操作时,由于分区内的数据已经基于ORDER BY进行了排序,所以能够找到那些相邻的重复数据
- 数据去重策略有2种:
- 如果没有设置ver版本号,则保留同一组重复数据的最后一行
- 如果设置了ver版本号,则保留同一组重复数据中ver字段取值最大的那一行
2. SummingMergeTree
SummingMergeTree用于仅关系聚合结果,而不关心具体明细的场景。并且数据汇总条件是预先明确的(group by 的条件明确,不会随意更改)。
SummingMergeTree可以在合并分区时,按照预先定义的条件,聚合汇总数据,将同一分区下的多行数据聚合成一行。这样既减少了数据行,又降低了后续聚合查询的开销。
2.1. ORDER BY 与 PRIMARY KEY
在MergeTree中,数据会按照ORDER BY 的字段在分区内进行排序。主键索引也会按照PRIMARY KEY 表达式取值并排序。而ORDER BY可以指代主键,所以在一般情况下,只单独声明ORDER BY即可,此时数据排序与主键索引均相同。
如果需要同时定义ORDER BY与PRIMARY KEY,便是明确希望他俩不同。这种情况通常会在使用SummingMergeTree或AggregatingMergeTree时会出现,因为它们的聚合都是根据ORDER BY 进行的。由此可以引出2点原因:主键与聚合的条件定义分离,为修改聚合条件留下空间。
举个例子,假设一张表使用的引擎为SummingMergeTree,包含6个字段,分别为A、B、C、D、E、F。如果需要按照A、B、C、D进行汇总,则有:
ORDER BY (A, B, C, D)
但是这样的写法也表示:表的PRIMARY KEY 被定义成A, B, C, D。若是在业务层面,仅需要对字段A进行查询过滤,则应只使用A字段创建主键。所以更好的一种写法是:
ORDER BY (A, B, C, D)
PRIMARY KEY A
如果同时声明了ORDER BY 与 PRIMARY KEY,则MergeTree会强制要求PRIMARY KEY列字段必须是ORDER BY的前缀。
例如下面的定义是错的:
ORDER BY (B, C)
PRIMARY KEY A
下面的是正确的:
ORDER BY (B, C)
PRIMARY KEY B
这种强制约束保障了即便在两者定义不同的情况下,主键认识排序键的前提,不会出现索引与数据顺序混乱的问题。
假设现在业务发生了细微的变化,需要减少字段,将先前的A、B、C、D改为按照A、B进行聚合,则可以按以下方式修改排序键:
ALTER TABLE table_name MODIFY ORDER BY (A, B)
在修改ORDER BY 时会有一些限制,只能在现有的基础上减少字段。如果是新增排序字段,则只能添加通过ALTER ADD COLUMN 新增的字段。但是ALTER 是一种元数据的操作,修改成本很低,相比不能被修改的主键,这已经非常便利了。
2.2. SummingMergeTree的使用
SummingMergeTree引擎的声明方式为:
ENGINE = SummingMergeTree((col1, col2, …))
这里col1,col2是选填参数,用于设置除主键外的其他数值类型字段,以指定被SUM聚合的列字段。若不指定此字段,则所有非主键的数值型字段均会进行SUM聚合。
下面举例说明:
创建表:
CREATE TABLE summing_table
(
`id` String,
`city` String,
`v1` UInt32,
`v2` Float64,
`create_time` DateTime
)
ENGINE = SummingMergeTree
PARTITION BY toYYYYMM(create_time)
PRIMARY KEY id
ORDER BY (id, city)
这里 order by 是关键配置,数据会以 order by 指定的列为维度进行聚合。
分批次插入数据:
INSERT INTO summing_table VALUES
('A001', 'wuhan', '10', '20', '2019-08-10 17:00:00')
('A001', 'jinzhou', '20', '30', '2019-08-10 17:00:00')
INSERT INTO summing_table VALUES
('A001', 'wuhan', '10', '20', '2019-02-10 17:00:00')
('A001', 'wuhan', '20', '30', '2019-08-20 17:00:00')
INSERT INTO summing_table VALUES
('A002', 'wuhan', '60', '50', '2019-10-10 17:00:00')
当前数据结果:
SELECT *
FROM summing_table
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ wuhan │ 20 │ 30 │ 2019-08-20 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ wuhan │ 10 │ 20 │ 2019-02-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
┌─id───┬─city────┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ jinzhou │ 20 │ 30 │ 2019-08-10 17:00:00 │
│ A001 │ wuhan │ 10 │ 20 │ 2019-08-10 17:00:00 │
└──────┴─────────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A002 │ wuhan │ 60 │ 50 │ 2019-10-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
执行 OPTIMIZE TABLE 后查看结果:
OPTIMIZE TABLE summing_table FINAL
SELECT *
FROM summing_table
┌─id───┬─city────┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ jinzhou │ 20 │ 30 │ 2019-08-10 17:00:00 │
│ A001 │ wuhan │ 30 │ 50 │ 2019-08-10 17:00:00 │
└──────┴─────────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ wuhan │ 10 │ 20 │ 2019-02-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A002 │ wuhan │ 60 │ 50 │ 2019-10-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
可以看到,在不同分区内,数据以ORDER BY 的字段<id, city> 进行了聚合,聚合的字段为v1 与 v2。而非可聚合字段create_time 取的是同组内第一行数据的取值。
SummingMergeTree也支持嵌套类型的字段,在使用嵌套类型字段时,需要被SUM聚合的字段名称必须以Map后缀结尾,例如:
CREATE TABLE summing_table_nested
(
`id` String,
`nestMap` Nested(id UInt32, key UInt32, val UInt64),
`create_time` DateTime
)
ENGINE = SummingMergeTree
PARTITION BY toYYYYMM(create_time)
ORDER BY id
插入一条数据并查看结果:
INSERT INTO summing_table_nested VALUES
('A001', [1,1,2], [10,20,30], [40,50,60], '2019-08-10 17:00:00')
SELECT *
FROM summing_table_nested
┌─id───┬─nestMap.id─┬─nestMap.key─┬─nestMap.val─┬─────────create_time─┐
│ A001 │ [1,2] │ [30,30] │ [90,60] │ 2019-08-10 17:00:00 │
└──────┴────────────┴─────────────┴─────────────┴─────────────────────┘
可以看到此结果是按照 id 进行了聚合,相同 nestMap.id 的条目在nestMap.key 与 nestMap.val 上进行了sum 聚合。
在使用嵌套数据类型的时候,也支持使用复合Key作为数据聚合的条件。为了使用复合Key,在嵌套类型的字段中,除第一个字段外,任何名称是以Key、Id或Type为后缀结尾的字段,都将和第一个字段一起组合成为复合Key。例如将上面的例子中小写key改为大写Key:
(
`id` String,
`nestMap` Nested(id UInt32, Key UInt32, val UInt64),
`create_time` DateTime
)
这样数据就会以<id, Key> 作为条件进行聚合。
2.3. SummingMergeTree总结
SummingMergeTree的处理逻辑为:
- 用ORDER BY排序键作为聚合数据的条件Key
- 只有在合并分区的时候才会触发聚合的逻辑
- 以分区为单位进行聚合,不同分区的数据之间不会聚合
- 如果在定义引擎时指定了column聚合列(非主键的数值类型字段),则sum聚合这些字段;如未指定,则聚合所有非主键的数值类型字段
- 在进行聚合时,由于分区内的数据已经基于ORDER BY排序,所以能够找到相邻且拥有相同聚合Key的数据
- 在聚合数据时,同一分区内,相同聚合Key的多行数据会合并为一行;聚合字段进行SUM运算;对于非聚合字段,使用第一行数据的值
- 支持嵌套结构,但列字段名必须以Map后缀结尾。嵌套类型中,默认以第1个字段作为聚合Key。除第1个字段外,任何名称以Key、Id或Type为后缀结尾的字段,都将和第1个字段一起组成复合Key
3. AggregatingMergeTree
相信大家应该有了解过“数据立方体“(Cube)的概念,它在数仓领域非常常见。它的主要理念是:通过空间换取时间,对需要聚合的数据进行预计算,并将结果保存。在后续进行聚合查询的时候,可以直接使用结果数据,提升查询性能。
AggregatingMergeTree有些许“数据立方体“的意思,它能在合并分区的时候,按照预先定义的条件聚合数据。同时,根据预先定义的聚合函数,计算数据并以二进制的格式存入表内。
将同一分组下的多行数据聚合成一行,既减少了数据行,又降低了后续聚合查询的开销。可以说AggregatingMergeTree是SummingMergeTree的升级版,它们的许多设计思路是一致的,例如同时定义ORDER BY与PRIMARY KEY的原因和目的。但在使用方法上,两者存在明显差异。
AggregatingMergeTree没有任何额外参数,在分区合并时,在每个数据分区内,会按照ORDER BY聚合。而使用何种聚合函数,以及针对哪些列字段计算,则是通过定义AggregateFunction数据类型实现的。
3.1. AggregatingMergeTree的使用
如下建表语句:
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)
PRIMARY KEY id
ORDER BY (id, city)
上例中列字段id和city是聚合条件,等同于下面语义:
GROUP BY id, city
而 code 和 value 是聚合字段,其语义等同于:
UNIQ(code), SUM(value)
AggregateFunction是ClickHouse提供的一种特殊的数据类型,它能够以二进制的形式存储中间状态结果。其使用方法也非常特殊,对于AggregateFunction类型的列字段,数据的写入和查询都与寻常不同:
- 在写入数据时,需要调用 *State函数;
- 在查询数据时,需要调用相应的*Merge函数
这里*表示定义时使用的聚合函数。
例如,在示例中定义的code和value使用了uniq和sum函数:
code AggregateFunction(uniq, String)
value AggregateFunction(sum, UInt32)
则在写入数据时需要调用与uniq、sum对应的uniqState和sumState函数,并使用INSERT SELECT语法:
INSERT INTO TABLE agg_table
SELECT 'A000','wuhan',
uniqState('code1'),
sumState(toUInt32(100)),
'2019-09-10 17:00:00'
在查询数据时,如果直接使用列名去访问这里的code 与 value列时,结果为乱码。这是因为它们的格式为二进制格式,在查询时需要调用与uniq、sum对应的uniqMerge和sumMerge函数:
SELECT
id,
city,
uniqMerge(code),
sumMerge(value)
FROM agg_table
GROUP BY
id,
city
┌─id───┬─city──┬─uniqMerge(code)─┬─sumMerge(value)─┐
│ A000 │ wuhan │ 1 │ 100 │
└──────┴───────┴─────────────────┴─────────────────┘
以上 insert into 的方式并非是AggregatingMergeTree的常见用法,它更为常见的用法是结合物化视图使用,将它作为物化视图表的引擎。
3.2. 结合物化视图使用
举例说明,首先建立明细数据表(也就是俗称的底表):
CREATE TABLE agg_table_basic
(
`id` String,
`city` String,
`code` String,
`value` UInt32
)
ENGINE = MergeTree()
PARTITION BY city
ORDER BY (id, city)
通常使用MergeTree作为底表,用于存储全量的明细数据,并以此对外提供实时查询。
然后建立一张物化视图:
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
物化视图使用AggregatingMergeTree表引擎,用于特定场景的数据查询,相比MergeTree,它拥有更高的性能。
在新增数据时,面向的对象是底表MergeTree,例如:
INSERT INTO TABLE agg_table_basic VALUES
('A000','wuhan','code1',100),('A000','wuhan','code2',200),('A000','zhuhai','code1',200)
SELECT *
FROM agg_table_basic
┌─id───┬─city───┬─code──┬─value─┐
│ A000 │ zhuhai │ code1 │ 200 │
└──────┴────────┴───────┴───────┘
┌─id───┬─city──┬─code──┬─value─┐
│ A000 │ wuhan │ code1 │ 100 │
│ A000 │ wuhan │ code2 │ 200 │
└──────┴───────┴───────┴───────┘
数据会自动同步到物化视图,并按照AggregatingMergeTree引擎的规则进行处理:
SELECT
id,
city,
sumMerge(value),
uniqMerge(code)
FROM agg_view
GROUP BY
id,
city
┌─id───┬─city───┬─sumMerge(value)─┬─uniqMerge(code)─┐
│ A000 │ zhuhai │ 400 │ 1 │
│ A000 │ wuhan │ 600 │ 2 │
└──────┴────────┴─────────────────┴─────────────────┘
整个逻辑如下图所示:
3.3. AggregatingMergeTree总结
AggregatingMergeTree的处理逻辑为:
- 使用ORDER BY 排序键作为聚合数据的条件key
- 使用AggregateFunction字段类型定义聚合函数的类型以及聚合的字段
- 只有在合并分区的时候才会触发聚合计算的逻辑
- 以数据分区为单位聚合数据,仅在同一分区内能够进行聚合,无法跨分区进行聚合
- 在进行数据计算时,由于分区内的数据已经基于ORDER BY进行排序,所以能够找到那些相邻且拥有相同聚合Key的数据
- 在聚合数据时,同一分区内,相同聚合Key的多行数据会合并成一行。对于那些非主键、非AggregateFunction类型字段,则会使用第一行数据的值
- AggregateFunction类型的字段使用二进制,在写入数据时需要调用 *State函数;而在查询数据时,需要调用相应的 *Merge 函数。其中 * 表示定义时使用的聚合函数
- AggregatingMergeTree通常作为物化视图的表引擎,与普通MergeTree搭配使用
4. CollapsingMergeTree
CollapsingMergeTree是支持行级数据修改和删除的引擎。在大数据领域,修改和删除是代价非常高的操作,所以在实现时不会去修改源文件,而是将修改和删除操作转换为新增操作。
CollapsingMergeTree定义了一个sign标记位字段,用于记录数据行的状态:
- sign为1:表示这是一行有效数据
- sign为 -1:表示这行数据需要被删除
在合并分区时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。这种相互抵消的操作,犹如将纸折叠一样,所以这可能也是折叠合并树(CollapsingMergeTree)名称的由来。折叠过程如下图所示:
4.1. CollapsingMergeTree的使用
建表:
CREATE TABLE collapse_table(
id String,
code Int32,
create_time DateTime,
sign Int8
) ENGINE = CollapsingMergeTree(sign)
PARTITION BY toYYYYMM(create_time)
ORDER BY id
同其他MergeTree 引擎一样,CollapsingMergeTree也是通过ORDER BY 排序字段判断数据唯一性;除此之外,CollapsingMergeTree还支持其他2种特殊操作:修改与删除。
下面举例:
--插入原始数据,后续会被修改
INSERT INTO TABLE collapse_table VALUES ('A000', 100, '2019-02-20 00:00:00', 1)
--原始数据的镜像数据,ORDER BY 字段与原条目必须相同(其他字段可以不同),sign取 -1,
--它会和原始条目折叠
INSERT INTO TABLE collapse_table VALUES ('A000', 100, '2019-02-20 10:00:00', -1)
--强制OPTIMIZE 合并后查看结果,可以看到结果为空
SELECT *
FROM collapse_table
0 rows in set. Elapsed: 0.001 sec.
--修改后的数据,sign为1
INSERT INTO TABLE collapse_table VALUES ('A000', 120, '2019-02-20 00:00:00', 1)
--检查结果
SELECT *
FROM collapse_table
┌─id───┬─code─┬─────────create_time─┬─sign─┐
│ A000 │ 120 │ 2019-02-20 00:00:00 │ 1 │
└──────┴──────┴─────────────────────┴──────┘
CollapsingMergeTree在折叠数据时,遵循以下规则:
- 如果sign=1比sign=-1的数据多一行,则保留最后一行sign=1的数据
- 如果sign=-1比sign=1的数据多一行,则保留第一行sign=-1的数据
- 如果sign=1和sign=-1的数据行一样多,且最后一行为sign=1,则保留第一行sign=-1和最后一行sign=1的数据
- 如果sign=1和sign=-1的数据行一样多,且最后一行为sign=-1,则什么也不保留
- 其余情况ClickHouse会打印警告日志,但不会报错,此时查询结果未知
4.2. CollapsingMergeTree注意事项
1. 折叠数据并非实时触发,而是在分区合并时在进行。所以在分区合并前,旧数据仍对用户可见。解决此问题的方式有2种:
1.1. 在查询数据前,执行 OPTIMIZE TABLE table_name FINAL 强制触发分区合并,但此方法效率很低,在生产环境慎用。
1.2. 改变查询方式。以collapse_table为例,假设原始SQL为:
SELECT id, SUM(code), COUNT(code), AVG(code), uniq(code)
FROM collapse_table
GROUP BY id
则可以改写为:
SELECT id, SUM(code * sign), COUNT(code * sign), AVG(code * sign), uniq(code * sign)
FROM collapse_table
GROUP BY id
HAVING SUM(sign) > 0
2. 只有相同分区内的数据才可能被折叠,不过删除或修改数据时,一般分区规则都是一致的。
3. CollapsingMergeTree最大的限制在于:对于写入数据的顺序有非常严格的要求。前面例子我们可以看到,若是先写sign=1再写sign=-1,则没有任何问题。但若是先写sign=-1再写sign=1 的话呢:
INSERT INTO TABLE collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', -1)
INSERT INTO TABLE collapse_table VALUES('A000', 102, '2019-02-20 00:00:00', 1)
OPTIMIZE TABLE collapse_table FINAL
select * from collapse_table;
┌─id───┬─code─┬─────────create_time─┬─sign─┐
│ A000 │ 101 │ 2019-02-20 00:00:00 │ -1 │
│ A000 │ 102 │ 2019-02-20 00:00:00 │ 1 │
└──────┴──────┴─────────────────────┴──────┘
可以看到在顺序置换后,并没有进行折叠。若是在单线程写入时,顺序是可以保证的,但是在多线程并行写入就不一定了。为了解决这个问题,ClickHouse另外提供了一个名为VersionedCollapsingMergeTree的表引擎。
5. VersionedCollapsingMergeTree
VersionedCollapsingMergeTree表引擎的功能与CollapsingMergeTree完全相同,不同之处在于:它对数据的写入顺序没有要求,同一分区内,任意顺序的数据都能完成折叠操作。通过版本号实现。
在定义VersionedCollapsingMergeTree时,除了需要指定sign标记字段外,还需要指定一个UInt8类型的ver版本号字段。例如:
CREATE TABLE ver_collapse_table(
id String,
code Int32,
create_time DateTime,
sign Int8,
ver UInt8
) ENGINE = VersionedCollapsingMergeTree(sign,ver)
PARTITION BY toYYYYMM(create_time)
ORDER BY id
VersionedCollapsingMergeTree是如何使用版本号的呢?很简单,在定义了ver字段后,它会自动将此字段作为排序条件增加到ORDER BY 后面。例如上面定义的表,在每个数据分区内,数据均会以 ORDER BY id, ver DESC 进行排序。所以无论写入时数据的顺序如何,在折叠处理时,都能回到正确的顺序。
例如:
删除一行数据:
INSERT INTO TABLE ver_collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', -1, 1)
INSERT INTO TABLE ver_collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', 1, 1)
OPTIMIZE TABLE ver_collapse_table FINAL
SELECT *
FROM ver_collapse_table
0 rows in set. Elapsed: 0.001 sec.
修改数据:
INSERT INTO TABLE ver_collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', -1, 1)
INSERT INTO TABLE ver_collapse_table VALUES('A000', 102, '2019-02-20 00:00:00', 1, 1)
INSERT INTO TABLE ver_collapse_table VALUES('A000', 103, '2019-02-20 00:00:00', 1, 2)
OPTIMIZE TABLE ver_collapse_table;
select * from ver_collapse_table;
┌─id───┬─code─┬─────────create_time─┬─sign─┬─ver─┐
│ A000 │ 103 │ 2019-02-20 00:00:00 │ 1 │ 2 │
└──────┴──────┴─────────────────────┴──────┴─────┘
6. MergeTree家族关系总结
MergeTree表引擎向下派生出6个变种表引擎:
7. 组合关系
上面是MergeTree关系,下面介绍ReplicatedMergeTree系列。
ReplicatedMergeTree与普通的MergeTree的区别:
虚线部分是MergeTree的能力边界,而ReplicatedMergeTree是在MergeTree能力的基础之上增加了分布式协同的能力。它借助了ZooKeeper的消息日志广播功能,实现了副本实例之间的数据同步功能。
ReplicatedMergeTree系列通过组合关系来理解,如下图所示:
在为MergeTree加上Replicated前缀后,又可以组合出7种新的表引擎,这些ReplicatedMergeTree拥有副本协同的能力。之后会详细说明。