In a Laravel blog index, how does Post::with('author') stop a Blade loop printing $post->author->name from running one query per post?
answer
- load the relation before the loop
- one extra query per relation
- where id in (...) over collected keys
- results matched to parents in PHP
- dynamic property reads the loaded relation
basics
~20 sPost::with('author') runs the posts query, then one extra query that fetches every needed author by id and attaches each to its post. The Blade loop then reads already-loaded authors: two queries instead of one per post.
solid answer
~40 sWithout eager loading, `$post->author` is resolved on first access, so a loop over 50 posts runs one `users` query per post after the posts query. `Post::with('author')->get()` changes when the data arrives: Eloquent runs the posts query, collects the `user_id` values, runs **one** `select * from users where id in (...)` query, and calls `setRelation('author', ...)` on each post in PHP. In the view, `$post->author` finds the relation already loaded and runs no query. Each relation you name costs exactly one extra query (`with(['author', 'category'])` is three queries in total), no matter how many posts are on the page. Put the `with()` in the controller or query, not in the view.
code
php · 19 lines<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\View\View;
class PostController extends Controller
{
public function index(): View
{
$posts = Post::with('author')
->withCount('comments')
->latest()
->paginate(20);
return view('posts.index', ['posts' => $posts]);
}
}go deeper
Recall that with('author') loads the relation for the whole result in one extra query, and that the view then reads it without querying. Be ready to count the statements before and after.
Explain the mechanics: collect the foreign keys, one where-in query per relation, match by key in PHP, set the relation on each parent. Add loadCount or withCount for totals shown beside each row.
Show how you find the leak on a real page, where the query belongs in the codebase, and how you stop it from coming back rather than fixing one loop at a time.
Frame eager loading as a contract between the query and the view: who decides what a page may load, and how the team keeps views from reaching back into the database.
## The situation: a blog index that gets slower with every post A typical blog index controller fetches the latest posts and hands them to a Blade view that prints each title with its author's name: ```php // controller $posts = Post::latest()->paginate(20); // resources/views/posts/index.blade.php @foreach ($posts as $post) <h2>{{ $post->title }}</h2> <p>by {{ $post->author->name }}</p> @endforeach ``` `author` here is a `belongsTo` relation on the `Post` model. Reading `$post->author` as a **dynamic property** asks Eloquent for the relation's value. If the relation has not been loaded on that model yet, Eloquent runs the relation's query right then and caches the result on that one post. So the page runs one query for the posts plus one `users` query per post: 21 statements for a page of 20. ## What `with()` changes `Post::with('author')` registers an **eager load** on the query builder. When the builder's `get()` (which `paginate()` also uses for its page of rows) has hydrated the posts, it runs the eager loads before returning the collection: 1. Run the main query: `select * from posts order by created_at desc limit 20`. 2. Collect the foreign keys (`user_id`) from the hydrated posts. 3. Run **one** query for the relation: `select * from users where users.id in (...)`. 4. Build a dictionary of authors keyed by `id`, then call `setRelation('author', $author)` on every matching post. The view code does not change. When Blade reads `$post->author`, Eloquent sees the relation is already loaded and returns it without touching the database. The page now costs two statements whether it shows 20 posts or 200. ## Rules of thumb that interviewers probe - **One query per relation, not per row.** `with(['author', 'category'])` adds two queries in total. The count grows with the number of relations you name, not with the number of posts. - **It is not a join.** The posts SQL is unchanged; the related rows arrive in a separate `where ... in` query and are stitched together in PHP. Author columns never appear on the post row. - **Load in the controller or query**, where the data is fetched. A view should only read what it was handed; a query hidden in a template is the hardest kind to spot. - **A missing parent is fine.** If a post's `user_id` matches no author, the relation is set to `null` and reading it runs no query; guard the view with `$post->author?->name`. - **Lazy loading still caches per model.** Reading `$post->author` twice on the *same* post runs one query, not two. The waste is across posts, not within one. - **Name the relation exactly as the method is named.** A typo in `with('autor')` throws `Illuminate\Database\Eloquent\RelationNotFoundException` when the query runs. ## Comment counts on the same page The same index often shows "12 comments" under each post. Reading `$post->comments->count()` would load every comment row just to count them, and `$post->comments()->count()` runs one `count` query per post. The fix is an aggregate column: `withCount('comments')` on the query (its details belong to relation querying) or `$posts->loadCount('comments')` on an already-fetched collection. Either adds a `comments_count` attribute that the view reads as `{{ $post->comments_count }}`. ## Putting it together | Code in the controller | Statements for 20 posts | |---|---| | `Post::latest()->paginate(20)` with `$post->author` in the view | 21 | | `Post::with('author')->latest()->paginate(20)` | 2 | | `Post::with('author')->withCount('comments')->latest()->paginate(20)` | 2 (the count is a subquery in the posts query) | The paginator also runs its own `count` query for the total, so a real page shows one more statement than these columns; the point is that none of them grows with the number of posts. ## Why this is the staple question Interviewers use it because it separates people who have only written Eloquent from people who have watched its queries. The expected answer names `with()`, says the relation is loaded in one extra query for the whole page, and places the call in the controller. A stronger answer adds that the result is matched in PHP rather than joined, and mentions `loadCount` or `withCount` for the comment totals.
- Does with('author') make Eloquent join the users table onto the posts query?No. The posts query stays as written. After it returns, Eloquent runs a second query, `select * from users where id in (...)`, using the collected `user_id` values, then matches authors to posts by key in PHP and sets the `author` relation on each post. That is why author columns never appear on the post row and why sorting posts by author name needs a join or subquery of your own.
- How do you show each post's comment count without loading every comment?Ask for an aggregate instead of the rows. Add `withCount('comments')` to the posts query, or call `$posts->loadCount('comments')` on a collection you already have. Both add a `comments_count` attribute. Avoid `$post->comments->count()`, which loads all comments for each post, and `$post->comments()->count()`, which runs one count query per post.
- Why put with() in the controller rather than calling load() from the Blade view?The controller is where the page's data is fetched, so the eager load sits next to the query it belongs to and is visible in review. A `load()` inside a template still batches the rows, but it hides a query in presentation code, runs again for every include that repeats it, and couples the view to the database. Views should only read what they were handed.
Lazy loading is driving to the shop every time a recipe needs an ingredient; eager loading is writing one list for the week's recipes and making a single trip. The number of trips follows the number of shops (relations), not the number of meals (posts).
saying these in an interview costs you the question
- with('author') joins users onto the posts query in one SQL statement
- Lazy loading caches the author across all posts, so it queries once
- Eager loading adds one query per post instead of one per relation
- Eager loading fetches every row of the users table
- The fix belongs in the Blade view, by calling load() inside the loop