数据库基础体系 · 第 110/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
Oracle 物化视图与查询重写:刷新、日志、一致性和性能
物化视图(Materialized View,MV)不是“带索引的视图”,也不是简单的查询缓存。它是一个实际存储查询结果的数据库对象,并由 Oracle 按指定策略重新计算或增量维护。
物化视图解决的是一个明确的问题:
是否可以用预先计算并存储的结果,替代运行时对明细表的重复扫描、连接和聚合?
要回答这个问题,必须同时理解四个部分:
- 物化视图存储了什么;
- 刷新如何把源表变化传递到物化视图;
- 物化视图日志如何支持快速刷新;
- 优化器何时可以把用户查询自动改写为访问物化视图。
这四部分分别对应存储、维护、一致性和性能。只创建了物化视图,并不代表查询一定会使用它,也不代表其中的数据永远是最新的。
一、物化视图到底是什么
普通视图只保存查询定义:
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. 物化视图的核心状态
可以把物化视图抽象成:
其中:
- 是时间 时源表的数据;
- 是物化视图定义中的查询;
- 是对源表在时间 的快照执行查询得到的结果。
如果源表之后变为 ,但物化视图没有刷新,那么实际存储的仍然是:
而不是:
这就是物化视图的“陈旧”(stale)问题。陈旧不一定是错误,它可能是明确选择的性能与时效性折中;但应用必须知道自己接受什么程度的陈旧。
二、刷新:如何让物化视图跟上源表
刷新(refresh)是重新生成或更新物化视图数据的过程。Oracle 主要提供以下刷新方法:
COMPLETE:完整刷新;FAST:快速刷新;FORCE:优先快速刷新,不能快速刷新时改用完整刷新。
刷新时机主要包括:
ON DEMAND:由用户、作业或应用显式触发;ON COMMIT:源表事务提交时自动刷新。
BUILD IMMEDIATE 和 BUILD 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:只处理变化量
快速刷新不重新计算全部结果,而是利用源表的变化记录计算增量。
设某个物化视图的结果是:
如果源表发生以下变化:
- 插入一行金额 ;
- 删除一行金额 ;
- 更新一行,从 改为 。
那么结果变化量为:
新的结果是:
对于分组聚合,变化还必须带有分组键。例如:
旧数据:
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 的语义是:
- Oracle 尝试快速刷新;
- 如果当前物化视图或变化条件不满足快速刷新要求,则使用完整刷新。
例如:
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(*) 并不是装饰。对于聚合物化视图,计数信息经常是增量维护的重要组成部分;如果后续查询需要计算平均值,计数也可用于:
查询当前结果:
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 之前没有再次刷新,用户读到的仍然是:
而不是:
所以需要区分:
- 刷新内部一致性:本次物化结果是否正确反映了刷新所处理的源表变化;
- 源表实时一致性:物化结果是否反映源表最新已提交状态。
前者通常是数据库维护保证的一部分,后者取决于刷新时机。
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.REFRESH 的 atomic_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,天级结果可以继续求和:
对于 COUNT(*),也可以继续求和:
但对于平均值,不能直接平均各天平均值:
错误:
(当天平均值 + 第二天平均值) / 2
正确公式是:
例如:
第一天:总额 100,行数 1,平均值 100
第二天:总额 10,行数 10,平均值 1
直接平均两个平均值:
正确平均值:
因此,一个用于查询重写的聚合物化视图通常需要保存 SUM 和 COUNT,而不是只保存 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_FAST、REWRITE 等能力是否为 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 BY或SORT 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 的重写分析接口检查“为什么不能重写”。诊断问题时要区分三类情况:
- 不能重写:定义或一致性条件不满足;
- 可以重写但没选:优化器成本判断不划算;
- 曾经重写,后来计划变化:统计信息、数据分布、参数或对象状态发生变化。
这三类问题的处理方式完全不同。
七、物化视图与索引、统计信息和 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;
/
统计信息影响两个决策:
- 是否认为查询重写可行;
- 使用物化视图还是源表更便宜。
物化视图刚创建或大幅刷新后,如果统计信息严重过期,可能出现:
- 物化视图明明更小,却没有被选中;
- 估算行数错误;
- 连接顺序不合理;
- 访问路径异常。
统计信息不是让查询“必然使用 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;
如果 sales 和 products 都可能发生变化,维护结果时需要知道两张表分别发生了什么,以及连接关系如何变化。因此通常需要为相关明细表创建日志,并满足 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等状态:定义或依赖发生变化,需要重新编译或处理依赖。
实际状态名称和可用列应以当前版本数据字典为准。
修复时不能直接假定再次执行快速刷新一定成功。一般处理顺序是:
- 查看错误日志和物化视图状态;
- 检查源表和日志是否仍然存在;
- 重新执行能力分析;
- 若增量依据已丢失,执行完整刷新;
- 刷新后重新收集统计信息;
- 验证查询结果与执行计划。
例如强制完整刷新:
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
↓
写物化视图日志
↓
刷新时读取日志
↓
维护物化结果和索引
可以用一个简化模型表示:
其中:
- :查询阶段成本;
- :刷新阶段成本;
- :源表 DML 写日志的成本;
- :物化数据和索引的存储成本。
物化视图值得使用的条件,不是“查询更快”这一条,而是整体成本和时效性都符合业务要求:
这也是为什么同一个物化视图可能适合 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. 单次变化量与全量数据量的比例
设源表总行数为 ,一次变化量为 。
当:
快速刷新更可能有优势。
当:
完整刷新可能更简单,甚至更快。
这不是固定阈值,因为还取决于:
- 聚合分组数量;
- 连接复杂度;
- 日志读取成本;
- 索引数量;
- 并行度;
- I/O 和 CPU 分布。
3. 查询是否稳定依赖该结果
如果报表可以在 MV 暂时陈旧时显示“截至某个刷新时间”的数据,那么 STALE_TOLERATED 或按时刷新可能合适。
如果结果用于:
- 账户余额;
- 库存扣减;
- 订单状态判断;
- 权限判定;
- 财务结算;
则不能只依赖“通常会及时刷新”的经验,需要明确禁止陈旧重写,或者直接查询事务源表。
物化视图的核心不是“提前算一次 SQL”,而是建立了一条新的数据链路:
源表 DML
→ 物化视图日志
→ 快速或完整刷新
→ 物化结果
→ 查询重写
→ 优化器选择执行计划
其中任何一环都可能成为边界:
- 没有日志,快速刷新可能无法成立;
- 刷新不及时,物化结果会陈旧;
- 一致性级别过宽,查询可能读到旧结果;
- 查询重写能力不足,SQL 仍会访问源表;
- 统计信息不正确,优化器可能放弃更优的物化视图;
- 日志和索引维护成本过高,OLTP 写入可能变慢。
因此,评估物化视图时应同时回答四个问题:
- 结果允许旧到什么时间点;
- 变化是否能够被可靠地增量维护;
- 查询是否能在语义上安全地重写;
- 刷新、日志和索引的总成本是否低于查询节省。
只有这四个问题都得到明确答案,物化视图才是可验证的性能设计,而不是仅凭执行计划偶尔变快的配置。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:Oracle 分区与大表治理:Range、List、Hash、交换和裁剪
- 下一篇:Oracle 安全治理:用户、角色、Profile、TDE、审计和最小权限
- 延伸:Oracle 索引与优化器:统计信息、执行计划、Hint 和 SQL 调优
- 延伸:OLTP、OLAP 与 Lakehouse:工作负载、存储布局和数据链路选型
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论