Materialized Views
materialized_view 物化类型应当是对已有(源)表执行的 SELECT 查询。与 PostgreSQL 不同,ClickHouse 中的 materialized view 不是“静态”的(也没有对应的 REFRESH 操作)。相反,它充当一个插入触发器,对插入到源表中的行应用已定义的 SELECT 转换,并将得到的新行插入到目标表中。有关 ClickHouse 中 materialized view 工作方式的更多细节,请参阅 ClickHouse materialized view 文档。
关于通用的物化概念和共享配置(engine、order_by、partition_by 等),请参阅 Materializations 页面。
如何管理目标表
当你使用 materialized_view 物化方式时,dbt-clickhouse 需要同时创建一个 materialized view 和一个用于插入转换后数据行的 目标表。管理目标表有两种方式:
| Approach | Description | Status |
|---|---|---|
| Implicit target | dbt-clickhouse 会在同一个模型中自动创建并管理目标表。目标表的 schema 会根据该 materialized view 的 SQL 自动推断。 | Stable |
| Explicit target | 你将目标表定义为单独的 table 物化,并在 materialized view 模型中通过 materialization_target_table() 宏引用该表。该 materialized view 会带有指向该表的 TO 子句进行创建。此功能从 dbt-clickhouse 版本 1.10 开始提供。注意:该功能处于 beta 阶段,API 可能会根据社区反馈而调整。 | Beta |
你选择的方式会影响到 schema 变更、全量刷新以及多 materialized view 场景的处理方式。下文将详细介绍每种方式。
隐式目标的物化
这是默认行为。当你定义一个 materialized_view 模型时,适配器将会:
- 使用模型名称创建一个目标表
- 创建一个名为
<model_name>_mv的 ClickHouse materialized view
目标表的表结构会根据该 materialized view 的 SELECT 语句中的列自动推断。所有资源(目标表和 materialized view)共享同一套模型配置。
有关更多示例,请参见测试文件。
多个 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 在物化视图的 SQL 中遇到不同的列时,可以控制目标表 schema 的迭代方式。
默认情况下,dbt 不会对目标表应用任何更改(设置为 ignore),但你可以更改此设置,使其表现出与incremental models中的 on_schema_change 配置相同的行为。
此外,你可以将此设置用作一种安全机制。如果将其设置为 fail,当物化视图(MV)的 SQL 中的列与第一次执行 dbt run 所创建的目标表中的列不一致时,构建将会失败。
数据补齐
默认情况下,在创建或重新创建 materialized view (MV) 时,会先将目标表用历史数据填充,然后才创建 MV 本身(catchup=True)。可以通过将 catchup 配置设置为 False 来禁用该行为。
| Operation | catchup: True (default) | catchup: False |
|---|---|---|
初始部署(dbt run) | 目标表回填历史数据 | 目标表创建为空 |
全量刷新(dbt run --full-refresh) | 目标表重建并回填 | 目标表被重新创建为空,现有数据将丢失 |
| 正常运行 | materialized view 捕获新的插入数据 | materialized view 捕获新的插入数据 |
在使用 catchup: False 并执行 dbt run --full-refresh 时,将会丢弃目标表中的所有现有数据。该表会被重新创建为空,并且之后只会捕获新的数据。若后续可能需要历史数据,请确保事先做好备份。
使用显式目标进行物化 (Beta)
此功能处于 beta 阶段,并从 dbt-clickhouse 1.10 版本 开始可用。API 可能会根据社区反馈而变更。
默认情况下,dbt-clickhouse 会在单个模型中创建并管理目标表和 materialized view(即上文所述的隐式目标方法)。这种方式存在一些限制:
- 所有资源(目标表 + MV)共享同一套配置。如果多个 MV 指向同一个目标表,必须使用
UNION ALL语法在同一处定义。 - 这些资源无法单独迭代处理,必须通过同一个模型文件统一管理。
- 无法方便地单独控制每个 MV 的名称。
- 目标表与 MV 之间共享所有配置,难以分别为每个资源单独配置,也不容易判断每项配置分别属于哪个资源。
显式目标 功能允许你将目标表单独定义为常规的 table 物化方式,然后在 materialized view 模型中引用该目标表。
优点
- 资源完全隔离:现在可以单独定义每个资源,从而提升可读性。
- dbt 与 CH 之间 1:1 的资源对应关系:现在可以使用 dbt 工具分别管理和迭代这些资源。
- 可为不同资源使用不同配置:现在可以为每个资源应用不同的配置。
- 不再需要遵守命名约定:现在所有资源都使用用户指定的名称创建,而不是使用为物化视图添加
_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 块中调用
用法
步骤 1:将目标表定义为常规表模型
模型 events_daily.sql:
这是我们在限制部分中提到的变通方案。你可能会因此丢失部分 dbt 校验,但在 ClickHouse 端仍然会对 schema 进行检查。
步骤 2:定义指向目标表的 materialized view
例如,你可以在不同的模型中按如下所示定义不同的 MV,即使它们指向同一个目标表。注意新的 {{ materialization_target_table(ref('events_daily')) }} 宏调用,它用于为该 MV 配置目标表。
模型 page_events_aggregator.sql:
模型 mobile_events_aggregator.sql:
配置选项
在使用显式目标表时,适用以下配置:
在目标表上(materialized='table'):
| Option | Description | Default |
|---|---|---|
mv_on_schema_change | 当该表被 dbt 管理的 MVs 使用时,如何处理 schema 变更。其行为与 增量模型中的 on_schema_change 配置相同。 | 注意:如果一个 materialized='table' 模型没有任何 MV 指向它,它会像往常一样工作,因此即使配置了此设置,也会被忽略。如果该表是 MVs 的目标表,为保护这些表中的数据,此配置的默认值将为 mv_on_schema_change='fail'。 |
repopulate_from_mvs_on_full_refresh | 在执行 --full-refresh 时,不运行该表自身的 SQL,而是通过基于所有指向该表的 MVs 的 SQL 执行 INSERT-SELECT 来重建该表。 | False |
在 materialized view 上(materialized='materialized_view'):
| Option | Description | Default |
|---|---|---|
catchup | 在创建 MV 时,是否回填历史数据。 | True |
通常只需要在 MVs 中将 catchup 设置为 True,或在其目标表中将 repopulate_from_mvs_on_full_refresh 设置为 True。如果两者都设置为 True,可能会导致数据重复。
常用操作
使用显式目标进行完全刷新
当使用 --full-refresh 时,显式目标表将被重新创建(因此如果在此过程中正在进行数据摄取,你可能会丢失数据)。具体行为会根据你的配置有所不同:
选项 1:--full-refresh 的默认行为。所有对象都会被重新创建,但在重新创建物化视图(MV)的期间,目标表将为空或仅部分加载。
所有对象都会被删除并重新创建。如果你希望使用物化视图的 SQL 重新插入数据,请保持设置 catchup=True:
选项 2:我想重新创建目标表,并且不希望在重建 MV 期间读到空数据。
如果你需要先更新 MV 的 SQL,可以在其中设置 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
- 以原子方式交换这些表
因此,在 MV 被重建的过程中,这张表的使用者不会看到空数据。
更改目标表
在不执行 --full-refresh 的情况下,无法更改物化视图(MV)的目标表。如果在修改 materialization_target_table() 引用后尝试运行普通的 dbt run,构建会失败,并出现一条错误信息,提示目标表已发生更改。
要更改目标表:
- 更新
materialization_target_table()调用 - 运行
dbt run --full-refresh -s your_mv_model
常见问题排查
在执行 run 期间或之后目标表为空
出现这种情况可能有以下几种原因:
- materialized view 可能被配置为
catchup=False,或者目标表被配置为repopulate_from_mvs_on_full_refresh=False,因此在创建 materialized view 或重建目标表时不会执行回填。这是预期行为,因此如果希望使用 materialized view 的 SQL 重新插入数据,请确保在 materialized view 中设置catchup=True(默认值),或者在目标表中设置repopulate_from_mvs_on_full_refresh=True。注意不要同时启用这两个设置,以避免产生重复数据。更多详情请查看配置部分。 - 当执行
dbt run --full-refresh时,如果 materialized view 使用默认的catchup=True,目标表会被重建,这些 materialized view 会依次重新插入数据。为避免这种情况,请查看对显式目标执行 Full refresh。
在目标表中执行 dbt run --full-refresh 且设置 repopulate_from_mvs_on_full_refresh=True 时,会使用旧版本 materialized view 的逻辑,而不是项目中当前的 SQL 定义
repopulate_from_mvs_on_full_refresh=True 会使用 ClickHouse 中已存在的 materialized view SQL 定义。要确保使用新的 materialized view 定义,请先对每个 materialized view 执行一次 dbt run,然后再对目标表执行 dbt run --full-refresh。
在执行一次 run 之后出现重复数据
可能原因:
- materialized view 上设置了
catchup=True,并且目标表上设置了repopulate_from_mvs_on_full_refresh=True:根据你希望执行的操作,仅保留其中一个。有关更多细节,请查看配置章节。 - 目标表未使用
WHERE 0定义:目标表应在创建时为空,但如果未包含WHERE 0,内部查询可能会插入数据。请确保包含该子句。
在执行 dbt run --full-refresh 后进行活跃摄取时的数据丢失
在执行 dbt run --full-refresh 之后,源表中的部分行在目标表中缺失。
ClickHouse materialized view 的作用类似于 insert 触发器——它们只会在自身存在期间捕获数据。在完整刷新过程中,会有一个短暂的时间窗口,MV 会被删除并重新创建(“盲窗口”)。在此窗口期间插入到源表中的任何行都不会被捕获。有关更多详情,请参见活跃摄取期间的行为一节。
调试方法
检查 ClickHouse 中当前 MV 的写入目标
查询 system.tables,以查看 materialized view 当前写入到哪里:
检查 dbt 是否将某个表识别为 materialized view 目标
在执行 dbt run 时,留意如下日志条目:
Table
<table_name>is used as a target by a dbt-managed materialized view. Defaulting mv_on_schema_change to "fail" to prevent data loss.
如果出现这条消息,说明 dbt 已检测到该表被至少一个由 dbt 管理的 materialized view 作为目标使用。如果你预期会看到这条消息但实际没有,请确认以下事项:
- materialized view 模型是否正确地定义了
{{ materialization_target_table(ref('your_target')) }} - materialized view 模型在其配置中是否包含
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,每个名称将是:
<old_model_name>_mv_<name_in_comments>
之前在 my_model.sql 中的写法为(隐式目标,单个包含 UNION ALL 的模型):
之后(显式目标,独立的模型文件):
3. 按需根据显式目标部分中的说明进行迭代。
隐式目标与显式目标方法的行为对比
它们的一般行为方式
| Operation | 隐式 target | 显式 target |
|---|---|---|
| First dbt run | 创建所有资源 | 创建所有资源 |
| Next dbt run | 资源无法单独管理,所有变更一次性执行: target table: 使用 on_schema_change 设置来管理变更。默认值为 ignore,因此新列不会被处理。Materialized views:全部通过 alter table modify query 操作进行更新 | 变更可以单独应用: target table: 自动检测其是否为由 dbt 定义的 materialized views 的 target table。如果是,则列演进默认通过 mv_on_schema_change 设置为 fail 来管理,因此在列发生变更时会报错。我们将此默认值作为一层保护机制。Materialized views:其 SQL 会通过 alter table modify query 操作进行更新。 |
| dbt run --full-refresh | 资源无法单独管理,所有变更一次性执行: target table: target table 会被重新创建为空表。可以通过 catchup 配置,使用所有 materialized views 的 SQL 一次性进行回填。catchup 的默认值为 True。Materialized views:全部会被重新创建。 | 变更将被单独应用: target table: 将按常规方式被重新创建。 Materialized views:先 drop 再重新创建。 catchup 可用于初始回填。catchup 的默认值为 True。注意:在此过程中,在 materialized views 重新创建完成之前,target table 将为空或仅部分加载。为避免这种情况,请查看下一节关于如何迭代 target table 的内容。 |
活跃摄取期间的行为
在迭代你的模型时,需要了解不同操作如何与正在插入的数据交互:
- 由于 ClickHouse 的 materialized view 充当插入触发器(insert trigger),它们只会在自身存在期间捕获数据。如果在某个时间窗口内(例如在执行
--full-refresh期间)一个 materialized view 被删除并重新创建,那么在该窗口中插入到源表的任何行都不会被该 materialized view 处理。这种情况被称为该 materialized view 处于“盲区”(blind)状态。 - 各种不同的
catchup过程都基于使用 materialized view 的 SQL 执行的INSERT INTO ... SELECT操作,并且独立于 materialized view 的工作方式。一旦INSERT开始执行,新的数据将不会被该INSERT捕获,但会被已附加的 materialized view 捕获。
下表总结了在源表上存在持续插入时,各类操作的安全性。
隐式目标操作
| Operation | Internal process | Safety while inserts are happening |
|---|---|---|
First dbt run | 1. 创建目标表 2. 插入数据(如果 catchup=True)3. 创建 materialized view | ⚠️ 在步骤 1 到 3 之间,materialized view 处于“盲区”。 在此时间窗口内插入到源表的任何行都不会被捕获。 |
Subsequent dbt run | ALTER TABLE ... MODIFY QUERY | ✅ 安全。materialized view 会以原子方式更新。 |
dbt run --full-refresh | 1. 创建备份表 2. 插入数据(如果 catchup=True)3. 删除 materialized view 4. 交换表 5. 重新创建 materialized view | ⚠️ 在重新创建期间,materialized view 处于“盲区”。 在步骤 3 到 5 之间插入到源表的数据不会出现在新的目标表中。 |
显式目标操作
materialized view 模型:
| Operation | Internal process | Safety while inserts are happening |
|---|---|---|
第一次执行 dbt run | 1. 创建 MV(带有 TO 子句)2. 运行追赶补齐(如果 catchup=True) | ✅ 会先创建 MV,因此新的插入会被立即捕获。 ⚠️ 追赶补齐可能导致数据重复 —— 回填查询可能与 MV 已在处理的行产生重叠。如果使用去重引擎(例如 ReplacingMergeTree)则是安全的。 |
后续执行的 dbt run | ALTER TABLE ... MODIFY QUERY | ✅ 安全。MV 会以原子方式更新。 |
针对 MVs 运行 dbt run --full-refresh | 1. 删除并重新创建 MV 2. 运行追赶补齐(如果 catchup=True) | ⚠️ MV 在重建期间处于“盲区”(在 drop 和 create 之间)。 ⚠️ 如果插入操作同时在进行,追赶补齐可能导致数据重复。 |
目标表模型:
| Operation | Internal process | Safety while inserts are happening |
|---|---|---|
dbt run | 按照 mv_on_schema_change 设置应用 schema 变更 | ✅ 安全。没有数据移动。 |
dbt run --full-refresh(默认) | 重新创建表(使其为空) | ⚠️ 目标表会一直为空,直到 MVs 将其回填。一旦新表存在,MVs 会继续向其插入数据。 |
使用 repopulate_from_mvs_on_full_refresh=True 运行 dbt run --full-refresh | 1. 创建备份表 2. 使用每个 MV 的 SQL 插入数据 3. 原子性交换表 | ⚠️ MV 在重建期间处于“盲区”。 在步骤 1 和 3 之间插入的数据不会出现在新表中。这一行为在后续版本中可能会改变 |
- 如有可能,在 dbt 操作期间暂停摄取:这将使所有操作都是安全的,并且不会丢失数据。
- 如有可能,在目标表上使用去重引擎(例如
ReplacingMergeTree),以处理追赶补齐重叠可能带来的重复数据。 - 在可能的情况下优先选择
ALTER TABLE ... MODIFY QUERY(不带--full-refresh的常规dbt run)—— 这始终是安全的。 - 在 dbt 操作期间留意存在风险的时间窗口。
可刷新materialized view
Refreshable Materialized Views 是 ClickHouse 中的一种特殊类型的 materialized view,会定期重新执行查询并存储结果,类似于其他数据库中 materialized view 的工作方式。适用于需要周期性快照或聚合结果,而不是实时插入触发器的场景。
要使用可刷新materialized view,请在 MV 模型中添加一个 refreshable 配置对象,并包含以下选项:
| Option | Description | Required | Default Value |
|---|---|---|---|
| refresh_interval | interval 子句(必填) | Yes | |
| randomize | 随机化子句,该子句将出现在 RANDOMIZE FOR 之后 | ||
| append | 如果设置为 True,每次刷新都会向表中插入行而不删除已存在的行。该插入操作不是原子的,与普通的 INSERT SELECT 一样。 | False | |
| depends_on | 可刷新materialized view 的依赖列表。请按如下格式提供依赖:{schema}.{view_name} | ||
| depends_on_validation | 是否验证 depends_on 中提供的依赖是否存在。如果某个依赖未包含 schema,则会在 default schema 上进行验证 | False |
隐式目标示例
显式指定目标的示例
限制
- 在 ClickHouse 中创建带有依赖项的可刷新 materialized view(MV)时,如果在创建时指定的依赖项不存在,ClickHouse 不会抛出错误。相反,该可刷新 MV 会保持在非活动状态,等待依赖项被满足后才开始处理更新或执行刷新。此行为是按设计实现的,但如果未及时创建或配置所需依赖项,可能会导致数据可用性延迟。建议用户在创建可刷新 materialized view 之前,确保所有依赖项都已正确定义并已存在。
- 截至目前,MV 与其依赖项之间不存在实际的 “dbt linkage”,因此无法保证创建顺序。
- 可刷新功能尚未在多个 MV 指向同一目标模型的场景下进行测试。