为什么SQL触发器中不能使用所有聚合函数来维护数据库事务和一致性?
- 内容介绍
- 文章标签
- 相关推荐
本文共计806个文字,预计阅读时间需要4分钟。
简化版开头内容,无需试图解答案,不要数落,不超过100字,直接输出结果:
触发器是行级同步执行,聚合函数却是表级扫描
每次 INSERT/UPDATE/DELETE 一行,触发器就跑一次。如果里面写 SUM()、COUNT() 或子查询套聚合,等于每行都触发一次全表扫描——比如在订单表的 AFTER INSERT 里查 SELECT SUM(amount) FROM orders WHERE user_id = NEW.user_id,1000 个订单插入就要扫 1000 次用户订单记录。
- MySQL 的 InnoDB 在这种场景下极易出现
Lock wait timeout exceeded或Deadlock found when trying to get lock - PostgreSQL 会卡在
SHARE ROW EXCLUSIVE锁上,尤其当聚合查询没走索引时 - 即使加了索引,高并发下多个触发器同时读同一张汇总表,也会争抢
consistent read版本,拖慢主事务
NEW 和 OLD 是快照,聚合结果却要实时算
触发器能直接用 NEW.amount、OLD.status,因为它们是内存里的变更前/后值,零开销。但 SELECT AVG(x) FROM t WHERE y = NEW.y 是实打实去磁盘捞数据,还可能被其他事务正在改的行阻塞。
-
BEFORE INSERT中没有OLD,AFTER DELETE中没有NEW,强行引用报Unknown column 'OLD.xxx' in 'field list' - 想“实时更新用户总消费”,别查表,直接用
NEW.user_total := OLD.user_total + NEW.amount(仅限BEFORE UPDATE) - 真要依赖外部状态,查询必须命中主键或唯一索引,且加
LIMIT 1;否则宁可抛异常,也别让触发器变慢查询入口
事务上下文共享导致复制和一致性风险
触发器代码运行在主 SQL 的同一事务里,所有修改原子提交或回滚。但聚合函数常依赖非确定性逻辑(如 NOW()、RAND()),在 MySQL 主从复制中若用语句级(SBR)模式,从库重放时结果不一致,轻则数据偏差,重则复制中断。
- 自定义函数必须显式声明
DETERMINISTIC或READS SQL DATA,否则CREATE FUNCTION都会被拒 - 用
binlog_format = ROW可绕过部分问题,但 binlog 体积暴涨,网络同步压力翻倍 - PostgreSQL 的触发器函数若调
pg_notify()是安全的,但一旦嵌套PERFORM SELECT ... INTO,就可能触发40P01 deadlock_detected
最常被忽略的一点:触发器里根本没法做“分组聚合”。你不能在 BEFORE INSERT 里写 SELECT dept_id, AVG(salary) FROM emp GROUP BY dept_id 然后拿结果去比对——它要么报错“subquery must return only one row”,要么锁住整个 emp 表。需要聚合的地方,提前算好存字段,或者扔给应用层异步补全。
本文共计806个文字,预计阅读时间需要4分钟。
简化版开头内容,无需试图解答案,不要数落,不超过100字,直接输出结果:
触发器是行级同步执行,聚合函数却是表级扫描
每次 INSERT/UPDATE/DELETE 一行,触发器就跑一次。如果里面写 SUM()、COUNT() 或子查询套聚合,等于每行都触发一次全表扫描——比如在订单表的 AFTER INSERT 里查 SELECT SUM(amount) FROM orders WHERE user_id = NEW.user_id,1000 个订单插入就要扫 1000 次用户订单记录。
- MySQL 的 InnoDB 在这种场景下极易出现
Lock wait timeout exceeded或Deadlock found when trying to get lock - PostgreSQL 会卡在
SHARE ROW EXCLUSIVE锁上,尤其当聚合查询没走索引时 - 即使加了索引,高并发下多个触发器同时读同一张汇总表,也会争抢
consistent read版本,拖慢主事务
NEW 和 OLD 是快照,聚合结果却要实时算
触发器能直接用 NEW.amount、OLD.status,因为它们是内存里的变更前/后值,零开销。但 SELECT AVG(x) FROM t WHERE y = NEW.y 是实打实去磁盘捞数据,还可能被其他事务正在改的行阻塞。
-
BEFORE INSERT中没有OLD,AFTER DELETE中没有NEW,强行引用报Unknown column 'OLD.xxx' in 'field list' - 想“实时更新用户总消费”,别查表,直接用
NEW.user_total := OLD.user_total + NEW.amount(仅限BEFORE UPDATE) - 真要依赖外部状态,查询必须命中主键或唯一索引,且加
LIMIT 1;否则宁可抛异常,也别让触发器变慢查询入口
事务上下文共享导致复制和一致性风险
触发器代码运行在主 SQL 的同一事务里,所有修改原子提交或回滚。但聚合函数常依赖非确定性逻辑(如 NOW()、RAND()),在 MySQL 主从复制中若用语句级(SBR)模式,从库重放时结果不一致,轻则数据偏差,重则复制中断。
- 自定义函数必须显式声明
DETERMINISTIC或READS SQL DATA,否则CREATE FUNCTION都会被拒 - 用
binlog_format = ROW可绕过部分问题,但 binlog 体积暴涨,网络同步压力翻倍 - PostgreSQL 的触发器函数若调
pg_notify()是安全的,但一旦嵌套PERFORM SELECT ... INTO,就可能触发40P01 deadlock_detected
最常被忽略的一点:触发器里根本没法做“分组聚合”。你不能在 BEFORE INSERT 里写 SELECT dept_id, AVG(salary) FROM emp GROUP BY dept_id 然后拿结果去比对——它要么报错“subquery must return only one row”,要么锁住整个 emp 表。需要聚合的地方,提前算好存字段,或者扔给应用层异步补全。

