1.CollapsingMergeTree
- 在排序键(ORDER BY)的所有字段都相同的条件下,如果特定的字段具有1和-1的值,CollapsingMergeTree将异步删除(折叠)成对的行。没有配对的行将保留。
- 能显著降低存储空间并提升SELECT查询的效率。
- 变相的实现了数据的更新和删除逻辑。
指定表引擎:
CollapsingMergeTree(sign)
参数:sign,标识行类型的列名称,1是状态行,-1是取消行。列的数据类型是Int8。使用特定的列Sign,如果Sign=1,则表示该行是对象的状态,称之为"状态"行。如果Sign=-1,则表示取消具有属性的对象的状态,称之为"取消"行。
例如,计算用户在某个站点访问的页面数以及停留的时长。在某个时刻,将用户的活动状态写入下面的行:
一段时间后,将用户活动的变化写入以下两行:
第一行取消对象(用户)的先前状态,它应该复制被取消状态行的排序键字段,字段sign设置为-1标 识取消状态。
第二行包含当前状态取消状态的行包含:排序键字段的拷贝和相反的Sign值。 取消状态的行增加了存储的大小,但是却可以 快速写入数据。
合并算法
当ClickHouse合并数据片段时,具有相同排序键的每一组连续行被缩减为不超过两行,一行的Sign=1, 另一行的Sign=-1。换句话说,条目将折叠。
- 如果”state”行与”cancel”行的数目匹配,且最后一行是”state”行,则保留第一个”cancel”和最后一个”state”行。
- 如果”state”行比”cancel”行的数目多,则保留最后一个”state”行。
- 如果”cancel”行比”state”行的数目多,则保留第一个”cancel”行。
- 其它情况,不保留行。
Note:当”state”行比”cancel”行数目至少多2个,或者”cancel”行比”state”行多至少2个时,合并将继续,但是ClickHouse将此情况视为逻辑错误并将其在server的日志记录下来。如果同一份数据被插入了多次,则会发生此错误。
聚合统计
合并算法不能保证所有具有相同排序键的行都位于相同的结果数据片段中,甚至位于同一个物理服务器上。 ClickHouse使用多线程处理SELECT查询,并且无法预测结果中的行顺序。如果需要从CollapsingMergeTree表中完全”折叠”数据,则需要结合Sign字段使用聚合。
例如:计算使用,使用sum(Sign)而不是count()。计算sum,使用sum(Sign * x)而不是sum(x),以此类推。
并且需要添加HAVING sum(Sign) > 0.聚合的count、sum和avg可以通过这种方式计算,如果一个对象的至少一个状态未折叠,则可以计算聚合 uniq。无法计算聚合的最小值和最大值,因为CollapsingMergeTree不会保存折叠状态的历史记录。
如果要提取数据但不想使用聚合,则可以对FROM子句使用FINAL修饰符,这种方法效率明显不高。
2.案例(State行和Cancel行匹配示例)
示例:State行和Cancel行匹配示例
验证:如果"state"行与"cancel"行的数目匹配,且最后一行是"state"行,则保留第一个"cancel"行和最后一个"state"行。建表语句:
CREATE TABLE UAct ( UserID UInt64, PageViews UInt8, Duration UInt8, Sign Int8 ) ENGINE = CollapsingMergeTree(Sign) ORDER BY UserID;
首先清空表:
truncate table UAct;
执行如下的数据插入语法:
INSERT INTO UAct VALUES (4324182021466249494, 1, 11, -1),(4324182021466249494, 2, 12, -1); INSERT INTO UAct VALUES (4324182021466249494, 3, 13, 1); INSERT INTO UAct VALUES (4324182021466249494, 4, 14, 1);
查看数据:
xxxx2 :) select * from UAct; SELECT * FROM UAct ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 4 │ 14 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘ ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 1 │ 11 │ -1 │ │ 4324182021466249494 │ 2 │ 12 │ -1 │ └─────────────────────┴───────────┴──────────┴──────┘ ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 3 │ 13 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘ 4 rows in set. Elapsed: 0.010 sec. xxxx2 :)
执行计划外的片段合并操作:
xxxx2 :) optimize table UAct; OPTIMIZE TABLE UAct Ok. 0 rows in set. Elapsed: 0.004 sec. xxxx2 :) select * from UAct; SELECT * FROM UAct ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 1 │ 11 │ -1 │ │ 4324182021466249494 │ 4 │ 14 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘ 2 rows in set. Elapsed: 0.008 sec. xxxx2 :)
从示例中可以观察到,最终的数据片段保留了第一个"cancel"行和最后一个"state"行。
3.示例2:聚合查询
示例数据:
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 5 │ 146 │ 1 │ │ 4324182021466249494 │ 5 │ 146 │ -1 │ │ 4324182021466249494 │ 6 │ 185 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘
建表:
DROP TABLE UAct; CREATE TABLE UAct ( UserID UInt64, PageViews UInt8, Duration UInt8, Sign Int8 ) ENGINE = CollapsingMergeTree(Sign) ORDER BY UserID;
插入数据:
INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1); INSERT INTO UAct VALUES (4324182021466249494, 5, 146, -1),(4324182021466249494, 6, 185, 1);
上面执行了两个INSERT语句,创建了两个不同的数据片段。如果使用一个INSERT语句,ClickHouse将创建一个数据片段,并且将永远不会执行任何合并。
查询数据:
xxxx2 :) select * from UAct; SELECT * FROM UAct ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 5 │ 146 │ -1 │ │ 4324182021466249494 │ 6 │ 185 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘ ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 5 │ 146 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘ 3 rows in set. Elapsed: 0.012 sec. xxxx2 :)
通过两个INSERT语句,创建了两个数据片段。SELECT查询是在两个线程中执行的,我们得到了随机顺序的行。由于尚未合并数据片段,折叠还未发生。
我们无法预测ClickHouse在何时执行数据片段的合并。因此,我们需要使用聚合:SELECT UserID, sum(PageViews * Sign) AS PageViews, sum(Duration * Sign) AS Duration FROM UAct GROUP BY UserID HAVING sum(Sign) > 0;
如果不使用聚合,可以对FROM子句使用FINAL修饰符进行强制合并:
select * from UAct FINAL;
效果如下:
xxxx2 :) select * from UAct FINAL; SELECT * FROM UAct FINAL ┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 6 │ 185 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘ 1 rows in set. Elapsed: 0.010 sec. xxxx2 :)
4.示例3:Cancel状态行取反的聚合查询
示例数据:
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ │ 4324182021466249494 │ 5 │ 146 │ 1 │ │ 4324182021466249494 │ -5 │ -146 │ -1 │ │ 4324182021466249494 │ 6 │ 185 │ 1 │ └─────────────────────┴───────────┴──────────┴──────┘
这个方法的核心思想是仅考虑关键字段,在"cancel"行中,可以指定负数,这些负数的值等于行的前一个版本的关键字段值的取反,这样在求和时就可以不使用Sign列。对于这种方法,必须更改PageViews和Duration字段的数据类型,从UIn8改成Int16。
CREATE TABLE UAct ( UserID UInt64, PageViews Int16, Duration Int16, Sign Int8 ) ENGINE = CollapsingMergeTree(Sign) ORDER BY UserID;
插入数据,并测试:
insert into UAct values(4324182021466249494, 5, 146, 1); insert into UAct values(4324182021466249494, -5, -146, -1); insert into UAct values(4324182021466249494, 6, 185, 1);