materialized_view 物化类型应基于现有的 (源) 表执行 SELECT。与 PostgreSQL 不同,ClickHouse 的 materialized view 不是“静态”的 (也没有对应的 REFRESH 操作) 。相反,它充当插入触发器:对插入源表的行应用已定义的 SELECT 转换,并将新行插入目标表。有关 materialized view 在 ClickHouse 中的工作方式的更多信息,请参阅 ClickHouse materialized view 文档。
有关物化的一般概念和共享配置 (engine、order_by、partition_by 等) ,请参阅 Materializations 页面。
如何管理目标表
materialized_view 物化 时,dbt-clickhouse 需要同时创建 materialized view 和接收转换后行的 目标表。目标表有两种管理方式:
你选择的方式会影响 schema 变更、全量刷新以及多 MV 配置的处理方式。以下各节将详细介绍这两种方式。
使用隐式目标进行物化
materialized_view 模型时,适配器会:
- 使用模型名称创建一个目标表
- 创建一个名为
<model_name>_mv的 ClickHouse materialized view
SELECT 语句中的列推断出来。所有资源 (目标表 + MV) 共享同一份模型配置。
多个 materialized view
UNION,并使用 --my_mv_name:begin 和 --my_mv_name:end 形式的注释来包裹每个 materialized view 的 SQL。
例如,下面的配置会构建两个 materialized view,它们都会将数据写入该模型的同一个目标表。materialized view 的名称将采用 <model_name>_mv1 和 <model_name>_mv2 的形式:
如何演进目标表 schema
dbt run 在 MV 的 SQL 中发现列存在差异时,你可以控制目标表 schema 的演进方式。
ignore) ,但你可以修改此设置,使其遵循in incremental models 中 on_schema_change 配置的相同行为。
此外,你也可以将此设置用作一种安全机制。如果将其设为 fail,那么当 MV 的 SQL 中的列与首次执行 dbt run 时创建的目标表不一致时,构建就会失败。
数据补齐
catchup=True) 。你可以将 catchup 配置设为 False 以禁用此行为。
使用显式目标进行物化 (Beta)
- 所有资源 (目标表 + MVs) 共享同一套配置。如果多个 MVs 指向同一个目标表,就必须使用
UNION ALL语法将它们一起定义。 - 这些资源都无法单独处理,必须通过同一个模型文件统一管理。
- 你无法轻松控制每个 MV 的名称。
- 目标表和 MVs 共享所有设置,因此很难分别配置各个资源,也不容易判断哪些配置属于哪个资源。
table 物化,然后在 materialized view 模型中引用它。
优势
- 资源完全分离:现在每个资源都可以单独定义,可读性更高
- dbt 与 CH 之间实现 1:1 资源对应:现在你可以使用 dbt 工具分别管理和迭代这些资源。
- 现可使用不同配置:现在可以为每个资源分别应用不同的配置。
- 无需再遵循命名约定:现在所有资源都会使用你指定的名称创建,而不是像 MV 那样额外添加
_mv后缀的自定义名称。
局限性
- 目标表定义对 dbt 来说并不算自然:它不是一段会从源表读取数据的 SQL,因此这里无法享受到 dbt 的校验。不过,MV 的 SQL 仍会通过 dbt 工具进行校验,而它与目标表各列的兼容性则会在 CH 层面校验。
- 我们发现了一些与
ref()函数局限性相关的问题:我们需要用它在模型之间建立引用,但它只能引用上游模型,不能引用下游模型。这给这种实现方式带来了一些问题。我们已在 dbt-core 仓库中提交了一个 issue,目前也正在与他们讨论,寻找可能的解决方案 (dbt-labs/dbt-core#12319) :- 当在 config 块内部调用
ref()时,它返回的是当前模型,而不是共享的那个模型。这使我们无法在 config() 部分中定义它,只能通过注释添加这个依赖。我们采用了与 dbt 文档中相同的模式,即 “—depends_on:” 方法。 ref()对我们是可行的,因为它会强制先创建目标表;但在生成文档中的依赖关系图里,目标表会被绘制成另一个上游依赖,而不是下游依赖,这会让图有些难以理解。unit-test也会迫使我们为目标表定义一些数据,即使本意并不是从中读取数据。变通方法就是将这个表的数据留空。
- 当在 config 块内部调用
用法
events_daily.sql:
{{ materialization_target_table(ref('events_daily')) }} macro 调用,它会为 MV 配置目标表。
模型 page_events_aggregator.sql:
mobile_events_aggregator.sql:
配置选项
materialized='table') :
在 materialized view 上 (
materialized='materialized_view') :
通常只需要在 MV 中将
catchup 设为 True,或在其目标表中将 repopulate_from_mvs_on_full_refresh 设为 True。如果两者都设为 True,可能会导致数据重复。常见操作
使用显式目标执行完全刷新
--full-refresh 时,显式目标表会被重新创建 (因此如果在此过程中正在进行数据摄取,可能会导致数据丢失) 。具体表现取决于你的配置:
选项 1:默认的 --full-refresh 行为。所有内容都会被重新创建,但在重新创建 MVs 期间,目标表将为空,或仅加载了部分数据。
所有内容都会被删除并重新创建。如果你希望通过 MVs SQL 重新插入数据,请保留 catchup=True 设置:
catchup=False,然后对这些 MV 执行 dbt run 或 dbt run --full-refresh。请确保在对目标表运行 --full-refresh 之前,这些 MV 已创建完成,因为它会使用 ClickHouse 中的 MV 定义。
在目标表模型上设置 repopulate_from_mvs_on_full_refresh=True。执行 dbt run --full-refresh 时,这将会:
- 创建一个新的临时表
- 使用每个 MV 的 SQL 执行 INSERT-SELECT
- 以原子方式交换表
更改目标表
--full-refresh,则无法更改 MV 的目标表。如果你在修改 materialization_target_table() 引用后尝试运行常规的 dbt run,构建会失败,并报错提示目标已发生更改。
如需更改目标:
- 更新
materialization_target_table()调用 - 运行
dbt run --full-refresh -s your_mv_model
常见问题排查
执行 run 期间/之后目标表为空
- materialized views 可能配置了
catchup=False,或者目标表配置了repopulate_from_mvs_on_full_refresh=False,因此在创建 materialized views 或重新创建目标表时,不会执行回填。这是预期行为。因此,如果你想通过 materialized views SQL 重新插入数据,请确保在 materialized view 中将catchup设为True(默认值) ,或在目标表中将repopulate_from_mvs_on_full_refresh设为True。注意不要同时启用这两项,以免产生重复数据。更多详情请参阅配置部分。 - 执行
dbt run --full-refresh时,如果 materialized views 使用默认的catchup=True,目标表会被重新创建,而 MVs 会按顺序重新插入数据。要避免这种情况,请参阅使用显式目标进行完全刷新。
在目标表中将 repopulate_from_mvs_on_full_refresh=True 与 dbt run --full-refresh 一起使用时,使用的是旧版 materialized view 的逻辑,而不是项目中当前的 SQL
repopulate_from_mvs_on_full_refresh=True 会使用 ClickHouse 中已经定义的现有 MV SQL。为确保使用新的 materialized view 定义,请先对每个 materialized view 执行一次 dbt run,然后再对目标表执行 dbt run --full-refresh。
执行运行后出现重复数据
- materialized views 上的
catchup=True和目标表上的repopulate_from_mvs_on_full_refresh=True可能同时启用:根据你要执行的操作,只保留其中一个。更多详情请参阅配置部分。 - 目标表定义时未使用
WHERE 0:目标表应创建为空表,但如果未包含WHERE 0,内部查询可能会插入数据。请确保包含该子句。
执行 dbt run --full-refresh 后,在持续摄取期间发生数据丢失
dbt run --full-refresh 后,源表中的某些行没有出现在目标表中。
ClickHouse materialized view 的作用类似插入触发器——它们只会在存在期间捕获数据。完全刷新期间,MV 会先被删除再重新创建,中间会有一个短暂的窗口期 (“盲窗”) 。在这个窗口期内插入到源表的任何行都不会被捕获。更多详情,请参阅持续摄取期间的行为部分。
调试技巧
检查 ClickHouse 中 MV 当前的目标表
system.tables,查看 materialized view 正在写入哪个位置:
检查 dbt 是否将某个表识别为 materialized view 的目标表
表 <table_name> 被 dbt 管理的 materialized view 用作目标表。为防止数据丢失,默认将 mv_on_schema_change 设为 “fail”。
如果出现此消息,说明 dbt 已检测到该表是至少一个由 dbt 管理的 materialized view 的目标表。如果你预期会看到这条消息却没有看到,请确认:
- materialized view 模型已正确定义
{{ materialization_target_table(ref('your_target')) }} - materialized view 模型在其 config 中设置了
materialized='materialized_view' - materialized view 和目标表都至少已运行过一次
从隐式目标迁移到显式目标
materialized='table' 定义与当前 MV 目标表相同的 schema。使用 WHERE 0 子句创建一个空表。名称应与当前隐式 materialized view 模型的名称相同。这样一来,你现在就可以使用该模型对目标表进行迭代调整。
materialization_target_table() 宏调用。如果你之前使用了 UNION ALL,请移除这部分以及注释。
模型名称需要遵循以下命名约定:
- 如果只定义了一个 MV,其名称应为:
<old_model_name>_mv - 如果定义了多个 MV,则每个 MV 的名称应为:
<old_model_name>_mv_<name_in_comments>
my_model.sql 中 (隐式目标,单个包含 UNION ALL 的模型) :
隐式目标与显式目标方式的行为比较
它们的总体表现
持续摄取期间的行为
- 由于 ClickHouse materialized view 充当插入触发器,它们只能在存在期间捕获数据。如果某个 materialized view 被删除后又重新创建 (例如在执行
--full-refresh期间) ,那么在这段时间窗口内插入源表的任何行都不会被该 materialized view 处理。这种情况称为 materialized view 处于“盲区”状态。 - 各种
catchup过程都基于使用 materialized view SQL 的INSERT INTO ... SELECT操作,与 materialized view 本身的工作机制无关。一旦INSERT开始,它就不会捕获新数据,但这些新数据会被已附加的 materialized view 捕获。
隐式目标操作
显式目标操作
目标表模型:
可刷新materialized views
refreshable 配置对象,并使用以下选项:
隐式目标示例
显式目标示例
局限性
- 在 ClickHouse 中创建带有依赖项的可刷新materialized view (MV) 时,如果指定的依赖项在创建时不存在,ClickHouse 不会报 错。相反,可刷新 MV 会保持 非活动状态,在依赖项满足之前一直处于等待状态,之后才会开始处理更新或执行刷新。 这种行为是有意设计的,但如果未及时处理所需的依赖项,可能会导致数据可用性延迟。 你应确保在创建可刷新 materialized view 之前,所有依赖项都已正确定义且确实存在。
- 截至目前,mv 与其依赖项之间实际上没有真正的“dbt 关联”,因此无法 保证创建顺序。
- 可刷新功能尚未针对多个 mvs 指向同一目标模型的情况进行测试。