应选用触发器+汇总表而非物化视图,因其支持增量更新、避免全量重算与唯一索引依赖,适合高频写入与低延迟场景;但聚合维度多、需窗口函数或高TPS时应回归物化视图。

PostgreSQL本身不支持物化视图的“自动刷新”,但你可以用触发器维护一个功能等价的汇总表,效果比 REFRESH MATERIALIZED VIEW 更可控、更轻量,也规避了并发刷新对唯一索引的强制依赖。
为什么不用物化视图而选触发器+汇总表?
物化视图的 CONCURRENTLY 刷新要求必须存在 UNIQUE 索引,且刷新过程仍需全量重算、临时空间和额外 I/O;而触发器驱动的汇总表能实现真正的增量更新,只改一行、不锁全表、不依赖索引结构。尤其适合高频写入、低延迟要求的场景(比如实时订单统计、用户行为计数)。
- 物化视图刷新是“快照替换”,哪怕只改 1 行,也要重跑整个
SELECT查询 - 触发器汇总表是“差分更新”,
INSERT时加 1,DELETE时减 1,UPDATE时先减后加 - 物化视图无法响应部分列变更(如只更新
status但不影响聚合逻辑),而触发器可精确判断是否需要更新汇总值
如何用触发器实现带条件的增量汇总?
关键在于把业务逻辑下沉到触发器函数里,而不是依赖物化视图的静态定义。例如:按状态统计订单数,只在 status IN ('paid', 'shipped') 时计入。
- 创建汇总表时显式定义字段,不依赖
SELECT *:CREATE TABLE order_summary (status TEXT PRIMARY KEY, count BIGINT NOT NULL DEFAULT 0) - 触发器函数中用
NEW.status和OLD.status判断变化方向:IF NEW.status IN ('paid','shipped') AND OLD.status NOT IN ('paid','shipped')则 +1 - 务必使用
FOR EACH ROW(不是STATEMENT),否则无法感知单行状态迁移 - 避免在触发器里执行复杂 JOIN 或子查询——这会拖慢主事务,应只做原子数值增减
触发器维护汇总表的典型陷阱
最常踩的坑不是语法错,而是语义错和并发错。
-
AFTER INSERT OR UPDATE OR DELETE是必要但不充分的——如果源表允许NULL值参与聚合(如SUM(amount)),触发器必须显式处理NEW.amount IS NULL场景,否则汇总值会偏高 - 多个触发器叠加时,顺序很重要:
CREATE TRIGGER ... BEFORE INSERT可用于预校验,AFTER才该更新汇总;混用BEFORE和AFTER触发器容易导致重复计算 - 没有处理
TRUNCATE:触发器对TRUNCATE完全无感,必须额外建EVENT TRIGGER捕获sql_drop事件,或约定禁止直接TRUNCATE - 初始数据不同步:新建汇总表后,必须用
INSERT INTO ... SELECT一次性初始化,不能依赖后续 DML 自动补全
什么时候该放弃触发器、回归物化视图?
当汇总逻辑开始出现以下任一特征时,说明已超出触发器友好边界,该换方案了:
- 聚合字段超过 3 个且组合维度多(如
GROUP BY region, product_type, quarter),触发器分支爆炸,维护成本陡增 - 需要窗口函数(
ROW_NUMBER(),RANK())或递归 CTE,这些无法在行级触发器中表达 - 源表写入 TPS 超过 500/s,每个 INSERT 都触发 PL/pgSQL 函数开销明显,此时
pg_cron定时全量刷新物化视图反而更稳 - 业务接受分钟级延迟,且查询模式固定——这时物化视图 +
CONCURRENTLY的语义清晰性远胜一堆触发器逻辑
触发器不是银弹,它把刷新时机从“数据库层”挪到了“应用逻辑层”,换来的是精度和灵活性,代价是必须亲手扛起一致性责任。上线前务必在事务嵌套、批量导入、异常中断等边界场景做实测。










