Queries & Eloquent
Why eager loading changes the number of queries
The mistake
Most people read with() as a speed tweak: the query is the same, it just runs
faster. So they add it late, when a page feels slow, and treat it as polish.
It is not polish. Without it, the author query hides inside the loop and fires once per row. With it, that work moves out of the loop into a single query. The count does not shrink because the work got faster. It shrinks because the queries moved.
The machine
The counter, the query log and the code panel all run on the tested reducer.
Drive it
- Run the loop with eager loading off. The counter climbs to 51. One query fetched the posts, then the loop ran one more for every post it touched.
- Now turn eager loading on. The counter drops to 2, and the per-post queries vanish from the log. Nothing about the loop changed.
- Scrub the timeline back to any iteration to see exactly when each query fired, and which line it came from.
The mechanism
Eloquent loads relations lazily. Post::all() runs one query. The first time
you read $post->author, Eloquent runs another query to fetch that author,
because it was not loaded yet. Inside a loop, that second query repeats once per
row. Fifty posts means fifty author queries, plus the one for the posts.
Fifty-one.
with('author') tells Eloquent to load the authors up front. It runs the posts
query, collects the author ids, then fetches all of those authors in a single
query using where id in (...). Two queries, whatever the row count. When the
loop reads $post->author, the author is already in memory, so nothing hits the
database.
In your code
// N+1: one query for the posts, then one more per post
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // a query, every iteration
}
// Eager load: two queries, whatever the row count
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name; // already in memory
}
The fine print
- Connection and round-trip overhead. Each query here counts as one, but not every query costs the same.
- The query cache, which can hide repeated queries in ways that confuse the count.
- Lazy loading prevention, which turns an accidental N+1 into an error instead of a slow page.
Further reading
- Laravel: eager loading
covers
with(), nested loads, and constraining the relations you pull in. - Laravel: preventing lazy loading makes an accidental N+1 throw in development, so it never reaches production.
Spotted a problem, or have a way to make this clearer? Suggest an improvement.