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

Simulator · N+1

The counter, the query log and the code panel all run on the tested reducer.

50 posts loaded with one query. Run the loop to see what reading each author does.

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

Spotted a problem, or have a way to make this clearer? Suggest an improvement.