核心是提前加载关联数据以避免n+1问题:用with()在查询主模型时预加载关联,load()用于已存在模型的延迟加载,withcount和select可优化字段获取。

核心是提前加载关联数据,而不是在循环里临时访问。不预加载,每遍历一个主模型就查一次关联表,100条数据就是100次额外查询——这就是N+1问题的实质。
必须用 with() 的场景
当你第一次从数据库取主模型(比如 User::all() 或 User::where(...)->get()),又需要访问它的关联属性(如 $user->posts)时,不加 with() 就一定会触发 N+1。
- with() 必须写在 get()/first() 之前,它会改写 SQL,用 JOIN 或额外 SELECT 一次性拉取关联数据
- 支持嵌套:如 User::with('posts.comments.author')->get()
- 多个关联可一起写:User::with(['posts', 'profile', 'roles'])->get()
- 带条件过滤:User::with(['posts' => function ($q) { $q->where('published', true); }])->get()
什么时候该用 load() 而不是 with()
当你已经拿到了模型或集合(比如从缓存读的、或只查了 ID),但后续才决定要补上关联数据,这时 with() 已经没机会用了,只能用 load()。
- 它作用于已存在的实例,比如:$users = User::select('id')->get(); $users->load('posts');
- 单个模型也能用:$user->load('profile');
- 底层会发起新查询,但自动加上外键条件(如 WHERE user_id IN (1,2,3)),不会全表扫
别忽略 withCount 和 select 字段优化
如果只需要关联数量或几个字段,没必要加载整张关联表。
- 统计类用 withCount():Post::withCount('comments')->get(),结果直接带 comments_count 属性
- 查特定字段用 select() 配合 with:User::with(['posts' => function ($q) { $q->select('id', 'title', 'user_id'); }])->get()
- 避免在 Blade 中写 $post->user->name 却没预加载 user,这是最常见出问题的地方
关联定义和访问也要注意
模型关系写法本身也影响性能。
- belongsTo 关系返回单模型,不能 foreach;访问前先判空,否则报错:$product->category?->cat_name
- hasManyThrough 等跨层关系,同样支持 with 预加载,比如 Country::with('posts')->get()
- 访问器(accessor)里别再发起查询,尤其是循环中多次调用;该预加载的提前 with,该缓存的加 memoize 逻辑











