查得慢八成是sql未走索引,应先用db::select('explain...')查看执行计划,紧盯type、key、extra字段,针对性添加单字段/复合/软删除索引,结合withcount、cursorpaginate和analyze优化查询性能。

查得慢八成不是 Eloquent 写得丑,而是 SQL 没走索引——你改了十遍模型关系、加了三层缓存,结果数据库执行计划里还写着 type=ALL,全表扫描正吭哧吭哧扫完 200 万行。
先让数据库自己开口说话
别猜,别改代码,先看 EXPLAIN。
在 Tinker 或临时路由里执行带参数绑定的语句:
DB::select('EXPLAIN SELECT * FROM users WHERE email = ?', ['test@example.com']);
这一步必须用 【DB::select('EXPLAIN ...', [...])】,不能用 toSql()——后者把 ? 替换成字符串后,MySQL 看到的是常量值,而真实查询走的是预编译参数,执行路径可能完全不同。
重点盯三个字段:type、key、Extra。type 是 ALL?立刻停手;key 为空?哪怕建了索引也可能被跳过;Extra 出现 Using filesort 或 Using temporary?说明排序或分组没走索引。
给 WHERE 字段补索引
方法一:单字段索引
生成迁移:php artisan make:migration add_index_to_users_email
编辑迁移文件,在 up() 方法中写:$table->index('email');
运行迁移:php artisan migrate
方法二:复合索引(等值条件在前,范围条件在后)
比如常用查询是 WHERE status = ? AND created_at > ?,那就建联合索引:$table->index(['status', 'created_at']);
【顺序不能颠倒】——如果写成 ['created_at', 'status'],MySQL 只能用上 created_at,后面 status 就失效了。
方法三:软删除字段必须索引
有 deleted_at 的表,只要写 whereNull('deleted_at'),就必须给它加索引。推荐覆盖索引:$table->index(['deleted_at', 'id']);
这能同时支撑 WHERE deleted_at IS NULL 和后续按 ID 排序分页。
揪出 N+1 查询并干掉它
第一步:打开 Debugbar 或 Telescope,刷新页面,看「Queries」面板里有没有重复出现的相似 SQL,比如:
SELECT * FROM posts WHERE user_id IN (1,2,3,4,5)
SELECT * FROM comments WHERE post_id IN (101,102,103,104,105)
第二步:定位 Blade 模板里触发懒加载的地方——{{$user->posts->count()}} 这种写法,每个用户都单独查一次 posts 表。
第三步:控制器里改写为预加载:
$users = User::withCount('posts')->withCount('comments')->get();
这会生成三条 SQL:SELECT COUNT(*) FROM posts GROUP BY user_id、SELECT COUNT(*) FROM comments GROUP BY user_id、主表查询。比 with(['posts', 'comments']) 轻得多。
第四步:如果真要取关联数据而非只计数,且层级 ≤2,才用 with('posts.comments');超过两层,立刻切到 join() 手动拼接。
处理大数据量分页和 COUNT
方法一:用 cursorPaginate() 替代 paginate()
它不依赖 COUNT(*),也不用 OFFSET,靠 WHERE id > ? ORDER BY id LIMIT 20 推进,十万行也秒出。
方法二:COUNT 查询必须指定字段
别写 User::count(),改用:
DB::table('users')->count('id');
MySQL 对主键字段的 COUNT 会直接走索引统计,不扫表。而 User::select('id')->count() 在 Laravel 里仍等价于 COUNT(*),毫无优化效果。
方法三:检查统计信息是否过期
在生产库执行:ANALYZE TABLE users;
AWS RDS 或高并发环境下,表统计信息可能滞后,导致优化器误判,宁可多跑一次 ANALYZE 也别赌它自动更新。
验证索引是否真被用上
在 MySQL 客户端执行原始查询后,紧跟一句:
SHOW INDEX FROM users;
确认你刚建的索引出现在结果里,且 Seq_in_index 顺序符合预期。
再跑一次 EXPLAIN,对比 key 字段是否从 NULL 变成了你的索引名,type 是否从 ALL 降为 ref 或 range。
如果 key 有值但 rows 仍接近总行数,检查有没有隐式类型转换——比如字符串字段查整数:WHERE phone = 13800138000,MySQL 会把 phone 全转成数字再比,索引直接失效。











