真正可靠的防重组合是索引+应用层重试+触发器收尾:底层用带业务语义的unique索引(如(user_id, date(created_at)))保障并发安全,中间层由应用捕获唯一冲突错误并退避重试,上层触发器仅执行轻量副作用操作。

靠触发器单点拦截重复数据写入,基本不可靠——它既不能替代唯一约束,又容易在高并发下失效或拖垮性能。真正能拦住重复的,是 UNIQUE 索引;触发器只适合做格式校验、默认值填充或抛出更友好的错误提示。
BEFORE INSERT 触发器只能做轻量预检,不能查表判重
很多人写 SELECT COUNT(*) FROM orders WHERE order_no = NEW.order_no 再 SIGNAL,这逻辑看着对,实际危险:没索引会全表扫,有索引也挡不住并发“查-判-插”窗口期。MySQL 和 PostgreSQL 都无法保证这个判断原子性。
- 真正该做的:把
order_no字段加上UNIQUE约束,这是数据库引擎级的并发安全机制 - 触发器里只保留必要预检,比如
IF NEW.order_no REGEXP '^[A-Z]{3}-[0-9]{8}$' THEN SIGNAL ... END IF - 如果业务需要区分“重复插入”和“格式错误”,可在应用层捕获错误码:
1062(MySQL)、23505(PostgreSQL),再映射成不同提示
UNIQUE 索引必须覆盖业务语义,不能只建单字段
“同一用户当天只能提交一条订单”这种需求,UNIQUE(order_no) 不够用。必须让索引能表达时间范围逻辑,否则触发器查 DATE(created_at) 就成了唯一手段,而这条路已被证明高危。
- PostgreSQL 支持函数索引:
CREATE UNIQUE INDEX ON orders ((user_id, DATE(created_at))) - MySQL 8.0+ 同样支持,老版本需加生成列:
ALTER TABLE orders ADD COLUMN order_date DATE AS (DATE(created_at)) STORED,再建UNIQUE KEY (user_id, order_date) - 注意:
NULL值在UNIQUE索引中不互斥,如果created_at可为空,得加CHECK (created_at IS NOT NULL)或用默认值兜底
触发器里查表极易引发死锁或性能雪崩
在 BEFORE INSERT 中执行 SELECT ... FROM orders WHERE user_id = NEW.user_id,哪怕加了索引,也会在高并发时变成热点行锁等待源。更糟的是,MySQL 的触发器能看到同事务未提交数据,PostgreSQL 却看不到——行为不一致进一步放大风险。
- 绝对避免在触发器里做跨行聚合,比如
SELECT COUNT(*)、SUM()或JOIN大表 - 如果非得依赖其他表状态(如用户是否冻结),把字段冗余进当前表,或用缓存键值对(如 Redis)在应用层查
- PostgreSQL 中不要试图用
EXECUTE动态拼查询,计划器无法缓存执行计划,每次解析都开销大
MySQL 和 PostgreSQL 触发器关键差异必须盯死
语法像,但底层行为差很多。一个漏掉 RETURN NEW,另一个 SIGNAL 写错位置,都会导致逻辑静默失效。
- MySQL:
BEFORE INSERT中可直接赋值SET NEW.created_at = NOW();AFTER INSERT里改NEW无效 - PostgreSQL:函数末尾必须写
RETURN NEW;,否则插入直接报错trigger function must return type "trigger" - 两者都禁止在触发器里调用存储过程或外部服务——延迟不可控,事务卡住就等于整个写入链路卡死
最常被忽略的一点:触发器永远不是第一道防线。它跑在约束之后(主键、NOT NULL、CHECK),又跑在唯一索引冲突之前。你花三小时写的防重触发器,可能连第一条重复数据都拦不住——因为数据库早在触发器启动前就因主键冲突报错了。










