问题背景

在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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88850.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88850.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/88850.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php命名空间用正斜杠还是反斜杠?

在PHP中,命名空间使用反斜杠(\)作为分隔符,这是PHP语言规范明确规定的。反斜杠在命名空间中扮演路径分隔的角色,用于区分不同层级的命名空间。 具体说明:语法规则 PHP命名空间使用反斜杠(\&…

《从依赖纠缠到接口协作:ASP.NET Core注入式开发指南》

在C#的ASP.NET Core开发中,依赖注入绝非简单的技术技巧,而是重构代码关系的底层逻辑。它像一套隐形的神经网络,让程序模块摆脱硬编码的束缚,在运行时实现动态连接,从而为系统注入可测试、可进化的核心生命力。理解其深…

星云ERP本地环境搭建笔记

看到星云ERP两个比较实用的功能,编号规则和打印模板,如下图所示,于是本地跑起来学习学习。开发环境必备:1. JDK 1.82. MySQL 5.73. Redis 44. RabbitMQ 3.12.45. nodejs 206. pnpm 9.7.1 (npm install -g pnpm9.7.1)其他开发工具&…

RedisJSON 的 `JSON.ARRAPPEND`一行命令让数组动态生长

1 、 为什么选择 JSON.ARRAPPEND 在传统的键值模型里,若要往数组尾部追加元素,通常需要 取→改→写 三步: GET 整个 JSON;在应用层把元素 push 进数组;SET 回 Redis。 一条 JSON.ARRAPPEND 则可一次完成,具…

14:00开始面试,14:08就出来了,问的问题有点变态。。。

从小厂出来,没想到在另一家公司又寄了。 到这家公司开始上班,加班是每天必不可少的,看在钱给的比较多的份上,就不太计较了。没想到4月一纸通知,所有人不准加班,加班费不仅没有了,薪资还要降40%…

Unity物理系统由浅入深第四节:物理约束求解与稳定性

Unity物理系统由浅入深第一节:Unity 物理系统基础与应用 Unity物理系统由浅入深第二节:物理系统高级特性与优化 Unity物理系统由浅入深第三节:物理引擎底层原理剖析 Unity物理系统由浅入深第四节:物理约束求解与稳定性 物理引擎的…

深入浅出Kafka Consumer源码解析:设计哲学与实现艺术

一、Kafka Consumer全景架构 1.1 核心组件交互图 #mermaid-svg-JDEEOd2M5PzLkYa6 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JDEEOd2M5PzLkYa6 .error-icon{fill:#552222;}#mermaid-svg-JDEEOd2M5PzLkYa6 .erro…

Matplotlib(一)- 数据可视化与Matplotlib

文章目录一、数据可视化1. 数据可视化的概念2. 数据可视化流程3. 数据可视化目的4. 常见的可视化图表4.1 折线图4.2 柱形图4.3 条形图4.4 堆积图4.4.1 堆积面积图4.4.2 堆积柱形图和堆积条形图4.5 直方图4.6 箱形图4.7 饼图4.8 散点图4.9 气泡图4.10 误差棒图4.11 雷达图二、Py…

传输层协议UDP原理

端口号回顾端口号的作用类似pid,用来标识进程的唯一性。只是为了与系统解耦,所以有了端口号。通过ip来确定唯一主机,再通过端口号找到指定的进程。就可以让全网内唯一的两个进程通信了。所以一个完整的报文至少要携带ip和端口号,i…

【牛客刷题】小红的数字删除

文章目录 一、题目介绍1.1 题目描述1.2 输入描述:1.3 输出描述:1.4 示例11.5 示例2二、解题思路2.1 核心观察2.2 关键问题处理三、算法实现四、算法分析4.1 算法流程图4.2 为什么这么设计算法?4.3 算法复杂度五、模拟演练数据示例1: "103252"示例2: "333&quo…

《大数据技术原理与应用》实验报告三 熟悉HBase常用操作

目 录 一、实验目的 二、实验环境 三、实验内容与完成情况 3.1 用Hadoop提供的HBase Shell命令完成以下任务 3.2 现有以下关系型数据库中的表和数据,要求将其转换为适合于HBase存储的表并插入数据: 四、问题和解决方法 五、心得体会 一、实验目的…

微服务初步入门

服务拆分原则 单一职责原则 单一职责原则原本是面向对象设计的一个基本原则,是指一个类应该专注于单一的功能,不要存在多于一个导致类变更的原因 在微服务架构中,是指一个微服务只负责一个功能或者业务领域,每个服务应该由清晰的定…

Liunx操作系统笔记5

用户管理命令: useradd命令: useradd命令的功能是创建并设置用户信息。使用useradd命令可以自动完成用户信息、基本组、家目录等的创建工作,并在创建过程中对用户初始信息进行定制。语法格式:useradd 参数 用户名常用参数: -M 不建立用…

spring-ai-alibaba 接入Tushare查询股票行情

最近spring-ai-alibaba主干分支新增了对Tushare的支持&#xff0c;一起来看看如何使用简单样例老样子&#xff0c;分三步进行&#xff1a;第一步&#xff1a;添加依赖<dependency><groupId>com.alibaba.cloud.ai</groupId><artifactId>spring-ai-aliba…

Java使用Langchai4j接入AI大模型的简单使用(一)

一、LangChain4j 简介 LangChain4j 是 Java 生态中的 LangChain 实现&#xff0c;是一个用于构建大语言模型(LLM)应用程序的框架。它提供了与各种LLM服务集成的能力&#xff0c;并简化了构建复杂AI应用的过程。 LangChain4j官方文档&#xff1a;Integrations | LangChain4j …

Linux —— A / 基础指令

建议学习路径&#xff1a;Linux系统与系统编程 ⇒ Linux网络和网络编程 ⇒ MySQL一、初识shell命令 1.1、关于 Linux 桌面很多同学的 Linux 启动进⼊图形化的桌⾯. 这个东西⼤家以后就可以忘记了。以后的工作中没有机会使用图形界面。思考: 为什么不使用图形界面? 1.2、下…

[论文阅读] 人工智能 + 软件工程 | 用大语言模型+排名机制,让代码评论自动更新更靠谱

LLMCup&#xff1a;用大语言模型排名机制&#xff0c;让代码评论自动更新更靠谱 LLMCup: Ranking-Enhanced Comment Updating with LLMsarXiv:2507.08671 LLMCup: Ranking-Enhanced Comment Updating with LLMs Hua Ge, Juan Zhai, Minxue Pan, Fusen He, Ziyue Tan Comments: …

悲观锁 乐观锁

悲观锁 乐观锁 在没有加锁的秒杀场景下 每秒打进来的请求是巨大的 高并发场景下 我们发现不仅异常率高的可怕 库存竟然还变成了负数 这产生的结果肯定是很大损失的 那为什么会出现超卖问题呢 我们假设有下面两个线程线程1查询库存&#xff0c;发现库存充足&#xff0c;创建订单…

如何使用Cisco DevNet提供的免费ACI学习实验室(Learning Labs)?(Grok3 回答)

Cisco DevNet 提供的免费 ACI&#xff08;Application Centric Infrastructure&#xff09;学习实验室&#xff08;Learning Labs&#xff09;是帮助用户学习和实践 Cisco ACI 技术&#xff08;包括 APIC 控制器&#xff09;的优秀资源&#xff0c;适合网络工程师、开发者和准备…

Combine的介绍与使用

目录一、Combine 框架介绍二、核心概念三、基础使用示例3.1、创建 Publisher & 订阅3.2、操作符链式调用3.3、Subject 使用&#xff08;手动发送值&#xff09;3.4、网络请求处理3.5、组合多个 Publisher3.6、错误处理四、核心操作符速查表 Operator五、UIKit 绑定示例六、…