目前在ClickHouse中,按照特色能够将表引擎大体分红6个系列,分别是合并树、外部存储、内存、文件、接口和其余,每个系列的表引擎都有着独自的特色与使用场景。在它们之中,最为核心的当属MergeTree系列,由于它们拥有最为强大的性能和最普遍的使用场合。算法
你们应该已经知道了MergeTree有两层含义:数据库
其一,表示合并树表引擎家族;多线程
其二,表示合并树家族中最基础的MergeTree表引擎。函数
而在整个家族中,除了基础表引擎MergeTree以外,经常使用的表引擎还有ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree和VersionedCollapsingMergeTree。每一种合并树的变种,在继承了基础MergeTree的能力以后,又增长了独有的特性。其名称中的“合并”二字奠基了全部类型MergeTree的基因,它们的全部特殊逻辑,都是在触发合并的过程当中被激活的。在本章后续的内容中,会逐一介绍它们的特色以及使用方法。性能
MergeTree做为家族系列最基础的表引擎,提供了数据分区、一级索引和二级索引等功能。测试
TTL即Time To Live,顾名思义,它表示数据的存活时间。在MergeTree中,能够为某个列字段或整张表设置TTL。当时间到达时,若是是列字段级别的TTL,则会删除这一列的数据;若是是表级别的TTL,则会删除整张表的数据;若是同时设置了列级别和表级别的TTL,则会以先到期的那个为主。不管是列级别仍是表级别的TTL,都须要依托某个DateTime或Date类型的字段,经过对这个时间字段的INTERVAL操做,来表述TTL的过时时间,例如:大数据
TTL time_col + INTERVAL 3 DAY
上述语句表示数据的存活时间是time_col时间的3天以后。又例如:spa
TTL time_col + INTERVAL 1 MONTH
上述语句表示数据的存活时间是time_col时间的1月以后。INTERVAL完整的操做包括SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER和YEAR。线程
若是想要设置列级别的TTL,则须要在定义表字段的时候,为它们声明TTL表达式,主键字段不能被声明TTL。如下面的语句为例:设计
CREATE TABLE ttl_table_v1 ( id String, create_time DateTime, code String TTL create_time + INTERVAL 10 SECOND, type UInt8 TTL create_time + INTERVAL 10 SECOND ) ENGINE = MergeTree PARTITION BY toYYYYMM(create_time) ORDER BY id ;
其中,create_time是日期类型,列字段code与type均被设置了TTL,它们的存活时间是在create_time的取值基础之上向后延续10秒。如今写入测试数据,其中第一行数据create_time取当前的系统时间,而第二行数据的时间比第一行增长10分钟:
SELECT * FROM ttl_table_v1;
接着心中默数10秒,而后执行optimize命令强制触发TTL清理:
OPTIMIZE TABLE ttl_table_v1 FINAL;
再次查询ttl_table_v1则可以看到,因为第一行数据知足TTL过时条件(当前系统时间 >= create_time + 10秒),它们的code和type列会被还原为数据类型的默认值:
若是想要修改列字段的TTL,或是为已有字段添加TTL,则可使用ALTER语句,示例以下:
ALTER TABLE ttl_table_v1 MODIFY column code String TTL create_time + INTERVAL 1 DAY
目前ClickHouse没有提供取消列级别TTL的方法。
表级别TTL
若是想要为整张数据表设置TTL,须要在MergeTree的表参数中增长TTL表达式,例以下面的语句:
CREATE TABLE tt1_table_v2( id String, create_time DateTime, code String TTL create_time + INTERVAL 1 MINUTE , type UInt8 ) ENGINE = MergeTree PARTITION BY toYYYYMM(create_time) ORDER BY create_time TTL create_time + INTERVAL 1 DAY ;
ttl_table_v2整张表被设置了TTL,当触发TTL清理时,那些知足过时时间的数据行将会被整行删除。一样,表级别的TTL也支持修改,修改的方法以下:
ALTER TABLE tt1_table_v2 MODIFY TTL create_time + INTERVAL 3 DAY;
表级别TTL目前也没有取消的方法。
TTL的运行机理
在知道了列级别与表级别TTL的使用方法以后,如今简单聊一聊TTL的运行机理。若是一张MergeTree表被设置了TTL表达式,那么在写入数据时,会以数据分区为单位,在每一个分区目录内生成一个名为ttl.txt的文件。以刚才示例中的ttl_table_v2为例,它被设置了列级别TTL:
code String TTL create_time + INTERVAL 1 MINUTE
同时被设置了表级别的TTL:
TTL create_time + INTERVAL 1 DAY
那么,在写入数据以后,它的每一个分区目录内都会生成ttl.txt文件:
进一步查看ttl.txt的内容:
经过上述操做会发现,原来MergeTree是经过一串JSON配置保存了TTL的相关信息,其中:
❑ columns用于保存列级别TTL信息;
❑ table用于保存表级别TTL信息;
❑ min和max则保存了当前数据分区内,TTL指定日期字段的最小值、最大值分别与INTERVAL表达式计算后的时间戳。
若是将table属性中的min和max时间戳格式化,并分别与create_time最小与最大取值对比:
则可以印证,ttl.txt中记录的极值区间刚好等于当前数据分区内create_time最小与最大值增长1天(1天= 86400秒)所表示的区间,与TTL表达式create_time +INTERVAL 1 DAY的预期相符。
在知道了TTL信息的记录方式以后,如今看看它的大体处理逻辑。
(1)MergeTree以分区目录为单位,经过ttl.txt文件记录过时时间,并将其做为后续的判断依据。
(2)每当写入一批数据时,都会基于INTERVAL表达式的计算结果为这个分区生成ttl. txt文件。
(3)只有在MergeTree合并分区时,才会触发删除TTL过时数据的逻辑。
(4)在选择删除的分区时,会使用贪婪算法,它的算法规则是尽量找到会最先过时的,同时年纪又是最老的分区(合并次数更多,MaxBlockNum更大的)。
(5)若是一个分区内某一列数据由于TTL到期所有被删除了,那么在合并以后生成的新分区目录中,将不会包含这个列字段的数据文件(.bin和.mrk)。
这里还有几条TTL使用的小贴士。
(1)TTL默认的合并频率由MergeTree的merge_with_ttl_timeout参数控制,默认86400秒,即1天。它维护的是一个专有的TTL任务队列。有别于MergeTree的常规合并任务,若是这个值被设置的太小,可能会带来性能损耗。
(2)除了被动触发TTL合并外,也可使用optimize命令强制触发合并。例如,触发一个分区合并:
optimize TABLE table_name;
触发全部分区合并:
optimize TABLE table_name FINAL;
(3)ClickHouse目前虽然没有提供删除TTL声明的方法,可是提供了控制全局TTL合并任务的启停方法:
SYSTEM STOP/START TTL MERGES;
虽然还不能作到按每张MergeTree数据表启停,但聊胜于无吧。
虽然MergeTree拥有主键,可是它的主键却没有惟一键的约束。这意味着即使多行数据的主键相同,它们仍是可以被正常写入。在某些使用场合,用户并不但愿数据表中含有重复的数据。ReplacingMergeTree就是在这种背景下为了数据去重而设计的,它可以在合并分区时删除重复的数据。它的出现,确实也在必定程度上解决了重复数据的问题。为何说是“必定程度”?此处先按下不表。
建立一张ReplacingMergeTree表的方法与建立普通MergeTree表无异,只须要替换Engine:
ENGINE = ReplacingMergeTree(ver)
其中,ver是选填参数,会指定一个UInt*、Date或者DateTime类型的字段做为版本号。这个参数决定了数据去重时所使用的算法。
接下来,用一个具体的示例说明它的用法。首先执行下面的语句建立数据表:
CREATE TABLE replace_table( id String, code String, create_time DateTime ) ENGINE = ReplacingMergeTree() partition by toYYYYMM(create_time) ORDER BY(id,code) PRIMARY KEY id ;
注意这里的ORDER BY是去除重复数据的关键,排序键ORDER BY所声明的表达式是后续做为判断数据是否重复的依据。在这个例子中,数据会基于id和code两个字段去重。假设此时表内的测试数据以下:
那么在执行optimize强制触发合并后,会按照id和code分组,保留分组内的最后一条(观察create_time日期字段):
optimize TABLE replace_table FINAL;
将其他重复的数据删除:
从执行的结果来看,ReplacingMergeTree在去除重复数据时,确实是以ORDERBY排序键为基准的,而不是PRIMARY KEY。由于在上面的例子中,ORDER BY是(id, code),而PRIMARY KEY是id,若是按照id值去除重复数据,则最终结果应该只剩下A00一、A002和A003三行数据。
到目前为止,ReplacingMergeTree看起来完美地解决了重复数据的问题。事实果然如此吗?如今尝试写入一批新数据:
insert into replace_table values ('A001','C1','2020-07-02 12:01:01');
写入以后,执行optimize强制分区合并,并查询数据:
再次观察返回的数据,能够看到A001:C1依然出现了重复。这是怎么回事呢?这是由于ReplacingMergeTree是以分区为单位删除重复数据的。只有在相同的数据分区内重复的数据才能够被删除,而不一样数据分区之间的重复数据依然不能被剔除。这就是上面说ReplacingMergeTree只是在必定程度上解决了重复数据问题的缘由。
如今接着说明ReplacingMergeTree版本号的用法。如下面的语句为例:
CREATE TABLE replace_table_v ( id String, code String, create_time DateTime ) ENGINE = ReplacingMergeTree(create_time) PARTITION BY toYYYYMM(create_time) ORDER BY id ;
replace_table_v基于id字段去重,而且使用create_time字段做为版本号,假设表内的数据以下所示:
那么在删除重复数据的时候,会保留同一组数据内create_time时间最长的那一行:
在知道了ReplacingMergeTree的使用方法后,如今简单梳理一下它的处理逻辑。
(1)使用ORBER BY排序键做为判断重复数据的惟一键。
(2)只有在合并分区的时候才会触发删除重复数据的逻辑。
(3)以数据分区为单位删除重复数据。当分区合并时,同一分区内的重复数据会被删除;不一样分区之间的重复数据不会被删除。
(4)在进行数据去重时,由于分区内的数据已经基于ORBER BY进行了排序,因此可以找到那些相邻的重复数据。
(5)数据去重策略有两种:
❑ 若是没有设置ver版本号,则保留同一组重复数据中的最后一行。
❑ 若是设置了ver版本号,则保留同一组重复数据中ver字段取值最大的那一行。
假设有这样一种查询需求:终端用户只须要查询数据的汇总结果,不关心明细数据,而且数据的汇总条件是预先明确的(GROUP BY条件明确,且不会随意改变)。
对于这样的查询场景,在ClickHouse中如何解决呢?最直接的方案就是使用MergeTree存储数据,而后经过GROUP BY聚合查询,并利用SUM聚合函数汇总结果。这种方案存在两个问题。
❑ 存在额外的存储开销:终端用户不会查询任何明细数据,只关心汇总结果,因此不该该一直保存全部的明细数据。
❑ 存在额外的查询开销:终端用户只关心汇总结果,虽然MergeTree性能强大,可是每次查询都进行实时聚合计算也是一种性能消耗。
SummingMergeTree就是为了应对这类查询场景而生的。顾名思义,它可以在合并分区的时候按照预先定义的条件聚合汇总数据,将同一分组下的多行数据汇总合并成一行,这样既减小了数据行,又下降了后续汇总查询的开销。
在先前介绍MergeTree原理时曾说起,在MergeTree的每一个数据分区内,数据会按照ORDER BY表达式排序。主键索引也会按照PRIMARY KEY表达式取值并排序。而ORDER BY能够指代主键,因此在通常情形下,只单独声明ORDER BY便可。此时,ORDER BY与PRIMARY KEY定义相同,数据排序与主键索引相同。
若是须要同时定义ORDER BY与PRIMARY KEY,一般只有一种可能,那即是明确但愿ORDER BY与PRIMARY KEY不一样。这种状况一般只会在使用SummingMergeTree或AggregatingMergeTree时才会出现。这是为什么呢?这是由于SummingMergeTree与AggregatingMergeTree的聚合都是根据ORDER BY进行的。由此能够引出两点缘由:主键与聚合的条件定义分离,为修改聚合条件留下空间。
如今用一个示例说明。假设一张SummingMergeTree数据表有A、B、C、D、E、F六个字段,若是须要按照A、B、C、D汇总,则有:
ORDER BY (A,B,C,D)
可是如此一来,此表的主键也被定义成了A、B、C、D。而在业务层面,其实只须要对字段A进行查询过滤,应该只使用A字段建立主键。因此,一种更加优雅的定义形式应该是:
ORDER BY (A,B,C,D) PRIMARY KEY A
若是同时声明了ORDER BY与PRIMARY KEY, MergeTree会强制要求PRIMARYKEY列字段必须是ORDER BY的前缀。例以下面的定义是错误的:
ORDER BY(B,C) PRIMARY KEY A
PRIMARY KEY必须是ORDER BY的前缀:
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是一种元数据的操做,修改为本很低,相比不能被修改的主键,这已经很是便利了。
如今开始正式介绍SummingMergeTree的使用方法。表引擎的声明方式以下所示:
ENGINE = SummingMergeTree((col1,col2,...))
其中,col一、col2为columns参数值,这是一个选填参数,用于设置除主键外的其余数值类型字段,以指定被SUM汇总的列字段。如若不填写此参数,则会将全部非主键的数值类型字段进行SUM汇总。接来下用一组示例说明它的使用方法:
CREATE TABLE summing_table( id String, city String, v1 UInt32, v2 Float64, create_time DateTime ) ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(create_time) ORDER BY (id,city) PRIMARY KEY id ;
注意,这里的ORDER BY是一项关键配置,SummingMergeTree在进行数据汇总时,会根据ORDER BY表达式的取值进行聚合操做。假设此时表内的数据以下所示:
执行optimize强制进行触发和合并操做:
optimize TABLE summing_table FINAL
再次查询,表内数据会变成下面的样子:
至此可以看到,在第一个分区内,同为A001:wuhan的两条数据汇总成了一行。其中,v1和v2被SUM汇总,不在汇总字段之列的create_time则选取了同组内第一行数据的取值。而不一样分区之间,数据没有被汇总合并。
SummingMergeTree也支持嵌套类型的字段,在使用嵌套类型字段时,须要被SUM汇总的字段名称必须以Map后缀结尾,例如:
CREATE TABLE summing_table_nested( id1 String, nestMap Nested( id UInt32, key UInt32, val UInt64 ), create_time DateTime ) ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(create_time) ORDER BY id1 ;
在使用嵌套数据类型的时候,默认状况下,会以嵌套类型中第一个字段做为聚合条件Key。假设表内的数据以下所示:
上述示例中数据会按照第一个字段id聚合,汇总后的数据会变成下面的样子:
数据汇总的逻辑示意以下所示:
在使用嵌套数据类型的时候,也支持使用复合Key做为数据聚合的条件。为了使用复合Key,在嵌套类型的字段中,除第一个字段之外,任何名称是以Key、Id或Type为后缀结尾的字段,都将和第一个字段一块儿组成复合Key。例如将上面的例子中小写key改成Key:
上述例子中数据会以id和Key做为聚合条件。在知道了SummingMergeTree的使用方法后,如今简单梳理一下它的处理逻辑。
(1)用ORBER BY排序键做为聚合数据的条件Key。
(2)只有在合并分区的时候才会触发汇总的逻辑。
(3)以数据分区为单位来聚合数据。当分区合并时,同一数据分区内聚合Key相同的数据会被合并汇总,而不一样分区之间的数据则不会被汇总。(4)若是在定义引擎时指定了columns汇总列(非主键的数值类型字段),则SUM汇总这些列字段;若是未指定,则聚合全部非主键的数值类型字段。
(5)在进行数据汇总时,由于分区内的数据已经基于ORBER BY排序,因此可以找到相邻且拥有相同聚合Key的数据。
(6)在汇总数据时,同一分区内,相同聚合Key的多行数据会合并成一行。其中,汇总字段会进行SUM计算;对于那些非汇总字段,则会使用第一行数据的取值。
(7)支持嵌套结构,但列字段名称必须以Map后缀结尾。嵌套类型中,默认以第一个字段做为聚合Key。除第一个字段之外,任何名称以Key、Id或Type为后缀结尾的字段,都将和第一个字段一块儿组成复合Key。
有过数据仓库建设经验的读者必定知道“数据立方体”的概念,这是一个在数据仓库领域十分常见的模型。它经过以空间换时间的方法提高查询性能,将须要聚合的数据预先计算出来,并将结果保存起来。在后续进行聚合查询的时候,直接使用结果数据。
AggregatingMergeTree就有些许数据立方体的意思,它可以在合并分区的时候,按照预先定义的条件聚合数据。同时,根据预先定义的聚合函数计算数据并经过二进制的格式存入表内。将同一分组下的多行数据聚合成一行,既减小了数据行,又下降了后续聚合查询的开销。能够说,AggregatingMergeTree是SummingMergeTree的升级版,它们的许多设计思路是一致的,例如同时定义ORDER BY与PRIMARY KEY的缘由和目的。可是在使用方法上,二者存在明显差别,应该说AggregatingMergeTree的定义方式是MergeTree家族中最为特殊的一个。
声明使用AggregatingMergeTree的方式以下:
ENGINE = AggrefatingMergeTree()
AggregatingMergeTree没有任何额外的设置参数,在分区合并时,在每一个数据分区内,会按照ORDER BY聚合。而使用何种聚合函数,以及针对哪些列字段计算,则是经过定义AggregateFunction数据类型实现的。如下面的语句为例:
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) ORDER BY(id,city) PRIMARY KEY id ;
上例中列字段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('code'), sumState(toUInt32(200)), now();
在查询数据时,若是直接使用列名访问code和value,将会是没法显示的二进制形式。此时,须要调用与uniq、sum对应的uniqMerge、sumMerge函数:
SELECT id,city,uniqMerge(code),sumMerge(value) FROM agg_table GROUP BY id,city;
讲到这里,你是否会认为AggregatingMergeTree使用起来过于烦琐了?连正常进行数据写入都须要借助INSERT…SELECT的句式并调用特殊函数。若是直接像刚才示例中那样使用AggregatingMergeTree,确实会很是麻烦。不过各位读者并不须要忧虑,由于目前介绍的这种使用方法,并非它的主流用法。
AggregatingMergeTree更为常见的应用方式是结合物化视图使用,将它做为物化视图的表引擎。而这里的物化视图是做为其余数据表上层的一种查询视图
如今用一组示例说明。首先,创建明细数据表,也就是俗称的底表:
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) ;
数据会自动同步到物化视图,并按照AggregatingMergeTree引擎的规则处理。
在查询数据时,面向的对象是物化视图AggregatingMergeTree:
SELECT id,sumMerge(value),uniqMerge(code) FROM agg_view GROUP BY id,city ;
接下来,简单梳理一下AggregatingMergeTree的处理逻辑。
(1)用ORBER BY排序键做为聚合数据的条件Key。
(2)使用AggregateFunction字段类型定义聚合函数的类型以及聚合的字段。
(3)只有在合并分区的时候才会触发聚合计算的逻辑。
(4)以数据分区为单位来聚合数据。当分区合并时,同一数据分区内聚合Key相同的数据会被合并计算,而不一样分区之间的数据则不会被计算。(5)在进行数据计算时,由于分区内的数据已经基于ORBER BY排序,因此可以找到那些相邻且拥有相同聚合Key的数据。
(6)在聚合数据时,同一分区内,相同聚合Key的多行数据会合并成一行。对于那些非主键、非AggregateFunction类型字段,则会使用第一行数据的取值。
(7)AggregateFunction类型的字段使用二进制存储,在写入数据时,须要调用State函数;而在查询数据时,则须要调用相应的Merge函数。其中,*表示定义时使用的聚合函数。
(8)AggregatingMergeTree一般做为物化视图的表引擎,与普通MergeTree搭配使用。
假设如今须要设计一款数据库,该数据库支持对已经存在的数据实现行级粒度的修改或删除,你会怎么设计?一种最符合常理的思惟多是:首先找到保存数据的文件,接着修改这个文件,删除或者修改那些须要变化的数据行。然而在大数据领域,对于ClickHouse这类高性能分析型数据库而言,对数据源文件修改是一件很是奢侈且代价高昂的操做。相较于直接修改源文件,它们会将修改和删除操做转换成新增操做,即以增代删。
CollapsingMergeTree就是一种经过以增代删的思路,支持行级数据修改和删除的表引擎。它经过定义一个sign标记位字段,记录数据行的状态。若是sign标记为1,则表示这是一行有效的数据;若是sign标记为-1,则表示这行数据须要被删除。当CollapsingMergeTree分区合并时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。这种1和-1相互抵消的操做,犹如将一张瓦楞纸折叠了通常。这种直观的比喻,想必也正是折叠合并树(CollapsingMergeTree)名称的由来,其折叠的过程如图所示。
声明CollapsingMergeTree的方式以下:
ENGINE = CollapsingMergeTree(sign)
其中,sign用于指定一个Int8类型的标志位字段。一个完整的使用示例以下所示:
CREATE TABLE collpase_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排序键做为后续判断数据惟一性的依据。按照以前的介绍,对于上述collpase_table数据表而言,除了常规的新增数据操做以外,还可以支持两种操做。
其一,修改一行数据:
--修改前的源数据,它须要被修改 INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1); --镜像数据,ORDER BY字段与源数据相同(其余字段能够不一样),sign取反为-1,它会和源数据折叠 INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',-1); --修改后的数据,sign为1 INSERT INTO TABLE collpase_table VALUES('A000',120,'2019-02-20 00:00:00',1);
其二,删除一行数据:
--修改前的源数据,它须要被修改 INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1); --镜像数据,ORDER BY字段与源数据相同(其余字段能够不一样),sign取反为-1,它会和源数据折叠 INSERT INTO TABLE collpase_table VALUES('A000',100,'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会打印警告日志,但不会报错,在这种情形下,查询结果不可预知。在使用CollapsingMergeTree的时候,还有几点须要注意。
在使用CollapsingMergeTree的时候,还有几点须要注意。
(1)折叠数据并非实时触发的,和全部其余的MergeTree变种表引擎同样,这项特性也只有在分区合并的时候才会体现。因此在分区合并以前,用户仍是会看到旧的数据。解决这个问题的方式有两种。
❑ 在查询数据以前,使用optimize TABLE table_name FINAL命令强制分区合并,可是这种方法效率极低,在实际生产环境中慎用。
❑ 须要改变咱们的查询方式。以collpase_table举例,若是原始的SQL以下所示:
SELECT id,SUM(code),COUNT(code),AVG(code),uniq(code) FROM collpase_table GROUP BY id ;
则须要改写成以下形式:
SELECT id,SUM(code * sign),COUNT(code * sign),AVG(code * sign),UNIQ(code * sign) FROM collpase_table GROUP BY id HAVING SUM(sign) > 0;
(2)只有相同分区内的数据才有可能被折叠。不过这项限制对于CollapsingMergeTree来讲一般不是问题,由于修改或者删除数据的时候,这些数据的分区规则一般都是一致的,并不会改变。
(3)最后这项限制多是CollapsingMergeTree最大的命门所在。CollapsingMergeTree对于写入数据的顺序有着严格要求。如今用一个示例说明。若是按照正常顺序写入,先写入sign=1,再写入sign=-1,则可以正常折叠:
INSERT INTO TABLE collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1); INSERT INTO TABLE collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1);
SELECT * FROM collpase_table;
optimize table collpase_table FINAL; SELECT * FROM collpase_table;
如今将写入的顺序置换,先写入sign=-1,再写入sign=1,则不可以折叠:
INSERT INTO TABLE collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1); INSERT INTO TABLE collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1);
这种现象是CollapsingMergeTree的处理机制引发的,由于它要求sign=1和sign=-1的数据相邻。而分区内的数据基于ORBER BY排序,要实现sign=1和sign=-1的数据相邻,则只能依靠严格按照顺序写入。
若是数据的写入程序是单线程执行的,则可以较好地控制写入顺序;若是须要处理的数据量很大,数据的写入程序一般是多线程执行的,那么此时就不能保障数据的写入顺序了。在这种状况下,CollapsingMergeTree的工做机制就会出现问题。为了解决这个问题,ClickHouse另外提供了一个名为VersionedCollapsingMergeTree的表引擎。
VersionedCollapsingMergeTree表引擎的做用与CollapsingMergeTree彻底相同,它们的不一样之处在于,VersionedCollapsingMergeTree对数据的写入顺序没有要求,在同一个分区内,任意顺序的数据都可以完成折叠操做。VersionedCollapsingMergeTree是如何作到这一点的呢?其实从它的命名各位就应该可以猜出来,是版本号。
在定义VersionedCollapsingMergeTree的时候,除了须要指定sign标记字段之外,还须要指定一个UInt8类型的ver版本号字段:
ENGINE = VersionedCollapsingMergeTree(sign,ver)
一个完整的例子以下:
CREATE TABLE ver_collpase_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字段以后,VersionedCollapsingMergeTree会自动将ver做为排序条件并增长到ORDER BY的末端。以上面的ver_collpase_table表为例,在每一个数据分区内,数据会按照ORDER BY id , ver DESC排序。因此不管写入时数据的顺序如何,在折叠处理时,都能回到正确的顺序。
能够用一组示例证实,首先是删除数据:
--删除 INSERT INTO TABLE ver_collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1,1); INSERT INTO TABLE ver_collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1,1); select * from ver_collpase_table;
接着是修改数据:
optimize table ver_collpase_table FINAL; select * from ver_collpase_table;
INSERT INTO TABLE ver_collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1,1); INSERT INTO TABLE ver_collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1,1); INSERT INTO TABLE ver_collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1,2); select * from ver_collpase_table;
optimize table ver_collpase_table FINAL; select * from ver_collpase_table;