刷新可以延迟,结果不能过期
物化视图(Materialized View,MV)通过预先计算并持久化高频查询结果,以额外的存储空间换取更低的查询延迟和扫描成本。然而,在生产环境中,真正困难的并不是“保存一份查询结果”,而是在源表持续变化时,同时控制刷新成本、保证结果正确、避免阻塞写入,并让聚合状态、物理布局和数据生命周期能够独立维护。
Databend 新物化视图以独立的只读 FUSE 表保存预计算结果,利用 Change Tracking 识别自上次刷新以来的变化,并通过 Checkpoint 记录已经消费的源表位置。它将“异步维护”和“一致性读取”分开处理:刷新任务可以暂时落后,但读取路径会根据变化类型选择直接读取、合并未物化增量或安全回源。因此,刷新延迟首先影响查询性能,而不会让查询静默返回错误的旧结果。
本文将依次介绍主流物化视图的维护模型、Databend 的核心设计、刷新与一致性读取机制、聚合状态和存储维护方式,以及当前推荐的使用场景。
物化视图的实现模型与设计权衡
物化视图通常需要在两个维度上做出选择:一是何时维护,即随写入同步更新,还是在后台异步刷新;二是如何维护,即只处理发生变化的数据,还是重新执行完整查询。不同产品会组合这些策略。下表比较的是几种常见实现模型,而不是限定某个产品只能采用其中一种方式。
| 实现模型 | 典型方式 | 查询新鲜度 | 对写入的影响 | 维护成本与适用性 |
|---|---|---|---|---|
| 同步增量维护 | 在写入事务内同步更新派生结果,常见于 insert-triggered 或写入驱动的 MV | 提交后立即最新 | 写放大、锁竞争和失败面进入写入路径;复杂聚合可能显著拉长写入延迟 | 适合写入较轻、低延迟读取优先、派生逻辑简单的场景 |
| 异步全量刷新 | 定时或显式重新执行定义查询,并原子替换结果 | 刷新间隔内可能陈旧,除非回源计算 | 通常不阻塞普通写入;刷新时需要扫描和重写全量数据 | 实现直观、语义稳定;适合中小表、低频刷新,以及更新或删除较多的聚合场景 |
| 异步增量刷新 | 基于日志、Stream、Change Data Capture(CDC)或 Snapshot 差异,只处理变化区间 | 取决于调度;需要额外机制避免读到旧结果 | 写入只需保留可追踪的变化,通常不等待 MV 计算完成 | 适合 append-only 事实表、事件表和高频刷新;要求可靠的变更语义与 Checkpoint |
| 异步增量刷新 + 查询补偿 | 异步物化存量;查询时合并尚未消费的增量,无法补偿时回源 | 结果可以保持最新 | 不把刷新工作放入写入事务,但读路径在刷新落后时需要额外计算 | 适合既要写入吞吐又要求查询正确性的分析系统;需要明确的补偿条件 |
Databend 如何回答四个关键问题
Databend 选择异步维护物化结果,并根据源表的真实变化决定采用增量刷新还是全量重建。为了不把刷新延迟转化为错误结果,查询端再根据物化进度选择 Fresh、Hybrid 或 Live Fallback 路径。
| 关键问题 | 同步更新 | 异步更新 | Databend 的选择 |
|---|---|---|---|
| 是否影响写入? | 会,派生计算属于写入提交路径 | 通常不会等待 MV 刷新完成 | 刷新固定一个源 Snapshot;后续写入可以继续提交到更新的 Snapshot |
| 刷新时是否总是扫描全表? | 不一定,但复杂变更通常需要昂贵的维护 | 可以选择全量或增量 | 初次刷新,以及聚合视图遇到 |
| 刷新落后时能否读取? | 一般不存在刷新落后 | 取决于产品:可能读取旧结果或回源 | Fresh 直接读取,Hybrid 执行 read fix,其他情况回源;不会返回错误的旧结果 |
| 如何处理复杂聚合? | 在写入时维护状态或限制可用算子 | 可以重算,但成本会随数据规模增长 | 存储 Aggregate State;遇到无法安全撤销的变更时回退为全量重建 |
Databend 新物化视图的核心模型
Databend 新物化视图不是附着在源表 Block 上的索引,而是一张特殊的只读 FUSE 表,其引擎为
engine = MATERIALIZED_VIEW
从 Aggregating Index 到独立存储
Aggregating Index 属于 Databend 早期的物化视图模型,它类似一个 block 级别的 Projection 索引伴随原表流动,设计意图上与新物化视图都能够减少重复计算,但两者的存储归属、刷新进度和维护方式并不相同。
| 维度 | Aggregating Index | 新物化视图 |
|---|---|---|
| 存储归属 | 依附于源表 Block | 使用独立 FUSE 存储 |
| 物理布局 | 受源 Block 和源表维护影响 | 可以独立设置 Cluster Key、Block 与维护策略 |
| 查询方式 | 主要由优化器透明使用 | 可以直接查询,也可以由优化器改写使用 |
| 刷新进度 | 难以表达独立消费端点 | 持有源表 Checkpoint 与独立 Snapshot |
| 源表 compact/recluster | 旧 Block 变化后通常需要重建相应索引 | MV 存储可以独立 compact/recluster |
元数据与事务边界
为了同时描述物化结果、定义查询和源表依赖关系,物化视图在 Meta 中保存三类信息。
| 元数据 | 关键内容 | 目的 |
|---|---|---|
| Physical Schema、MV Snapshot、源表 ID/Sequence、源 Snapshot Location | 描述可读写的独立表与刷新端点 |
| 原始查询、重写后的 Physical Query、Logical Schema | 保存体积较大的定义内容,避免频繁更新 |
| 源表绑定与 Generation | 维护源表到 MV 的依赖关系 |
物化视图的创建、替换和删除,会将表元数据、MV 定义以及源表依赖关系放入同一个 Meta 事务。创建物化视图时,系统也会原子启用源表的 Change Tracking,从而避免出现“MV 已经发布,但源表变化尚不可追踪”的中间状态。
从刷新到读取:如何保证结果正确
Databend 将刷新进度、物化数据和读取路径连接成一套完整机制。刷新端通过 Change Tracking 和 Checkpoint 消费源表变化;读取端则比较 MV Checkpoint Snapshot 与源表当前 Snapshot,判断已有结果是否足以直接使用。

Change Tracking 与 Checkpoint:识别并记录变化
Databend 复用 Stream 的 change-table 语义,读取两次消费端点之间发生的变化,而不是为物化视图另建一套 CDC。变化记录包含以下内部列:
| 内部列 | 含义 |
|---|---|
| 变化动作,例如 |
| 是否属于一次更新 |
| 源表行的稳定身份 |
刷新成功时,物化视图数据和以下 Checkpoint 会在同一个事务中提交:
materialized_view_source_table_seq
materialized_view_source_snapshot_location
原子提交保证系统不会出现“数据已经写入但 Checkpoint 没有推进”,或者“Checkpoint 已经推进但数据尚未写入”的状态。即使某批增量完全被物化视图的
WHERE
刷新策略:根据真实变化选择增量或全量
刷新开始后,系统会锁定物化视图本身,重新加载定义,并固定本轮使用的源 Snapshot。刷新期间产生的后续写入仍然可以提交到更新的 Snapshot,不会被纳入当前批次。最终采用哪种刷新方式,由两个消费端点之间的真实变化决定。
| 源表端点状态 | 刷新动作 | 原因 |
|---|---|---|
| 第一次刷新 | 使用 | 尚无起始 Checkpoint,需要建立初始结果 |
仅有 | 对增量执行 Physical MV Query,再执行 | Append-only 变化可以安全追加 Aggregate State 或结果行 |
非聚合 MV 出现 | 根据 | 通过源行稳定身份,将删除和更新准确投射到 MV |
聚合 MV 出现 | 使用 | |
| Snapshot 未变化 | 仅处理 Checkpoint 语义 | 没有新的数据需要物化 |
| 源表为空 | 使用空端点 | 避免不必要的扫描 |
对于聚合物化视图,纯追加刷新写入的是聚合状态,而不是最终结果。同一个 Group Key 即使暂时分布在多个 Block 中,后续读取仍可通过
sum_merge
count_merge
min_merge
三种读路径:Fresh、Hybrid 与 Live Fallback
读取物化视图时,Binder 会比较 MV Checkpoint Snapshot 与源表当前 Snapshot,并选择以下三种路径之一。
| 读路径 | 触发条件 | 读取计划 | 正确性与成本 |
|---|---|---|---|
| Fresh | 两个 Snapshot 相同 | 直接扫描 MV Physical Storage;聚合 MV 再执行 State Merge | 成本最低,结果最新 |
| Hybrid(read fix) | MV 落后,但 Checkpoint 之后只有 Append-only 变化 | Persisted MV Storage UNION ALL Physical MV Query(Source Delta),再合并状态 | 查询时临时补齐增量;不修改 MV,也不等待后台刷新 |
| Live Fallback | 尚未首次刷新、增量包含更新或删除、历史 Snapshot 已清理,或者源表绑定失效 | 忽略 MV 存储,基于源表执行原始逻辑查询 | 成本较高,但不会返回陈旧或错误的结果 |
Read fix 是一种查询计划补偿,并不是后台自动刷新。由此可以得到一个重要保证:刷新延迟首先表现为性能差异,而不是结果正确性差异。
聚合状态与独立存储如何持续维护
增量物化不仅要解决“如何写入新增结果”,还要保证不同批次的聚合状态能够正确合并,并控制长期刷新产生的状态行和文件碎片。
Logical Schema 与 Physical Schema
用户查询时看到的逻辑列,不一定等同于物理写入的列。例如,
avg(amount)
| 层次 | | 作用 |
|---|---|---|
| Logical Schema | | 用户查询时看到的结果列 |
| Physical Schema | | 保存增量写入和跨 Block 合并所需的状态 |
| 读取投影 | | 将物理状态还原为逻辑结果 |
当前初步支持的主流聚合包括
sum
min
max
avg
count
approx_count_distinct
avg
sum_state + count_state
_mv_source_row_id
Reaggregate、Compact 与 Recluster
经过多次 append-only 刷新后,同一个 Group Key 可能产生多份状态行。此时结果仍然正确,但 State Merge 的输入数量和文件碎片会持续增加。Databend 会在 compact 或 recluster 写出新 Block 之前,对该 Block 内相同 Group Key 的 Aggregate State 执行 reaggregate。
| 操作 | 执行内容 | 不包含的行为 |
|---|---|---|
| Reaggregate | 在本次新 Block 内按照业务列分组,合并重复的 Aggregate State | 不保证跨全表或跨全部 Block 一次性收敛 |
| 整理 MV Block,并在输出 Block 上触发 reaggregate | 不允许用户改变 MV 的逻辑数据 |
| 按照 MV 自身的 Cluster Key 重整物理布局 | 不继承源表的聚簇布局 |
对于包含
GROUP BY
_mv_source_row_id
查询改写与完整使用流程
用户既可以直接查询物化视图,也可以继续查询源表。优化器会匹配输出表达式、过滤条件、Group Key、聚合函数、聚合粒度和查询所需列;匹配成功后,源表查询会被改写为物化视图的读取计划。改写后的计划仍会根据数据状态选择 Fresh、Hybrid 或 Live Fallback 路径。
以下示例创建一个按客户汇总已支付订单的物化视图:
CREATE MATERIALIZED VIEW paid_orders_by_customer
(customer_id, total_amount, order_count, average_amount)
CLUSTER BY (customer_id)
AS
SELECT
customer_id,
sum(amount),
count(*),
avg(amount)
FROM orders
WHERE paid
GROUP BY customer_id;
物化视图创建时只发布定义,并不会立即填充数据。第一次刷新之前,直接查询物化视图会走 Live Fallback,因此结果仍然正确,但暂时无法获得物化存储带来的加速。完成首次刷新后,后续纯追加变化可以增量消费;随着增量批次增加,还可以按需整理物理存储。
-- 第一次刷新,建立初始结果
REFRESH MATERIALIZED VIEW paid_orders_by_customer;
-- 后续只有新增订单时,刷新仅消费增量
REFRESH MATERIALIZED VIEW paid_orders_by_customer;
-- 随增量批次增加,按需整理存储
OPTIMIZE TABLE paid_orders_by_customer COMPACT;
ALTER MATERIALIZED VIEW paid_orders_by_customer RECLUSTER FINAL;
使用场景与选型建议
Databend 新物化视图的收益取决于源表的变化模式、定义查询的复杂度,以及刷新和查询对性能与正确性的要求。它更适合变化可以安全增量消费的单表分析场景,而不是现阶段所有持续计算任务的通用替代方案。
推荐使用的场景
-
日志、埋点、事件与事实表。 这类数据通常以
为主,历史记录较少更新或删除,因此可以持续使用低成本的 append-only 增量刷新。随着新数据进入,物化视图只需要处理尚未消费的变化,而不必重复计算整张源表。INSERT -
高频 Group By、指标看板和客户或区域汇总。 对固定维度反复执行聚合的查询,可以利用 Aggregate State 增量合并结果;当查询形态匹配时,优化器还可以透明地将源表查询改写为物化视图读取,从而减少明细数据扫描和重复计算。
-
写入吞吐优先,但不能接受旧结果。 刷新任务异步执行,不需要把物化计算放入源表写入事务;当刷新暂时落后且新增变化可以安全合并时,read fix 会在查询阶段补齐增量。这样既能避免刷新阻塞写入,也不会为了低延迟而返回已过期的结果。
-
源表与查询需要不同物理布局。 当源表的写入方式和下游查询模式不同,物化视图可以使用独立的 Cluster Key、compact 和 recluster 策略。例如,源表可以围绕写入时间组织,而物化结果可以按照客户或业务维度重新布局。
使用前需关注事项
-
定义查询目前聚焦单表 FUSE 场景。 物化视图当前只能基于
Catalog 中的一张持久化 FUSE 表创建,支持简单的default。多表查询、SELECT ... FROM ... [WHERE ...] [GROUP BY ...]、子查询、集合运算、窗口函数和非确定性函数暂不在支持范围内。因此,如果工作负载依赖复杂宽表建模或多表持续聚合,仍需要使用其他转换与编排方式。JOIN -
聚合物化视图对更新和删除存在重建成本。 非聚合物化视图可以借助
将_mv_source_row_id和UPDATE准确投射到物化结果;但对于聚合物化视图,为了优先保证正确性,一旦源表出现DELETE或UPDATE,当前实现会执行全量重建,尚未支持分区级增量重算。频繁更新或删除的大型聚合表需要特别评估刷新成本。DELETE -
刷新调度仍需显式配置。 当前可以通过 Task 定时执行
,自动生成刷新任务属于后续 Databend Cloud 的演进方向。read fix 也不是刷新机制本身:只有 append-only 增量能够走 Hybrid 路径,无法安全补偿时,查询会回到源表执行原始逻辑。REFRESH MATERIALIZED VIEW -
增量能力依赖 Change Tracking 的历史连续性。 如果增量刷新或读取补偿所需的历史 Snapshot 已经被清理,系统将无法基于这些历史变化继续增量处理,此时读路径会选择回源。数据保留策略、刷新频率和 Change Tracking 的可用范围需要结合写入速度与查询要求统一规划。
-
物化视图是只读对象。 普通的
、INSERT和UPDATE会被拒绝,用户不能直接修改物化结果;物理存储的整理也受到专用维护操作的限制。这一约束可以保护物化结果与 Checkpoint 之间的一致性,但也意味着它不能被当作普通业务表写入。DELETE
增量维护的价值最终取决于正确性
Databend 新物化视图的核心并不只是“把查询结果缓存起来”,而是建立了一条完整的增量计算链路:Change Tracking 识别变化,Checkpoint 记录消费位置,Physical Schema 保存可合并状态,刷新过程以原子事务推进数据与端点,read fix 则在异步维护期间保证一致性读取。
这套机制尤其适合持续写入、以追加为主,并且需要稳定分析结果的单表场景。面对多表依赖、频繁更新或删除,以及需要自动调度的工作负载,数据工程师应评估全量重建成本和后续 Dynamic Table 能力,而不应将当前物化视图视为通用的持续计算引擎。全面理解这些能力,才能判断物化视图究竟是在减少计算,还是把刷新和维护成本转移到了另一个环节。
增量物化视图特性在 Databend 企业版
后引入和更新。v1.2.934
订阅我们的新闻简报
及时了解功能发布、产品规划、支持服务和云服务的最新信息!






