Laravel数据库查询慢90%源于Eloquent使用不当或索引缺失,需用DB::enableQueryLog()定位N+1、with()显式嵌套预加载、为WHERE/JOIN字段建复合索引、select限制字段、避免函数导致索引失效。

PHP 8.1 的 Laravel 项目数据库查询慢,90% 不是 PHP 版本拖累,而是 Eloquent 使用方式或索引缺失导致的。Laravel 8/9/10 在 PHP 8.1 下运行完全正常,性能瓶颈几乎全在 SQL 层——查多少次、查什么字段、有没有走索引、是否触发全表扫描。
怎么快速定位 N+1 查询
别靠经验猜。直接在开发环境控制器里加两行:
DB::enableQueryLog();
$posts = Post::with('user')->get();
dd(DB::getQueryLog());看输出里是不是有大量重复的 select * from users where id = ?。如果有,说明 with() 没生效或没写对;如果只有两条 SQL(一条查 posts,一条查 users),那 N+1 已解决。
- 常见失效场景:
Post::with('user')但模板里用了$post->user->profile->avatar,必须显式写成Post::with('user.profile') - 用
withCount('comments')替代循环里调$post->comments->count(),否则每个 count 都是一次SELECT COUNT(*) - 调试时别只跑
php artisan tinker查模型,要走完整请求链路(比如访问对应路由),否则中间件、事件、隐式加载可能漏掉
whereRaw 怎么写才安全又快
whereRaw 不是万能加速器,但它在复杂条件组合时确实能绕过 Eloquent 自动拼接的冗余逻辑,让 MySQL 更好地用上索引。关键在两点:参数绑定 + 索引匹配。
立即学习“PHP免费学习笔记(深入)”;
比如这个原始写法会强制全表扫描:
$query->where('status', '!=', 'cancelled')
->where(function ($q) {
$q->where('created_at', '>', now()->subDays(30))
->orWhere('priority', 'high');
});改用 whereRaw 并确保 status、created_at、priority 已建复合索引:
$query->whereRaw(
'(status != ? AND (created_at > ? OR priority = ?))',
['cancelled', now()->subDays(30), 'high']
);- 必须用数组传参,不能字符串拼接,否则有 SQL 注入风险
- 执行前先用
EXPLAIN看原生 SQL 是否命中索引(type字段不为ALL) - 函数包裹字段会让索引失效,比如
whereRaw('DATE(created_at) = ?')就不如把时间范围转成 datetime 直接比较
select() 和索引怎么配着用
select() 不只是减少网络传输,它直接影响 MySQL 的执行计划和内存占用。尤其当关联表字段多、有 TEXT 或 JSON 类型时,select * 会让缓冲区和网络都吃紧。
正确做法是两级控制:
- 主表只选必要字段:
Post::select('id', 'title', 'user_id', 'created_at')->with('user:id,name') - 关联表也限制字段:
with(['user' => function ($q) { $q->select('id', 'name'); }]) - 外键字段(如
user_id)必须加索引,否则JOIN或WHERE user_id = ?会全表扫描 - 复合查询条件(如
WHERE status = ? AND created_at > ?)要建联合索引:$table->index(['status', 'created_at']),顺序不能反
索引不是“加了就完事”。MySQL 的最左匹配原则意味着 INDEX(a,b,c) 能用于 WHERE a=? 或 WHERE a=? AND b>? ,但对 WHERE b=? 无效。
分页和大数据量怎么避免 OFFSET 慢
用 paginate() 是对的,但当数据量超百万,LIMIT 10000, 20 这种带大偏移的分页会越来越慢,因为 MySQL 仍要扫描前 10000 行。
- 前端列表页优先用游标分页:
Post::where('id', 'orderByDesc('id')->limit(20)->get() - 导出或后台批量处理用
chunkById()(比chunk()更稳,避免 ORDER BY 失效) - 不要在事务里做长耗时查询,大事务会锁表,影响并发
- 缓存读多写少的数据,比如热门文章:
Cache::remember('popular_posts', 3600, fn() => Post::where('views', '>', 1000)->get())
真正容易被忽略的是:PHP 8.1 本身不会拖慢查询,但如果你开了 PDO::ATTR_PERSISTENT => true,在 PHP-FPM 场景下反而会导致连接状态污染和泄漏。务必在 config/database.php 里显式设为 false。



















