软删除下unique约束失效,因标准unique不区分逻辑删除行;应改用带where条件的部分唯一索引(如postgresql的where deleted_at is null)或触发器兜底,并配合orm层统一过滤deleted_at。

软删除字段参与唯一性约束时,必须显式排除已删除记录,否则 UNIQUE 会把 deleted_at IS NOT NULL 的旧数据也纳入校验范围,导致插入新同名记录失败。
为什么 UNIQUE (name) 在软删除下会失效
软删除通常靠 deleted_at(或 is_deleted)标记逻辑删除状态,但标准 UNIQUE 约束对所有行一视同仁。哪怕某条 name = 'admin' 的记录已被软删除(deleted_at IS NOT NULL),它仍会阻挡另一条新 name = 'admin' 记录的插入 —— 因为数据库认为“两行 name 值相同”,不管是否已删。
常见错误现象:INSERT INTO users (name, deleted_at) VALUES ('alice', NULL) 成功;再次执行同语句却报错 Violation of UNIQUE KEY constraint,而表里明明只有一条未删除的 alice。
- 根本原因:SQL 标准中
UNIQUE不支持条件过滤,它总是全表扫描 - MySQL、PostgreSQL、SQL Server 都默认如此,无例外
- 试图用
WHERE deleted_at IS NULL写在约束定义里?语法直接报错 ——UNIQUE不接受 WHERE 子句
用唯一索引替代 UNIQUE 约束(推荐)
唯一索引(UNIQUE INDEX)比 UNIQUE CONSTRAINT 更灵活,多数主流数据库支持带 WHERE 的部分索引(Partial Index)或函数索引(Functional Index),能精准限定校验范围。
PostgreSQL 示例(最干净):
CREATE UNIQUE INDEX idx_users_name_active ON users (name) WHERE deleted_at IS NULL;
MySQL 8.0+ 示例(需函数索引):
CREATE UNIQUE INDEX idx_users_name_active ON users ((IF(deleted_at IS NULL, name, NULL)));
SQL Server 示例(用计算列 + 索引):
ALTER TABLE users ADD name_active AS (CASE WHEN deleted_at IS NULL THEN name END);<br>CREATE UNIQUE INDEX idx_users_name_active ON users (name_active);
- 关键点:所有方案都确保只有
deleted_at IS NULL的行才参与唯一性检查 - MySQL 5.7 及更早不支持函数索引,只能退回到触发器方案(见下一条)
- 索引名建议带
_active后缀,避免和原始约束混淆
用 INSTEAD OF / BEFORE INSERT 触发器兜底(兼容旧版本)
当数据库不支持部分索引(如 MySQL 5.7、SQL Server 2014 及更早),只能靠触发器手动拦截重复插入。
SQL Server 示例(INSTEAD OF INSERT):
CREATE TRIGGER tr_uq_users_name_active ON users INSTEAD OF INSERT AS<br>BEGIN<br> IF EXISTS (<br> SELECT 1 FROM inserted i<br> INNER JOIN users u ON i.name = u.name<br> WHERE u.deleted_at IS NULL AND i.deleted_at IS NULL<br> )<br> BEGIN<br> THROW 50000, 'Name must be unique among active records', 1;<br> RETURN;<br> END<br> INSERT INTO users (name, deleted_at) SELECT name, deleted_at FROM inserted;<br>END;
- 必须用
INSTEAD OF(而非AFTER),否则插入已发生,回滚代价高 - 务必检查
i.deleted_at IS NULL和u.deleted_at IS NULL两个条件,漏掉任一都会误判 - 批量插入(多行
inserted)必须用集合操作,禁止写游标 - 触发器里别调
GETDATE()或远程调用,容易引发阻塞
真正麻烦的不是实现,而是团队里有人忘了查 deleted_at 就直接写 SELECT * FROM users WHERE name = ? —— 软删除的唯一性约束再严密,也防不住业务查询绕过逻辑删除状态。所以索引或触发器只是底线,配套的 ORM 层封装和代码审查才是关键防线。










