SQL 软删除字段该如何设计?

推荐使用 deleted_at 时间戳字段实现软删除,NULL 表示未删除,非空表示已删除,支持审计、归档与索引优化,并需在 ORM 层统一过滤。

软删除字段通常用一个布尔型或时间戳字段来标记数据是否被“删除”,而不是真正从数据库中移除。设计时核心目标是:不影响原有查询逻辑、便于恢复、避免误删、兼顾性能和可读性。

推荐用 deleted_at 时间戳字段

比布尔字段(如 is_deleted)更灵活,能记录删除时间,支持按时间范围审计、自动归档或定时清理。值为 NULL 表示未删除,非空表示已软删除。

  • 建表时添加:deleted_at DATETIME NULL DEFAULT NULL
  • 查询未删除数据时统一加条件:WHERE deleted_at IS NULL
  • 删除操作改为更新:UPDATE users SET deleted_at = NOW() WHERE id = 123
  • 恢复操作只需置空:UPDATE users SET deleted_at = NULL WHERE id = 123

配合索引提升查询效率

大量数据下,WHERE deleted_at IS NULL 可能走全表扫描。建议为该字段建立单独索引,或与高频查询字段组合成联合索引。

  • 单列索引(适合软删除比例高、常需查有效数据的场景):INDEX idx_deleted_at (deleted_at)
  • 联合索引(如常按状态+时间查):INDEX idx_status_created (deleted_at, created_at)
  • 注意:MySQL 中 IS NU

    LL
    可走索引,但部分旧版本对函数索引支持有限,避免写成 IFNULL(deleted_at, '1970-01-01') 这类表达式

业务层需统一拦截“未删除”条件

避免每个 SQL 都手动写 WHERE deleted_at IS NULL,应在 ORM 或 DAO 层做默认过滤。

  • 例如 Laravel 的 Eloquent 支持 SoftDeletes trait,自动追加全局作用域
  • MyBatis 可通过拦截器或 BaseMapper 封装通用查询条件
  • 显式查含已删除数据时,应使用明确命名的方法,如 findWithDeleted() 或传参 includeDeleted: true

慎用布尔字段 is_deleted

虽然简单,但存在语义歧义(比如初始值设 true 还是 false?逻辑反转易出错),且无法记录删除时间,后续扩展性差。

  • 若必须用布尔型,务必设默认值为 FALSE,并确保所有插入不显式赋值
  • 索引效果不如时间戳(MySQL 对低基数布尔字段索引效率较低)
  • 不建议在新项目中首选,仅用于遗留系统兼容或极简场景
软删除不是银弹——长期堆积的“已删”数据会拖慢备份、迁移和统计;定期归档或物理清理仍有必要。设计时把 deleted_at 当作一等公民字段对待,从建表、索引、查询到业务逻辑层层对齐,才能真正落地可靠。