---
title: 'Databend 增量物化视图：基于 Change Tracking 的增量刷新与一致性读取'
date: 2026-09-01
slug: category-engineering/incremental-materialized-view
cover_url: 'https://static.databend.cn/img/202609/databend-incremental-materialized-view-banner-cn-simplified-v2.png'
description: '物化视图并不只是缓存查询结果。本文将深入解析 Databend 如何通过 Change Tracking、Checkpoint 和 Read Fix，在降低刷新成本的同时保证查询正确性，并帮助数据工程师判断增量物化视图适合哪些应用场景。'
tags: [databend]
category: Engineering
authors:
  - sundyli
---

## 刷新可以延迟，结果不能过期

物化视图（Materialized View，MV）通过预先计算并持久化高频查询结果，以额外的存储空间换取更低的查询延迟和扫描成本。然而，在生产环境中，真正困难的并不是“保存一份查询结果”，而是在源表持续变化时，同时控制刷新成本、保证结果正确、避免阻塞写入，并让聚合状态、物理布局和数据生命周期能够独立维护。

{/* truncate */}

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|
|刷新时是否总是扫描全表？|不一定，但复杂变更通常需要昂贵的维护|可以选择全量或增量|初次刷新，以及聚合视图遇到 `UPDATE`/`DELETE` 时执行全量刷新；纯追加时执行增量刷新|
|刷新落后时能否读取？|一般不存在刷新落后|取决于产品：可能读取旧结果或回源|Fresh 直接读取，Hybrid 执行 read fix，其他情况回源；不会返回错误的旧结果|
|如何处理复杂聚合？|在写入时维护状态或限制可用算子|可以重算，但成本会随数据规模增长|存储 Aggregate State；遇到无法安全撤销的变更时回退为全量重建|

## Databend 新物化视图的核心模型

Databend 新物化视图不是附着在源表 Block 上的索引，而是一张特殊的只读 FUSE 表，其引擎为 `engine = MATERIALIZED_VIEW`。它拥有独立的 Table ID、TableMeta、Snapshot、Segment、Block、Cluster Key 和存储参数，因此可以独立表达刷新进度，并按照自身的查询模式维护物理布局。

### 从 Aggregating Index 到独立存储

Aggregating Index 属于 Databend 早期的物化视图模型，它类似一个 block 级别的 Projection 索引伴随原表流动，设计意图上与新物化视图都能够减少重复计算，但两者的存储归属、刷新进度和维护方式并不相同。

|维度|Aggregating Index|新物化视图|
|---|---|---|
|存储归属|依附于源表 Block|使用独立 FUSE 存储|
|物理布局|受源 Block 和源表维护影响|可以独立设置 Cluster Key、Block 与维护策略|
|查询方式|主要由优化器透明使用|可以直接查询，也可以由优化器改写使用|
|刷新进度|难以表达独立消费端点|持有源表 Checkpoint 与独立 Snapshot|
|源表 compact/recluster|旧 Block 变化后通常需要重建相应索引|MV 存储可以独立 compact/recluster|

### 元数据与事务边界

为了同时描述物化结果、定义查询和源表依赖关系，物化视图在 Meta 中保存三类信息。

|元数据|关键内容|目的|
|---|---|---|
|`TableMeta(mv_id)`|Physical Schema、MV Snapshot、源表 ID/Sequence、源 Snapshot Location|描述可读写的独立表与刷新端点|
|`MVDefinition(mv_id)`|原始查询、重写后的 Physical Query、Logical Schema|保存体积较大的定义内容，避免频繁更新 `TableMeta` 带来写放大|
|`SourceTableMV(source_id, mv_id)`|源表绑定与 Generation|维护源表到 MV 的依赖关系|

物化视图的创建、替换和删除，会将表元数据、MV 定义以及源表依赖关系放入同一个 Meta 事务。创建物化视图时，系统也会原子启用源表的 Change Tracking，从而避免出现“MV 已经发布，但源表变化尚不可追踪”的中间状态。

## 从刷新到读取：如何保证结果正确

Databend 将刷新进度、物化数据和读取路径连接成一套完整机制。刷新端通过 Change Tracking 和 Checkpoint 消费源表变化；读取端则比较 MV Checkpoint Snapshot 与源表当前 Snapshot，判断已有结果是否足以直接使用。

![](https://static.databend.cn/img/202609/20260901-img1.png)

### Change Tracking 与 Checkpoint：识别并记录变化

Databend 复用 Stream 的 change-table 语义，读取两次消费端点之间发生的变化，而不是为物化视图另建一套 CDC。变化记录包含以下内部列：

|内部列|含义|
|---|---|
|`change$action`|变化动作，例如 `INSERT` 或 `DELETE`|
|`change$is_update`|是否属于一次更新|
|`change$row_id`|源表行的稳定身份|

刷新成功时，物化视图数据和以下 Checkpoint 会在同一个事务中提交：

```Plaintext
materialized_view_source_table_seq
materialized_view_source_snapshot_location
```

原子提交保证系统不会出现“数据已经写入但 Checkpoint 没有推进”，或者“Checkpoint 已经推进但数据尚未写入”的状态。即使某批增量完全被物化视图的 `WHERE` 条件过滤，Checkpoint 也会继续推进，避免后续刷新重复消费同一批变化。

### 刷新策略：根据真实变化选择增量或全量

刷新开始后，系统会锁定物化视图本身，重新加载定义，并固定本轮使用的源 Snapshot。刷新期间产生的后续写入仍然可以提交到更新的 Snapshot，不会被纳入当前批次。最终采用哪种刷新方式，由两个消费端点之间的真实变化决定。

|源表端点状态|刷新动作|原因|
|---|---|---|
|第一次刷新|使用 `INSERT OVERWRITE` 执行全量计算|尚无起始 Checkpoint，需要建立初始结果|
|仅有 `INSERT`|对增量执行 Physical MV Query，再执行 `INSERT INTO`|Append-only 变化可以安全追加 Aggregate State 或结果行|
|非聚合 MV 出现 `UPDATE` `DELETE`|根据 `_mv_source_row_id` 在内部执行 `MERGE`|通过源行稳定身份，将删除和更新准确投射到 MV|
|聚合 MV 出现 `UPDATE` `DELETE`|使用 `INSERT OVERWRITE` 全量重建|`MIN` `MAX` 和近似去重等聚合状态无法通用、正确地撤销|
|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)` 不能只存储一个平均值，再对不同增量批次的平均值继续求平均；为了得到正确结果，系统必须保存可以合并的中间状态。

|层次|`avg(amount)` 示例|作用|
|---|---|---|
|Logical Schema|`average_amount`|用户查询时看到的结果列|
|Physical Schema|`sum_state(amount)`、`count_state(amount)`|保存增量写入和跨 Block 合并所需的状态|
|读取投影|`sum_merge(sum_state) / count_merge(count_state)`|将物理状态还原为逻辑结果|

当前初步支持的主流聚合包括 `sum`、`min`、`max`、`avg`、`count` 和 `approx_count_distinct`。其中，`avg` 会被重写为 `sum_state + count_state`。非聚合物化视图的 Physical Schema 则会额外保存 `_mv_source_row_id`，用于 Standard 刷新过程中的行定位。

### Reaggregate、Compact 与 Recluster

经过多次 append-only 刷新后，同一个 Group Key 可能产生多份状态行。此时结果仍然正确，但 State Merge 的输入数量和文件碎片会持续增加。Databend 会在 compact 或 recluster 写出新 Block 之前，对该 Block 内相同 Group Key 的 Aggregate State 执行 reaggregate。

|操作|执行内容|不包含的行为|
|---|---|---|
|Reaggregate|在本次新 Block 内按照业务列分组，合并重复的 Aggregate State|不保证跨全表或跨全部 Block 一次性收敛|
|`OPTIMIZE TABLE mv COMPACT`|整理 MV Block，并在输出 Block 上触发 reaggregate|不允许用户改变 MV 的逻辑数据|
|`ALTER MATERIALIZED VIEW mv RECLUSTER [FINAL]`|按照 MV 自身的 Cluster Key 重整物理布局|不继承源表的聚簇布局|

对于包含 `GROUP BY`、但不包含聚合函数的物化视图，维护过程也会执行去重。非聚合物化视图会保存 `_mv_source_row_id`，因此两条业务列相同、但对应不同源记录的数据，不会在 compact 过程中被错误合并。

## 查询改写与完整使用流程

用户既可以直接查询物化视图，也可以继续查询源表。优化器会匹配输出表达式、过滤条件、Group Key、聚合函数、聚合粒度和查询所需列；匹配成功后，源表查询会被改写为物化视图的读取计划。改写后的计划仍会根据数据状态选择 Fresh、Hybrid 或 Live Fallback 路径。

以下示例创建一个按客户汇总已支付订单的物化视图：

```SQL
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，因此结果仍然正确，但暂时无法获得物化存储带来的加速。完成首次刷新后，后续纯追加变化可以增量消费；随着增量批次增加，还可以按需整理物理存储。

```SQL
-- 第一次刷新，建立初始结果
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 新物化视图的收益取决于源表的变化模式、定义查询的复杂度，以及刷新和查询对性能与正确性的要求。它更适合变化可以安全增量消费的单表分析场景，而不是现阶段所有持续计算任务的通用替代方案。

### 推荐使用的场景

- **日志、埋点、事件与事实表。** 这类数据通常以 `INSERT` 为主，历史记录较少更新或删除，因此可以持续使用低成本的 append-only 增量刷新。随着新数据进入，物化视图只需要处理尚未消费的变化，而不必重复计算整张源表。

- **高频 Group By、指标看板和客户或区域汇总。** 对固定维度反复执行聚合的查询，可以利用 Aggregate State 增量合并结果；当查询形态匹配时，优化器还可以透明地将源表查询改写为物化视图读取，从而减少明细数据扫描和重复计算。

- **写入吞吐优先，但不能接受旧结果。** 刷新任务异步执行，不需要把物化计算放入源表写入事务；当刷新暂时落后且新增变化可以安全合并时，read fix 会在查询阶段补齐增量。这样既能避免刷新阻塞写入，也不会为了低延迟而返回已过期的结果。

- **源表与查询需要不同物理布局。** 当源表的写入方式和下游查询模式不同，物化视图可以使用独立的 Cluster Key、compact 和 recluster 策略。例如，源表可以围绕写入时间组织，而物化结果可以按照客户或业务维度重新布局。

### 使用前需关注事项

- **定义查询目前聚焦单表 FUSE 场景。** 物化视图当前只能基于 `default` Catalog 中的一张持久化 FUSE 表创建，支持简单的 `SELECT ... FROM ... [WHERE ...] [GROUP BY ...]`。多表查询、`JOIN`、子查询、集合运算、窗口函数和非确定性函数暂不在支持范围内。因此，如果工作负载依赖复杂宽表建模或多表持续聚合，仍需要使用其他转换与编排方式。

- **聚合物化视图对更新和删除存在重建成本。** 非聚合物化视图可以借助 `_mv_source_row_id` 将 `UPDATE` 和 `DELETE` 准确投射到物化结果；但对于聚合物化视图，为了优先保证正确性，一旦源表出现 `UPDATE` 或 `DELETE`，当前实现会执行全量重建，尚未支持分区级增量重算。频繁更新或删除的大型聚合表需要特别评估刷新成本。

- **刷新调度仍需显式配置。** 当前可以通过 Task 定时执行 `REFRESH MATERIALIZED VIEW`，自动生成刷新任务属于后续 Databend Cloud 的演进方向。read fix 也不是刷新机制本身：只有 append-only 增量能够走 Hybrid 路径，无法安全补偿时，查询会回到源表执行原始逻辑。

- **增量能力依赖 Change Tracking 的历史连续性。** 如果增量刷新或读取补偿所需的历史 Snapshot 已经被清理，系统将无法基于这些历史变化继续增量处理，此时读路径会选择回源。数据保留策略、刷新频率和 Change Tracking 的可用范围需要结合写入速度与查询要求统一规划。

- **物化视图是只读对象。** 普通的 `INSERT`、`UPDATE` 和 `DELETE` 会被拒绝，用户不能直接修改物化结果；物理存储的整理也受到专用维护操作的限制。这一约束可以保护物化结果与 Checkpoint 之间的一致性，但也意味着它不能被当作普通业务表写入。

## 增量维护的价值最终取决于正确性

Databend 新物化视图的核心并不只是“把查询结果缓存起来”，而是建立了一条完整的增量计算链路：Change Tracking 识别变化，Checkpoint 记录消费位置，Physical Schema 保存可合并状态，刷新过程以原子事务推进数据与端点，read fix 则在异步维护期间保证一致性读取。

这套机制尤其适合持续写入、以追加为主，并且需要稳定分析结果的单表场景。面对多表依赖、频繁更新或删除，以及需要自动调度的工作负载，数据工程师应评估全量重建成本和后续 Dynamic Table 能力，而不应将当前物化视图视为通用的持续计算引擎。全面理解这些能力，才能判断物化视图究竟是在减少计算，还是把刷新和维护成本转移到了另一个环节。



> 增量物化视图特性在 Databend 企业版 `v1.2.934` 后引入和更新。



