Laravel查询慢八成因SQL未走索引,应先用DB::select('EXPLAIN...')分析执行计划,type为ALL即全表扫描;WHERE字段无索引需立即添加,复合查询建联合索引时等值条件在前、范围条件在后;with()预加载仅适用于主表少、关联有索引、层级≤2场景,深层嵌套宜用join或hasManyThrough;chunkById()比chunk()更稳但不可用于事务,UUID主键需按created_at分段并加索引;soft delete字段deleted_at未索引会导致全表扫描,建议建(deleted_at, id)覆盖索引。

查得慢,先看 EXPLAIN 而不是改代码
Laravel 查询慢,八成不是 Eloquent 写得丑,而是底层 SQL 没走索引。别急着加 ->with() 或拆查询,先让数据库自己说清楚它在干嘛。
在 Tinker 或临时路由里执行:
DB::select('EXPLAIN SELECT * FROM users WHERE email = ?', ['test@example.com']);观察 type 字段:要是 ALL,说明全表扫描;ref 或 range 才算健康。注意 Laravel 的 ->toSql() 不带绑定值,直接拼进去看执行计划容易误判——务必用 DB::select('EXPLAIN ...', [...]) 这种带参数绑定的方式。
-
WHERE条件字段没建索引?立刻加:php artisan make:migration add_index_to_users_email→$table->index('email') - 复合查询(比如
WHERE status = ? AND created_at > ?)要建联合索引,顺序不能错:等值条件在前,范围条件在后 - MySQL 8.0+ 支持隐藏索引,测试时可用
ALTER TABLE users ALTER COLUMN email SET INVISIBLE临时关掉某个索引看影响
with() 预加载不是万能的,嵌套深了反而更慢
写 ->with(['posts.comments.author']) 看起来很优雅,但 Laravel 默认用 N+1 式的多条 SELECT 拼合数据,中间层一多,连接数、内存、序列化开销全上来。
真正要压平关联,得靠 ->join() + 手动映射,或者换 hasManyThrough 这类原生关系。Eager loading 只适合「主表少、关联表有索引、层级 ≤2」的场景。
- 检查生成的 SQL:如果
with()后出现多个SELECT ... FROM comments WHERE post_id IN (1,2,3,...),且post_id没索引,就等于把压力从数据库转移到 PHP 进程 -
->withCount()比->with()更轻量,只要总数不要详情时优先选它 - 想查「用户+最新一条帖子」?别用
->with(['posts' => fn($q) => $q->latest()->limit(1)]),这会为每个用户都跑一次子查询——改用JOIN+ 窗口函数或分组MAX(id)
chunkById() 比 chunk() 更稳,但别在事务里用
处理几万行数据时,->chunk(1000) 看似安全,实际依赖 id BETWEEN ? AND ?,一旦中间删过记录,就会漏数据或重复处理。而 chunkById() 基于主键偏移,跳过已处理 ID,可靠性高得多。
但它有个硬限制:必须用自增整型主键,且不能在数据库事务中调用——因为每次 chunk 是独立查询,事务里锁表会导致后续 chunk 等待超时。
- 替代方案:用
cursorPaginate()(Laravel 8.76+),基于游标,无状态,适合队列任务 - 如果主键是 UUID,
chunkById()无效,得手动按created_at分段,且确保该字段有索引+不重复 - chunk 过程中更新同表数据?小心 MySQL 的
REPEATABLE READ隔离级别导致幻读——加SELECT ... FOR UPDATE锁住范围,或改用WHERE id > ? ORDER BY id LIMIT ?手动翻页
软删除字段没索引,deleted_at IS NULL 就是性能黑洞
Laravel 默认给所有带 SoftDeletes 的模型自动加 WHERE deleted_at IS NULL,但如果 deleted_at 没索引,这个条件会让原本能走索引的查询直接退化为全表扫描。
尤其在高频查询的列表页(如后台订单页),加上软删除后响应时间翻倍,往往就是这个原因。
- 补索引很简单:
$table->index('deleted_at'),但要注意:单列索引对IS NULL效果有限,MySQL 5.7+ 推荐建INDEX(deleted_at, id)这类覆盖索引 - 如果 99% 数据都是未删除的,索引收益低,可考虑关闭软删除,改用状态字段 + 应用层过滤
- 运行
php artisan tinker时用User::withTrashed()->where('deleted_at', '!=', null)->count()查下真实删除比例,再决定要不要索引
索引不是越多越好,但 deleted_at 这种全局过滤字段,漏掉它基本等于给所有查询埋雷。


















