数据库基础体系 · 第 110/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Oracle 物化视图与查询重写:刷新、日志、一致性和性能

物化视图(Materialized View,MV)不是“带索引的视图”,也不是简单的查询缓存。它是一个实际存储查询结果的数据库对象,并由 Oracle 按指定策略重新计算或增量维护。

物化视图解决的是一个明确的问题:

是否可以用预先计算并存储的结果,替代运行时对明细表的重复扫描、连接和聚合?

要回答这个问题,必须同时理解四个部分:

  1. 物化视图存储了什么;
  2. 刷新如何把源表变化传递到物化视图;
  3. 物化视图日志如何支持快速刷新;
  4. 优化器何时可以把用户查询自动改写为访问物化视图。

这四部分分别对应存储、维护、一致性和性能。只创建了物化视图,并不代表查询一定会使用它,也不代表其中的数据永远是最新的。


一、物化视图到底是什么

普通视图只保存查询定义:

CREATE VIEW v_product_sales AS
SELECT product_id, SUM(amount) AS total_amount
FROM sales
GROUP BY product_id;

查询普通视图时,Oracle 通常仍然需要访问 sales 表并执行聚合。普通视图主要提供逻辑封装,不自动保存结果。

物化视图则保存查询结果:

CREATE MATERIALIZED VIEW mv_product_sales
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND
AS
SELECT product_id, SUM(amount) AS total_amount
FROM sales
GROUP BY product_id;

创建完成后,mv_product_sales 中有实际数据。后续查询可以直接访问这些数据:

SELECT product_id, total_amount
FROM mv_product_sales
WHERE product_id = 10;

物化视图仍然具有查询定义,但定义和结果是两件事:

  • 查询定义描述“应该如何计算”;
  • 物化结果描述“上一次刷新后计算出的内容”。

因此,物化视图天然存在一个状态问题:结果与源表当前状态之间可能存在时间差

1. 物化视图的核心状态

可以把物化视图抽象成:

Mt=Q(St)M_t = Q(S_t)

其中:

  • StS_t 是时间 tt 时源表的数据;
  • QQ 是物化视图定义中的查询;
  • MtM_t 是对源表在时间 tt 的快照执行查询得到的结果。

如果源表之后变为 St+1S_{t+1},但物化视图没有刷新,那么实际存储的仍然是:

Mt=Q(St)M_t = Q(S_t)

而不是:

Q(St+1)Q(S_{t+1})

这就是物化视图的“陈旧”(stale)问题。陈旧不一定是错误,它可能是明确选择的性能与时效性折中;但应用必须知道自己接受什么程度的陈旧。


二、刷新:如何让物化视图跟上源表

刷新(refresh)是重新生成或更新物化视图数据的过程。Oracle 主要提供以下刷新方法:

  • COMPLETE:完整刷新;
  • FAST:快速刷新;
  • FORCE:优先快速刷新,不能快速刷新时改用完整刷新。

刷新时机主要包括:

  • ON DEMAND:由用户、作业或应用显式触发;
  • ON COMMIT:源表事务提交时自动刷新。

BUILD IMMEDIATEBUILD DEFERRED 则控制创建物化视图时是否立即生成数据。


1. COMPLETE:重新计算整个结果

完整刷新本质上是重新执行物化视图查询,得到全量结果。

对于:

CREATE MATERIALIZED VIEW mv_product_sales
REFRESH COMPLETE ON DEMAND
AS
SELECT product_id, SUM(amount) AS total_amount
FROM sales
GROUP BY product_id;

完整刷新可以近似理解为:

-- 逻辑过程,不是建议用户手工执行的替代脚本
DELETE FROM mv_product_sales;

INSERT INTO mv_product_sales
SELECT product_id, SUM(amount)
FROM sales
GROUP BY product_id;

Oracle 的实际实现会根据刷新选项使用不同的内部操作,例如截断、插入、交换或其他维护方式,不能简单假定一定执行上述两条语句。但从数据语义上看,它重新计算的是整个结果集。

手工刷新:

BEGIN
  DBMS_MVIEW.REFRESH(
    list           => 'MV_PRODUCT_SALES',
    method         => 'C',
    atomic_refresh => TRUE
  );
END;
/

其中:

  • method => 'C' 表示 COMPLETE
  • atomic_refresh => TRUE 表示尽量以原子方式完成该次刷新。

完整刷新的优点是:

  • 不依赖物化视图日志;
  • 对复杂查询通常更容易成立;
  • 结果逻辑简单,故障诊断相对直接。

代价是:

  • 需要重新扫描源表;
  • 需要重新执行连接、排序和聚合;
  • 对大表和高频刷新场景成本很高。

如果源表每天增长数亿行,而最近只新增了几万行,完整刷新仍然要重新计算全部数据,这通常不是理想方案。


2. FAST:只处理变化量

快速刷新不重新计算全部结果,而是利用源表的变化记录计算增量。

设某个物化视图的结果是:

R=SUM(amount)R = \text{SUM}(amount)

如果源表发生以下变化:

  • 插入一行金额 +100+100
  • 删除一行金额 3030
  • 更新一行,从 5050 改为 8080

那么结果变化量为:

ΔR=+10030+(8050)=100\Delta R = +100 - 30 + (80 - 50) = 100

新的结果是:

Rnew=Rold+ΔRR_{\text{new}} = R_{\text{old}} + \Delta R

对于分组聚合,变化还必须带有分组键。例如:

旧数据:
product_id = 10, amount = 50

新数据:
product_id = 10, amount = 80

Oracle 需要知道:

product_id = 10 的 SUM 增加 30

如果更新同时修改了分组键:

product_id = 10, amount = 50
变为
product_id = 20, amount = 80

则增量不是简单地增加 30,而是:

product_id = 10 减少 50
product_id = 20 增加 80

这就是快速刷新的本质:源表的变更必须包含足够的信息,Oracle 才能推导出物化结果的变化。

快速刷新不是“总是更快”。它通常适合:

  • 源表很大;
  • 单次变化量相对较小;
  • 物化视图定义满足快速刷新能力要求;
  • 维护日志的成本低于全量重算成本。

如果一次批量装载几乎重写了整个源表,快速刷新可能并不比完整刷新有优势。


3. FORCE:先尝试 FAST,失败时使用 COMPLETE

FORCE 的语义是:

  1. Oracle 尝试快速刷新;
  2. 如果当前物化视图或变化条件不满足快速刷新要求,则使用完整刷新。

例如:

CREATE MATERIALIZED VIEW mv_product_sales
REFRESH FORCE ON DEMAND
AS
SELECT product_id, SUM(amount) AS total_amount
FROM sales
GROUP BY product_id;

FORCE 适合希望“尽量增量维护,但不能刷新失败”的场景。不过它有一个重要风险:

以为自己使用的是快速刷新,实际某次刷新可能退化为完整刷新。

因此生产环境不能只看刷新是否成功,还要观察:

  • 刷新耗时;
  • 读取行数;
  • CPU、临时空间和 I/O;
  • 刷新方法;
  • 物化视图是否变为陈旧或不可用状态。

对于刷新窗口严格受限的系统,通常应先通过能力检查确认 FAST 是否可行,再决定是否允许 FORCE 作为故障兜底。


4. BUILD IMMEDIATE 与 BUILD DEFERRED

CREATE MATERIALIZED VIEW mv_product_sales
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND
AS
SELECT product_id, SUM(amount)
FROM sales
GROUP BY product_id;

BUILD IMMEDIATE 创建对象时立即生成数据。创建操作可能需要长时间扫描源表。

CREATE MATERIALIZED VIEW mv_product_sales
BUILD DEFERRED
REFRESH COMPLETE ON DEMAND
AS
SELECT product_id, SUM(amount)
FROM sales
GROUP BY product_id;

BUILD DEFERRED 先创建定义,之后再刷新生成数据。它适合:

  • 先完成部署,再安排数据构建;
  • 避免 DDL 阶段长时间占用资源;
  • 需要在低峰期完成第一次全量构建。

第一次构建不能依赖不存在的旧结果,通常需要完整刷新。


三、物化视图日志:快速刷新的变化依据

物化视图日志(Materialized View Log)是建立在源表上的日志对象,用于记录源表变化,以便后续快速刷新。

创建示例:

CREATE MATERIALIZED VIEW LOG ON sales
WITH ROWID, SEQUENCE
(
  product_id,
  sale_day,
  amount
)
INCLUDING NEW VALUES;

这里假设源表结构类似:

CREATE TABLE sales (
  sale_id    NUMBER PRIMARY KEY,
  product_id NUMBER NOT NULL,
  sale_day   DATE   NOT NULL,
  amount     NUMBER NOT NULL
);

日志不是物化视图本身的数据,也不是通用审计日志。它的主要用途是为物化视图维护提供必要的增量信息。


1. 日志为什么必须记录列

假设物化视图是:

SELECT product_id, sale_day, SUM(amount)
FROM sales
GROUP BY product_id, sale_day;

快速刷新至少需要知道:

  • 哪个产品发生变化;
  • 哪一天发生变化;
  • 金额变化了多少;
  • 变化是插入、删除还是更新。

如果日志只记录 sale_id,但没有记录分组列和度量列,刷新过程就可能无法仅通过日志计算出:

哪个分组增加或减少了多少金额

因此,日志列必须服务于物化视图定义。对于连接、聚合、过滤等更复杂的查询,要求也会相应增加。

INCLUDING NEW VALUES 用于让日志记录更新后的列值。对于更新操作,增量维护通常需要同时知道旧值和新值,尤其是更新了聚合列或分组列时。


2. ROWID、主键与 SEQUENCE

常见日志标识方式包括:

  • ROWID:用物理行地址标识明细行;
  • PRIMARY KEY:用主键标识明细行;
  • SEQUENCE:为变化记录提供顺序信息,某些快速刷新场景需要;
  • 相关列:记录查询中需要的列值。

例如:

CREATE MATERIALIZED VIEW LOG ON sales
WITH PRIMARY KEY, SEQUENCE
(
  product_id,
  sale_day,
  amount
)
INCLUDING NEW VALUES;

使用 ROWID 还是 PRIMARY KEY,取决于物化视图定义、源表结构和快速刷新能力要求。不能因为表有主键,就认为所有物化视图都自动具备快速刷新条件;也不能因为创建了日志,就认为所有查询都支持快速刷新。

日志还会带来持续成本:

  • 每次源表 DML 都可能额外写日志;
  • 日志会增加 redo、undo 和 I/O;
  • 日志本身需要维护空间;
  • 日志清理和刷新进度需要管理;
  • 高并发 OLTP 表可能因日志写入增加事务负担。

因此,日志不是免费的性能优化。


3. 单表聚合的完整示例

下面给出一个结构相对简单、便于理解的例子。

创建源表

CREATE TABLE sales (
  sale_id    NUMBER PRIMARY KEY,
  product_id NUMBER NOT NULL,
  sale_day   DATE NOT NULL,
  amount     NUMBER NOT NULL
);

插入初始数据:

INSERT INTO sales VALUES (1, 10, DATE '2025-01-01', 100);
INSERT INTO sales VALUES (2, 10, DATE '2025-01-01', 50);
INSERT INTO sales VALUES (3, 20, DATE '2025-01-01', 80);

COMMIT;

创建物化视图日志

CREATE MATERIALIZED VIEW LOG ON sales
WITH PRIMARY KEY, SEQUENCE
(
  product_id,
  sale_day,
  amount
)
INCLUDING NEW VALUES;

创建物化视图

CREATE MATERIALIZED VIEW mv_sales_by_product_day
BUILD IMMEDIATE
REFRESH FAST ON DEMAND
ENABLE QUERY REWRITE
AS
SELECT
  product_id,
  sale_day,
  SUM(amount) AS total_amount,
  COUNT(*) AS row_count
FROM sales
GROUP BY product_id, sale_day;

这里包含 COUNT(*) 并不是装饰。对于聚合物化视图,计数信息经常是增量维护的重要组成部分;如果后续查询需要计算平均值,计数也可用于:

AVG(amount)=SUM(amount)COUNT(amount)AVG(amount)=\frac{SUM(amount)}{COUNT(amount)}

查询当前结果:

SELECT *
FROM mv_sales_by_product_day
ORDER BY product_id;

预期结果:

PRODUCT_ID  SALE_DAY    TOTAL_AMOUNT  ROW_COUNT
----------  ----------  ------------  ----------
10          2025-01-01  150           2
20          2025-01-01  80            1

修改源表但不刷新

INSERT INTO sales
VALUES (4, 10, DATE '2025-01-01', 30);

COMMIT;

此时源表中产品 10 的总金额已经变为 180,但物化视图仍可能是:

PRODUCT_ID  TOTAL_AMOUNT
----------  ------------
10          150

因为 ON DEMAND 不会在提交时自动刷新。

执行快速刷新

BEGIN
  DBMS_MVIEW.REFRESH(
    list           => 'MV_SALES_BY_PRODUCT_DAY',
    method         => 'F',
    atomic_refresh => TRUE
  );
END;
/

刷新后,Oracle 读取物化视图日志,计算:

product_id = 10, sale_day = 2025-01-01, amount 增加 30

然后将物化结果从 150 更新为 180。

如果这个物化视图不满足快速刷新条件,指定 method => 'F' 通常会报错,而不是自动改做完整刷新。希望允许回退时可以使用 method => 'F' 之外的 FORCE 策略,具体行为仍应通过刷新日志和执行监控验证。


四、刷新时的一致性边界

“一致”必须先说明是相对于哪个时间点的一致。

1. 物化视图内部的一致性

一次刷新应当把物化视图推进到某个源表状态,而不是把不同时间点的明细变化任意混合。

例如源表有以下事务:

事务 T1:插入一笔销售记录
事务 T2:删除另一笔销售记录

如果刷新只看到了部分未提交变化,就可能产生错误结果。因此 Oracle 的刷新机制会依据事务提交和日志信息维护可见数据,而不是简单读取所有正在修改的行。

但这不表示物化视图自动等于源表当前状态。刷新完成时,源表可能又发生了新的已提交变化:

t0:物化视图刷新完成,得到 Q(S_t0)
t1:源表提交新事务,变为 S_t1
t2:用户查询物化视图

如果 t2 之前没有再次刷新,用户读到的仍然是:

Q(St0)Q(S_{t0})

而不是:

Q(St1)Q(S_{t1})

所以需要区分:

  • 刷新内部一致性:本次物化结果是否正确反映了刷新所处理的源表变化;
  • 源表实时一致性:物化结果是否反映源表最新已提交状态。

前者通常是数据库维护保证的一部分,后者取决于刷新时机。


2. ON COMMIT 刷新

可以这样定义:

CREATE MATERIALIZED VIEW mv_sales_by_product_day
REFRESH FAST ON COMMIT
AS
SELECT ...

ON COMMIT 的语义是:源表事务提交时触发相关物化视图刷新。

其数据流大致如下:

源表 DML
  ↓
物化视图日志记录变化
  ↓
事务准备提交
  ↓
物化视图执行维护
  ↓
提交完成

这与 ON DEMAND 的区别非常大。

ON DEMAND

源表提交成功
  ↓
物化视图暂时陈旧
  ↓
稍后由作业或用户刷新

ON COMMIT

源表提交
  ↓
同时承担物化视图维护成本
  ↓
提交路径变长

因此 ON COMMIT 不等于“免费实时”。它把刷新成本放到了写事务的提交路径中,可能造成:

  • 提交延迟变长;
  • 源表事务与物化视图维护争用资源;
  • 批量 DML 的提交成本明显上升;
  • 刷新失败时事务提交受到影响。

它更适合:

  • 变化量较小;
  • 物化视图较小;
  • 读取必须接近提交后可见;
  • 可以接受写入事务承担维护成本。

它通常不适合:

  • 高吞吐 OLTP;
  • 大批量导入;
  • 一个提交会影响大量聚合分组;
  • 物化视图数量很多。

ON COMMIT 还受到快速刷新能力、查询定义和部署环境的限制。复杂连接、远程对象或不满足快速刷新的定义不能简单套用这一模式。


3. 非原子刷新与可见性风险

DBMS_MVIEW.REFRESHatomic_refresh 参数影响刷新过程的原子性与资源使用方式。

概念上:

  • 原子刷新更强调刷新过程对外的整体性;
  • 非原子刷新可能使用更高效的截断或批量装载方式,但过程中可能存在更明显的中间状态。

非原子刷新可能降低 undo 压力或缩短部分刷新操作,但必须确认应用是否允许:

  • 刷新期间读取到空结果或部分结果;
  • 多个相关物化视图之间短暂不一致;
  • 失败后需要重新执行完整刷新。

如果报表系统要求“切换前一直读旧版本,切换后一次性读新版本”,就不能只为了速度盲目使用非原子刷新。需要选择适合的刷新组织方式,例如在维护窗口执行,并结合应用读写边界、分区设计或其他版本切换方案验证实际可见性。


五、查询重写:用户不改 SQL,也可能使用物化视图

查询重写(Query Rewrite)是 Oracle 优化器在语义等价的前提下,将用户 SQL 改写为访问物化视图的过程。

用户提交:

SELECT
  product_id,
  sale_day,
  SUM(amount) AS total_amount
FROM sales
WHERE product_id = 10
GROUP BY product_id, sale_day;

优化器可能识别到:

MV_SALES_BY_PRODUCT_DAY

已经保存了相同粒度的聚合结果,于是改为访问物化视图:

SELECT
  product_id,
  sale_day,
  total_amount
FROM mv_sales_by_product_day
WHERE product_id = 10;

这只是逻辑示意。实际执行计划不一定出现完全相同的 SQL 文本,但计划中通常会出现物化视图对象,而不是源表全量聚合。

启用查询重写通常需要:

ALTER SESSION SET QUERY_REWRITE_ENABLED = TRUE;

物化视图本身也可以声明:

ENABLE QUERY REWRITE

数据库级或会话级参数决定是否允许这类转换。即使允许查询重写,也不代表优化器一定选择该物化视图。


1. 查询重写必须满足语义等价

查询重写不是“结果差不多就能用”。对于精确查询,改写后的结果必须符合原查询语义。

例如,物化视图按天聚合:

GROUP BY product_id, sale_day

它可以直接回答:

GROUP BY product_id, sale_day

但对于:

GROUP BY product_id

是否能够进一步汇总天级结果,取决于物化视图中是否保存了足够的度量信息以及 Oracle 对该查询的重写能力判断。

对于 SUM,天级结果可以继续求和:

productamount=dayproduct, dayamount\sum_{\text{product}} amount = \sum_{\text{day}}\sum_{\text{product, day}} amount

对于 COUNT(*),也可以继续求和:

COUNTproduct=dayCOUNTproduct, dayCOUNT_{\text{product}} = \sum_{\text{day}}COUNT_{\text{product, day}}

但对于平均值,不能直接平均各天平均值:

错误:
(当天平均值 + 第二天平均值) / 2

正确公式是:

AVG=amountCOUNT(amount)AVG = \frac{\sum amount}{COUNT(amount)}

例如:

第一天:总额 100,行数 1,平均值 100
第二天:总额 10,行数 10,平均值 1

直接平均两个平均值:

(100+1)/2=50.5(100+1)/2=50.5

正确平均值:

(100+10)/(1+10)=10(100+10)/(1+10)=10

因此,一个用于查询重写的聚合物化视图通常需要保存 SUMCOUNT,而不是只保存 AVG


2. 查询重写的几个必要条件

物化视图必须允许查询重写

例如:

CREATE MATERIALIZED VIEW mv_sales_by_product_day
ENABLE QUERY REWRITE
AS
SELECT ...

如果定义为 DISABLE QUERY REWRITE,优化器不会把普通查询自动转换为访问该物化视图。

物化视图定义必须具备重写能力

Oracle 会检查:

  • 连接是否可以等价替代;
  • 分组粒度是否足够;
  • 过滤条件是否安全;
  • 聚合函数是否可推导;
  • 是否缺少必要的列;
  • 是否存在不确定函数、语义差异或其他限制。

可以使用物化视图能力分析工具进行检查。典型流程是准备能力表,然后执行:

BEGIN
  DBMS_MVIEW.EXPLAIN_MVIEW(
    mv => 'MV_SALES_BY_PRODUCT_DAY'
  );
END;
/

查询能力分析结果:

SELECT capability_name, possible, msgtxt
FROM mv_capabilities_table
ORDER BY seq;

不同版本对支持的查询形式和输出列可能有差异,应以当前数据库版本的 DBMS_MVIEW 文档和实际结果为准。重点不是只看是否存在物化视图,而是看 REFRESH_FASTREWRITE 等能力是否为 Y,以及失败原因。

数据新鲜度必须符合一致性设置

Oracle 的 QUERY_REWRITE_INTEGRITY 控制查询重写对数据新鲜度和约束可信度的要求。常见取值包括:

  • ENFORCED
  • TRUSTED
  • STALE_TOLERATED

它们表达的不是性能等级,而是“允许优化器依赖哪些正确性前提”。

大致可以理解为:

  • ENFORCED:要求更严格的数据库强制保证;
  • TRUSTED:允许依赖数据库认为可信的关系或约束;
  • STALE_TOLERATED:允许使用陈旧物化视图。

如果允许使用陈旧结果:

ALTER SESSION SET QUERY_REWRITE_INTEGRITY = STALE_TOLERATED;

优化器可能使用一个没有反映最新源表变化的物化视图。这在汇总报表、近实时分析中可能是有意选择,但在余额、库存、订单状态等精确业务查询中通常不可接受。

优化器成本模型仍然会参与决策

即使查询可被改写,Oracle 也可能认为直接访问源表更便宜。例如:

  • 物化视图很大;
  • 用户查询选择性很高;
  • 物化视图缺少合适索引;
  • 统计信息过期;
  • 访问物化视图还需要额外聚合;
  • 源表已经有高选择性索引。

因此:

ENABLE QUERY REWRITE

表示“允许考虑”,不是“强制使用”。


六、如何确认查询真的使用了物化视图

只看到 ENABLE QUERY REWRITE 不足以证明发生了重写。

先执行目标查询:

SELECT
  product_id,
  sale_day,
  SUM(amount) AS total_amount
FROM sales
WHERE product_id = 10
GROUP BY product_id, sale_day;

然后查看执行计划:

SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

重点检查:

  • TABLE ACCESS 或索引访问的对象名称;
  • 是否访问了 MV_SALES_BY_PRODUCT_DAY
  • 是否仍然扫描 SALES
  • 实际行数与估算行数是否严重偏差;
  • 是否存在额外的 HASH GROUP BYSORT GROUP BY

如果只有估算计划,也可以使用:

EXPLAIN PLAN FOR
SELECT
  product_id,
  sale_day,
  SUM(amount) AS total_amount
FROM sales
WHERE product_id = 10
GROUP BY product_id, sale_day;

SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY);

EXPLAIN PLAN 不一定反映真实执行时的绑定变量、统计信息和运行环境,生产诊断应优先查看实际游标计划。

还可以使用 DBMS_MVIEW 的重写分析接口检查“为什么不能重写”。诊断问题时要区分三类情况:

  1. 不能重写:定义或一致性条件不满足;
  2. 可以重写但没选:优化器成本判断不划算;
  3. 曾经重写,后来计划变化:统计信息、数据分布、参数或对象状态发生变化。

这三类问题的处理方式完全不同。


七、物化视图与索引、统计信息和 Hint 的关系

物化视图改变了可访问的数据结构,但不替代索引和优化器统计信息。

1. 物化视图上的索引

例如报表经常按产品和日期过滤:

CREATE INDEX ix_mv_sales_prod_day
ON mv_sales_by_product_day (product_id, sale_day);

物化视图上的索引也需要随着刷新维护。索引可能提升查询性能,但会增加:

  • 刷新期间的维护成本;
  • 存储空间;
  • DML 或批量装载后的索引处理时间。

如果物化视图主要用于全表扫描报表,索引未必有益;如果主要是高选择性点查或范围查,索引可能很重要。

2. 统计信息

物化视图是实际存储的数据对象,通常需要收集统计信息:

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS(
    ownname => USER,
    tabname => 'MV_SALES_BY_PRODUCT_DAY',
    cascade  => TRUE
  );
END;
/

统计信息影响两个决策:

  1. 是否认为查询重写可行;
  2. 使用物化视图还是源表更便宜。

物化视图刚创建或大幅刷新后,如果统计信息严重过期,可能出现:

  • 物化视图明明更小,却没有被选中;
  • 估算行数错误;
  • 连接顺序不合理;
  • 访问路径异常。

统计信息不是让查询“必然使用 MV”的开关,而是让成本模型有正确输入。

3. Hint 不是查询重写的替代品

某些 Hint 可以影响查询重写或禁止重写,但不应把 Hint 当作物化视图维护机制。

例如:

SELECT /*+ NO_REWRITE */
       product_id,
       sale_day,
       SUM(amount)
FROM sales
GROUP BY product_id, sale_day;

这个 Hint 的意图是禁止查询重写。它适合诊断:

使用物化视图的计划是否真的更快?

而不是作为长期解决方案。长期依赖 Hint 可能掩盖:

  • 统计信息问题;
  • 查询重写能力不足;
  • 物化视图陈旧;
  • 数据分布变化;
  • 计划稳定性问题。

如果必须固定访问某个对象,应先确认业务语义、刷新状态和成本模型,再考虑 SQL Plan Baseline 等计划管理机制。


八、复杂查询为什么可能无法快速刷新或查询重写

物化视图日志存在,并不意味着所有物化视图都支持快速刷新。

常见困难包括:

1. 多表连接

对于:

SELECT p.category_id, SUM(s.amount)
FROM sales s
JOIN products p ON p.product_id = s.product_id
GROUP BY p.category_id;

如果 salesproducts 都可能发生变化,维护结果时需要知道两张表分别发生了什么,以及连接关系如何变化。因此通常需要为相关明细表创建日志,并满足 Oracle 对连接、键和聚合的能力要求。

只给 sales 建日志,而 products 发生了类别变更,物化结果就无法仅依赖 sales 的变化推导出来。

2. 更新连接键

如果修改了连接键或分组键,增量维护必须同时处理旧关联和新关联:

旧:product_id = 10, category = A, amount = 100
新:product_id = 20, category = B, amount = 100

结果需要:

A 组减去 100
B 组增加 100

日志缺少旧值或新值时,快速刷新可能不成立。

3. 非确定性表达式和语义复杂的表达式

包含当前时间、随机值或其他每次执行可能不同的表达式时,结果不能简单由行变化量推导。复杂分析函数、某些子查询、集合操作和不受支持的 SQL 结构,也可能阻止快速刷新或查询重写。

4. 外部状态变化

如果结果依赖数据库外部文件、远程系统或函数内部状态,Oracle 不能仅通过本地 DML 日志知道结果是否改变。此时完整刷新往往更可靠。


九、刷新失败、陈旧和不可用状态

生产系统必须把刷新看成一个有失败路径的运维流程,而不是一条永远成功的命令。

典型失败原因包括:

  • 物化视图日志被删除或结构不满足要求;
  • 日志中所需的历史变化已被清理;
  • 物化视图定义发生变化;
  • 源表结构变化;
  • 物化视图失效;
  • 快速刷新能力不再满足;
  • 空间、undo、临时表空间不足;
  • 锁等待或资源管理限制;
  • 远程数据库或数据库链接不可用。

应检查物化视图状态和刷新信息:

SELECT
  owner,
  mview_name,
  staleness,
  compile_state,
  last_refresh_type,
  last_refresh_date,
  refresh_mode,
  refresh_method
FROM dba_mviews
WHERE mview_name = 'MV_SALES_BY_PRODUCT_DAY';

在无权访问 DBA_MVIEWS 时,可以查询:

SELECT
  mview_name,
  staleness,
  compile_state,
  last_refresh_type,
  last_refresh_date,
  refresh_mode,
  refresh_method
FROM user_mviews
WHERE mview_name = 'MV_SALES_BY_PRODUCT_DAY';

常见状态含义:

  • FRESH:物化结果被认为与相关源数据同步;
  • STALE:源表发生了尚未反映到物化视图的变化;
  • UNUSABLE:物化视图数据不能正常使用,通常需要重新构建或刷新;
  • NEEDS_COMPILE 等状态:定义或依赖发生变化,需要重新编译或处理依赖。

实际状态名称和可用列应以当前版本数据字典为准。

修复时不能直接假定再次执行快速刷新一定成功。一般处理顺序是:

  1. 查看错误日志和物化视图状态;
  2. 检查源表和日志是否仍然存在;
  3. 重新执行能力分析;
  4. 若增量依据已丢失,执行完整刷新;
  5. 刷新后重新收集统计信息;
  6. 验证查询结果与执行计划。

例如强制完整刷新:

BEGIN
  DBMS_MVIEW.REFRESH(
    list           => 'MV_SALES_BY_PRODUCT_DAY',
    method         => 'C',
    atomic_refresh => TRUE
  );
END;
/

如果物化视图已经不可编译或定义依赖已破坏,仅执行刷新可能不够,需要先修复对象定义或重新创建。


十、物化视图日志的清理边界

日志不能无限增长。Oracle 会在满足相关物化视图刷新进度的条件下清理不再需要的日志记录,但清理能力受以下因素影响:

  • 是否存在多个依赖该日志的物化视图;
  • 某个物化视图是否长时间未刷新;
  • 是否存在失败或断开的刷新链路;
  • 是否有远程物化视图或其他部署边界;
  • 日志保留策略和刷新时间点。

如果一个物化视图长期不刷新,日志可能必须保留大量历史变化,以等待它未来进行快速刷新。这会把“没有刷新”的问题转化为源表 DML 的额外空间和 I/O 成本。

因此需要同时监控:

  • 源表行数;
  • 物化视图行数;
  • 日志大小;
  • 最后刷新时间;
  • 陈旧时间;
  • 刷新耗时;
  • 刷新失败次数。

如果业务已经放弃某个物化视图,应同时清理它和不再需要的日志依赖;不能只删除查询对象而忽略日志成本。


十一、一个完整的刷新与查询重写验证流程

下面是一套适合测试环境的最小流程。

第一步:确认源表变化

SELECT product_id, sale_day, SUM(amount) AS source_total
FROM sales
GROUP BY product_id, sale_day
ORDER BY product_id, sale_day;

第二步:确认物化视图结果

SELECT product_id, sale_day, total_amount
FROM mv_sales_by_product_day
ORDER BY product_id, sale_day;

如果两者不一致,先不要讨论查询重写,因为物化视图本身已经是陈旧的。

第三步:执行刷新

BEGIN
  DBMS_MVIEW.REFRESH(
    list           => 'MV_SALES_BY_PRODUCT_DAY',
    method         => 'F',
    atomic_refresh => TRUE
  );
END;
/

第四步:再次比较结果

SELECT product_id, sale_day, total_amount
FROM mv_sales_by_product_day
ORDER BY product_id, sale_day;

确认结果与源表聚合一致。

第五步:检查新鲜度

SELECT mview_name, staleness, last_refresh_type, last_refresh_date
FROM user_mviews
WHERE mview_name = 'MV_SALES_BY_PRODUCT_DAY';

第六步:检查查询执行计划

ALTER SESSION SET QUERY_REWRITE_ENABLED = TRUE;
ALTER SESSION SET QUERY_REWRITE_INTEGRITY = ENFORCED;

SELECT
  product_id,
  sale_day,
  SUM(amount) AS total_amount
FROM sales
WHERE product_id = 10
GROUP BY product_id, sale_day;

然后查看实际游标计划:

SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

应重点确认计划实际访问了哪个对象,而不是只依据 SQL 是否成功执行。


十二、性能分析:节省了什么,又增加了什么

物化视图主要减少的是查询时的工作量。

原始查询可能需要:

扫描明细表
  ↓
过滤
  ↓
连接维表
  ↓
排序或哈希聚合
  ↓
返回报表

使用物化视图后可能变为:

扫描较小的汇总结果
  ↓
索引过滤
  ↓
返回报表

但成本没有消失,只是转移到了刷新路径:

业务 DML
  ↓
写物化视图日志
  ↓
刷新时读取日志
  ↓
维护物化结果和索引

可以用一个简化模型表示:

Ctotal=Cquery+Crefresh+Clog+CstorageC_{\text{total}} = C_{\text{query}} + C_{\text{refresh}} + C_{\text{log}} + C_{\text{storage}}

其中:

  • CqueryC_{\text{query}}:查询阶段成本;
  • CrefreshC_{\text{refresh}}:刷新阶段成本;
  • ClogC_{\text{log}}:源表 DML 写日志的成本;
  • CstorageC_{\text{storage}}:物化数据和索引的存储成本。

物化视图值得使用的条件,不是“查询更快”这一条,而是整体成本和时效性都符合业务要求:

查询节省>日志写入+刷新维护+存储与运维\text{查询节省} > \text{日志写入} + \text{刷新维护} + \text{存储与运维}

这也是为什么同一个物化视图可能适合 OLAP 报表,却不适合高并发 OLTP 主交易路径。


十三、常见误解与实际边界

误解一:创建物化视图后,所有查询都会变快

不正确。必须同时满足:

  • 查询可以被语义等价地重写;
  • 物化视图没有违反当前一致性要求;
  • 查询重写已启用;
  • 优化器认为访问 MV 成本更低;
  • MV 和相关索引的统计信息可信。

否则查询仍可能访问源表。

误解二:物化视图一定是实时数据

不正确。

  • ON DEMAND 明确允许陈旧窗口;
  • 定时刷新只能保证刷新任务完成后的某个时点;
  • ON COMMIT 也只适用于满足条件的维护路径,并且会增加提交成本;
  • 源表在刷新完成后继续变化时,MV 又会变为陈旧。

误解三:有物化视图日志就一定可以 FAST 刷新

不正确。日志只是增量维护的必要基础之一,不是充分条件。查询定义、连接关系、聚合形式、列信息和数据库版本能力都可能影响结果。

误解四:FORCE 就等于快速刷新

不正确。FORCE 可能退化到完整刷新。必须观察实际刷新类型和耗时。

误解五:把 QUERY_REWRITE_INTEGRITY 设置为 STALE_TOLERATED 只是性能开关

不正确。它改变了允许使用陈旧结果的范围,实际改变的是查询正确性的时间边界。报表可以接受,不代表交易查询可以接受。

误解六:物化视图可以替代所有索引

不正确。物化视图解决的是预计算和数据缩减;索引解决的是在某个存储结构中快速定位行。两者可能互相配合,也可能分别适用于不同查询。

误解七:物化视图类似 SQLite 的普通视图

SQLite 支持普通视图,但没有 Oracle 这种由数据库原生维护、支持刷新策略和查询重写的同等物化视图机制。若在 SQLite 中实现类似能力,通常需要应用层维护汇总表、触发器或定时任务,不能直接套用 Oracle 的刷新日志和优化器语义。


十四、如何选择刷新方式

可以从三个变量开始判断:

1. 允许多旧的数据

如果结果允许延迟数分钟或数小时:

  • 使用 ON DEMAND
  • 由调度任务在低峰期刷新;
  • 通过状态和监控确认刷新是否完成。

如果必须在事务提交后尽快可见:

  • 评估 ON COMMIT
  • 先验证快速刷新能力;
  • 测量提交延迟和高峰期资源争用。

2. 单次变化量与全量数据量的比例

设源表总行数为 NN,一次变化量为 ΔN\Delta N

当:

ΔNN\Delta N \ll N

快速刷新更可能有优势。

当:

ΔNN\Delta N \approx N

完整刷新可能更简单,甚至更快。

这不是固定阈值,因为还取决于:

  • 聚合分组数量;
  • 连接复杂度;
  • 日志读取成本;
  • 索引数量;
  • 并行度;
  • I/O 和 CPU 分布。

3. 查询是否稳定依赖该结果

如果报表可以在 MV 暂时陈旧时显示“截至某个刷新时间”的数据,那么 STALE_TOLERATED 或按时刷新可能合适。

如果结果用于:

  • 账户余额;
  • 库存扣减;
  • 订单状态判断;
  • 权限判定;
  • 财务结算;

则不能只依赖“通常会及时刷新”的经验,需要明确禁止陈旧重写,或者直接查询事务源表。


物化视图的核心不是“提前算一次 SQL”,而是建立了一条新的数据链路:

源表 DML
  → 物化视图日志
  → 快速或完整刷新
  → 物化结果
  → 查询重写
  → 优化器选择执行计划

其中任何一环都可能成为边界:

  • 没有日志,快速刷新可能无法成立;
  • 刷新不及时,物化结果会陈旧;
  • 一致性级别过宽,查询可能读到旧结果;
  • 查询重写能力不足,SQL 仍会访问源表;
  • 统计信息不正确,优化器可能放弃更优的物化视图;
  • 日志和索引维护成本过高,OLTP 写入可能变慢。

因此,评估物化视图时应同时回答四个问题:

  1. 结果允许旧到什么时间点;
  2. 变化是否能够被可靠地增量维护;
  3. 查询是否能在语义上安全地重写;
  4. 刷新、日志和索引的总成本是否低于查询节省。

只有这四个问题都得到明确答案,物化视图才是可验证的性能设计,而不是仅凭执行计划偶尔变快的配置。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。