物化视图把昂贵的结果冻结下来
视图是一条查询的名字;物化视图则是把这条查询的结果真的写成数据。你用新鲜度换速度,而这场交换只有在动手之前就先定好「读者能容忍多旧」时才成立。 正是这个决定让这套机制有意思:刷新不是可以忘掉的日常维护,它才是让答案保持正确的那件事;而刷新的错误做法会加一把锁,把所有想读你预先算好那份数据的人全部挡住。 存下来的结果,而不是实时的查询 普通视图是一条注册好的查询,每次读都要重算一遍。物化视图把结果落成一张真实的表,你可以给它建索引、直接查它,于是多表 JOIN 和大范围扫描发生在刷新时,而不是每个请求里。在 sqlite 3.53 上,200000 行销售数据分布在 200 个区域、400 天里,按区域汇总做成视图每次读要 15.286 ms,同一份答案预先算好只要 0.039 ms——390 倍。也就是说这份保存下来的答案在约 390 次读取之后就回本,按每秒五十次读取算,不到十秒。 代价并没有消失,只是换了地方。预计算的表只在最后一次刷新的那一刻是正确的,而 PostgreSQL 默认不提供任何增量维护:REFRESH MATERIALIZED VIEW 会完整替换内容、丢弃旧内容,不管实际改了多少,都要把整个结果重建一遍。 刷新时机和刷新方式都由你决定 整表重建很粗暴,所以要选一个能廉价修补的粒度去物化。重建整张区域汇总花了 16.41 ms,改写了全部 200 行,而源头只有两行发生了变化。改成按天粒度物化之后,增量刷新只需要删掉并重算受影响的那一天:2.08 ms,4 行。答案是同一个,工作量却从「表的大小」变成了「变化的大小」。 唯一索引不是形式上的要求,它正是 CONCURRENTLY 能做差分的前提:刷新要把新旧两份结果逐行比对,而只有当存在一个能唯一定位每一行的键时,「哪些行变了」这个问题才有答案;索引里出现重复键,这个问题本身就无解。 CONCURRENTLY 的存在完全是因为那把锁。普通刷新拿的是 ACCESS EXCLUSIVE,它和包括裸 SELECT 在内的所有锁模式都冲突;REFRESH MATERIALIZED VIEW CONCURRENTLY 拿的是 EXCLUSIVE,允许并发读,只挡住写。这个价码是实打实的:视图上至少要有一个只使用列名、且覆盖全部行的唯一索引——不能是表达式索引,也不能带 WHERE 子句;视图必须已经填充过数据;WITH NO DATA 不能和它同时使用;而且同一个视图同一时刻只允许一个刷新在跑。 ...