Laravel Eloquent Interview Questions: Relationship Queries, N+1 Bugs, and Loading Boundaries
Quick Overview
Use a ten-post relationship trace to choose the Eloquent loading operation that matches the response contract.
“Use eager loading” is an incomplete answer to an Eloquent performance question. You need to know which relationship the code reads, when it reads it, whether the query filters parents or related rows, and whether the view needs full models or only counts. This guide uses a small post-and-comment fixture to make those choices observable instead of treating with() as a universal fix.
Official framework facts: the behavior of with, whereHas, withWhereHas, load, loadMissing, withCount, chaperone, and preventLazyLoading below comes from Laravel's 12.x relationship documentation. Candidate reports: no employer-specific Laravel interview format is asserted. Original exercise and inference: the query-count trace is a teaching fixture; real query counts depend on models, scopes, database behavior, and what the template accesses.

Five verified PracHub questions to practice
| Practice question | What to rehearse |
|---|---|
| Many-to-Many Schema Design With a Join Table, Composite Primary Key, and Queries | Explain relationship keys and SQL shape |
| Explain Database Index Benefits and Write Costs | Connect eager queries to indexes |
| Defend a Project's Schema, Indexes, and Concurrency Model | Justify data-access design |
| Order SQL query logical processing steps | Read filter and join semantics |
| Explain database transactions and ACID | Separate read performance from write integrity |
Which relationship access causes N+1?
Consider a Post model with a comments() has-many relationship. A controller executes Post::limit(10)->get(), then a Blade view loops over posts and renders $post->comments for each one. The first query obtains posts. Accessing the relationship property on each previously unloaded post triggers one more query per post. In this simplified fixture, that is 1 + 10 queries. The “N” is the number of parent models actually visited, not necessarily the total table size.
Replace the parent query with Post::with('comments')->limit(10)->get(). In the ordinary simple case, Eloquent fetches the posts and then the related comments using an IN query for those post IDs: approximately two queries. The exact generated SQL and count should be measured for the application, especially with nested relations, global scopes, polymorphic relations, or chunking. The key claim is that query count no longer grows one-for-one with parent rows for this access pattern.
The Laravel 12.x relationship guide describes property access as lazy loading and with() as eager loading. Explain the access direction. Eager loading posts.comments solves reading comments from each post, but it does not necessarily hydrate the parent post on each comment. The official documentation explicitly shows an N+1 when code loops through eagerly loaded comments and then reads $comment->post->title. That is why an interview answer must trace the actual nested expression rather than say “I already used with.”
For this specific child-to-parent read, you might change the view to use the $post already in the outer loop, eager load the inverse relationship when appropriate, or use Laravel's documented chaperone() support to hydrate parent instances on children. Choose based on what data the view truly needs. A syntactic patch that loads every possible relationship can trade query count for substantial memory and transfer cost.
Trace a page before changing it
Original practice fixture: ten posts, each with twenty comments. The page shows post title, an approved-comment count, and the names of the first three approved commenters. It does not show comment bodies. A candidate sees Post::all() followed by $post->comments->where('approved', true)->count() and then $post->comments->take(3) in the view.
The first mistake is not just N+1. Accessing $post->comments pulls the full relationship for each post, potentially 200 comment models, while the UI needs a count and at most 30 commenter names. The collection where filters in PHP after loading; it is not a SQL predicate. Counting an already loaded collection may avoid an extra query on each iteration, but it still loads too much data. Inspect generated SQL and hydrated row count, not query count alone.
Here is a useful follow-up: what if the page needs the existence of approved comments but not their count? A relationship-existence query can express that predicate without hydrating comments. If it needs both the count and a preview, calculate each from the same approval definition and verify that a post with zero approved comments behaves as intended. Do not infer that a model's $post->comments_count attribute is automatically populated just because the relationship exists; the query must request that aggregate. Name the response contract first, then select the least data that satisfies it.
The official withCount documentation supports a constrained count such as Post::withCount(['comments as approved_comments_count' => fn ($q) => $q->where('approved', true)]). Then eager load approved comments only if their data must be displayed. For “first three per post,” be careful: a global limit(3) on an eager-loading relation can have different semantics from three per parent unless the framework's supported eager-limit behavior and ordering are used. State a deterministic order, such as newest approved comments with an ID tie-breaker, and verify that each parent gets the intended three rows.
| Page need | First tool to consider | Boundary to verify |
|---|---|---|
| Render full comments for each post | with('comments') with appropriate constraints | Memory and row count |
| Render only a count | withCount('comments') | Does count use the same approval filter? |
| Filter posts that have approved comments | whereHas('comments', ...) | Filtering parents does not load comments |
| Filter parents and load matching comments | withWhereHas('comments', ...) | Same predicate on selection and loading |
| Decide after posts are fetched | load(...) or loadMissing(...) | Do not repeat loads unintentionally |

whereHas, with, and withWhereHas answer different questions
Suppose a product requirement says: “Show posts with at least one approved comment, and show only approved comments under each.” whereHas('comments', fn ($q) => $q->where('approved', true)) selects the qualifying posts; it does not fill $post->comments. with(['comments' => fn ($q) => $q->where('approved', true)]) loads approved comments for the selected posts; by itself it does not exclude posts with no approved comments. The code can use both, or Laravel's documented withWhereHas applies the same relationship condition to existence filtering and eager loading.
The distinction matters for correctness, not merely speed. If whereHas checks approved comments but plain with('comments') loads all comments, the UI can expose pending or rejected entries that the filter implied were excluded. If with is constrained but the parent query is not, the result set may contain posts with an empty comments collection. Ask for the intended semantics and test a post with only unapproved comments.
load() is useful after the parent collection is already fetched, when a branch of the code determines that related data is needed. loadMissing() avoids loading a relationship that is already loaded. Neither makes an unbounded response safe: load() can still hydrate many rows, and code calling it inside an iteration may still produce repeated requests. Work at collection scope when the API supports it, then measure.
For nested access, inspect every edge. Post::with('comments.author')->get() can load comment authors, but if the view also reads tags, reactions, and moderation events, a new N+1 can remain. A query log or instrumentation trace should show counts grouped by SQL fingerprint as post count grows from one to ten to one hundred. The signature of N+1 is growth proportional to entity count. A constant number of slow queries points instead to indexing, selectivity, network latency, or oversized payloads.
Where does the loading boundary belong?
Put the data-access plan close to the use case: controller, query object, repository method, or resource transformation that knows what the response renders. Hiding eager loading in a global model default can be convenient for a universally required relation but wasteful when another endpoint only needs a scalar. Conversely, leaving every Blade component free to trigger lazy loading turns query behavior into a template side effect. The interview answer should name the boundary and show how it is tested.
Laravel's preventing lazy loading section documents Model::preventLazyLoading(...), often enabled outside production to expose accidental property access. This is a diagnostic policy, not a replacement for modeling the query. A violation tells you where a lazy access occurred; it does not tell you whether eager loading, a count, a join, or a changed UI is the best fix. When adding the guard to an existing application, expect failures in previously implicit paths and prioritize by traffic and response cost.
Ask about pagination. Loading 20 parents and their matching children is different from loading every parent and paginating a PHP collection. The parent query should paginate before eager loading. If a relation is genuinely huge, consider pagination or a separate endpoint for the children rather than embedding all of them. A two-query design that returns a million rows can be worse than a few well-indexed queries.
Select only columns needed, but preserve keys required for relationship matching. A parent select that omits the primary key or a child select that omits its foreign key can break hydration or produce confusing empty relations. Test filtered and empty cases. A database index on comments.post_id, possibly with a predicate-relevant column, can improve the second query; confirm with an execution plan because index utility depends on data distribution and query shape.
Security constraints belong in the same review. If posts are tenant-scoped but a nested relation query omits a tenant predicate or relies on a scope that can be bypassed, faster eager loading can expose the wrong records. A query-count test will not detect that. Include two tenants in the fixture and assert that neither the count nor the loaded children cross the boundary. If authorization happens at the application layer, ensure the list query and the detail endpoint enforce compatible rules. Performance work is complete only when it preserves the data contract and access policy.
How would you prove the fix in an interview?
State the initial behavior and expected query growth. For the ten-post fixture, show roughly 11 queries when comments are accessed lazily. With with('comments'), expect roughly two for the simple relation, but still count returned comment rows. If the page needs only counts, show withCount and verify approved-only semantics. If it needs approved child rows, test withWhereHas against three posts: one with approved comments, one with only unapproved comments, and one with no comments.
Then scale the fixture. Compare query counts and total rows for 1, 10, and 100 posts. Confirm response content, order, and authorization constraints. Capture SQL with Laravel's query instrumentation or a testing helper in your application, and assert the expected upper bound for the intended page shape. Avoid brittle assertions that every driver or framework version must issue exactly two SQL statements; the important regression is proportional growth or incorrect data.
A concise answer is: “I would first trace which relationship property the view reads and measure SQL as parent count grows. with() solves a specific parent-to-child N+1; whereHas() filters parent existence; withWhereHas() aligns selection with loaded children; withCount() avoids hydrating rows when only a count is needed. I would put that plan at the response boundary and test both query growth and returned data.”
Practice connecting this trace to Explain Database Index Benefits and Write Costs. Faster SQL and correct Eloquent loading are complementary; neither excuses a response that returns the wrong children.
Comments (0)