MySQL触发器维护父子表统计值必须分INSERT/UPDATE/DELETE三类独立处理,混写易致错乱;禁用TRUNCATE;跨维聚合应弃实时改定时任务。

MySQL里用触发器维护父子表统计值必须分三类操作写
直接在一张触发器里处理INSERT/UPDATE/DELETE三种操作,几乎必然导致统计错乱。因为每种操作对父表count、sum等字段的影响逻辑完全不同,混写容易漏判或重复计算。
常见错误现象:AFTER INSERT ON child 触发器里只做UPDATE parent SET total_children = total_children + 1,但没防NEW.parent_id IS NULL;或者UPDATE触发器里只加新值不减旧值,结果越改越大。
- INSERT:只加,但要检查
NEW.parent_id IS NOT NULL且该父记录存在(可用EXISTS (SELECT 1 FROM parent WHERE id = NEW.parent_id)) - UPDATE:必须同时处理
OLD.parent_id和NEW.parent_id——先SET total_children = total_children - 1(若OLD.parent_id非空),再SET total_children = total_children + 1(若NEW.parent_id非空且不等于旧值) - DELETE:只减,同样要判断
OLD.parent_id IS NOT NULL,否则减了个NULL,整行变NULL
PostgreSQL可以用CONCURRENTLY刷新物化视图,但MySQL只能手写聚合表
MySQL根本没有MATERIALIZED VIEW语法,所谓“自动维护统计”本质是建一张普通表+一堆触发器同步增删改。但限制极多:触发器内禁止修改其他表(除非是INSERT ... ON DUPLICATE KEY UPDATE这种原子操作),也不能执行子查询赋值再更新。
典型报错:Can't update table 'parent' in stored function/trigger——这是MySQL硬性限制,不是配置问题。
- 必须用
INSERT INTO parent_summary (parent_id, child_count) VALUES (NEW.parent_id, 1) ON DUPLICATE KEY UPDATE child_count = child_count + 1实现“存在则加,不存在则插” - 目标表
parent_summary必须有唯一键,比如UNIQUE(parent_id),否则ON DUPLICATE KEY找不到冲突目标 - 不能写
SELECT COUNT(*) FROM child WHERE parent_id = NEW.parent_id INTO @cnt再UPDATE,严格模式下直接报错
TRUNCATE不触发任何DML触发器,这点最容易被忽略
业务中如果用TRUNCATE TABLE child清空子表,所有AFTER DELETE触发器完全不执行,父表的统计值会彻底失真,且无任何报错提示。
PostgreSQL里可以建EVENT TRIGGER监听sql_drop事件来兜底,但MySQL不支持EVENT TRIGGER。你只有两个选择:
- 禁用
TRUNCATE权限,在文档中强制要求“清空请用DELETE FROM child”,确保走触发器路径 - 在应用层或定时任务里补救:每次执行
TRUNCATE后,立刻跑一条UPDATE parent SET total_children = (SELECT COUNT(*) FROM child WHERE child.parent_id = parent.id)批量重算
跨表JOIN聚合无法靠单表触发器维护
比如你要统计“每个父节点下,不同状态的子记录数量”,即按parent_id和status两维聚合,触发器根本没法可靠维护。
原因很简单:触发器只响应child表的变更,但它不知道parent表的name或category有没有被改过——而这些字段可能出现在你的最终报表SQL里。
- 如果业务允许,把聚合维度收拢到子表里(比如冗余
parent_category字段),再用单表触发器维护 - 更稳妥的做法是放弃实时性,改用定时任务(如MySQL
EVENT)每5分钟跑一次REPLACE INTO parent_agg SELECT ... FROM parent JOIN child GROUP BY ... - 不要试图在触发器里查
parent表做关联更新——高并发下锁等待严重,且MySQL禁止触发器中读写同表

















