ClickHouse介绍(三)MergeTree系列表引擎

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的处理逻辑为:

  1. 使用ORDER BY 排序键作为判断数据是否重复的唯一键
  2. 只有在合并分区时才会触发删除重复数据的逻辑
  3. 仅有相同分区的数据会进行去重,无法跨分区进行去重
  4. 在进行去重操作时,由于分区内的数据已经基于ORDER BY进行了排序,所以能够找到那些相邻的重复数据
  5. 数据去重策略有2种:
    1. 如果没有设置ver版本号,则保留同一组重复数据的最后一行
    2. 如果设置了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的处理逻辑为:

  1. 用ORDER BY排序键作为聚合数据的条件Key
  2. 只有在合并分区的时候才会触发聚合的逻辑
  3. 以分区为单位进行聚合,不同分区的数据之间不会聚合
  4. 如果在定义引擎时指定了column聚合列(非主键的数值类型字段),则sum聚合这些字段;如未指定,则聚合所有非主键的数值类型字段
  5. 在进行聚合时,由于分区内的数据已经基于ORDER BY排序,所以能够找到相邻且拥有相同聚合Key的数据
  6. 在聚合数据时,同一分区内,相同聚合Key的多行数据会合并为一行;聚合字段进行SUM运算;对于非聚合字段,使用第一行数据的值
  7. 支持嵌套结构,但列字段名必须以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类型的列字段,数据的写入和查询都与寻常不同:

  1. 在写入数据时,需要调用 *State函数;
  2. 在查询数据时,需要调用相应的*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        │
└──────┴────────┴─────────────────┴─────────────────┘

 

整个逻辑如下图所示:

ClickHouse介绍(三)MergeTree系列表引擎

 

 

3.3. AggregatingMergeTree总结

AggregatingMergeTree的处理逻辑为:

  1. 使用ORDER BY 排序键作为聚合数据的条件key
  2. 使用AggregateFunction字段类型定义聚合函数的类型以及聚合的字段
  3. 只有在合并分区的时候才会触发聚合计算的逻辑
  4. 以数据分区为单位聚合数据,仅在同一分区内能够进行聚合,无法跨分区进行聚合
  5. 在进行数据计算时,由于分区内的数据已经基于ORDER BY进行排序,所以能够找到那些相邻且拥有相同聚合Key的数据
  6. 在聚合数据时,同一分区内,相同聚合Key的多行数据会合并成一行。对于那些非主键、非AggregateFunction类型字段,则会使用第一行数据的值
  7. AggregateFunction类型的字段使用二进制,在写入数据时需要调用 *State函数;而在查询数据时,需要调用相应的 *Merge 函数。其中 * 表示定义时使用的聚合函数
  8. AggregatingMergeTree通常作为物化视图的表引擎,与普通MergeTree搭配使用

 

4. CollapsingMergeTree

CollapsingMergeTree是支持行级数据修改和删除的引擎。在大数据领域,修改和删除是代价非常高的操作,所以在实现时不会去修改源文件,而是将修改和删除操作转换为新增操作。

CollapsingMergeTree定义了一个sign标记位字段,用于记录数据行的状态:

  • sign为1:表示这是一行有效数据
  • sign为 -1:表示这行数据需要被删除

 

在合并分区时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。这种相互抵消的操作,犹如将纸折叠一样,所以这可能也是折叠合并树(CollapsingMergeTree)名称的由来。折叠过程如下图所示:

ClickHouse介绍(三)MergeTree系列表引擎

 

 

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在折叠数据时,遵循以下规则:

  1. 如果sign=1比sign=-1的数据多一行,则保留最后一行sign=1的数据
  2. 如果sign=-1比sign=1的数据多一行,则保留第一行sign=-1的数据
  3. 如果sign=1和sign=-1的数据行一样多,且最后一行为sign=1,则保留第一行sign=-1和最后一行sign=1的数据
  4. 如果sign=1和sign=-1的数据行一样多,且最后一行为sign=-1,则什么也不保留
  5. 其余情况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个变种表引擎:

ClickHouse介绍(三)MergeTree系列表引擎

 

 

7. 组合关系

上面是MergeTree关系,下面介绍ReplicatedMergeTree系列。

ReplicatedMergeTree与普通的MergeTree的区别:

ClickHouse介绍(三)MergeTree系列表引擎

 

 

虚线部分是MergeTree的能力边界,而ReplicatedMergeTree是在MergeTree能力的基础之上增加了分布式协同的能力。它借助了ZooKeeper的消息日志广播功能,实现了副本实例之间的数据同步功能。

ReplicatedMergeTree系列通过组合关系来理解,如下图所示:

ClickHouse介绍(三)MergeTree系列表引擎

 

 

在为MergeTree加上Replicated前缀后,又可以组合出7种新的表引擎,这些ReplicatedMergeTree拥有副本协同的能力。之后会详细说明。

 

上一篇:Spring Cloud服务的注册与发现


下一篇:学习Clickhouse_目录