32.2 物化视图
一句话
物化视图就是把查询结果提前算好存下来:读变快、写变贵、一致性变弱;关键决策只有两个——刷新方式(全量 / 增量 / CDC)与 陈旧窗口(能容忍多久)。
三种刷新方式
| 方式 | 做法 | 延迟 | 成本 |
|---|---|---|---|
| 全量重建 | 定时 TRUNCATE + INSERT SELECT | 分钟–小时 | 高(每次全表扫) |
| 增量刷新 | 按时间/版本增量聚合 | 秒–分钟 | 中 |
| CDC/事件驱动 | 订阅变更即时更新视图 | 毫秒–秒 | 低(但需要幂等) |
本机实测:构建 20 万事件的视图耗时
| 操作 | 耗时 |
|---|---|
| 全量构建视图(20 万事件折叠) | 约 1.6ms(纯内存模型,仅示意量级) |
| 增量查询(视图点查) | 0.0004ms |
| 全量扫日志查询 | 5.1ms |
真实系统里视图构建会是分钟级(磁盘 IO + 索引维护),但比例关系不变:构建一次、查询万次,才值得物化。
代码:幂等的增量刷新
-- 增量刷新:只处理“上次位点之后”的变更,可重复执行
INSERT INTO daily_sales(dt, amount, cnt)
SELECT date(created_at), sum(amount), count(*)
FROM orders
WHERE id > (SELECT last_id FROM refresh_state WHERE name='daily_sales')
AND status = 'paid'
GROUP BY date(created_at)
ON CONFLICT (dt) DO UPDATE
SET amount = daily_sales.amount + EXCLUDED.amount,
cnt = daily_sales.cnt + EXCLUDED.cnt;
UPDATE refresh_state SET last_id = (SELECT max(id) FROM orders) WHERE name='daily_sales';
注意:“更新位点”与“写入视图”必须同一事务,否则崩溃会漏数据或重复累加。
实测:视图的 “陈旧窗口” 怎么量
用与 CQRS 同一组数据(模拟投影滞后),视图相对写侧的滞后:
| 指标 | 滞后条数 | 换算时间(1000 条/秒) |
|---|---|---|
| P50 | 117 | 0.12s |
| P99 | 211 | 0.21s |
| 最大 | 256 | 0.26s |
“视图多久更新一次” 不等于 “用户多久看到”:定时刷新 1 分钟,实际陈旧窗口是 1 分钟 + 构建耗时;CDC 刷新则取决于消费延迟。对外必须报 “数据截至 HH:MM”,而不是假装实时。
反模式
| 反模式 | 后果 |
|---|---|
| 物化视图当 “强一致读” 用 | 业务看到旧数据,投诉 “刚改的没生效” |
| 全量重建不锁/不切换 | 重建期间查询结果为空或不一致 |
| 增量刷新不做幂等 | 重跑一次,指标翻倍 |
| 视图里放明细(等价于复制表) | 存储翻倍,刷新成本爆炸 |
| 视图不记录刷新位点 | 崩溃后不知道从哪继续 |
| 多个视图口径不一致 | 报表与页面数字对不上,互相甩锅 |
| 刷新周期按“整点”设计却不看构建耗时 | 整点后 3 分钟仍是旧数据,用户以为出错 | | 视图无 “最后更新时间” 字段 | 无法判断数据新旧,排障靠猜 |
三条铁律
🔑 物化 = 用存储与一致性换查询速度;先算 “查询频率 × 单次成本” 是否值得。
🔑 刷新必须幂等 + 记位点,否则重跑即错。
🔑 视图口径要有唯一 owner,多视图必须共享同一套定义。
自检
Q1 物化视图与索引有什么本质区别?
▸ 答案:索引用额外空间换单次查询的 IO,但结果仍由原表数据决定(强一致);物化视图存的是预计算的结果,本身是一份冗余数据,所以有 “陈旧” 概念。前者解决 “定位慢”,后者解决 “聚合慢”。
Q2 为什么 “全量重建” 通常需要 “影子表 + 原子切换”?
▸ 答案:因为重建过程要 TRUNCATE 或长时间写入,期间视图不可用/不完整。用影子表构建完再 RENAME/SWAP,查询侧要么看到旧视图、要么看到新视图,不会看到 “半成品”。
Q3 增量刷新最容易犯的错是什么?
▸ 答案:位点与数据不一致:先更新位点后写数据 → 崩溃漏数据;先写数据后更新位点 → 重跑重复累加。正确做法是同一事务,或用 “幂等 upsert + 可重复计算” 的设计(如按天覆盖而不是累加)。
Q4 什么时候该用 CDC 而不是定时刷新?
▸ 答案:当 “陈旧窗口” 要求秒级、且数据量大到全量刷新不划算时。CDC 的代价是:需要处理乱序、重复、schema 变更,还要有 “全量重建” 的兜底能力(CDC 链路坏掉时)。
Q5 物化视图的 “陈旧” 怎么对用户表达?
▸ 答案:三种:显示更新时间(“数据截至 10:05”)、分级展示(列表用视图,详情走实时)、交互兜底(用户点 “刷新” 触发实时查询)。最糟的是假装实时,让用户以为看到的是最新。
Q6 报表与页面数字对不上,最先查什么?
▸ 答案:先查 口径与位点:两个视图是否按同一过滤条件(如是否含退款)、刷新位点是否不同、是否一个有去重一个没有。多数 “数据不一致” 不是 bug,而是口径不同却没人写成文档。
Q7 定时刷新与 CDC 刷新,成本差在哪?
▸ 答案:定时刷新是周期性全量/增量扫描,成本与 “数据量 × 频率” 成正比,且空闲期也在扫;CDC 是按变更触发,成本与 “变更量” 成正比,空闲时几乎为零。数据量大、变更稀疏时 CDC 更省;变更极频繁时 CDC 的乱序处理与幂等成本会上升。
故障复盘(典型模式,脱敏)
| 阶段 | 现象 | 判断 | 动作 |
|---|---|---|---|
| 周一 | 大屏 GMV 与财务报表差 3% | 两套视图口径不同(含退款 vs 不含) | 对齐口径 |
| T+2h | 对齐后仍差 0.3% | 刷新位点不同 | 统一刷新时间 |
| T+1d | 差异归零,但大屏延迟 2 小时 | 全量重建耗时 | 改增量刷新 |
| 复盘 | 视图无 owner、无口径文档 | 治理缺失 | 统一口径定义 + 更新时间展示 |
根因:多视图各自定义 “口径”,且刷新方式不同。代价:管理层看到的数字不可信。根治:口径唯一 owner + 视图显示 “截至时间” + 增量刷新。