4、物化视图的查询重写:优化器如何“偷梁换柱”

这一章,我们来聊聊物化视图最核心、也最神奇的能力——查询重写。

说白了,就是数据库优化器在收到你的 SQL 后,会偷偷地“调包”。它发现你的查询其实可以用一个已经存在的物化视图来加速,于是它就不去碰那些原始的大表了,而是直接去读物化视图里的小结果集。你写的 SQL 没变,但实际执行路径已经被换了。

我第一次接触这个特性时,心里想的是:“这不就是作弊吗?” 但后来我发现,这种“作弊”正是数据库性能优化的精髓。

4.1 查询重写机制:优化器的“小算盘”

查询重写,也叫 Query Rewrite 或 Transparent Rewrite。它的工作流程大致是这样的:

  1. 你提交一条 SQL 查询。
  2. 优化器解析 SQL,生成逻辑执行计划。
  3. 优化器扫描系统中可用的物化视图定义。
  4. 优化器判断:你的查询结果,是否可以从某个物化视图中“推导”出来?
  5. 如果可以,优化器就把你的查询计划,替换成直接扫描物化视图的计划。
  6. 最终执行时,读的是物化视图,而不是原始表。

整个过程对应用层完全透明。你的代码不用改,连接池不用动,DBA 在后台加个物化视图,查询速度可能就翻了几倍。

核心要点: 查询重写不是简单的“SQL 文本替换”,而是基于代价的、逻辑等价的计划变换。优化器会确保改写后的结果,和原查询的结果完全一致。

我在项目中遇到过一种情况:业务方抱怨某个报表查询越来越慢,我检查后发现,其实两年前就有人建了一个物化视图,但查询重写一直没生效。原因很简单——物化视图的刷新状态是 INCOMPLETE。优化器很聪明,它不会用一个“脏数据”来回答你的查询。

4.2 优化器如何利用物化视图:匹配与代价

优化器不是随便抓个物化视图就用的。它有一套严格的匹配逻辑。我把它总结为三个核心条件:

条件 说明 个人经验
数据一致性 物化视图的数据必须与基表数据一致(或可接受一定延迟) 我习惯用 REFRESH FAST ON COMMIT 来保证实时一致性
查询包含性 物化视图的查询范围,必须能覆盖你的查询需求 比如物化视图聚合了所有月份,你只查某个月,是可以的
代价优势 扫描物化视图的代价,必须小于扫描原始表的代价 如果物化视图太大,优化器可能放弃重写

为什么会这样?你想想看,如果物化视图本身也很大,或者它的索引不好,优化器算来算去发现还不如直接扫原表,那它就不会用。优化器是个“精明的会计”,它只做划算的买卖。

我记得有一次,我在 Oracle 里调试一个慢查询。执行计划显示优化器没有使用物化视图。我手动加了 /*+ REWRITE */ 提示,它才用上。后来发现,是因为物化视图的统计信息过期了,优化器误判了代价。刷新统计信息后,重写就自动触发了。

4.3 REWRITE 关键字的使用:手动干预的艺术

虽然优化器会自动判断是否重写,但有时候我们需要手动干预。大多数数据库(如 Oracle、PostgreSQL)都提供了 REWRITE 相关的提示或参数。

在 Oracle 中,你可以这样用:

-- 强制使用查询重写
SELECT /*+ REWRITE(sales_mv) */
       product_id, SUM(amount)
FROM sales
WHERE sale_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY product_id;

-- 禁用查询重写
SELECT /*+ NOREWRITE */
       product_id, SUM(amount)
FROM sales
WHERE sale_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY product_id;

在 PostgreSQL 中,虽然没有直接的 REWRITE 提示,但可以通过设置 enable_materialized_view_rewrite 参数来控制:

-- 启用物化视图重写(默认开启)
SET enable_materialized_view_rewrite = on;

-- 禁用
SET enable_materialized_view_rewrite = off;

我的建议: 不要轻易在生产环境禁用查询重写。除非你明确知道某个物化视图的数据已经过时,或者你正在做性能对比测试。我曾经因为误关了重写,导致一个核心报表查询从 2 秒变成了 30 秒,被业务方追着问了一下午。

4.4 查询重写的常见场景与限制

不是所有查询都能被重写。我整理了一张表,方便你对照:

场景 能否重写 说明
简单聚合查询(SUM、COUNT、AVG) ✅ 能 最常用的场景,物化视图预聚合
多表 JOIN 查询 ✅ 能 物化视图预连接,减少运行时 JOIN 开销
带 WHERE 条件过滤 ✅ 能 只要物化视图的数据范围能覆盖查询条件
使用 DISTINCT 或 GROUP BY ✅ 能 物化视图已经去重或分组
子查询或 CTE ⚠️ 部分能 取决于数据库优化器的能力,Oracle 支持较好
窗口函数(ROW_NUMBER、RANK) ❌ 通常不能 物化视图不支持窗口函数的预计算
使用了非确定性函数(SYSDATE、RANDOM) ❌ 不能 结果不确定,无法保证一致性

嗯,这里要注意。窗口函数不能重写,是我踩过的一个坑。当时我想用物化视图加速一个排名查询,建好之后发现执行计划根本没变。查了文档才知道,物化视图不支持窗口函数的预计算。后来我换了个思路,把排名逻辑放到应用层去处理,物化视图只负责提供基础数据。

4.5 知识体系图:查询重写的完整链路

下面这张图,帮你把查询重写的整个流程串起来。从你提交 SQL,到优化器决定是否重写,再到最终执行,每一步都清晰可见。

物化视图查询重写流程 查询重写完整链路 用户提交 SQL 查询 优化器解析 & 生成计划 判断:是否存在匹配的物化视图? 数据一致性 + 查询包含性 + 代价优势 否 是 执行原始查询计划 重写为物化视图计划 返回查询结果 说明:优化器基于代价和逻辑等价性做决策。即使存在物化视图,也不一定会被使用。 可以通过 EXPLAIN PLAN 或执行计划查看是否发生了查询重写。

避坑指南: 我曾经在生产环境遇到过一个诡异的问题。物化视图明明存在,查询重写也开启了,但执行计划就是不走物化视图。排查了半天,发现是物化视图的 STALE 状态导致的。Oracle 中,如果物化视图的基表发生了 DDL 变更(比如加了一列),物化视图会标记为 STALE,优化器会拒绝使用它。解决办法是重新编译物化视图:ALTER MATERIALIZED VIEW mv_name COMPILE;