触发器无法捕获唯一键冲突,因约束校验在触发器执行之后、事务提交之前发生,冲突语句直接报错且触发器根本不运行;可靠方案是应用层异常捕获、数据库日志解析或存储过程封装写入逻辑。

触发器无法捕获唯一键冲突,因为冲突发生在触发器执行之后
SQL 触发器(BEFORE INSERT、BEFORE UPDATE)在约束检查前运行,但唯一约束(UNIQUE KEY、PRIMARY KEY)的校验是在语句执行阶段后期完成的——具体在触发器之后、事务提交之前。这意味着:一旦唯一键冲突发生,语句已失败,INSERT 或 UPDATE 报错(如 MySQL 的 1062 Duplicate entry,PostgreSQL 的 23505),而触发器根本不会被调用。
所以,试图靠触发器“捕获”冲突请求是行不通的。这不是写法问题,而是数据库执行顺序决定的硬性限制。
真正能记录冲突请求的方式只有应用层或日志层拦截
你需要把检测点前移到语句执行之前,或在错误返回后立即捕获上下文。常见可行路径:
- 在应用代码中统一封装
INSERT/UPDATE操作,用try/catch捕获唯一约束异常,并将原始 SQL、参数、时间戳、用户 ID 写入审计表或日志文件 - 启用数据库的通用查询日志(MySQL
general_log)或 PostgreSQL 的log_statement = 'all'+log_error_verbosity = 'verbose',再配合日志解析脚本过滤含duplicate key、unique violation的行 - 对关键表改用
INSERT ... ON CONFLICT DO NOTHING/UPDATE(PostgreSQL)或INSERT IGNORE/ON DUPLICATE KEY UPDATE(MySQL),并在DO NOTHING分支里显式插入一条审计记录——但这只适用于你控制写入逻辑的场景
为什么不能用 AFTER INSERT 触发器加异常处理兜底?
部分开发者会想:那我在 AFTER INSERT 里故意抛错,再用外部机制捕获?不行。原因很直接:
-
AFTER触发器本身只在语句成功执行后才触发;冲突语句根本进不到这一步 - 触发器内部不支持
TRY...CATCH(SQL Server 除外,但其INSTEAD OF触发器也无法绕过约束校验) - 即使你在触发器里查
SELECT COUNT(*) FROM t WHERE key = NEW.key做预检,也解决不了并发冲突:两次插入可能同时通过预检,然后都撞上唯一约束
也就是说,任何基于触发器的“冲突感知”逻辑,都会漏掉真正的冲突时刻,且引入竞态风险。
如果必须在数据库内留痕,唯一可靠做法是重写写入逻辑
放弃“自动捕获”,改为让所有写请求走存储过程封装。例如 PostgreSQL 中:
CREATE OR REPLACE FUNCTION upsert_with_audit(
p_id INT,
p_name TEXT
) RETURNS VOID AS $$
BEGIN
INSERT INTO users(id, name) VALUES (p_id, p_name)
ON CONFLICT (id) DO NOTHING;
IF NOT FOUND THEN
INSERT INTO conflict_audit(table_name, conflict_key, conflict_value, occurred_at)
VALUES ('users', 'id', p_id::TEXT, NOW());
END IF;
END;
$$ LANGUAGE plpgsql;
注意:这仅记录“本次尝试因冲突未写入”,不是记录“被拒绝的原始请求”。它依赖业务主动调用该函数,且无法覆盖 ORM 自动生成的 SQL 或 DBA 直连执行的语句。
真正完整的冲突溯源,始终要靠应用层埋点或数据库代理层(如 ProxySQL、pgBouncer 配合日志插件)来实现——触发器在这里没有位置。











