BEFORE DELETE触发器必须用SIGNAL SQLSTATE '45000'显式报错才能中止删除,其他写法无效;仅能基于OLD字段直判,禁用子查询和函数;TRUNCATE/DROP不触发触发器,权限控制才是兜底。

MySQL 的 BEFORE DELETE 触发器不会自动阻止删除,必须用 SIGNAL SQLSTATE '45000' 主动报错才能真正中止操作;其他写法(如 SELECT 1/0、INSERT INTO log、SET @var = 'x')全无效——删照样执行。
BEFORE DELETE 里必须用 SIGNAL,否则删操作照常提交
触发器只是“在删之前跑一段代码”,不显式中断,MySQL 就默认放行。常见错误包括:
-
SELECT 1/0:语法合法,但只抛错不阻断,事务继续提交 -
INSERT INTO audit_log VALUES (OLD.id, 'delete_blocked'):日志写了,数据也删了 -
RAISERROR或RAISE EXCEPTION:MySQL 不识别,直接语法报错,且可能已删部分行
唯一可靠写法是:
IF OLD.role IN ('admin', 'system') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '禁止删除系统账户';
END IF;
SQLSTATE '45000' 是 MySQL 5.5+ 原生支持的通用自定义错误码,客户端收到 ERROR 1644 (45000) 后事务自动回滚,本行不删。
校验只能用 OLD 字段直判,禁用子查询和函数
OLD 是触发器里唯一可用的待删数据快照,所有判断必须基于它字段直判:
- ✅ 正确:
IF OLD.is_protected = 1 THEN SIGNAL ... END IF; - ✅ 正确:
IF OLD.id IN (1, 100, 999) THEN SIGNAL ... END IF; - ❌ 禁止:
SELECT COUNT(*) FROM orders WHERE user_id = OLD.id—— 易死锁、拖慢主操作 - ❌ 禁止:调用自定义函数(哪怕只读),函数内若含
SELECT或访问其他表,风险同上
软删除场景需额外判断:IF OLD.deleted_at IS NOT NULL THEN SIGNAL ... END IF;,防止硬删已软删记录。
TRUNCATE 和 DROP 完全不触发触发器,权限控制才是兜底
这是最容易被忽略的盲区:TRUNCATE TABLE users 和 DROP TABLE users 是 DDL 操作,不走任何行级触发器,也不记 binlog 的 DELETE 事件,不可回滚。
- 你在
users表上建十个BEFORE DELETE触发器,对TRUNCATE或DROP零作用 - 真正可行的兜底只有权限控制:
REVOKE DELETE ON prod_db.users FROM 'dev_user'@'%'; - 高危操作必须走工单审批,DBA 账号启用二次认证,禁用
sql_log_bin = 0等绕过手段
触发器只是最后一道脆弱防线,它防不住权限足够高的账号、防不住备份恢复前的窗口期,也防不住应用层直连后绕过触发器逻辑。
真正可落地的防护组合不是靠一个触发器
单靠触发器拦误删,线上几乎必然失效。更实际的做法是分层设防:
- 事前:全局启用
sql_safe_updates=ON,让DELETE FROM users;直接报错 - 事中:所有变更必须带影响行数预估,超阈值(如 > 100 行)自动拒掉请求
- 事后:开启通用查询日志写入表,定时扫描
mysql.general_log匹配DROP TABLE、TRUNCATE TABLE、无WHERE的DELETE,匹配即告警并 kill 连接
复杂点在于:你写的触发器再严谨,也挡不住一个有 DROP 权限的人执行 TRUNCATE,而这种操作连日志都难追溯——所以权限隔离永远比触发器优先级更高。

















