软删除需用deleted_at时间戳而非is_deleted布尔字段,配合视图或RLS自动过滤、幂等UPDATE、部分索引及关联查询显式过滤,确保审计、性能与逻辑正确。

软删除不能代替物理删除,它只是用字段标记+查询过滤模拟“删了”,真删还得靠后期归档清理。直接用 UPDATE 替 DELETE 是起点,但漏掉索引、视图或关联过滤,反而让系统更脆弱。
加什么字段:deleted_at 比 is_deleted 更可靠
优先选 deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,不是布尔型 is_deleted。原因很实际:
-
deleted_at能区分“没删过”和“删过又手动改回 0”的异常状态,is_deleted = true无法回溯时间 - 审计、定时恢复、自动归档都依赖时间戳,
is_deleted只能回答“删没删”,不能回答“啥时候删的” - PostgreSQL 对
IS NULL的索引优化成熟,而is_deleted = false在部分场景下可能被优化器忽略(尤其配合 OR 条件时)
所有 SELECT 都得自动过滤,靠视图比靠 ORM 更底层
ORM 层拦截(如 MyBatis-Plus 的 logic-delete)容易绕过或失效,视图是数据库级兜底:
- 建视图:
CREATE VIEW users_active AS SELECT * FROM users WHERE deleted_at IS NULL; - 应用代码查表时,必须改成查
users_active,而不是在每个 SQL 里手写WHERE deleted_at IS NULL - 注意:RLS(行级安全策略)也可替代视图,但超级用户默认不生效,测试必须用普通角色登录验证
- 别信“前端传参过滤”,
deleted_at = NULL(错误写法)和deleted_at IS NULL(正确写法)语义完全不同
UPDATE 代替 DELETE 必须带条件且幂等
执行软删除不是简单 UPDATE ... SET deleted_at = NOW(),要防重复、防误操作:
- 正确写法:
UPDATE users SET deleted_at = NOW() WHERE id = 123 AND deleted_at IS NULL; - 加
AND deleted_at IS NULL保证幂等:重复执行不影响结果,返回影响行数为 0 也属正常 - 千万别先
SELECT再UPDATE——并发下可能两次都查到“未删”,导致重复软删失败或逻辑错乱 - 如果业务需要记录谁删的,额外加
deleted_by INT字段,但不要把它和deleted_at混进同一个索引,优先保证deleted_at IS NULL查询性能
索引不建对,软删除就是性能炸弹
没索引的 deleted_at IS NULL 在 10 万行以上就会明显变慢,尤其当软删比例超过 20% 后,全表扫描风险陡增:
- 必须建部分索引:
CREATE INDEX idx_users_active ON users (id) WHERE deleted_at IS NULL; - 高频搜索场景(如按 name 查活跃用户)用复合索引:
CREATE INDEX idx_users_name_active ON users (name) WHERE deleted_at IS NULL; - 别建全局索引
ON users(deleted_at)——它对IS NULL过滤帮助极小,还拖慢所有写入 - 唯一约束也要适配:比如邮箱唯一,得改
UNIQUE (email) WHERE deleted_at IS NULL,否则已软删邮箱会拦住新注册
最常被忽略的是关联查询和统计逻辑——JOIN 时不加 AND b.deleted_at IS NULL,COUNT 就会把已删数据算进去;分页列表里带“已删订单数”这种字段,得单独子查询,不能靠主查询的 WHERE 过滤来“顺便”统计。软删除真正难的不是加字段,而是让整条查询链路都意识到“有些行其实不算存在”。

















