预加载必须在查询执行前用with()声明,如user::with('posts')->get();错误写法如user::all()->with('posts')无效;嵌套需with('posts.comments');带条件须闭包;外键无索引会拖慢性能。

循环里调 $model->relation 就是 N+1,预加载不能靠“循环中补”,必须在查询发起前用 with() 声明——这是唯一能真正压平查询次数的方式。
with() 必须写在 get() / first() / paginate() 之前
常见错误是先执行查询再加预加载,比如 User::all()->with('posts') 或 User::find(1)->with('posts'),这会直接报错或完全无效。Eloquent 的 with() 是查询构建器方法,只对未执行的查询生效。
- ✅ 正确:
User::with('posts')->get()、Post::with('user')->first()、User::with('profile')->paginate(20) - ❌ 错误:
User::all()->with('posts')(Collection没有with方法)、User::find(1)->with('posts')(find()已触发查询) - ⚠️ 注意:
load()是补救手段,适用于已查出模型集合后按需加载,如$users = User::get(); $users->load('posts'),但它仍是单次批量查询,不解决原始 N+1 设计问题
嵌套预加载用点号,别拆成多个 with()
想加载 User → posts → comments,必须写成 with('posts.comments')。写成 with('posts')->with('comments') 不仅无效(comments 不属于 User),还会触发两次独立查询且中间层关系缺失。
- ✅ 正确:
User::with('posts.comments')->get()→ 发 3 条 SQL(users、posts IN、comments IN) - ❌ 错误:
User::with('posts')->with('comments')->get()→comments被忽略,posts加载但comments仍懒加载 - ⚠️ 多路径混搭可写数组:
User::with(['posts', 'profile.avatar']),但posts.comments和posts.tags别拆开写,否则posts会被查两次
带条件的预加载必须用闭包,where() 不能甩在外面
with('posts') 默认拉全部关联数据;如果只想加载已发布的文章,->where('published', 1) 放在 with() 后面只会过滤用户表,对 posts 完全无效。
- ✅ 正确:
User::with(['posts' => function ($q) { $q->where('published', 1)->latest(); }])->get() - ❌ 错误:
User::with('posts')->where('published', 1)->get()(查的是users表)、User::with(['posts' => $q->where()](语法错) - ⚠️
limit()在闭包里不保证生效:要“每个用户只取最新 3 篇”,->limit(3)可能被忽略,得换子查询或分步处理
预加载不是万能药,外键没索引反而拖慢
with() 底层靠 IN 查询批量拉数据,比如 SELECT * FROM posts WHERE user_id IN (1,2,3...)。如果 posts.user_id 没索引,这条 IN 查询本身就会变慢,预加载反而放大延迟。
- ✅ 查证方式:启用
DB::enableQueryLog(),确认生成 SQL 条数稳定(主表 + 每个关系一层),且EXPLAIN显示外键字段走索引 - ⚠️ 超过 3 层嵌套(如
user.posts.comments.author)易内存溢出,建议拆成两段查或改用 API 分页聚合 - ⚠️ 预加载默认查所有字段,宽表或大文本字段(如
content)会显著增加内存和序列化开销,必要时用select()限定字段
真正卡住性能的,往往不是没写 with(),而是写了却没索引、嵌套太深、或闭包里用了不被支持的操作(比如 limit)。验证它是否生效,最直接的办法就是看 SQL 日志里到底发了几条查询、每条有没有走索引。











