4. 物化视图的查询重写
这一章,我们来聊聊物化视图最核心、也最神奇的能力——查询重写。
说白了,就是数据库优化器在收到你的 SQL 后,会偷偷地“调包”。它发现你的查询其实可以用一个已经存在的物化视图来加速,于是它就不去碰那些原始的大表了,而是直接去读物化视图里的小结果集。你写的 SQL 没变,但实际执行路径已经被换了。
我第一次接触这个特性时,心里想的是:“这不就是作弊吗?” 但后来我发现,这种“作弊”正是数据库性能优化的精髓。
4.1 查询重写机制:优化器的“小算盘”
查询重写,也叫 Query Rewrite 或 Transparent Rewrite。它的工作流程大致是这样的:
- 你提交一条 SQL 查询。
- 优化器解析 SQL,生成逻辑执行计划。
- 优化器扫描系统中可用的物化视图定义。
- 优化器判断:你的查询结果,是否可以从某个物化视图中“推导”出来?
- 如果可以,优化器就把你的查询计划,替换成直接扫描物化视图的计划。
- 最终执行时,读的是物化视图,而不是原始表。
整个过程对应用层完全透明。你的代码不用改,连接池不用动,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,到优化器决定是否重写,再到最终执行,每一步都清晰可见。
避坑指南: 我曾经在生产环境遇到过一个诡异的问题。物化视图明明存在,查询重写也开启了,但执行计划就是不走物化视图。排查了半天,发现是物化视图的 STALE 状态导致的。Oracle 中,如果物化视图的基表发生了 DDL 变更(比如加了一列),物化视图会标记为 STALE,优化器会拒绝使用它。解决办法是重新编译物化视图:ALTER MATERIALIZED VIEW mv_name COMPILE;