SQL Server无原生物化视图,需用触发器+inserted/deleted表同步聚合表;UPDATE须双路处理增减量;并发需MERGE原子操作防竞态;复杂聚合宜用索引视图或异步方案。

SQL Server 不存在“物化视图”这个原生对象,所谓“维护物化视图”,实际是用触发器同步更新一张**业务自建的聚合表**(比如 user_summary),它存着预计算结果。直接套用 PostgreSQL 的 REFRESH MATERIALIZED VIEW 思路会失败,因为 SQL Server 不支持该语法,也没有 CONCURRENTLY 机制。
SQL Server 触发器必须用 inserted/deleted 表做集合操作
SQL Server 的 AFTER INSERT/UPDATE/DELETE 触发器不提供单行上下文变量(如 OLD/NEW),而是通过两个内存临时表获取变更数据:inserted 和 deleted。它们不是物理表,但可参与 JOIN、GROUP BY、MERGE 等集合运算。
常见错误包括:
- 在
INSERT触发器里查deleted(它为空) - 在
DELETE触发器里查inserted(它为空) - 把
inserted.id当单值用,写成SELECT @id = id FROM inserted,导致只处理第一行 - 忽略多行场景:一个
UPDATE影响 500 行,inserted和deleted各含 500 行,必须用聚合+分组,不能标量赋值
UPDATE 场景必须同时读 inserted 和 deleted 并区分增减
聚合类同步(如订单数、金额总和)中,UPDATE 是最易出错的环节:它既不是纯新增,也不是纯删除,而是旧值退出、新值进入。必须用两段逻辑分别处理——对旧值做扣减,对新值做累加。
推荐做法是用 UNION ALL 合并两部分变更:
SELECT customer_id, COUNT(*) AS delta FROM inserted GROUP BY customer_id UNION ALL SELECT customer_id, -COUNT(*) AS delta FROM deleted GROUP BY customer_id
然后用 MERGE 或分步 UPDATE+INSERT 更新目标表。不能只靠 inserted 做“覆盖式更新”,否则删掉一行再插入同用户另一行,订单数就少算一次。
并发写入时必须防竞态,避免 SUM 值错乱
多个事务同时更新同一用户的订单,若都读取当前 order_count 再加 1,就会发生丢失更新。SQL Server 不会自动帮你加锁保护聚合字段。
安全做法有两类:
- 用
MERGE ... WHEN MATCHED THEN UPDATE SET cnt = cnt + src.delta,依赖底层行锁和原子性 - 对目标表关键字段(如
user_id)加唯一索引,并在MERGE中使用WHEN NOT MATCHED THEN INSERT处理首次写入 - 避免在触发器里用
SELECT ... INTO @var+UPDATE模式,这在高并发下极易错乱
注意:NOLOCK 在这里绝对不能用——读到脏数据会导致聚合值永久偏差。
别指望触发器能处理 JOIN 聚合或跨表逻辑
如果聚合依赖多张基表(例如订单 + 用户 + 商品分类),SQL Server 触发器无法在单个表变更时可靠推导出所有影响路径。比如用户表改了昵称,不影响订单总数,但可能影响按用户分组的销售额统计——这种逻辑不该塞进订单表的触发器里。
更可行的方案是:
- 把聚合逻辑收口到存储过程中,由应用或定时任务调用
- 用
INDEXED VIEW(即带聚集索引的视图)替代手工聚合表,但要求严格满足SCHEMABINDING、确定性函数等条件 - 接受最终一致性:用消息队列异步刷新,而不是强依赖触发器实时同步
真正麻烦的从来不是怎么写触发器,而是当 orders 表每秒写入 30+ 行时,每个触发器都在执行 GROUP BY customer_id 和 MERGE,CPU 和日志 I/O 会迅速成为瓶颈。这时候得问一句:这个“实时性”是否真有必要?










