withTrashed()触发全表扫描主因是delete_time字段无索引或OR条件导致MySQL放弃索引;需建覆盖索引如(idx_delete_time, status),并确保字段类型匹配(如integer需声明$type)。

withTrashed() 一加,查询就变慢,甚至全表扫描——不是代码写错了,是 delete_time 字段没走索引,或 OR 条件让 MySQL 放弃了索引。
withTrashed() 为什么会触发全表扫描
ThinkPHP 的 withTrashed() 底层生成的 SQL 类似:WHERE (delete_time IS NULL OR delete_time > 0)。MySQL 对含 OR 的条件天然不友好,尤其当 delete_time 上只有单列索引时,优化器大概率放弃使用该索引,直接全表扫。
这不是 TP 的 bug,而是 SQL 层面的通用限制。更隐蔽的是:即使你加了索引,如果已删数据占比超过 ~20%,MySQL 也可能认为走索引+回表不如直接扫表快。
- 用
buildSql()看实际生成的 SQL,确认是否含OR或IS NOT NULL类条件 - 执行
EXPLAIN检查key和rows字段,看是否命中索引 - 注意:TP6 默认用
datetime类型存delete_time,但若你配成integer却没声明$type,字段可能被当成字符串比较,索引完全失效
delete_time 字段必须加索引,且类型要匹配
软删除字段不是“有就行”,它得能支撑高频的 IS NULL / IS NOT NULL 判断。MySQL 对 NULL 值的索引支持有限,尤其在复合查询中。
- 建表时显式加索引:
ALTER TABLE `user` ADD INDEX idx_delete_time (`delete_time`); - 如果用时间戳(
int),确保模型里声明:protected $type = ['delete_time' => 'integer'];,否则 TP 可能转成字符串存,索引形同虚设 - 避免用
varchar存软删标记(如'deleted'),这种字段几乎无法有效索引,withTrashed()查询会越来越卡
大表 + withTrashed() 时,别依赖单字段索引
单纯给 delete_time 加索引,在复杂查询里依然可能失效。比如你写:UserModel::withTrashed()->where('status', 1)->where('level', 'vip')->select(),即使 delete_time 有索引,优化器也很难同时高效处理三个条件。
- 优先建立覆盖索引,例如:
ALTER TABLE `user` ADD INDEX idx_del_status_level (delete_time, status, level); - 注意字段顺序:把
delete_time放第一位,因为withTrashed()的OR条件会让范围扫描从它开始 - 如果业务上「查已删数据」是低频操作,考虑单独建视图或定时归档表,而不是硬扛
withTrashed()+ 大表
withTrashed() + join 场景下索引更难生效
关联查询时,withTrashed() 只作用于主模型,但 JOIN 后的 WHERE 条件可能跨表,MySQL 优化器更容易放弃索引。例如:Order::withTrashed()->join('user', 'order.user_id', '=', 'user.id')->where('user.status', 1)->select(),此时 order.delete_time 的索引大概率被忽略。
- 拆开查:先用
withTrashed()查出订单 ID 列表,再用IN关联用户,避免 JOIN 中混入软删逻辑 - 给关联字段也加联合索引,例如:
order(user_id, delete_time),提升 JOIN 效率 - 不确定时,强制指定索引:
->bind(['idx_del_status_level'])(需 TP6.1+,且数据库支持)
真正卡住的从来不是 withTrashed() 这个方法,而是 delete_time 字段有没有被当作关键筛选条件来设计索引——它既不是普通业务字段,也不是纯日志字段,得按“高频过滤位”来对待。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











