应优先使用UNIQUE或PRIMARY KEY约束实现多列组合唯一性;触发器仅用于UNIQUE无法覆盖的场景,如大小写不敏感去重、时间维度规则、软删除行过滤、自定义函数校验等,且推荐用INSTEAD OF触发器并注意并发锁和性能问题。

触发器不是实现多列组合唯一性的首选方案
SQL Server 原生支持 UNIQUE 约束和 PRIMARY KEY 约束,对多列组合唯一性(比如 (order_id, product_id))应优先用这些内置机制。触发器是兜底手段,仅在约束无法覆盖的场景下使用——例如需要动态判断逻辑(“同一用户当天最多下单3次”)、跨表校验、或依赖函数结果(如 UPPER(email) 去重)时才考虑。
为什么不用 UNIQUE 约束而要用触发器?常见真实场景
以下情况 UNIQUE 无能为力,必须用触发器:
-
UNIQUE不支持表达式,如要求email字段忽略大小写唯一 → 需在触发器中比对UPPER(INSERTED.email) - 业务规则含时间维度,如“每个客户每月只能提交一份有效报价” → 涉及
GETDATE()和状态字段is_active = 1,约束无法建模 - 组合唯一需排除软删除行,如
WHERE is_deleted = 0→UNIQUE是全表级,不支持条件过滤 - 需调用自定义函数验证(如校验身份证号格式+地区码有效性)→ 约束中不允许函数调用(除某些标量函数且带
SCHEMABINDING)
INSTEAD OF 触发器比 AFTER 更适合做唯一性拦截
用 AFTER INSERT 触发器检测冲突后抛错,数据已插入再回滚,开销大且可能触发其他副作用(如日志、审计)。INSTEAD OF INSERT 在语句执行前介入,可直接拒绝非法数据,更轻量:
CREATE TRIGGER tr_uq_user_email_caseinsensitive
ON users
INSTEAD OF INSERT
AS
BEGIN
IF EXISTS (
SELECT 1 FROM inserted i
INNER JOIN users u ON UPPER(i.email) = UPPER(u.email)
WHERE u.id != i.id OR i.id IS NULL -- 处理新插入ID未知的情况
)
BEGIN
THROW 50000, 'Email must be unique (case-insensitive)', 1;
RETURN;
END
INSERT INTO users (name, email, created_at)
SELECT name, email, GETDATE() FROM inserted;
END;注意点:
- 必须手动完成实际
INSERT,否则数据不会入库 - 要处理
INSERTED中多行的情况(用集合操作,别写游标) - 避免在触发器里调用可能阻塞的函数(如远程 API、大表 JOIN)
- 测试时务必覆盖批量插入(
INSERT ... SELECT多行)场景
容易被忽略的并发与性能陷阱
触发器本身不自动加锁,高并发下仍可能出现竞态条件:
- 两个事务几乎同时插入相同
email,都查不到对方未提交的数据 → 都通过校验 → 都执行插入 → 唯一性破坏 - 解决方法:在校验查询中显式加锁,如
SELECT ... WITH (UPDLOCK, HOLDLOCK),让查询获得更新意向锁并持有到事务结束 - 触发器逻辑越重,写入吞吐越低;若唯一性校验需 JOIN 十万行表,单次插入延迟可能从毫秒级升至秒级
- 所有触发器逻辑必须在同一个事务上下文中,不能用
COMMIT/ROLLBACK手动控制(会报错)
真正复杂的多列唯一业务规则,往往意味着模型设计值得重新审视——比如把“用户+日期+类型”的组合唯一,其实该拆成独立实体表加 UNIQUE 约束,而不是靠触发器硬扛。

















