存储过程中无法直接添加UNIQUE约束,必须通过EXISTS查询+事务回滚+显式错误抛出实现唯一性检查,且最终应依赖表级UNIQUE INDEX或CONSTRAINT作为唯一可信防线。

存储过程里没法直接加 UNIQUE 约束——那玩意儿是 DDL 层的表结构定义,运行时不存在。真要在存储过程中做唯一性检查,得靠显式查询 + 条件拦截,而且必须配合事务和错误抛出,否则查了也白查。
用 EXISTS 而不是 COUNT(*) 做存在性判断
很多人写成 SELECT COUNT(*) FROM users WHERE email = @email 再判断是否 > 0,这会触发全表扫描或索引回表,性能差,还容易锁住无关行。正确做法是用 EXISTS,数据库能短路退出,只查到第一条匹配就停:
-
IF EXISTS (SELECT 1 FROM users WHERE email = @email)—— 快、轻量、语义清晰 - 避免
SELECT COUNT(*) > 0,尤其在大表上,COUNT 会统计全部匹配行 - 如果
email列没建索引,EXISTS也救不了性能,先确保字段上有UNIQUE INDEX或至少INDEX
必须显式 ROLLBACK 或 THROW,否则约束形同虚设
存储过程默认不自动回滚事务。哪怕你查出重复、RETURN 了,调用方可能还在 COMMIT,数据就脏了:
- 用
THROW 50001, 'Email already exists', 1(SQL Server)或SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Email conflict'(MySQL 5.5+)—— 这才能让外层事务感知失败并回滚 - 别只写
PRINT或RETURN,它们不会中断事务流 - 如果存储过程被嵌套调用,要确认外层是否开启事务;没开的话,得自己用
BEGIN TRAN/COMMIT包裹
跨表唯一性检查不能依赖触发器兜底
比如订单表要保证 email 不在用户表里重复,有人想在 INSERT INTO orders 后加触发器查 users 表——这有严重竞态风险:A 进程查完没冲突,B 进程插入用户,然后 A 才真正插入订单,冲突就漏了。
- 真正可靠的做法是:把
users.email设为UNIQUE,再让orders.email建外键引用它(类型一致、非空) - 如果业务要求“同一 tenant 下 email 唯一”,就在
users(tenant_id, email)上建复合UNIQUE约束,而不是在存储过程里拼条件查 - 触发器只适合无法改表结构的遗留系统,且必须用
INSTEAD OF INSERT+ 显式INSERT+THROW,否则静默丢数据
最易被忽略的一点:存储过程里的唯一性检查永远只是“应用层补丁”,不是替代方案。只要表结构允许重复,靠代码永远防不住所有路径——DDL 层的 UNIQUE CONSTRAINT 或 UNIQUE INDEX 才是唯一可信的防线。

















