ClickHouse物化视图里常见的7个坑,你踩过几个?

本文涉及的产品
阿里云百炼推荐规格 ADB PostgreSQL,4核16GB 100GB 1个月
云原生数据仓库AnalyticDB MySQL版,基础版 8ACU 100GB 1个月
简介: 在 OLAP 的业务场景中,不仅要把数据存起来,还需要把数据处理好。在 ClickHouse 中,为了提高数据处理性能,使用 Materialized View 是有效的方法之一。本文主要探讨 Materialized View(下文称 MV) 的工作原理与最佳实践,并介绍了使用过程中容易踩坑的一些问题和解决方案。


导读

在 OLAP 的业务场景中,不仅要把数据存起来,还需要把数据处理好。在 ClickHouse 中,为了提高数据处理性能,使用 Materialized View 是有效的方法之一。本文主要探讨 Materialized View(下文称 MV) 的工作原理与最佳实践,并介绍了使用过程中容易踩坑的一些问题和解决方案。

Materialized View 介绍

物化视图可以看做是一种特殊的触发器,当数据被插入时,它将数据上执行 SELECT 查询的结果存储为到一个目标表中。

其设计初衷可以概括为: 通过在数据插入时的预处理来加速查询。

CREATE MATERIALIZED VIEW mv TO dest
AS SELECT a, b, count() AS cnt
FROM source
GROUP BY a, b

其中 source 为 MV 的源表(source table),dest 为 MV 的目标表(target table)。

在使用 MV 时,需要谨记以下三个原则。

①  只有源表插入(Insert)才会触发 MV 的更新。MV 不感知对源表的 truncate、 alter delete、alter update、 drop partition、 drop table、 rename 等所有其他操作。 需要注意两种特殊场景: ReplicatedMergeTree 在副本( replica)之间的数据同步(replication) 不会触发 MV 更新,分布式表(Distributed)向其本地表(Local)转发会触发 MV 更新。

②  MV 不会对源表做查询,只会查询本次插入的数据块( insert block)。有个例外是在创建 MV 时如果指定了 populate 会对源表执行一次查询并将结果初始化到 MV 中。

③  MV 支持任意引擎源表,包括 Null 引擎或者 Kafka 引擎这种没有保存数据能力的引擎。

典型使用场景

3.1 用于数据预聚合(pre-aggregate)

这里主要使用 SummingMergeTree 或者 AggregatingMergeTree 作为 MV 的目标表引擎。 MV 中保存源表的数据的聚合结果,在对源表执行 Insert 时,触发对 MV 聚合数据的更新。 MV 中的数据量相比源表可以大幅下降,查询 MV 可以直接获取聚合结果,而不需要每次查询对源表的大量数据扫描

比如下面的例子使用了SummingMergeTree表引擎为每个date/project汇总hits值。

-- 创建 SummingMergeTree 引擎的目标表
CREATE TABLE wikistat_top_projects
(
    `date` Date,
    `project` LowCardinality(String),
    `hits` UInt32
) ENGINE = SummingMergeTree
ORDER BY (date, project);
-- 创建物化视图
CREATE MATERIALIZED VIEW wikistat_top_projects_mv TO wikistat_top_projects AS
SELECT
    date(time) AS date,
    project,
    sum(hits) AS hits
FROM wikistat
GROUP BY date, project;

3.2 数据冗余以支持不同维度的数据查询

Clickhouse MergeTree 引擎的查询效率受表的主键(primary key)和排序键(order by )的设计影响较大。我们可以通过 MV 来创建一张同步表,两张表具有不同的主键和排序键,可以满足不同的查询需求。

3.3 数据提取转换

比如有一个空表(Null table),有多个不同数据源的数据插入到这个空表中。空表有多个MV,它们执行不同的数据转换并将结果写入不同的目的地。

3.4 配合 Kafka 或者 RabbitMQ 引擎

Kafka 引擎和 RabbitMQ 引擎自身没有数据持久化能力,与 MV 配合可以进行数据的持久化。

常见问题

MV 的逻辑比较简单,仅用一句话就可以概括:在数据插入源表时,将数据同步到目标表。

ClickHouse 的 CK 几乎没有对使用做任何限制。 用户可以根据需要灵活的使用,并可以与其他的能力进行搭配,比如 MV + Distributed Table,  MV+ReplicatedMergeTree 等。在实际应用中我们时常遭遇如下情形:尽管已成功构建了 MV,且确信已将数据成功插入源表。然而,当我们在MV 内执行查询时,结果却未能与预期相符,出现数据缺失或者数据重复。

4.1  数据聚合应该使用 SummingMergeTree 或者 AggregatingMergeTree

考虑如下需求,使用 mv 对源表数据进行聚合,客户错误地使用非聚合引擎(MergeTree)作为 MV 的目标表。

❎ 错误范例:使用普通 MergeTree 引擎做聚合

CREATE TABLE source (a Int64, b Int64)
ENGINE = MergeTree PARTITION BY (a) ORDER BY (a,b);
-- 目标表错误的使用 MergeTree 引擎
CREATE TABLE dest (a Int64, b Int64, cnt Int64)
ENGINE = MergeTree PARTITION BY (a) ORDER BY (a,b);
-- 创建基于 dest 表创建物化视图
CREATE MATERIALIZED VIEW mv to dest
AS SELECT a, b, count() as cnt
FROM source GROUP BY a, b;
-- 执行源表插入
insert into source(a,b) values(1, 1), (1,1), (1,2);
insert into source(a,b) values(1, 1), (1,1), (1,2);
-- MV 实际查询结果,不符合预期
SELECT a,b,count() FROM dest GROUP BY a, b;
┌─a─┬─b─┬─count()─┐
│ 1 │ 2 │       2 │
│ 1 │ 1 │       2 │
└───┴───┴─────────┘
-- 预期的正确结果
SELECT a, b, count() as cnt FROM source GROUP BY a, b
┌─a─┬─b─┬─sum─┐
│ 1 │ 2 │   2 │
│ 1 │ 1 │   4 │
└───┴───┴─────┘

✅ 正确范例 1:使用SummingMergeTree

MV 中涉及到聚合操作,应该使用 SummingMergeTree 或者 AggregatingMergeTree 

首先如果聚合操作为 sum 操作,可以使用SummingMergeTree。

CREATE TABLE source (a Int64, b Int64)
ENGINE = MergeTree PARTITION BY (a) ORDER BY (a,b);
-- 创建 SummingMergeTree 引擎目标表
-- 需要注意这里通过 ORDER BY(a,b ) 指定 a,b 作为SummingMergeTree的聚合键
CREATE TABLE dest_2 (a Int64, b Int64, sum UInt64)
ENGINE = SummingMergeTree ORDER BY (a, b);
-- 创建 MV 
CREATE MATERIALIZED VIEW mv_2 to dest_2
AS SELECT a, b, sum(a) as sum
FROM source GROUP BY a, b;
insert into source(a,b) values(1, 1), (1,1), (1,2);
insert into source(a,b) values(1, 1), (1,1), (1,2);
-- 为什么数据并没有根据a,b聚合?
-- 这里是因为 SummingMergeTree 是在后台merge时异步进行聚合的
select * from mv_2;
┌─a─┬─b─┬─sum─┐
│ 1 │ 1 │   2 │
│ 1 │ 2 │   1 │
│ 1 │ 1 │   2 │
│ 1 │ 2 │   1 │
└───┴───┴─────┘
-- 因此查询 SummingMergeTree 时,需要添加 group by
select a,b,sum(a) as sum from mv_2 group by a;
┌─a─┬─b─┬───sum──┐
│ 1 │ 1 │      2 │
│ 1 │ 2 │      4 │
└───┴───┴────────┘
-- 或者手动执行 optimize table,手动触发 SummingMergeTree 的聚合
optimize table dest_2 final;
-- optimize 之后不需要 group by 
select * from mv_2;
┌─a─┬─b─┬─sum─┐
│ 1 │ 1 │   4 │
│ 1 │ 2 │   2 │
└───┴───┴─────┘

✅ 正确范例 2:使用 AggregatingMergeTree 引擎

如果包含有其他聚合操作, 需选择 AggregatingMergeTree,下面给出AggregatingMergeTree 的实现。

CREATE TABLE source (a Int64, b Int64)
ENGINE = MergeTree PARTITION BY (a) ORDER BY (a,b);
-- 创建 AggregatingMergeTree 引擎目标表
CREATE TABLE dest_3(
  a Int64, b Int64, 
  cnt AggregateFunction(count, Int64))
ENGINE = AggregatingMergeTree order by (a, b);
-- 创建 MV 
CREATE MATERIALIZED VIEW mv_3 TO dest_3 AS
              SELECT a, b, countState(a) AS cnt
              FROM source GROUP BY a, b
insert into source(a,b) values(1, 1), (1,1), (1,2);
insert into source(a,b) values(1, 1), (1,1), (1,2);
-- 使用 Merge 进行查询
select a,b,countMerge(cnt) from mv_3 group by a,b;
┌─a─┬─b─┬─countMerge(cnt)─┐
│ 1 │ 2 │               2 │
│ 1 │ 1 │               4 │
└───┴───┴─────────────────┘

4.2 聚合引擎需要注意排序键(ORDER BY) 与 SQL 中聚合键(GROUP BY) 保持一致

使用SummingMergeTree 时,需要注意其排序键(ORDER BY)和 GROUP BY 聚合字段保持一致。

CREATE TABLE source (a Int64, b Int64, d Date) ENGINE = MergeTree PARTITION BY (a) ORDER BY (a,b,d);

 ❎ 错误范例

CREATE TABLE dest_2 (a Int64, b Int64, d Date, sum UInt64)

ENGINE = SummingMergeTree PARTITION BY toYYYYMM(d) ORDER BY (a, b);

CREATE MATERIALIZED VIEW mv_wrong

  ENGINE = SummingMergeTree ORDER BY (a, b);

  to dest_2

AS SELECT a, b, d, sum(a) as sum

FROM source GROUP BY a, b, d;

 ✅ 正确范例

CREATE TABLE dest_3 (a Int64, b Int64, sum UInt64)

ENGINE = SummingMergeTree PARTITION BY toYYYYMM(d) ORDER BY (a, b, d);

CREATE MATERIALIZED VIEW mv_right

  ENGINE = SummingMergeTree ORDER BY (a, b);

  to dest_2

AS SELECT a, b, sum(a) as sum

FROM source GROUP BY a, b, d;

4.3 始终谨记 MV 同步只会查询 Insert Block,不会查询原表

如下面的 MV 定义中,每次插入只会对插入的数据执行 SELECT 查询,而不是查询source 整张表。

CREATE TABLE source (a UInt64, b UInt64) ENGINE = MergeTree PARTITION BY (a) ORDER BY (a,b);

-- 组内排序,按 a 进行分组,取每个组内出现次数最多的 b

 ❎ 错误范例

CREATE TABLE dest(

  a Int64, b Int64, 

  max_cnt AggregateFunction(max, UInt64))

ENGINE = AggregatingMergeTree order by (a);

CREATE MATERIALIZED VIEW mv to dest 

AS SELECT a,  maxState(cnt_by_a_b)  as max_cnt

FROM (

    SELECT a, b, 

    count() AS cnt_by_a_b

    FROM source

    GROUP BY a, b

)  GROUP BY a

❎ 正确范例

-- 同时插入两条数据

insert into source(a, b) values(1, 1),(1,1);

select a,  maxMerge(max_cnt)  as max_cnt from dest group by a;

┌─a─┬─max_cnt─┐

│ 1      │                  2 │

└───┴─────┘

-- 依次插入两条数据

insert into source(a, b) values(1, 1);

insert into source(a, b) values(1, 1);

┌─a─┬─max_cnt─┐

│ 1      │       1            │

└───┴─────┘

4.4 MV 的字段名需要与查询结果字段完全一致

CREATE TABLE source (a Int64, b Int64, d Date) ENGINE = MergeTree PARTITION BY (a) ORDER BY (a,b,d);

 ❎ 错误范例

CREATE TABLE dest_2 (a Int64, b Int64, d Date, sumA UInt64)

ENGINE = SummingMergeTree PARTITION BY toYYYYMM(d) ORDER BY (a, b);

CREATE MATERIALIZED VIEW mv_wrong

  ENGINE = SummingMergeTree ORDER BY (a, b);

  to dest_2

AS SELECT a, b, d, sum(a) 

FROM source GROUP BY a, b, d;

 ✅ 正确范例

CREATE TABLE dest_2 (a Int64, b Int64, d Date, sumA UInt64)

ENGINE = SummingMergeTree PARTITION BY toYYYYMM(d) ORDER BY (a, b);

CREATE MATERIALIZED VIEW mv_wrong

  ENGINE = SummingMergeTree ORDER BY (a, b);

  to dest_2

AS SELECT a, b, d, sum(a) as sumA

FROM source GROUP BY a, b, d;

4.5 MV 和 ReplicationMergeTree 结合

使用过程中需要注意:

  1. ReplicatedMergeTree 在副本( replica)之间的数据同步(replication)  不会触发 MV 更新。
  2. MV 在源表向目标表的数据同步,会传导到ReplicatedMergeTree 进行副本间同步。

如果需要在 replica 之间保障 MV 的数据一致,需要将 MV 的目标表定义为 ReplicatedMergeTree。

如下图数据同步的过程如下:① 在 Node1 中执行插入,数据写入本副本的 Table,② Insert 操作同步进行本副本内 MV 的同步,③ ReplicatedMergeTree 会将数据同步到 Node2 的 Table 中。④ Node1 的 MV 目标表为ReplicatedMergeTree 引擎,会触发同步数据到 Node2 的 MV 目标表。

数据重复问题

MV 和 ReplicatedMergeTree 有一个场景会导致数据重复。 如果对 ReplicatedMergeTree 执行 Populate 操作时,会触发数据重复。

关于该问题细节可以参考 https://github.com/ClickHouse/ClickHouse/issues/9081

4.6 MV 和 JOIN

需注意一点: 只有驱动表的 INSERT 操作才会触发 MV 更新,被驱动表不会。

如果希望驱动表和被驱动表插入时都会触发 MV 的更新,需要创建两个 MV。实例如下

CREATE MATERIALIZED VIEW mv1 TO dest
SELECT ...
FROM source left join some_dimension on (...)
CREATE MATERIALIZED VIEW mv2 TO dest
SELECT ...
FROM some_dimension right join source on (...)

4.7 MV 和 Distributed Table

可以分成四种情况分别讨论

方案

MV Source 

MV Target

INSERT  INTO

Data in Target 

数据同步过程,Distributed(D),  Local(L)

Local

Local

Distributed

YES

1. 写入 D 表,  D 表异步插入 Local 表

2. 触发 Local 表的 MV 更新

Local

YES

1. 数据写入 L 表,触发 MV 更新

Distributed

Distributed

Distributed

YES

1. 数据写入 D 表,D 表 MV 更新 

2. 数据写入 L 表中

Local

NO

1. 数据写入 L 表,不会触发 D 表 MV 更新

Local

Distributed

Distributed

YES

1. 数据写入 D 表,D 异步插入 L 表

2. 触发 L 表的 MV 更新

Local

YES

1. 数据写入 L 表,触发 MV 更新

Distributed

Local

Distributed

YES

1. 数据写入 D 表,D 异步插入 Local 表

2. 触发 D 表的 MV 更新

Local

NO

1. 数据写入 L 表,不会触发 D 表MV 更新

方案 ①  种较为常见,用于单 shard 场景。 该方案最简单、效率最高、且不会出现数据不同步问题。

方案 ② 和 ③ 用于多 shard 场景,数据基于 sharding key 规则分布于不同的 shard 中。

其中方案 ②  如果数据实际执行写入到 Local 表中,则会导致 MV 中的数据不同步。

了解更多

云数据库 ClickHouse 版是全托管云原生 Serverless 实时数仓服务。具有开箱即用,高吞吐写入,秒级实时分析、自动弹性优势。 广泛应用于流量分析、广告营销分析、行为分析、人群划分、客户画像、敏捷BI、数据集市、网络监控、分布式服务和链路监控等业务场景。详细内容见ClickHouse产品详情页

阿里云ClickHouse 企业版已于4.26日正式商业化,并推出了指定规格资源包首购优惠的折扣活动哦!首次购买规格在3000-9500CCU*H的计算包,以及首次购买1个月450G及以下规格的存储包,均可享 0.4折的超优惠价格!首次计算和存储资源组合购买不超过 99.58 元,欢迎登陆阿里云ClickHouse官网进行选购。

点击链接立刻体验:

计算包:https://common-buy.aliyun.com/?commodityCode=clickhouse_serverless_dp_cn

存储包:https://common-buy.aliyun.com/?commodityCode=clickhouse_storage_dp_cn

附录

[1] https://clickhouse.com/blog/using-materialized-views-in-clickhouse

[2] https://developer.aliyun.com/article/1327456

[3] https://den-crane.github.io/Everything_you_should_know_about_materialized_views_commented.pdf

[4] https://github.com/ClickHouse/ClickHouse/issues/9081

[5] https://github.com/ClickHouse/ClickHouse/issues/8336 

相关文章
|
存储
ClickHouse物化视图
ClickHouse物化视图
433 1
|
存储 监控 OLAP
【ClickHouse 技术系列】- 在 ClickHouse 物化视图中使用 Join
本文翻译自 Altinity 针对 ClickHouse 的系列技术文章。面向联机分析处理(OLAP)的开源分析引擎 ClickHouse,因其优良的查询性能,PB级的数据规模,简单的架构,被国内外公司广泛采用。本系列技术文章,将详细展开介绍 ClickHouse。
【ClickHouse 技术系列】- 在 ClickHouse 物化视图中使用 Join
|
7天前
|
存储 监控 数据挖掘
【Clikhouse 探秘】ClickHouse 物化视图:加速大数据分析的新利器
ClickHouse 的物化视图是一种特殊表,通过预先计算并存储查询结果,显著提高查询性能,减少资源消耗,适用于实时报表、日志分析、用户行为分析、金融数据分析和物联网数据分析等场景。物化视图的创建、数据插入、更新和一致性保证通过事务机制实现。
44 14
|
6月前
|
消息中间件 存储 监控
|
存储 SQL Cloud Native
一文教会你使用强大的ClickHouse物化视图
在现实世界中,数据不仅需要存储,还需要处理。处理通常在应用程序端完成。但是,有些关键的处理点可以转移到ClickHouse,以提高数据的性能和可管理性。ClickHouse中最强大的工具之一就是物化视图。在这篇文章中,我们将探秘物化视图以及它们如何完成加速查询以及数据转换、过滤和路由等任务。 如果您想了解更多关于物化视图的信息,我们后续会提供一个免费的培训课程。
28168 10
一文教会你使用强大的ClickHouse物化视图
|
1月前
|
存储 关系型数据库 MySQL
一个项目用5款数据库?MySQL、PostgreSQL、ClickHouse、MongoDB区别,适用场景
一个项目用5款数据库?MySQL、PostgreSQL、ClickHouse、MongoDB——特点、性能、扩展性、安全性、适用场景比较
|
1月前
|
存储 分布式计算 数据库
阿里云国际版设置数据库云分析工作负载的 ClickHouse 版
阿里云国际版设置数据库云分析工作负载的 ClickHouse 版
|
2月前
|
存储 SQL 缓存
数据库测试|Elasticsearch和ClickHouse的对决
由于目前市场上主流的数据库有许多,这次我们选择其中一个比较典型的Elasticsearch来和ClickHouse做一次实战测试,让大家更直观地看到真实的比对数据,从而对这两个数据库有更深入的了解,也就能理解为什么我们会选择ClickHouse。
数据库测试|Elasticsearch和ClickHouse的对决
|
1月前
|
存储 关系型数据库 MySQL
四种数据库对比MySQL、PostgreSQL、ClickHouse、MongoDB——特点、性能、扩展性、安全性、适用场景
四种数据库对比 MySQL、PostgreSQL、ClickHouse、MongoDB——特点、性能、扩展性、安全性、适用场景
|
5月前
|
DataWorks API 调度
DataWorks产品使用合集之在调度配置配置了节点的上游节点输出,没办法自动生成这个flow的依赖,该怎么操作
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。

热门文章

最新文章