预加载本质是主查询加批量IN查询而非JOIN,需外键索引支撑;嵌套预加载分层发独立查询,超3层宜拆分;whereHas()须在with()前使用,避免优化失效;load()/loadMissing()仅适用于补救场景,不能替代设计阶段的合理预加载。

预加载不是“加个 with() 就完事”,它解决的是 N+1 查询的表层症状,但真正卡住性能的,往往是没索引的外键、嵌套过深的 with()、或和 whereHas() 错位混用。
with() 为什么能减少查询次数
它不靠 JOIN,而是拆成主查询 + 若干 IN 查询。比如 User::with('posts') 实际执行:
SELECT * FROM usersSELECT * FROM posts WHERE user_id IN (1,2,3,...)
关键点在于:第二条 SQL 的 user_id 是从第一条结果里批量提取的 ID 列表,不是循环拼参数。所以无论查 10 条还是 1000 条用户,posts 只查一次。
但前提是 posts.user_id 有数据库索引——没索引时,IN 查询本身就会变慢,with() 反而放大延迟。
嵌套预加载(如 with('posts.comments'))的实际行为
它不会生成三层 JOIN,而是发三条独立查询:
SELECT * FROM usersSELECT * FROM posts WHERE user_id IN (...)SELECT * FROM comments WHERE post_id IN (...)
每层都依赖上层查出的外键值,自动做映射。好处是可控、不易爆内存;坏处是深度越大,PHP 层对象树越重。超过 3 层(如 user.posts.comments.author.profile)建议拆开查或改用 API 分页聚合。
别写 with(['posts.comments', 'posts.tags'])——这会触发两次 posts 查询,Laravel 不合并同名关联。
whereHas() 和 with() 不能随便混用
如果先写 ->whereHas('posts', $callback) 再跟 ->with('posts'),Eloquent 可能放弃 IN 优化,退化成全表扫描或错误 JOIN,尤其当 posts 表没索引或数据量大时。
正确顺序是:
- 先
whereHas()过滤主模型(比如只取有草稿的用户) - 再
with()预加载(只加载这些被过滤出来的用户的 posts)
更安全的替代是 withCount():比如查“至少有 2 篇草稿的用户”,用 withCount(['posts as draft_count' => $callback]) + having(),比手写 JOIN 更稳。
load() 和 loadMissing() 的真实适用场景
load() 是补救手段,不是设计选择。比如你已用 Post::all() 拿到集合,之后在某个 if 分支里才需要作者信息,这时用 $posts->load('user') 是合理且高效的。
但以下写法就是错的:
-
foreach ($posts as $post) { $post->load('user'); }→ 又变回 N+1 -
$post->loadMissing('user')在循环里调用 → 同样无效,它只判断单个模型是否已加载,不批量优化
真正该警惕的是:loadMissing() 容易让你误以为“我做了预加载”,其实只是懒加载的缓存开关,初始查询缺陷还在。
最常被忽略的一点:预加载解决不了字段冗余。哪怕只显示用户名,with('user') 默认查 users.*。必须显式 select() 或用闭包限定字段,否则内存和网络开销照样涨。


















