注销过的手机号再注册,唯一索引还说重复,逻辑删除是不是没清干净?
逻辑删除后唯一索引冲突,通常不是索引建错,而是删除行仍占着唯一值。按 2026 年项目交付经验,目标不是把 is_deleted 简单拼进唯一索引,而是让未删除记录保持唯一、已删除记录可以重复。常见做法是删除时间戳或删除版本号配合唯一索引,或用生成列、部分索引、主表物理删除加历史表归档。
为什么逻辑删除会和唯一索引撞上?
唯一索引约束的是整张表,不关心记录有没有被逻辑删除。只要行还在,唯一字段的值就仍被占用。手机号建了唯一索引,用户注销后只把 deleted 置为 1,同一手机号再注册,插入时就会报 Duplicate entry。逻辑删除想保留数据,唯一索引想防止重复,两者目标在这里冲突。
- 唯一索引管全表:删除标记只是普通字段,不会让唯一值失效。
- 逻辑删除不删行:数据还在,唯一值就还在。
- 再次插入相同值触发冲突:数据库在按索引规则履职。
- 冲突集中在复用字段:手机号、邮箱、身份证、订单号等容易撞。
在交付现场,这个冲突常不是设计阶段发现,而是运营重新启用老账号、导入历史数据或客服手工建单时暴露。到那时再改索引,往往要连带改查询、导入脚本和报表。
常见做法对比:先看改造成本和适用条件
把删除标记直接拼进唯一索引,只解决未删除记录唯一,没解决已删除记录可重复。下面几类做法各有条件,按数据库类型和并发写入强度选择。
- 删除时间戳或删除版本号 + 唯一索引:唯一索引改成类似 phone + deleted_at。未删除时 deleted_at 存固定值,删除时写当前时间戳或递增版本号。不要用 NULL 表示未删除,原因见常见问题。
- 生成列或部分索引:PostgreSQL 可评估部分索引;MySQL 可评估生成列,未删除时生成列等于唯一值,删除时生成列为 NULL,再对生成列建唯一索引。语法与数据库版本相关,动手前按官方文档核对。
- 主表物理删除 + 历史表归档:删除时把整行移到历史表,主表物理删除,唯一索引保持原样。适合删除后唯一值不再复用、且能接受关联查询变复杂的场景。
- 应用层查重加锁:不建唯一索引,查询判断是否存在,插入时加锁。并发下容易漏,通常只作辅助,不建议单独兜底。
方案对比:成本与风险的经验区间
- 删除时间戳/版本号:改索引与写入逻辑,经验区间约 0.5~2 人天;版本号比秒级时间戳更能避开同秒冲突。
- 生成列或部分索引:成本常见 0.5~3 人天,取决于数据库版本与团队熟悉度;须按官方文档核对语法。
- 主表物理删除 + 历史表:改删除入口、查询入口和报表,经验区间常见 3~10 人天,入口越多越接近上限。
- 应用层查重加锁:改造成本低,但并发漏判风险高,通常不单独使用。
按 2026 年常见交付习惯,团队用 MySQL、又不愿引入历史表时,删除时间戳或版本号配合唯一索引是改造成本较低的一种;删除量大、历史查询少时,历史表反而更干净。
怎么判断该用哪种?
判断顺序不是先选技术,而是先问业务:这个唯一字段在记录删除后还会不会被复用。会复用,就要释放唯一值;不会复用,可以保留原唯一索引,只做逻辑删除,但查询要加过滤。
- 唯一字段是否复用:手机号、邮箱、身份证注销后可能重新注册,应释放;订单号、合同号通常不复用,可保留原索引。
- 删除频率:日删除几十到几百条,删除时间戳通常够用;日删除到上万条,建议评估历史表或版本号,减少同秒冲突概率。
- 数据库类型:PostgreSQL 可评估部分索引;MySQL 没有部分索引,通常在时间戳、生成列、历史表里选。
- 能否接受历史表:历史表会让关联查询、报表、外键关系变复杂,适合查询入口少、删除后很少再查的场景。
- 并发写入强度:强并发下应用层查重不可靠,应让数据库唯一约束兜底。
经验区间:唯一字段会复用、日删除量在几十到几百条时,删除时间戳配合唯一索引是常见做法;日删除量到上万条时,优先考虑历史表或删除版本号,减少同秒冲突概率。
交付现场经验:临时加逻辑删除的返工代价
某后台管理系统预算和周期压得紧,逻辑删除是上线前临时加的,唯一索引已经建好。第一版把 is_deleted 拼进唯一索引,结果运营重新启用一个老手机号时,删除第二条同值记录就冲突。返工改成删除时间戳,连带调整查询条件、导入脚本和报表,代价是延期常见 1~3 天。约束条件是入口多、没人愿意改报表;做法是先列唯一索引和删除入口清单,再统一改写入和查询;结果比继续打补丁更稳,但也说明逻辑删除应在设计阶段定好删除策略。
适用与不适用边界
逻辑删除适合需要审计追溯、误删恢复、关联数据不能断、行业规范要求保留记录的业务表,比如用户主表、订单主表、资金流水表。这些表一旦物理删除,后续查账和客服追溯会很麻烦。
不适合的情况也要写清楚:临时表、日志表、缓存数据、导入中间表,直接物理删除通常更省事;唯一字段永远不会复用的场景,可以保留原唯一索引,只做逻辑删除,但查询仍要过滤。逻辑删除不是默认选项;如果只是为了看起来安全,物理删除加备份往往更简单。
常见问题
逻辑删除后唯一索引冲突,把 is_deleted 加到唯一索引里行不行?
不行,is_deleted 只有 0 和 1,删除第二条相同值仍会冲突。要用删除时间戳、版本号或历史表释放唯一值。
MySQL 唯一索引里多个 NULL 为什么不算冲突?
MySQL 认为 NULL 不等于 NULL,唯一索引允许多个 NULL。若用 NULL 表示未删除,同一唯一值的未删除记录也能共存,约束会失效。
删除时间戳和删除版本号,哪个更省事?
时间戳直观,但同一秒删两条同值仍可能冲突;版本号或毫秒时间戳更稳。常见区间:日删除几十到几百用时间戳,上万优先版本号或历史表。
主表物理删除加历史表,会影响查询吗?
会。历史表不参与主表 join,报表和关联查询要单独处理,改造成本较高。适合删除后很少再查、且唯一值不复用的场景。
逻辑删除后查询都要带删除过滤吗?
通常要。漏一处,已删除数据就可能重新出现在列表。常见做法是用视图、全局过滤器或 ORM 拦截统一加条件。
如果你们正准备给现有表加逻辑删除,先别改代码,先确认唯一索引有哪些、唯一字段删除后会不会复用、查询入口有多少。按 2026 年交付习惯,把删除策略、唯一索引形态、查询过滤方式写成核对清单,再动数据库。逻辑删除适合需要审计和恢复的业务表,临时表、日志表不必跟风。
-
系统程序开发,数据删除该用物理删还是逻辑删?
日期:2026年8月24日 阅读:125
-
同一个订单的支付和发货消息前后颠倒,每次都等队列自己排,顺序该不该在消费端自己保?
日期:2026年10月15日 阅读:71
-
运营每周都要改活动开关,每次都找研发发版,配置该不该挪到配置中心?
日期:2026年10月14日 阅读:69
-
上线后 OPTIONS 预检一直 401,跨域头也加了,为啥浏览器还报跨域?
日期:2026年10月13日 阅读:103
-
列表翻页第二页又出现上一页最后一条,前端按主键去重就能先上线吗?
日期:2026年10月12日 阅读:50




