应为 deleted_at 字段添加索引以避免全表扫描,具体包括:一、建 (deleted_at, id) 覆盖索引;二、临时禁用全局软删除作用域;三、对归档表改用硬删除;四、withTrashed() 时构建含 deleted_at 的复合索引;五、通过视图分离软删状态。

如果您在 Laravel 应用中启用软删除后发现查询变慢,很可能是 deleted_at 字段未被索引,导致 WHERE deleted_at IS NULL 条件触发全表扫描。以下是针对性优化此问题的操作步骤:
一、为 soft delete 字段添加覆盖索引
软删除机制依赖 deleted_at IS NULL 判断有效记录,若该字段无索引,MySQL 无法快速定位非删除行,将执行全表扫描。建立 (deleted_at, id) 覆盖索引可使查询直接走索引而无需回表。
1、创建迁移文件:php artisan make:migration add_deleted_at_id_index_to_users
2、在迁移的 up() 方法中添加:$table->index(['deleted_at', 'id']);
3、执行迁移:php artisan migrate
4、验证索引是否生效:DB::select('EXPLAIN SELECT * FROM users WHERE deleted_at IS NULL');
5、确认执行计划中 type 字段为 ref 或 range,而非 ALL
二、禁用全局软删除作用域进行临时绕过
Laravel 模型默认启用全局作用域强制追加 WHERE deleted_at IS NULL,但某些后台管理或数据导出场景需查全部记录(含已软删)。此时可临时移除该作用域,避免无谓的索引过滤开销。
1、在查询前调用 withoutGlobalScopes() 方法:User::withoutGlobalScopes()->get();
2、如仅需排除软删除作用域,使用 withoutGlobalScope(SoftDeletingScope::class)
3、对特定查询链式调用:User::where('status', 'active')->withoutGlobalScope(SoftDeletingScope::class)->first()
4、注意:此操作会返回已软删除的数据,仅限可信上下文使用
三、改用硬删除替代高频软删除查询
对于明确不再需要恢复、且查询频率极高的历史归档表(如 logs、events),持续维护 soft delete 状态反而增加索引负担和查询复杂度。直接物理删除可彻底消除 deleted_at 过滤成本。
1、确认数据不可恢复性及业务合规要求
2、将模型中的 use SoftDeletes; 移除
3、删除数据表中 deleted_at 字段:php artisan make:migration drop_deleted_at_from_logs
4、在迁移中执行:$table->dropColumn('deleted_at');
5、更新所有相关查询,移除 withTrashed()、onlyTrashed() 等调用
6、该方案适用于只读归档场景,不适用于需审计或恢复能力的业务实体
四、使用 withTrashed() 时显式指定索引字段
当必须调用 withTrashed() 查询全部记录(含 deleted_at IS NOT NULL 行)时,若 WHERE 条件中同时存在其他高选择性字段(如 status、created_at),应构建复合索引将 deleted_at 与这些字段组合,避免优化器放弃索引。
1、分析实际查询条件,例如:User::withTrashed()->where('status', 'banned')->where('created_at', '>', '2025-01-01')->get()
2、创建联合索引:$table->index(['deleted_at', 'status', 'created_at']);
3、确保索引顺序匹配查询中等值条件优先原则:deleted_at(等值判断 IS NULL / IS NOT NULL)、status(等值)、created_at(范围)
4、执行 EXPLAIN 验证:DB::select('EXPLAIN SELECT * FROM users WHERE deleted_at IS NOT NULL AND status = ? AND created_at > ?', ['banned', '2025-01-01']);
5、确认 key 字段显示为新创建的索引名,type 为 range
五、分离软删除状态至专用查询视图
针对复杂报表或搜索接口,主表频繁混合软删/非删数据导致索引失效,可将逻辑拆解:用数据库视图预过滤非删除行,应用层直接查询视图,规避 deleted_at 条件参与执行计划决策。
1、在数据库中创建视图:CREATE VIEW active_users AS SELECT * FROM users WHERE deleted_at IS NULL;
2、新建 Eloquent 模型 ActiveUser,设置 protected $table = 'active_users';
3、在控制器中使用:ActiveUser::where('email', 'like', '%@example.com')->get();
4、为视图底层基础表 users 的 email 字段单独建索引:$table->index('email');
5、视图不支持写入,仅用于只读高频查询场景


















