BEFORE DELETE触发器必须用SIGNAL SQLSTATE '45000'才能阻止删除,仅执行SELECT、INSERT等语句无效;校验须基于OLD字段直判,禁用子查询和函数;TRUNCATE/DROP不触发该触发器;不可依赖USER()等会话变量做权限控制。

BEFORE DELETE 触发器必须用 SIGNAL 才能真正阻止删除
不写 SIGNAL SQLSTATE '45000',哪怕你写 SELECT 1/0 或 INSERT INTO audit_log,DELETE 操作照常执行。MySQL 的 BEFORE DELETE 只是“在删之前跑一段代码”,不是“删之前问一句能不能删”。只有 SIGNAL 会中断当前语句并回滚事务,客户端收到明确报错(如 ERROR 1644 (45000): 禁止删除系统账户),数据毫发无损。
校验必须基于 OLD 字段直判,禁用子查询和函数
判断是否为管理员账号,只能靠 OLD.role、OLD.is_admin、OLD.username 这类字段直接比对。任何查表、调函数、用 IN (SELECT ...) 都会引入死锁风险或性能抖动,且 MySQL 在触发器中禁止某些只读子查询(尤其涉及同一张表时)。
- ✅ 正确:
IF OLD.role = 'admin' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '管理员账号禁止删除'; END IF; - ✅ 正确:
IF OLD.id IN (1, 2, 999) THEN SIGNAL ... END IF; - ❌ 错误:
IF (SELECT COUNT(*) FROM roles WHERE user_id = OLD.id AND level = 'super') > 0 THEN ... - ❌ 错误:
IF is_admin_user(OLD.id) THEN ...(自定义函数若含 SELECT,同样被禁)
TRUNCATE 和 DROP 完全不触发该触发器
这是最常被忽略的盲区:TRUNCATE TABLE users 和 DROP TABLE users 是 DDL 操作,MySQL 触发器机制根本不监听。哪怕你建了十个 BEFORE DELETE 触发器,对这两个命令也毫无反应。它们绕过所有 DML 触发器、不走事务、不记 binlog 的 DELETE 事件。
兜底手段只有:
REVOKE DROP ON prod_db.users FROM 'app_user'@'%';REVOKE DROP ON prod_db.* FROM 'dev_user'@'%';- 对高危账号启用
read_only=ON+super_read_only=ON(从库场景)
触发器里不能依赖会话变量做权限判断
有人想加一层“仅限 DBA 删除”,于是写 IF USER() != 'dba@localhost' THEN SIGNAL ...。这看似合理,但实际危险:应用连接池复用连接,USER() 返回的是连接建立时的认证用户,不是发起 SQL 的业务主体;更糟的是,如果中间件或代理层统一用一个高权账号连库,这个判断就完全失效。
真正可控的权限控制点在连接层或代理层(如 ProxySQL、ShardingSphere),而不是触发器内部。触发器只适合做数据级硬规则,比如“role = 'admin' 就不准删”,别让它承担访问控制职责。


















