In Eloquent, how do load(), loadMissing() and loadCount() on already-fetched models differ from with(), and when does the $with property backfire?
answer
- after the query, not during it
- load() always re-queries
- loadMissing() skips relations already set
- loadCount() adds relation_count attributes
- $with applies to every query of the model
basics
~20 sload() eager loads relations onto models you already have and always queries again; loadMissing() loads only relations not yet set; loadCount() adds count attributes. $with eager loads on every query of the model, needed or not.
solid answer
~40 s`with()` is set on the builder before the query runs; the `load` family is **lazy eager loading**: it batches a relation onto a model or collection you already hold, still one query per relation. `load('author')` always runs the query, replacing whatever was loaded. `loadMissing('author')` skips models whose relation is already set, which makes it the safe call in shared code such as a service or resource that may receive preloaded models. `loadCount('comments')` runs one aggregate query and adds a `comments_count` attribute to each model. The `$with` property makes a relation load on **every** query of that model, including when it is loaded as someone else's relation; it hides cost, and two models that name each other in `$with` build a loading cycle. Remove it per query with `without()` or replace it with `withOnly()`.
code
php · 21 lines<?php
namespace App\Services;
use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;
class PostDigest
{
/** @param Collection<int, Post> $posts */
public function build(Collection $posts): array
{
$posts->loadMissing('author')->loadCount('comments');
return $posts->map(fn (Post $post) => [
'title' => $post->title,
'author' => $post->author?->name,
'comments' => $post->comments_count,
])->all();
}
}go deeper
Recall that load() batches a relation onto models you already have and that loadCount() adds a count attribute without loading rows.
Explain load() versus loadMissing() on collections, how loadCount names its attribute, and how $with, without() and withOnly() interact on a single query.
Show where each belongs in shared code: loadMissing in services that receive models from many callers, and why a broad $with quietly taxes jobs and exports.
Decide team conventions: whether $with is allowed at all, who owns the loading contract between controllers and services, and how that is enforced in review.
## Eager loading after the fact `with()` belongs to the query builder: you name the relations before `get()` runs. Sometimes the models already exist when you learn what you need, for example when a controller receives a collection from a repository, or when a relation is only needed on one branch of the code. Eloquent's `load` family covers that case. The Laravel docs call it **lazy eager loading**: the timing is lazy, after the query, but the loading is still batched, one query per relation for the whole collection. All of these methods exist on both a single model and an Eloquent collection. On a collection they batch across every model in it. ## The three methods | Method | What it runs | When a relation is already loaded | |---|---|---| | `load('author')` | one where-in query per named relation | runs anyway and replaces the loaded value | | `loadMissing('author')` | the same query, but only for models that lack the relation | skips those models | | `loadCount('comments')` | one aggregate query keyed by the parents' ids | not applicable; adds `comments_count` | Details worth knowing: - `load()` and `loadMissing()` accept the same shapes as `with()`: dot paths (`'author.profile'`), arrays, and closures keyed by relation name for constraints. - `loadMissing('author.profile')` walks the path: it loads `author` only where it is missing, then `profile` only on authors that lack it. - `loadCount()` also takes closures, for example `loadCount(['comments' => fn ($q) => $q->where('approved', true)])`. Siblings exist for other aggregates (`loadSum`, `loadMax`, `loadExists`). - A collection method is called once on the collection, never inside a loop over its models. `$posts->load('author')` is one query; calling `$post->load('author')` per post is the N+1 again with extra steps. ## Where `loadMissing` earns its place Shared code often cannot know how its input was built. An export service may receive posts that a controller already loaded with `author`, or posts that came straight from a query. Calling `load('author')` there would re-query when the data is already present. `loadMissing('author')` makes the service safe for both callers: it costs nothing when the relation is set and batches the query when it is not. ## The `$with` property A model may declare relations it always eager loads: ```php class Post extends Model { protected $with = ['author']; } ``` `Post::all()`, `Post::find(1)` and every other query built from the model now load the author too. That includes queries that Eloquent builds for you: when `Author::with('posts')` loads posts, the posts query also applies `Post`'s `$with`. The trade-offs: 1. **Hidden cost.** Every read of the model pays for the relation, including queue jobs, exports and counts that never touch it. 2. **Cascades.** If `Author` also declares `$with = ['posts']`, loading either model starts a cycle: posts load authors, whose query loads posts, and so on, until the process runs out of memory or time. 3. **Invisible intent.** A reviewer reading `Post::latest()->get()` cannot see that it runs two queries. Per-query escape hatches exist: `Post::without('author')->get()` removes one default, and `Post::withOnly('category')->get()` discards all defaults and loads only what you name. Many teams prefer explicit `with()` at the call site and reserve `$with` for a relation that is genuinely needed everywhere the model is read, such as a translation row. ## Choosing between them 1. You are writing the query yourself: use `with()` on the builder. 2. You received a collection and know nothing loaded it: `load()`. 3. You received a collection that may already be loaded: `loadMissing()`. 4. You need a total, not the rows: `loadCount()` (or `withCount()` on the query). 5. A relation is truly needed on every read of the model: consider `$with`, and document it on the model. ## Common mistakes - Calling `$post->load(...)` inside a loop instead of `$posts->load(...)` once. - Using `load()` in shared code and doubling queries for callers that already eager loaded. - Reading `$post->comments->count()` for a total, which loads every comment, instead of `loadCount()`. - Treating `$with` as free because the page that motivated it needed the relation.
- Why call $posts->load('author') on the collection rather than $post->load('author') inside the loop?The collection method collects every post's foreign key and runs one where-in query for all of them. The per-model call runs one query for each post, which is exactly the N+1 pattern that eager loading exists to remove. Calling it on a single model is only right when you truly hold one model.
- How do you skip a relation listed in a model's $with for one query?Use `without()`, for example `Post::without('author')->get()`, to drop named defaults from that query's eager loads. To discard every default and load only what you name, use `withOnly('category')`. Both act on the one query; the property still applies everywhere else, including when the model is loaded as another model's relation.
saying these in an interview costs you the question
- load() skips relations that are already loaded on the model
- loadMissing() runs one query per model that lacks the relation
- $with only applies when you call the model's own query methods directly
- loadCount('comments') loads every comment and counts them in PHP
- Calling $post->load() inside a loop is still batched into one query