必须写在子表上,因父表无子记录、增删改均发生在子表,仅监听子表insert/delete/update才能实时感知数量变化;父表加触发器将无法响应子表操作。

触发器该写在父表还是子表上
必须写在子表上。父表本身不存子记录,所有增删改都发生在子表,只有监听子表的 INSERT、DELETE、UPDATE 才能实时感知数量变化。写在父表上等于没监听——父表行没动,触发器根本不会执行。
常见错误是误以为“父表要更新统计字段”,就去给父表加触发器,结果插入子记录时完全没反应。
-
INSERT:子表新增一行 → 父表对应child_count字段 +1 -
DELETE:子表删一行 → 父表对应child_count字段 -1 -
UPDATE:仅当parent_id字段被改(比如子记录换父)才需双向调整:原父ID的计数 -1,新父ID的计数 +1
UPDATE 触发器里怎么安全判断 parent_id 是否真的变了
不能只写 IF NEW.parent_id != OLD.parent_id ——如果 OLD.parent_id 或 NEW.parent_id 是 NULL,这个比较会返回 UNKNOWN,导致条件不成立,漏掉变更。
正确写法必须显式处理 NULL:
IF (OLD.parent_id IS NULL AND NEW.parent_id IS NOT NULL) OR (OLD.parent_id IS NOT NULL AND NEW.parent_id IS NULL) OR (OLD.parent_id != NEW.parent_id) THEN
更简洁的替代是用 IS DISTINCT FROM(PostgreSQL 支持),但 MySQL 不支持,所以老实用三段式判断。
- MySQL 用户必须手写 NULL 安全比较逻辑
- SQLite 支持
IS NOT,可写OLD.parent_id IS NOT NEW.parent_id - 别依赖
COALESCE(OLD.parent_id, 0) != COALESCE(NEW.parent_id, 0)——万一真实 parent_id 就是 0,就误判了
触发器里 UPDATE 父表时为什么不能用子查询算总数
直接写 UPDATE parent SET child_count = (SELECT COUNT(*) FROM child WHERE parent_id = NEW.parent_id) 看似稳妥,实则危险:在高并发下,多个触发器同时执行这个子查询,可能读到未提交的中间状态,导致最终计数错乱(比如两个 INSERT 同时查到旧值 5,都写 6,实际应为 7)。
必须用原子递增/递减,靠数据库行锁保证一致性:
UPDATE parent SET child_count = child_count + 1 WHERE id = NEW.parent_id;
- 增删操作一律用
child_count = child_count ± 1,不查不算 - UPDATE 场景下,先对原
parent_id减 1,再对新parent_id加 1,两步都用原子更新 - 如果父表某条记录被删除,而子表还有残留记录,触发器 UPDATE 会静默失败(影响行为为 0 行),此时需要额外清理机制,不能指望触发器兜底
MySQL 中触发器报错 “Can't update table 'parent' in stored function/trigger” 怎么办
这是 MySQL 的限制:触发器内不能修改当前正在被 DML 操作的表的关联表(哪怕只是父表),如果子表触发器去 UPDATE 父表,且父表恰好也在本次事务中被其他语句访问过(比如 SELECT FOR UPDATE),就可能触发该错误。
真正稳妥的做法不是绕开限制,而是接受它并换思路:
- 确保父表在事务中不被其他语句提前锁定(比如避免在同个事务里先
SELECT ... FOR UPDATE父表) - 把统计逻辑下沉到应用层异步更新(如用消息队列),或改用物化视图(MySQL 8.0+ 可用)
- 如果坚持用触发器,务必在测试中模拟并发 INSERT/UPDATE,观察是否出现死锁或报错——很多线上问题只在压测时暴露
最常被忽略的一点:触发器无法捕获批量操作(如 INSERT INTO child SELECT ...)中部分失败的情况,一旦出错回滚,触发器已执行的父表更新不会自动回滚,必须手动加事务控制或放弃触发器方案。










