批量清理过期数据应优先使用 db::delete(),它绕过模型层直接执行原生 delete 语句,不触发验证、事件或软删除逻辑,高效稳定;model::destroy() 仅适用于主键删除且易受软删除和事件影响。

清理任务不该走事件链——用 Db::delete() 直接删,别让 Model::destroy() 触发验证、关联或软删除判断,否则几百条就卡住或漏删。
Db::delete() 和 Model::destroy() 到底该用哪个
批量删过期数据(如日志、缓存)必须用 Db::delete()。它绕过模型层,生成原生 DELETE FROM 语句,不走事件、不校验、不查关联,快且稳。
Model::destroy() 只接受主键值或 ID 数组,不能直接传 where 条件;若强行写 UserModel::where('status', 0)->destroy(),会报错或静默失败。
- 想按非主键字段删(比如
expire_time ),只能两步:先 <code>column('id')拿 ID 列表,再destroy($ids) -
destroy()在启用了SoftDelete的模型里默认是软删,加forceDelete()才硬删——但它仍会触发deleting/deleted事件,不适合后台清理 -
Db::name('log')->where('created_at', 'limit(1000)->delete()是最推荐的写法
WHERE 条件里不加索引字段等于锁表
没索引的 WHERE 条件会导致全表扫描,大表(>10 万行)上执行可能直接拖慢整个数据库,甚至触发 MySQL 的锁等待超时。
必须确保条件字段(如 created_at、expire_time)已建索引。用 EXPLAIN 验证:
EXPLAIN DELETE FROM user_log WHERE created_at
- 如果
type是ALL,说明没走索引,得立刻加 - 别用
find()+ 循环delete():10 万条 = 10 万个查询 + 10 万个模型实例,内存和连接数直接爆 - 用
limit(1000)控制单次影响行数,删完检查返回值:if ($affected === 0) break;,防无限循环
MySQL 严格模式下 datetime 字段可能让删除静默失败
生产环境常开 STRICT_TRANS_TABLES 或 NO_ZERO_DATE,这时若字段定义为 DATETIME DEFAULT '0000-00-00 00:00:00',WHERE expire_time 可能被 MySQL 拒绝,不报错但返回 0 行。
先查实际值:
SELECT COUNT(*) FROM cache WHERE expire_time = '0000-00-00 00:00:00';
- 若有这类非法时间,WHERE 条件要兜底:
->whereOr('expire_time', 'exp', "''")或->whereOr('expire_time', 'is null') - 上线前在测试库跑:
SELECT @@sql_mode;,确认是否含严格模式 - 避免用
truncate()做条件清理——它不支持WHERE,只能清空整表
定时任务必须加锁 + 分片 + 日志标记
服务器多实例、容器重启后,没锁的清理命令可能被多个进程同时执行,导致重复删或漏删,还可能因长事务阻塞业务写入。
- 用文件锁最轻量:
file_put_contents(RUNTIME_PATH . 'clean_expired.lock', time(), LOCK_EX),删完unlink() - 每轮删完记日志:
trace("clean_expired: deleted {$affected} rows before " . date('Y-m-d')); - 别包
Db::transaction():删除本身是原子操作,加事务反而延长锁持有时间;只有跨表联动清理才考虑手动事务 - 命令行任务(如
php think clean:expired)默认无超时保护,务必在脚本里加set_time_limit(30)防卡死
真正难的不是写那几行 delete(),而是让删的动作在高并发、大数据、多实例、严格 SQL 模式下都稳定不出错——锁、索引、兜底条件、分片、日志,一个都不能少。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











