问题背景
在mysql数据库设计中,逻辑删除(软删除)是一种常见的实践,它通过设置标志位(如is_delete)来标记记录被"删除",而不是实际删除数据。然而,当表中存在唯一约束时,如在用户表中我们要求用户名必须唯一,并且用户数据不要物理删除,那这个时候可能会产生一个问题:
- 用户a(username=“tom”)被逻辑删除(is_delete=1)
- 新用户尝试使用username="tom"注册时
- 唯一约束阻止创建新记录,即使原始用户已被"删除"
本文将介绍解决此问题的方案。
问题复现
1.创建用户表
create table users ( id int auto_increment primary key , username varchar(50) not null comment '用户名', email varchar(100) not null comment '用户邮箱', is_delete tinyint(1) not null default 0 comment '逻辑删除标记,用于标记当前记录是否已删除,0:否,1:是' );
2.执行脚本:
-- 插入一条测试数据,用户名为:tom insert into users (username, email) values ('tom', 'tom@example.com'); -- 逻辑删除tom用户(is_delet设置为1) update users set is_delete = 1 where username = 'tom'; -- 创建同名用户(is_delete不同) insert into users (username, email) values ('tom', 'new_tom@example.com');
在执行第三步时,会报错如下:
[23000][1062] duplicate entry 'tom' for key 'users.idx_uq_username'
原因分析:从结果可以看到,在插入相同名字的记录时,违反了唯一约束idx_uq_username
,但实际上用户tom已经删除了,唯一索引阻止了用户名=tom的记录插入。
解决方案
解决方案1.复合唯一索引 + 时间戳删除字段
改动点:
1)添加一个字段delete_time,用于记录被删除的时间,默认值为null,当删除该记录时将该字段设置为当前时间
2)新建复合唯一索引,将用户名username和删除时间delete_time字段包含在复合唯一索引中
-- 方案1 create table users_test1 ( id int auto_increment primary key , username varchar(50) not null comment '用户名', email varchar(100) not null comment '用户邮箱', is_delete tinyint(1) not null default 0 comment '逻辑删除标记,用于标记当前记录是否已删除,0:否,1:是', delete_time datetime null default null comment '逻辑删除时间,默认为null' ); -- 添加复合唯一索引 alter table users_test1 add unique index idx_unique_username_dt (username, delete_time); -- 插入初始用户 insert into users_test1 (username, email) values ('tom', 'tom@example.com'); -- 逻辑删除用户(设置删除时间) update users_test1 set is_delete = 1,delete_time = now() where username = 'tom'; -- 创建同名新用户(delete_time为null) insert into users_test1 (username, email) values ('tom', 'new_tom@example.com');
执行完上面脚本发现并没有报错,执行查询sql
select * from users_test1;
结果如下:
+--+--------+-------------------+---------+-------------------+ |id|username|email |is_delete|delete_time | +--+--------+-------------------+---------+-------------------+ |1 |tom |tom@example.com |1 |2025-07-13 14:55:59| |2 |tom |new_tom@example.com|0 |null | +--+--------+-------------------+---------+-------------------+
解决方案2:删除后修改唯一字段值
改动点:
在逻辑删除时,为唯一字段添加特定前缀/后缀,使其不再与原有值冲突
-- 方案2 删除后修改唯一字段值 create table users_test2 ( id int auto_increment primary key , username varchar(50) not null comment '用户名', email varchar(100) not null comment '用户邮箱', is_delete int(1) null default 0 comment '逻辑删除标记,用于标记当前记录是否已删除,0:否,1:是' ); -- 添加复合唯一索引 alter table users_test2 add unique index idx_unique_username (username); -- 插入初始用户 insert into users_test2 (username, email) values ('tom', 'tom@example.com'); -- 逻辑删除用户,修改用户名 update users_test2 set is_delete = 1,username = concat(username, '_deleted_', uuid_short()) where username = 'tom'; -- 创建同名新用户 insert into users_test2 (username, email) values ('tom', 'new_tom@example.com'); select * from users_test2;
查询结果示例:
+--+------------------------------+-------------------+---------+ |id|username |email |is_delete| +--+------------------------------+-------------------+---------+ |1 |tom_deleted_100950808475992064|tom@example.com |1 | |2 |tom |new_tom@example.com|0 | +--+------------------------------+-------------------+---------+
解决方案3. 使用历史表
修改点:将删除的记录移动到专门的历史表,主表只保留有效记录
-- 方案3 -- 主表(活跃用户) create table users_test3 ( id int auto_increment primary key , username varchar(50) not null comment '用户名', email varchar(100) not null comment '用户邮箱', is_delete int(1) null default 0 comment '逻辑删除标记,用于标记当前记录是否已删除,0:否,1:是' ); -- 添加复合唯一索引 alter table users_test3 add unique index idx_unique_username (username); -- 历史表(已删除用户) create table users_test3_deleted ( id int auto_increment primary key , username varchar(50) not null comment '用户名', email varchar(100) not null comment '用户邮箱', is_delete int(1) null default 0 comment '逻辑删除标记,用于标记当前记录是否已删除,0:否,1:是' ); -- 添加历史表复合唯一索引 alter table users_test3_deleted add unique index idx_unique_username (username); -- 插入测试数据 insert into users_test3 (username, email) values ('tom', 'tom@example.com'); -- 逻辑删除:移动到历史表 insert into users_test3_deleted (id, username, email,is_delete) select id, username, email,1 from users_test3 where username = 'tom'; -- 删除原纪录 delete from users_test3 where username = 'tom'; -- 可以重新创建原用户名 insert into users_test3 (username, email) values ('tom', 'new_tom@example.com'); select * from users_test3; select * from users_test3_deleted;
解决方案4. 业务层校验+更新记录
修改点:
保持唯一索引不变,在业务层处理冲突
-- 创建表(普通唯一索引) create table users_test4 ( id int auto_increment primary key , username varchar(50) not null comment '用户名', email varchar(100) not null comment '用户邮箱', is_delete int(1) null default 0 comment '逻辑删除标记,用于标记当前记录是否已删除,0:否,1:是' ); -- 业务层逻辑示例伪代码: /* 1. 先查询是否存在已删除的同名用户 select id from users_test4 where username = ? and is_delete = 1 2. 如果存在,则更新原记录(恢复) update users_test4 set is_delete = 0, email = ? where username = ? 3. 如果不存在,则新建记录 insert into users_test4 (username, email) values (?, ?) */
总结
- 复合唯一索引 + 时间戳删除字段:需要需修改表结构,适用于新项目设计,需保留完整数据历史且查询频繁的系统;
- 删除后修改唯一字段值:需要修改业务字段(如用户名),可能影响日志或历史记录追溯,适用于临时解决方案;
- 使用历史表:需同步维护两个表结构,备份恢复方便,查询主表的数据量比较小,查询效率高,适用于数据量大、删除频繁且需要严格区分活跃/历史数据的系统;
- 业务层校验+更新记录:不用修改字段,需编写额外校验和恢复逻辑,可能存在并发问题风险;
综合以上,建议采用方案1和方案3
到此这篇关于mysql逻辑删除与唯一索引冲突解决的文章就介绍到这了,更多相关mysql逻辑删除与唯一索引冲突内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论