物化视图默认是静态快照,不自动刷新,需手动或借助TimescaleDB连续聚合、HLL扩展等方案实现近实时聚合与去重。

物化视图不是“开箱即用的实时方案”,它默认是静态快照,不自动刷新。想靠 MATERIALIZED VIEW 做近实时聚合,必须主动控制刷新时机和方式,否则数据延迟可能达数小时甚至更久。
为什么直接 CREATE MATERIALIZED VIEW 不够用
PostgreSQL 的原生物化视图(MATERIALIZED VIEW)本质是一张只读的物理表,创建后内容固定,除非显式执行 REFRESH MATERIALIZED VIEW。它不监听源表变更,也不支持增量更新——每次刷新都重跑整个 SELECT 查询,扫全量源数据。这对高频写入、大表聚合场景非常危险:
- 刷新期间会持
SHARE UPDATE EXCLUSIVE锁,阻塞后续刷新和部分 DML,容易堆积 - 若源表有 10 亿行,每分钟刷新一次,等于每分钟都在做全表 GROUP BY,CPU 和 I/O 压力爆炸
- 无法保证“最近 1 分钟”这类滚动窗口的语义,因为刷新是离散动作,不是连续流
手动定时刷新:简单但需权衡延迟与负载
适合业务能容忍秒级到分钟级延迟、且聚合逻辑稳定、源数据写入频率中等(如每秒新增 ≤ 100 行)的场景。关键在控制刷新节奏和避免锁冲突:
- 用
pg_cron扩展实现定时任务,例如每 30 秒执行一次REFRESH MATERIALIZED VIEW CONCURRENTLY mv_minutely_orders - 务必加
CONCURRENTLY参数,它允许刷新时不阻塞对物化视图的 SELECT 查询(但首次创建或无唯一索引时会失败) - 物化视图定义里避免跨天/跨月的
date_trunc('day', created_at)分组,优先用date_trunc('minute', created_at)或带时间戳范围过滤(如WHERE created_at > NOW() - INTERVAL '5 minutes'),缩小扫描范围 - 为物化视图的分组字段(如
channel,device_type)建唯一索引,否则CONCURRENTLY刷新会报错
增量式替代方案:TimescaleDB 连续聚合更省心
如果底层已用 TimescaleDB(基于 PostgreSQL 的时序扩展),直接用 CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous)。它把“刷新”变成了后台常驻进程:
- 自动跟踪超表(hypertable)的新增 chunk,只聚合新数据,不重算历史
- 支持
refresh_lag参数(如'10 seconds'),保证物化结果最多滞后 10 秒,比手动定时更稳 - 可对连续聚合再建二级聚合(如从每分钟汇总到每小时),形成聚合链
- 即使删除原始超表,连续聚合里的下采样数据仍保留,适合长期归档分析
注意:连续聚合要求源数据有明确时间列且已建为超表;普通 PostgreSQL 实例无法直接使用该功能,需额外部署 TimescaleDB。
别忽略 HLL:千万级去重指标的轻量解法
当聚合目标是“独立用户数(UV)”“独立设备数”这类基数统计,count(distinct user_id) 在大数据量下极慢。此时应放弃物化视图,改用 postgresql-hll 扩展:
- 在写入端就调用
hll_add(hll_hash_bigint(user_id))把 ID 映射进 HLL 结构,存入一个hll类型字段 - 查询时用
hll_cardinality()直接算近似唯一值,误差率默认 - 支持
hll_union_agg()跨时间分组合并,比如按小时 union 后再算全天 UV,完全规避了物化视图刷新的复杂度
真正难的不是“怎么建物化视图”,而是判断哪些聚合该预计算、哪些该用概率数据结构、哪些该交给专用分析引擎(如 Doris)。物化视图只是工具之一,选错粒度或刷新策略,反而会让系统更脆弱。

















