跳转到主要内容
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 模型时,适配器会:
  1. 使用模型名称创建一个目标表
  2. 创建一个名为 <model_name>_mv 的 ClickHouse materialized view
目标表的 schema 会根据 MV 的 SELECT 语句中的列推断出来。所有资源 (目标表 + MV) 共享同一份模型配置。
更多示例请参见测试文件
你也可以通过强制实施模型契约,在目标表上定义列级别的 codecttl。详情请参见列配置

多个 materialized view

ClickHouse 允许多个 materialized view 将记录写入同一个目标表。要在 dbt-clickhouse 中通过隐式目标方式支持这一点,可以在模型文件中构造一个 UNION,并使用 --my_mv_name:begin--my_mv_name:end 形式的注释来包裹每个 materialized view 的 SQL。 例如,下面的配置会构建两个 materialized view,它们都会将数据写入该模型的同一个目标表。materialized view 的名称将采用 <model_name>_mv1<model_name>_mv2 的形式:
更新包含多个 materialized view (MV) 的模型时,尤其是重命名其中一个 MV 时, dbt-clickhouse 不会自动删除旧的 MV。相反, 你会看到以下警告:Warning - Table <previous table name> was detected with the same pattern as model name <your model name> but was not found in this run. In case it is a renamed mv that was previously part of this model, drop it manually (!!!)

如何演进目标表 schema

dbt-clickhouse 1.9.8 版本起,当 dbt run 在 MV 的 SQL 中发现列存在差异时,你可以控制目标表 schema 的演进方式。
默认情况下,dbt 不会对目标表应用任何更改 (设置值为 ignore) ,但你可以修改此设置,使其遵循in incremental modelson_schema_change 配置的相同行为。 此外,你也可以将此设置用作一种安全机制。如果将其设为 fail,那么当 MV 的 SQL 中的列与首次执行 dbt run 时创建的目标表不一致时,构建就会失败。

数据补齐

默认情况下,创建或重新创建 materialized view (MV) 时,会先用历史数据填充目标表,再创建 MV 本身 (catchup=True) 。你可以将 catchup 配置设为 False 以禁用此行为。
完全刷新时存在数据丢失风险catchup: Falsedbt run --full-refresh 一起使用会丢弃目标表中的所有现有数据。该表会被重新创建为空表,之后只会捕获新数据。如果后续可能需要历史数据,请确保已做好备份。

使用显式目标进行物化 (Beta)

Beta此功能目前处于 Beta 阶段,自 dbt-clickhouse 1.10 版本起可用。API 可能会根据社区反馈而发生变化。
默认情况下,dbt-clickhouse 会在单个模型中同时创建和管理目标表以及 materialized views (即上文所述的隐式目标方式) 。这种方式有一些限制:
  • 所有资源 (目标表 + 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 也会迫使我们为目标表定义一些数据,即使本意并不是从中读取数据。变通方法就是将这个表的数据留空。

用法

第 1 步:将目标表定义为普通表模型 模型 events_daily.sql
这是我们在限制部分提到的权宜之计。这里可能会丢失一些 dbt 验证,但仍会在 ClickHouse 层面检查 schema。 第 2 步:定义指向目标表的 materialized views 例如,你可以像下面这样在不同模型中定义不同的 MV,甚至让它们指向同一个目标表。请注意新增的 {{ 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 设置:
选项 2:我想重建目标表,并且不希望在重建 MV 期间读到空数据。 如果你需要先更新 MV 的 SQL,可以先将其设置为 catchup=False,然后对这些 MV 执行 dbt rundbt run --full-refresh。请确保在对目标表运行 --full-refresh 之前,这些 MV 已创建完成,因为它会使用 ClickHouse 中的 MV 定义。 在目标表模型上设置 repopulate_from_mvs_on_full_refresh=True。执行 dbt run --full-refresh 时,这将会:
  1. 创建一个新的临时表
  2. 使用每个 MV 的 SQL 执行 INSERT-SELECT
  3. 以原子方式交换表
因此,在重建 MV 的过程中,你的表中不会出现空数据。

更改目标表

如果不使用 --full-refresh,则无法更改 MV 的目标表。如果你在修改 materialization_target_table() 引用后尝试运行常规的 dbt run,构建会失败,并报错提示目标已发生更改。 如需更改目标:
  1. 更新 materialization_target_table() 调用
  2. 运行 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=Truedbt 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 的目标表

在运行 dbt 时,查找以下日志消息:
<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 view 模型采用的是隐式目标方式,并且想迁移到显式目标方式,请按以下步骤操作: 1. 创建目标表模型 创建一个新的模型文件,使用 materialized='table' 定义与当前 MV 目标表相同的 schema。使用 WHERE 0 子句创建一个空表。名称应与当前隐式 materialized view 模型的名称相同。这样一来,你现在就可以使用该模型对目标表进行迭代调整。
2. 更新 MV 模型 创建新的模型,每个模型分别包含对应的 MV SQL,以及指向新目标表的 materialization_target_table() 宏调用。如果你之前使用了 UNION ALL,请移除这部分以及注释。 模型名称需要遵循以下命名约定:
  • 如果只定义了一个 MV,其名称应为:<old_model_name>_mv
  • 如果定义了多个 MV,则每个 MV 的名称应为:<old_model_name>_mv_<name_in_comments>
此前在 my_model.sql 中 (隐式目标,单个包含 UNION ALL 的模型) :
调整后 (显式目标,单独的模型文件) :
3. 如有需要,请按照显式目标部分中的说明对其进行调整。

隐式目标与显式目标方式的行为比较

它们的总体表现

持续摄取期间的行为

在迭代模型时,你需要了解不同操作与正在插入的数据之间如何相互影响:
  • 由于 ClickHouse materialized view 充当插入触发器,它们只能在存在期间捕获数据。如果某个 materialized view 被删除后又重新创建 (例如在执行 --full-refresh 期间) ,那么在这段时间窗口内插入源表的任何行都不会被该 materialized view 处理。这种情况称为 materialized view 处于“盲区”状态。
  • 各种 catchup 过程都基于使用 materialized view SQL 的 INSERT INTO ... SELECT 操作,与 materialized view 本身的工作机制无关。一旦 INSERT 开始,它就不会捕获新数据,但这些新数据会被已附加的 materialized view 捕获。
下表总结了在源表持续发生插入时,各种操作的安全性。

隐式目标操作

显式目标操作

materialized view 模型: 目标表模型:
针对有活跃摄取的生产环境的建议
  • 如果可能,请在执行 dbt 操作期间暂停摄取:这样所有操作都是安全的,也不会丢失数据。
  • 如果可能,请在目标表上使用支持去重的引擎 (例如 ReplacingMergeTree) ,以处理 catch-up 重叠可能带来的重复数据。
  • 尽量优先使用 ALTER TABLE ... MODIFY QUERY (不带 --full-refresh 的常规 dbt run) ——这始终是安全的。
  • 注意 dbt 操作期间的风险窗口

可刷新materialized views

可刷新materialized views 是 ClickHouse 中一种特殊的 materialized view,它会定期重新执行查询并存储结果,类似于其他数据库中的 materialized view。这适用于需要定期快照或聚合,而不是实时插入触发器的场景。
可刷新materialized views 可与 隐式目标显式目标 这两种方式配合使用。refreshable 配置与 target 表的管理方式无关。
要使用可刷新materialized view,请在 MV 模型中添加一个 refreshable 配置对象,并使用以下选项:

隐式目标示例

显式目标示例

局限性

  • 在 ClickHouse 中创建带有依赖项的可刷新materialized view (MV) 时,如果指定的依赖项在创建时不存在,ClickHouse 不会报 错。相反,可刷新 MV 会保持 非活动状态,在依赖项满足之前一直处于等待状态,之后才会开始处理更新或执行刷新。 这种行为是有意设计的,但如果未及时处理所需的依赖项,可能会导致数据可用性延迟。 你应确保在创建可刷新 materialized view 之前,所有依赖项都已正确定义且确实存在。
  • 截至目前,mv 与其依赖项之间实际上没有真正的“dbt 关联”,因此无法 保证创建顺序。
  • 可刷新功能尚未针对多个 mvs 指向同一目标模型的情况进行测试。
最后修改于 2026年6月19日