软删除模型必须用forceDelete()物理删除,但仅限实例调用;批量删需DB::table()或分块遍历;并发下须加锁或原子条件;删后需优化表空间。

软删除模型不能用 delete() 物理删掉,必须显式绕过 SoftDeletes 机制——否则只是更新 deleted_at 字段,数据还在表里。
forceDelete() 只能对模型实例调用,不支持 where 链式批量
forceDelete() 是模型实例方法,不是查询构建器方法。写 User::onlyTrashed()->forceDelete() 会报错或静默失败,Laravel 根本不识别这种语法。
- 必须先查出模型实例,再调用:例如
$user = User::withTrashed()->find(123); $user->forceDelete(); - 它会触发
deleting和deleted事件,适合需要联动清理附件、缓存等场景 - 若目标记录未被软删除(
deleted_at为NULL),forceDelete()仍会执行物理删除——没有内置状态校验,容易误删活跃数据 - 高并发下,两次请求之间可能有
restore()操作,导致刚查出的软删除状态失效;需配合lockForUpdate()或数据库原子条件
批量强制删除只能走 DB::table() 或分块遍历
要删几百条已软删除的记录,别指望模型层提供安全高效的批量接口。两种主流路径性能和语义差异明显:
- 用
DB::table('users')->whereNotNull('deleted_at')->delete():最快最省内存,但跳过所有模型事件、访问器、关联逻辑,删完还得手动清理外键依赖或缓存 - 用
User::onlyTrashed()->chunk(200, function ($users) { $users->each->forceDelete(); }):保留事件触发能力,但每次 chunk 都要加载模型实例,内存占用高,慢;尤其字段多或有 accessor 时更明显 - 大表建议加
limit分批:比如先pluck('id')拿 ID 列表,再whereIn('id', ...)->delete(),避免锁表太久
并发安全删除必须加数据库级条件或行锁
单纯“查出来再删”在并发下是危险的。两个请求同时查到同一条 deleted_at !== NULL 的记录,第二个请求删的是已被恢复的数据。
- 最简方案:用原子 SQL 条件,例如
DB::table('users')->where('id', 123)->whereNotNull('deleted_at')->delete(),一行完成「只删当前确实是软删除状态的」 - 若必须走模型并触发事件,得在事务中加锁:
User::withTrashed()->where('id', 123)->lockForUpdate()->first(),再调forceDelete() - 注意
lockForUpdate()在 MySQL 中要求事务上下文,且不能在无索引字段上生效;主键查询才可靠
删完记得检查空间和索引碎片
MySQL 的 DELETE 不立即释放磁盘空间,大量强制删除后,表文件体积不会缩小,查询性能可能下降。
- 确认数据已清空后,可手动执行
OPTIMIZE TABLE users(需有权限),回收空间并重建索引 - 生产环境慎用
OPTIMIZE TABLE,它会锁表;可改用ALTER TABLE users ENGINE=InnoDB替代(效果相同,部分版本更轻量) - 如果表有全文索引或 JSON 字段,优化行为可能不同,建议先在测试库验证
真正麻烦的从来不是“怎么删”,而是删之前没确认状态、删之后没处理关联、删完没管存储膨胀——这些点不卡在代码里,却卡在上线那一刻。


















