ClickHouse如何更新数据(精)

ClickHouse系列文章:

  1. ClickHouse如何更新数据
  2. ClickHouse Join为什么被大家诟病?
  3. 有必要了解下ClickHouse的索引原理

问题背景

在 OLAP 数据库中,可变数据通常不受欢迎。ClickHouse 也不欢迎可变数据。然而现实情况,更新情况不可避免。那么ClickHouse如何进行更新数据了?以及如何进行准实时更新了?

更新方法

Partition Operations

这个方法是比较早就提出的解法,大致思路是就是操作分区,有更新就将删掉原分区,然后用新的分区替代。

用法

下面是通过分区进行数据更新的步骤:

  1. Create modified partition with updated data on another table
  2. Copy data for this partition to detached directory
  3. DROP PARTITION in main table
  4. ATTACH PARTITION in main table

适用场景

分区交换对于低频率的批量数据更新比较有用,但当需要实时的高频率的更新数据时,它们就不那么方便了。此外,开发人员操作分区还是不太方便的,因此这种方法一般用的比较少。

详细内容请看:How to Update Data in ClickHouse

Incremental Log

Incremental log的思想是什么了?比如对于用户浏览统计表中的一条数据,如下所示:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 
│ 4324182021466249494 │         5 │      146 │    1 │ 
└─────────────────────┴───────────┴──────────┴──────┘

现在有更新了:用户又浏览了一个页面,所以我们应该改变pageview从5到6,以及持续时间从146到185。那么按照Incremental log的思想,再插入两行:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 
│ 4324182021466249494 │         5 │      146 │   -1 │ 
│ 4324182021466249494 │         6 │      185 │    1 │ 
└─────────────────────┴───────────┴──────────┴──────┘

第一个是删除行。它和我们已经得到的行是一样的只是Sign被设为-1。第二个更新行,所有数据设置为新值。之后我们有三行数据:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 
│ 4324182021466249494 │         5 │      146 │    1 │ 
│ 4324182021466249494 │         5 │      146 │   -1 │ 
│ 4324182021466249494 │         6 │      185 │    1 │ 
└─────────────────────┴───────────┴──────────┴──────┘

那么对于count,sum,avg的计算方法如下:

-- number of sessions
count() -> sum(Sign)  
-- total number of pages all users checked 
sum(PageViews) -> sum(Sign * PageViews)  
-- average session duration, how long user usually spent on the website 
avg(Duration) -> sum(Sign * Duration) / sum(Sign)

这就是Incremental log方法,这种方法的不足之处在于:

  • 首先需要获取到原数据,那么就需要先查一遍CK,或者将数据保存到其他存储中便于检索查询,然后我们才可以针对原数据插入一条 ‘delete’ rows;
  • Sign operations在某些计算场景并不适合,比如min、max、quantile等其他场景;
  • 额外的写入放大:当每个对象的平均更新次数为个位数时,更适合使用。

针对Incremental log方式的写入方案存储开销问题,clickhouse提供了CollapsingMergeTree,使用CollapsingMergeTree,“删除”行和旧的“删除”行将在合并过程中折叠。但是,注意这个引擎,只是解决了写放大问题,并不是说查询模式就不是Incremental Log这种,我们还是需要通过对sign的特殊计算方式,达到效果。

实践一下,建表如下:

CREATE TABLE UAct
(
    UserID UInt64,
    PageViews UInt8,
    Duration UInt8,
    Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID;

插入状态行,注意sign一列的值为1:

INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1);

插入一行取消行,用于抵消上述状态行。注意sign一列的值为-1,其余值与状态行一致;并且插入一行主键相同的新状态行,用来将PageViews从5更新至6,将Duration从146更新为185.

INSERT INTO UAct VALUES 
(4324182021466249494, 5, 146, -1), 
(4324182021466249494, 6, 185, 1);

查询数据:可以看到未Compaction之前,状态行与取消行共存:

为了获取正确的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 UAct 
GROUP BY UserID 
HAVING sum(Sign) > 0; 

最后,我们强制让其折叠一下:

optimize table UAct final;

再查询数据,结果已经是折叠后的的结果了

其他疑问:字段是否可以用case when 对标志位进行过滤?
答案:不行,因为达不到效果。我们要达到的效果,首先是删除原来的列,然后再新增一个有效列。没法根据标志位判断哪一个是有效的,除非标志位的值不断增加,然后不断增加,那就不属于这种方式,下文会提到根据时间argMax的方法。

详细内容请看:How to Update Data in ClickHouse

Alter/Update Table

ClickHouse团队在2018年发布了UPDATE和DELETE,但是它不是原生的UPDATE和DELETE语句,而是被实现为ALTER TABLE UPDATE语句,如下所示:

ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr;

还是针对上面的表,修改浏览数,ALTER UPDATE语句如下:

ALTER TABLE UPDATE PageViews=7 WHERE UserID=4324182021466249700;

然后查看结果:

其实并没有更新,这是为什么了?因为更新是一个异步的操作。当用户执行一个如上的Update操作获得返回时,ClickHouse内核其实只做了两件事情:
1.检查Update操作是否合法;
2.保存Update命令到存储文件中,唤醒一个异步处理merge和mutation的工作线程;

异步线程的工作流程极其复杂,总结其精髓描述如下:先查找到需要update的数据所在datapart,之后对整个datapart做扫描,更新需要变更的数据,然后再将数据重新落盘生成新的datapart,最后用新的datapart做替代并remove掉过期的datapart。

这就是ClickHouse对update指令的执行过程,可以看出,频繁的update指令对于ClickHouse来说将是灾难性的。(当然,我们可以通过设置,将这个异步的过程变成同步的过程,详细请看:Synchronicity of ALTER Queries,然而同步阻塞就会比较严重)。

ClickHouse对Update语句支持的不好,但是对于Insert语句,尤其是批量插入支持的很好。所以更新操作用Insert替代会很快就返回。 但是用Insert,我们如何完成更新这个动作,以及如何保证查询到最新数据了?

Insert+xxxMergeTree

用Insert加特定引擎,也可以实现更新效果。该方法适用于xxxMergeTree,如ReplacingMergeTree或AggregatingMergeTree。但是了,更新是异步的。因此刚插入的数据,并不能马上看到最新的结果,因此并不是准实时的。

比如使用AggregatingMergeTree,用法如下:

CREATE TABLE IF NOT EXISTS whatever_table ON CLUSTER default (     
  user_id UInt64,
  gender SimpleAggregateFunction(anyLast, Nullable(Enum('女' = 0, '男' = 1))),
  ...
)
ENGINE = AggregatingMergeTree() partition by toYYYYMMDD(reg_date) ORDER BY user_id;

就以上建标语句展开分析,AggregatingMergeTree会将除主键(user)外的其余列,配合anyLast函数,替换每行数据为一种预聚合状态。其中anyLast聚合函数声明聚合策略为保留最后一次的更新数据。

详细内容,请看:ClickHouse:抓住你的每一个目标用户,人群圈选业务的大杀器

实时性: 非准实时。

优点在于:
ClickHouse提供的这些mergeTree引擎,可以帮助我们达到最终一致性。

缺点在于:
xxxMergeTree并不能保证任何时候的查询都是聚合过后的结果,并且也没有提供标志位用于查询数据的聚合状态与进度。因此,为了确保数据在查询前处于已聚合的状态,还需手动下发optimize指令强制聚合过程的执行。

Insert+xxxxMergeTree+Final

用xxxMergeTree是异步的,如何达到准实时的效果了?ClickHouse提供了FINAL关键字来解决这个问题。。当指定FINAL后,ClickHouse会在返回结果之前完全合并数据,从而执行给定表引擎合并期间发生的所有数据转换。

用法

首先Insert数据:

INSERT INTO test_a (*) VALUES (1, 'a', 1) ;

查询时,加入final关键字,如下所示:

SELECT COUNT()FROM test_a FINAL

优缺点

对上述语句,explain后,查询执行计划如下所示:

Expression ((Projection + Before ORDER BY))
  Aggregating
    Expression (Before GROUP BY)
      SettingQuotaAndLimits (Set limits and quota after reading from storage)
        Expression (Remove unused columns after reading from storage)
          MergingFinal (Merge rows for FINAL)
            Expression (Calculate sorting key expression)
              ReadFromStorage (MergeTree with final)

从执行计划可以看出代价比较高:

  • 是一个串行过程;
  • 会进行分区合并;

因此,这个FINAL,也不宜频繁的使用。

Insert + argMax

用法

argMax 函数的参数如下所示,它能够按照 field2 的最大值取 field1 的值:

argMax(field1,field2)

当我们更新数据时,会写入一行新的数据,通过查询最大的 create_time 得到修改后的字段值,例如通过下面的语句可以得到最新的 score :

argMax(score, create_time) AS score

具体用法如下所示:

select ru_id,row_update_time,
       argMax(is_effective,row_update_time) is_effective
from t_ru_packaging_build
group by ru_id,row_update_time;

如果我们聚合统计指标,那么SQL语句如下:

select ru_id,sum(case when is_effective =1 then 1 else 0 end) from (
    select ru_id,
           row_update_time,
           argMax(is_effective,row_update_time) is_effective
    from t_ru_packaging_build
    group by ru_id,row_update_time
) group by ru_id;

为了简化SQL,我们可以建立一个视图,注意不是物化视图,如下所示:

CREATE VIEW view_ru_packaging_build as
SELECT ru_id,
       row_update_time,
       argMax(is_effective,row_update_time) is_effective
FROM t_ru_packaging_build
GROUP BY ru_id,row_update_time;

此时我们只需查询视图即可,查询语句如下所示:

SELECT ru_id,
       sum(case when is_effective =1 then 1 else 0 end) 
FROM   view_ru_packaging_build
GROUP BY ru_id;

实时性: 查询每次都是实时的。

优点如下:

  • 为更新数据提供了一个新的解法思路;

缺点如下:

  • 查询语句比较复杂;
  • 如果还要做一些聚合统计逻辑,那么就需要子查询;
  • 内存开销会大一些。

详细内容,请看:ClickHouse准实时数据更新的新思路 - 腾讯云

OPTIMIZE FINAL

因此在业务需要数据更新的场景下(如Mysql同步到Clickhouse),通常会使用ReplacingMergeTree或CollapsingMergeTree的数据合并逻辑绕行实现异步更新,这样一方面可以保证数据的最终一致性,另一方面Clickhouse性能开销也会比alter table小。但这种方式有一个缺点是MergeTree引擎的数据合并过程(merge)是Clickhouse基于策略控制的,执行时间比较随机,因此数据一致性缺少时间保证,极端情况下数据过了一天也没有完全合并。
而Optimize Table这个命令可以强制触发MergeTree引擎的数据合并,可以用来解决数据合并时间不确定的问题。

OPTIMIZE FINAL 可以进行强制刷新,使用方式如下:

OPTIMIZE TABLE {tableName} FINAL

注意OPTIMIZE操作速度慢,代价高,因此不能频繁的执行。

详细介绍,请看:Clickhouse Optimize Table全面解析- 云+社区 - 腾讯云

方法总结

上面提供了七种更新方法,这里对其进行一个对比总结:

方法名 实时性 优势 不足 适合场景
Partition Operations 非准实时 1、不适用于实时场景;
2、操作不便
大批量修改,非准实时场景
Incremental Log 准实时 某些场景可以用这个办法取巧解决 1、要先查原数据;
2、某些计算场景并不适合,比如min、max等其他场景;
对于sum、count、avg等飞unique场景
Alter/Update Table 非准实时 1、异步的过程
2、不能频繁操作
Insert+ xxxMergeTree 非准实时 最终一致性,非准实时; 对实时性要求不高的场景
Insert+ xxxMergeTree+Final 准实时 可以实现准实时 准实时;
Final不能频繁使用
如果是查的不频繁,可以用这个来实现准实时
Insert+argMax 准实时 实时性好,开销相对能接受 1、查询语句复杂;
2、内存开销稍微较大;
对比其他方法,这是对于修改较频繁的场景适用的方法
OPTIMIZE FINAL 准实时 操作后,一定是最新的 代价很大,耗时很长,不可频繁使用 某些验证场景,或者临时操作

总结一下,其实clickhouse在追求极致的速度面前,对更新其实还是支持的不是很好的,因此如果业务场景本身是更新很频繁,同时又对更新实时性有很高的要求,那么其实可能并不应该选择使用ClickHouse,而是应该使用其他OLAP引擎。

如果更新不是很频繁,且已经选定了要使用clickhouse,那就根据业务场景,以及对实时性的要求,选用上面6中更新方式中的一种吧。

参考文档

ClickHouse准实时数据更新的新思路 - 腾讯云
在ClickHouse 中处理实时更新- 掘金
ClickHouse - How to Update Data in ClickHouse
Clickhouse Optimize Table全面解析- 云+社区 - 腾讯云
https://altinity.com/blog/2018/10/16/updates-in-clickhouse
ClickHouse:抓住你的每一个目标用户,人群圈选业务的大杀器
Clickhouse UPDATE 和DELETE操作_vkingnew的博客
ClickHouse多种实时更新方法总结 - 墨天轮

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章