before insert触发器中必须用signal抛异常才能阻断插入,其他方式如rollback、select count()或仅if判断均无效;需配合delimiter、sqlstate'45000'及明确message_text;count()易致死锁与mvcc偏差,单用户限制须带where且字段类型匹配;高并发下应结合计数器表+分区归档实现稳定行数控制。

BEFORE INSERT 中必须用 SIGNAL 抛异常才能真正阻断插入
MySQL 5.7 支持 SIGNAL,这是唯一能在 BEFORE INSERT 触发器中干净中断插入操作的方式。其他写法——比如只写 IF row_count >= 1000 THEN 却不 SIGNAL,或者试图 ROLLBACK、DELETE、CALL 存储过程——都会让插入继续执行,触发器形同虚设。
必须搭配 DELIMITER 修改分隔符,否则语句会在第一个分号处截断;SQLSTATE '45000' 是安全通用的自定义错误码,MESSAGE_TEXT 要写明具体限制(如 'logs table reached max capacity (10000)')。
-
SIGNAL不依赖锁、不修改数据、不触发额外事务,客户端收到的就是标准 SQL 错误,可直接捕获处理 - 不能把
SIGNAL写在AFTER INSERT里——插入已完成,拦不住 -
INSERT IGNORE和REPLACE INTO可能绕过触发器逻辑,生产环境应禁用这两类语句或在触发器内显式校验NEW值是否已存在
SELECT COUNT(*) 在触发器里会引发死锁和 MVCC 偏差
在 BEFORE INSERT 触发器中执行 SELECT COUNT(*) FROM my_table,InnoDB 会尝试加一致性读锁,而当前插入事务已持有行级锁,极易触发死锁或报错:Can't update table 'my_table' in stored function/trigger。
即使没报错,也可能因 MVCC 快照看到“不含当前待插入行”的旧计数,导致实际插入后超限却未拦截。
- 别信“加
LOCK IN SHARE MODE就能查”的说法——触发器中禁止显式加锁 -
SELECT COUNT(*) INTO @cnt FROM my_table只是把错误延迟到运行时,不解决根本冲突 - 若表很大,每次插入都全表扫描
COUNT(*),性能会随数据增长急剧恶化
单用户行数限制要带 WHERE 条件,且字段类型必须匹配
限制“单个用户”最大记录数时,关键不是查全表,而是查该用户已有多少条:SELECT COUNT(*) FROM orders WHERE user_id = NEW.user_id。漏掉 WHERE 或字段名/类型不一致(比如 NEW.uid 是 INT,却拿字符串去比),都会导致误判。
如果表有软删除字段(如 is_deleted = 0),记得在 WHERE 中加上,否则已“删”记录仍被计入总数。
- 必须用
BEFORE INSERT,否则插入已完成,再查再拦没意义 -
UPDATE或DELETE触发器里加同类限制毫无必要,甚至有害——改一条记录不该触发“超限报错” - 若业务允许
UPDATE修改用户归属(如把user_id从 A 改成 B),单触发器无法原子协调两边计数,应在应用层先校验目标用户容量
真正稳定的行数控制不能只靠触发器
触发器解决不了竞争条件:两个会话同时查到 99 行,都允许插入第 100 行,结果变成 101 行。这不是理论漏洞,是高并发下真实发生的偏差。
生产环境更可靠的路径是:维护一张独立的计数器表(如 table_row_count),用 AFTER INSERT 和 AFTER DELETE 触发器原子更新它,BEFORE INSERT 只查这张小表——避免全表扫描,也规避了锁冲突。
如果表持续增长,硬限制不如分区 + 定时归档:按时间切分日志表,保留最近 N 个月,老数据移出或压缩,比死守“最多 10000 行”更可持续。











