2、物化视图的存储原理:数据冗余存储、刷新机制与存储开销分析

物化视图这个名字,说白了就是「把查询结果实体化」。它不像普通视图那样只是个虚拟的 SQL 壳子,而是实实在在把数据存成了一张物理表。我刚开始接触这个概念时也觉得奇怪——这不就是「空间换时间」的老套路吗?嗯,确实如此,但里面的门道比想象中要多。

2.1 数据冗余存储:为什么说它是「物理表」?

普通视图执行查询时,每次都要去跑一遍底层的 SQL。物化视图不一样,它把查询结果预先算好,存成一份独立的物理数据。你想想看,这相当于给查询结果拍了一张「快照」,后续读的时候直接读这张快照就行。

我在项目中遇到过这样一个场景:有个报表查询要关联 7 张表,跑一次要 45 秒。业务方说「这谁受得了?」后来我们建了物化视图,把查询结果提前算好存起来,查询时间直接降到 200 毫秒。代价是什么?多占了一份磁盘空间。

核心要点:物化视图存储的是查询结果的完整副本,不是指针,不是引用,是实打实的数据行。这意味着:

  • 基表有 1000 万行,物化视图可能也有 1000 万行(取决于聚合粒度)
  • 物化视图可以单独建索引,和基表的索引完全独立
  • 物化视图的数据和基表数据是异步的——除非你手动刷新

2.2 刷新机制:全量刷新 vs 增量刷新

数据存下来了,但基表一直在变啊。怎么让物化视图跟上节奏?这就引出了刷新机制。我习惯把刷新分为两种:全量刷新和增量刷新。它们各有各的适用场景,选错了可是要吃苦头的。

2.4.1 全量刷新(Complete Refresh)

全量刷新,就是重新执行一遍物化视图的定义 SQL,把结果全部覆盖写入。简单粗暴,但代价也大。

-- 全量刷新的典型语法(以 Oracle 为例)
EXEC DBMS_MVIEW.REFRESH('mv_sales_summary', method => 'C');

-- PostgreSQL 中
REFRESH MATERIALIZED VIEW mv_sales_summary;

优点:

  • 实现简单,不需要任何额外配置
  • 数据一致,不会出现遗漏或重复
  • 适合数据量小或刷新频率低的场景

缺点:

  • 刷新期间会锁表(某些数据库支持 CONCURRENTLY 选项)
  • 数据量大时,刷新时间可能长达数小时
  • 每次都是全量写,IO 压力巨大

我曾经踩过的坑:有一次给一个 5 亿行的物化视图配了全量刷新,凌晨 2 点开始跑,到早上 8 点还没跑完。业务方上班后发现报表数据还是昨天的,直接炸了。后来我改成增量刷新,刷新时间从 6 小时降到了 15 分钟。

2.4.2 增量刷新(Incremental Refresh / Fast Refresh)

增量刷新只处理基表中发生变化的数据。它需要依赖物化视图日志(Materialized View Log)或类似的变更追踪机制来记录基表的 INSERT、UPDATE、DELETE 操作。

-- 创建物化视图日志(Oracle)
CREATE MATERIALIZED VIEW LOG ON sales
WITH ROWID, SEQUENCE (product_id, sale_date, amount)
INCLUDING NEW VALUES;

-- 创建支持快速刷新的物化视图
CREATE MATERIALIZED VIEW mv_sales_summary
REFRESH FAST ON DEMAND
AS
SELECT product_id, sale_date, SUM(amount) as total_amount
FROM sales
GROUP BY product_id, sale_date;

增量刷新的核心条件:

  • 基表必须有物化视图日志(或类似机制)
  • 物化视图的定义 SQL 必须满足「可快速刷新」的条件(通常是分组聚合或简单连接)
  • 物化视图日志需要定期清理,否则会无限膨胀

我的个人习惯:对于数据量超过 1000 万行的物化视图,我优先考虑增量刷新。但要注意,增量刷新不是万能的——如果物化视图的 SQL 包含 DISTINCT、UNION、MINUS 等操作,很多数据库不支持快速刷新。这时候要么改 SQL,要么接受全量刷新。

2.3 存储开销分析:这笔买卖划算吗?

物化视图的存储开销,说白了就是「多存一份数据要花多少钱」。我一般从三个维度来评估:

维度 说明 典型场景
原始数据量 基表的总行数 × 行宽度 1TB 的原始表,物化视图可能占 200GB
聚合压缩比 聚合后的行数 / 原始行数 按天聚合,压缩比可达 1:1000
索引开销 物化视图上建的索引占用的空间 3-5 个索引,可能增加 30%-50% 的存储
日志开销 物化视图日志占用的空间 通常为基表的 5%-10%

你想想看,一个按天聚合的销售报表,原始表可能有 10 亿行,但物化视图只存 365 行。这种场景下,存储开销几乎可以忽略不计。但如果是按用户 ID 聚合,每个用户一条记录,那物化视图的大小可能和基表差不多。

存储开销评估公式(我常用的):

物化视图存储 = 数据行数 × 行宽度 + 索引大小 + 物化视图日志大小

其中:
- 数据行数 ≈ 基表行数 / 聚合粒度
- 索引大小 ≈ 数据大小的 0.3~0.5 倍(取决于索引数量)
- 日志大小 ≈ 基表每日变更量 × 保留天数

2.4 三种刷新策略的对比

在实际项目中,我一般根据业务容忍度来选择刷新策略。下面这张表是我自己整理的,分享给你:

策略 数据延迟 刷新耗时 锁表影响 适用场景
全量刷新 取决于刷新周期 高(全表扫描+写入) 高(刷新期间不可读) 小表、低频刷新、T+1 报表
增量刷新 近实时(分钟级) 低(只处理变更) 低(行级锁) 大表、高频刷新、实时看板
ON COMMIT 刷新 实时(事务提交时) 极低(但影响 DML 性能) 无(但事务变慢) 小表、强一致性要求

注意:ON COMMIT 刷新听起来很美好,但我建议你慎用。我曾经在一个高并发订单系统上试过,每次插入一条订单都要等物化视图刷新完才能提交,结果 TPS 直接掉了 70%。后来改成 ON DEMAND 增量刷新,每 5 分钟刷一次,性能问题才解决。

2.5 物化视图存储与刷新流程图

下面这张图是我画的物化视图核心流程,帮你把存储和刷新的关系理清楚:

物化视图存储与刷新流程 物化视图存储与刷新核心流程 基表(Base Table) 物化视图日志(MV Log) 全量刷新(Complete) 增量刷新(Fast) 物化视图(物理存储) 全量扫描 记录变更 读取变更 覆盖写入 合并写入 说明:全量刷新直接读取基表,增量刷新通过日志获取变更,最终都写入物化视图的物理存储

2.6 存储开销的实战评估方法

我在做存储规划时,一般会先跑一个预估查询,看看物化视图大概会占多少空间:

-- 预估物化视图的行数和大小(以 PostgreSQL 为例)
SELECT 
  COUNT(*) as estimated_rows,
  pg_size_pretty(COUNT(*) * 200) as estimated_size  -- 假设每行 200 字节
FROM (
  -- 这里放物化视图的定义 SQL
  SELECT product_id, sale_date, SUM(amount) as total_amount
  FROM sales
  GROUP BY product_id, sale_date
) mv;

我的经验法则:

  • 如果物化视图大小 < 基表的 10%,放心用,存储成本很低
  • 如果物化视图大小在 10%~50% 之间,评估一下查询加速带来的收益是否值得
  • 如果物化视图大小 > 基表的 50%,建议重新审视聚合粒度,或者考虑用其他方案

嗯,关于物化视图的存储原理,核心就是这三件事:数据怎么存、怎么刷新、要花多少存储。搞清楚了这些,后面聊查询加速和优化策略时,你就能理解为什么有些场景下物化视图能带来几十倍的性能提升,而有些场景却适得其反。