软删除字段应使用DATETIME或TIMESTAMP类型并设DEFAULT NULL,NULL表示未删除,非空时间表示已删除;需建索引如idx_deleted_at,并在Laravel中启用SoftDeletes trait,注意Eloquent与Query Builder行为差异。

软删除字段该用什么类型和默认值
直接加 deleted_at 字段最稳妥,类型必须是 DATETIME 或 TIMESTAMP,不能用 INT 或 BOOLEAN。MySQL 里 NULL 表示“未删除”,非空时间值表示“已删除”——这样能天然兼容 Laravel 的 SoftDeletes trait,也方便后续加查询条件(比如查“最近30天被软删的用户”)。
常见错误:用 is_deleted TINYINT(1),看似简单,但会导致两个问题:一是无法记录删除时间,审计困难;二是联合索引失效风险高(比如想查“按删除时间倒序”,is_deleted=1 + deleted_at 就比单字段更高效)。
ALTER TABLE users ADD COLUMN deleted_at DATETIME NULL DEFAULT NULL;- 建索引时带上
deleted_at:比如INDEX idx_deleted_at (deleted_at),避免全表扫描 - 别忘了在迁移脚本里补全旧数据:
UPDATE users SET deleted_at = NULL;
Laravel 中启用软删除要注意哪些地方
只要模型用了 use SoftDeletes;,Laravel 就自动拦截 delete()、destroy() 和 truncate(),转为更新 deleted_at。但这个机制有边界——它只对 Eloquent 模型生效,绕过 ORM 直接执行 DB::table('users')->delete() 会物理删除。
容易踩的坑:
立即学习“PHP免费学习笔记(深入)”;
- 控制器里写
User::where(...)->delete()是软删;但DB::table('users')->where(...)->delete()是硬删,完全不走软删逻辑 - 关联删除(比如
$user->posts()->delete())默认不触发子模型软删,得手动调用softDelete()或改用each()遍历 - 查询时记得加
withTrashed()或onlyTrashed(),否则get()默认忽略软删记录
手动实现软删除(不用框架)的关键逻辑
核心就三件事:改写 DELETE 为 UPDATE,所有 SELECT 加过滤条件,提供恢复入口。没有框架帮你兜底,就得自己把每处 SQL 检查一遍。
典型场景:
- 列表页查询要加
WHERE deleted_at IS NULL,漏掉就会显示已删数据 - 后台“恢复”操作不是
INSERT,而是UPDATE users SET deleted_at = NULL WHERE id = ? AND deleted_at IS NOT NULL - 统计类 SQL(比如
COUNT(*))必须明确是否包含软删记录,否则报表数字对不上 - 如果用 PDO,建议封装一个
softDelete($table, $id)函数,统一处理时间戳赋值和事务
软删除带来的查询性能和索引问题
软删后表体积只增不减,deleted_at IS NULL 这个条件如果没索引,大表查询会越来越慢。尤其当软删比例超过 20%,优化器可能放弃使用主键索引,转而走全表扫描。
实际影响点:
-
SELECT * FROM orders WHERE user_id = 123如果没对(user_id, deleted_at)建联合索引,即使user_id有索引也会变慢 - MySQL 8.0+ 可以建函数索引:
CREATE INDEX idx_soft_deleted ON users ((deleted_at IS NOT NULL));,加速WHERE deleted_at IS NOT NULL查询 - 定期归档老数据(比如软删超90天的记录)比长期堆积更可控,
INSERT INTO archived_users SELECT * FROM users WHERE deleted_at
真正麻烦的是跨服务协作——比如订单服务标记软删,但库存服务还拿着旧 ID 去扣减,没人校验这条订单是否已被逻辑删除。这种边界问题,光靠数据库字段解决不了。











