预加载关联应使用 with() 一次性载入,避免 N+1 查询;需配合外键索引、合理使用 withCount() 统计、慎用全局作用域中的 with(),并通过 query log 和 EXPLAIN 验证优化效果。

用 with() 预加载关联,别等循环里查
N+1 本质是:查 1 条主表数据,再为每条数据单独查 1 次关联表,变成 N 次额外查询。最直接的解法就是把“后面要查的”提前一次性载入。
比如用户列表页要显示每个用户的头像和所属部门,别这么写:
foreach ($users as $user) {
echo $user->avatar->url; // 触发第 2 次查询
echo $user->department->name; // 触发第 3 次查询
}改成:
$users = User::with('avatar', 'department')->get();这样只发 3 条 SQL:1 条查 users,1 条查 avatars(IN (...)),1 条查 departments(IN (...))。
-
with()支持嵌套,如with('posts.comments'),但注意深度越大,内存占用越高 - 如果只在部分模板里需要某个关联,用
load()延迟预加载更灵活,比如$user->load('profile') - 避免
with('relation')和后续whereHas()混用导致意外全表扫描——先whereHas过滤,再with
慎用 eagerLoad() 和全局作用域里的 with()
有些同学会在模型的 boot() 里加 static::addGlobalScope(new MyScope),然后在 scope 里调用 with(),这会出问题。
Laravel 的全局作用域不能触发预加载,with() 在 scope 里只是被忽略,不会报错,但 N+1 照样发生,非常难排查。
- 全局作用域只适合加
where、orderBy这类查询修饰,不负责数据加载 - 如果多个地方都需要固定预加载,考虑封装成作用域方法,比如
public function scopeWithProfile($query),再显式调用->withProfile() - 测试时用
DB::enableQueryLog()+dd(DB::getQueryLog())看实际发了几条 SQL,比猜靠谱
关联字段没索引?with() 也救不了你
预加载能减少查询次数,但每条预加载 SQL 本身可能很慢——尤其是关联字段没建索引时。
典型表现:预加载 posts 很慢,看日志发现 select * from posts where user_id in (1,2,3,...1000) 执行了 800ms。
- 检查外键字段是否加了索引,比如
posts.user_id必须有索引,否则 IN 查询会全表扫 - MySQL 5.7+ 对长 IN 列表有优化,但超过几千个 ID 仍建议分批,Laravel 的
chunkById()可配合使用 - 用
EXPLAIN看预加载语句的执行计划,重点看type是不是ref或range,别是ALL
复杂条件别硬塞进 with(),用 withCount() 或子查询替代
想查“每个用户有多少个已发布文章”,有人会写 with('posts' => fn ($q) => $q->where('status', 'published')),结果发现 $user->posts 变成筛选后的集合,但数量对不上预期——因为 with() 是加载数据,不是统计。
真正要的是数量,就该用:
User::withCount(['posts' => fn ($q) => $q->where('status', 'published')])->get();这样生成的是 SELECT ..., (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id AND status = 'published') as posts_count,高效又准确。
-
withCount()返回的是整数字段,不占内存,适合列表页做 badge 数字 - 如果既要数量又要部分数据(比如最新 3 篇),拆成
withCount()+with(),别试图一个with()全包 - 注意
withCount()默认字段名是{relation}_count,自定义用as('published_posts_count')
关联查询优化不是堆 with() 就完事;它得和索引、查询意图、数据量级一起看。最容易被跳过的其实是外键索引——加完可能性能直接翻倍,但没人记得去 explain 一眼。


















