物化视图 · 把慢查询的结果冻起来
普通 VIEW 只是 SQL 别名,每次查都重算;MATERIALIZED VIEW 把结果物化到磁盘并可加索引,读取 = 直接读物理表,比反复重算快几个量级。代价是数据不实时,要 REFRESH——而 REFRESH CONCURRENTLY 能在不锁读的情况下刷新。
物化视图 · 把慢查询的结果冻起来
有一类查询,算一次很贵(几张大表 JOIN + 聚合),结果却被反复读(BI 仪表盘每次打开都跑一遍)。每次重算,既慢又浪费。物化视图就是这个矛盾的解:算一次,把结果存下来,之后直接读存好的。
VIEW 与 MATERIALIZED VIEW:算不算存下来
先分清两个「视图」:
VIEW—— 只是给一段 SQL 起了个名字。每次查询视图,底层 SQL 重新执行一遍。它省的是「打字」,不省「计算」。MATERIALIZED VIEW—— 把查询结果物化到磁盘,变成一张真实的物理表,还能加索引。之后查它 = 直接读这张表,完全不重算原始查询。
实测把一份 5000 行的销售数据聚合成日汇总:
-- 普通视图:每次查都现算这 719 行
CREATE VIEW v_daily_sales AS
SELECT day, sum(amount) AS total FROM sales GROUP BY day;
-- 物化视图:719 行算一次,存进磁盘(实测占 64 kB)
CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT day, sum(amount) AS total FROM sales GROUP BY day;两者查询结果一样(都是 719 行),但物化视图读的是存好的结果,原始 5000 行的聚合一次都不用重跑。数据量从几千变成几千万时,这个差别就是「秒开」和「转圈十秒」的区别。
代价:数据不实时,要 REFRESH
天下没有免费的午餐。物化视图的结果是某一刻的快照,源数据后来变了,它不会自动更新:
往 sales 新增一行(2025-07-15, 99999)
→ 查 mv,那天的 total 仍是旧值(None,没刷新)
→ REFRESH MATERIALIZED VIEW 之后,才变成 99999所以物化视图适合能容忍一点延迟的场景:日报、周报、排行榜、仪表盘——晚几分钟无所谓。刷新可以用定时任务(如 pg_cron)周期性跑。
REFRESH CONCURRENTLY:刷新时不锁读
普通 REFRESH MATERIALIZED VIEW 有个问题:刷新期间会锁表,读也被挡住。对一个「大家随时在看」的仪表盘,这很致命。REFRESH ... CONCURRENTLY 解决它:
-- 前提:物化视图上得有唯一索引
CREATE UNIQUE INDEX ON mv_daily_sales (day);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales; -- 刷新时照常能读CONCURRENTLY 为什么需要唯一索引?因为它不是「清空重灌」,而是在后台把新结果算好,再和旧数据逐行比对,只更新有变化的行。要「逐行比对」,就必须能唯一标识每一行——这正是唯一索引的作用。代价是它比普通 REFRESH 慢(要算新结果 + 做 diff),但换来「刷新期间不阻塞读」,对在线服务几乎是必须的。
PostgreSQL 的短板:只能全量刷新
得诚实说一个短板:PostgreSQL 内置的物化视图只支持「全量刷新」——每次 REFRESH 都把整个查询重算一遍,哪怕源数据只改了一行。它没有 Oracle 那种「增量 / 快速刷新」(只算变化部分)。
如果你的物化视图很大、但每次源数据只变一点点,全量刷新会很浪费。解法有两个:① 用 pg_ivm 扩展做增量物化视图(Incremental View Maintenance,源表一变就增量更新);② 自己用「普通表 + 触发器 / 定时增量」手搓。选型时把这条记住:PG 原生物化视图 = 全量刷新,超大 + 高频更新的场景要另想办法。
横向对比
| 能力 | PostgreSQL | MySQL | Oracle | SQL Server |
|---|---|---|---|---|
| 物化视图 | 有 | ❌ 无(自己拿表 + 触发器凑) | 有(功能最强) | 索引视图(indexed view) |
| 并发刷新 | REFRESH CONCURRENTLY | — | 有 | 自动维护 |
| 增量刷新 | ❌ 需 pg_ivm 扩展 | — | 有(fast refresh) | 自动增量维护 |
| 查询自动改写 | ❌ 无 | — | 有(query rewrite) | 有限 |
这里 PostgreSQL 不是最强的:Oracle 的物化视图有增量刷新 + 查询改写(优化器自动把命中的原始查询改写到物化视图上),更省心。SQL Server 的索引视图自动维护(但限制多)。MySQL 干脆没有,只能用表 + 触发器 / 定时任务模拟。PostgreSQL 处在中间:有、好用、但要手动刷新、且默认全量。
应用场景
- BI 仪表盘 / 报表:复杂多表聚合预计算成物化视图,前端秒开;定时(
pg_cron)每几分钟REFRESH CONCURRENTLY。 - 排行榜 / 榜单:热度、销量榜算一次存下,读高频、更新低频,天生适配。
- 复杂 JOIN 中间结果:反复用到的多表连接结果物化掉,下游查询直接读。
- 跨库 / 外部数据快照:通过
postgres_fdw拉外部数据,物化成本地快照,减少远程往返。
小结与下一章
物化视图用「一点延迟」换「读取快几个量级」,是应对「贵查询被反复读」的经典手段。REFRESH CONCURRENTLY 让它能在线刷新不挡读。要清醒的是它的短板——原生只支持全量刷新,超大高频场景要靠 pg_ivm 或手搓增量。
分区、物化视图都是「让大表 / 慢查询变快」的手段,但前提是——你得先知道慢在哪。规模与性能的最后一章,教你用 EXPLAIN 读懂查询计划,这是排查慢查询的第一手段。👉 EXPLAIN:读懂 PostgreSQL 的执行计划。
参考文献
- [1] PostgreSQL 官方文档.「41. Materialized Views / SQL-CREATEMATERIALIZEDVIEW.」https://www.postgresql.org/docs/current/rules-materializedviews.html —— 物化视图概念与刷新。
- [2] PostgreSQL 官方文档.「REFRESH MATERIALIZED VIEW.」https://www.postgresql.org/docs/current/sql-refreshmaterializedview.html ——
CONCURRENTLY与唯一索引要求。