M
MeshWorld.
Laravel Eloquent PHP Database 6 min read

Nested Eager Loading in Laravel Explained

Vishnu
By Vishnu
| Updated: Mar 27, 2026

Nested eager loading is a Laravel Eloquent technique that loads relationships of relationships in a single batch of queries. Without it, accessing a related model’s own relationships triggers additional database queries for each parent record — the classic N+1 problem. With it, you load three or four levels of related data in as few as three or four total queries, regardless of how many records are in the result.

:::note[TL;DR]

  • Eager loading loads related models alongside the main query, preventing N+1 queries
  • Nested eager loading loads relationships-of-relationships: with('owner.contacts')
  • Use dot notation for nested chains: 'owner.contacts.phone'
  • Use array syntax to load multiple relationships at the same level: with(['owner' => ['contacts', 'company']])
  • Add constraints inside a closure to filter what gets loaded
  • Available and unchanged in Laravel 10, 11, and 12 :::

What is the N+1 problem and why does eager loading solve it?

Without eager loading, accessing a relationship inside a loop fires one query per iteration. If you have 50 projects and access $project->owner in a loop, you get 51 queries — one for the projects, one for each owner.

Eager loading fires a second query that loads all owners at once and binds them to the right projects. Two queries total, regardless of the collection size.

Nested eager loading extends this to deeper chains. If each Owner has Contacts, lazy loading would fire queries for each owner’s contacts. Nested eager loading fetches all contacts in one additional query.

What does nested eager loading look like in code?

Scenario: You’re rendering a project dashboard. Each project has an owner, each owner has contacts and a company. Without eager loading, displaying 20 projects on this page fires dozens of queries. With nested eager loading, it’s three queries total — projects, owners, contacts + company.

Set up the relationships first. These are standard Eloquent definitions:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    public function owner()
    {
        return $this->belongsTo(Owner::class);
    }
}

class Owner extends Model
{
    public function contacts()
    {
        return $this->hasMany(Contact::class);
    }

    public function company()
    {
        return $this->belongsTo(Company::class);
    }
}

Now load everything in one call using the with() method. This query fetches projects and eagerly loads owners, their contacts, and their company:

$projects = Project::with([
    'owner' => [
        'contacts',  // load the owner's contacts
        'company',   // load the company associated with the owner
    ],
])->get();

This runs four queries: one for projects, one for owners, one for contacts, one for companies. No matter how many projects come back.

How do I use dot notation for nested chains?

Dot notation is the shorthand form. Each dot adds one level of nesting. Both of these are equivalent to the array syntax above:

// Dot notation — clean for simple chains
$projects = Project::with(['owner.contacts', 'owner.company'])->get();

Dot notation is fine for shallow chains. Use array syntax when a single relationship has multiple children — it reads more clearly.

For three levels deep:

// Projects → Owner → Company → Address
$projects = Project::with('owner.company.address')->get();

That’s still just four queries, not one per project.

Can I add conditions to what gets eagerly loaded?

Yes. Pass a closure as the value for any relationship key. The closure receives the query builder for that relationship, and you can constrain it with any where clause.

This loads only active contacts for each owner:

$projects = Project::with([
    'owner' => function ($query) {
        $query->select('id', 'name', 'company_id'); // load only needed columns
    },
    'owner.contacts' => function ($query) {
        $query->where('active', true); // only active contacts
    },
    'owner.company',
])->get();

You can also filter what company data comes back, add ordering, or limit the count. The closure gives you full query builder access for that relationship’s query.

:::warning When you constrain an eager load with select(), always include the foreign key columns that Eloquent uses to bind the relationships together. If you select('name') on an Owner without including id and project_id, the binding back to Project will break and you’ll get null relationships. Include the key columns: select('id', 'name', 'company_id'). :::

How do I display nested data in a Blade view?

Once the data is loaded, access it like any other relationship — Eloquent has already bound everything in memory. No additional queries fire.

@foreach ($projects as $project)
    <h2>{{ $project->title }}</h2>
    <p>Owner: {{ $project->owner->name }}</p>

    <h4>Contacts</h4>
    <ul>
        @foreach ($project->owner->contacts as $contact)
            <li>{{ $contact->email }}</li>
        @endforeach
    </ul>

    <p>Company: {{ $project->owner->company->name }}</p>
@endforeach

Accessing $project->owner->contacts here doesn’t fire a query — it reads from the already-loaded collection. That’s the whole point.

When should I use lazy eager loading instead?

Sometimes you don’t know at query time whether you’ll need a relationship. load() lets you eager load on a collection you already have:

$projects = Project::all(); // no eager loading yet

// Later in the code, once you know you need contacts:
$projects->load('owner.contacts');

This fires the relationship queries at the point of load(). It’s not as efficient as eager loading at query time — you already ran the initial query — but it’s better than N+1 inside a loop.

:::warning Avoid calling load() inside a loop. That defeats the purpose. Call it once on the full collection before the loop starts. :::

Summary

  • Nested eager loading loads relationships-of-relationships in a fixed number of queries, regardless of collection size
  • Use with(['relation.nested']) dot notation for simple chains
  • Use with(['relation' => ['child1', 'child2']]) array syntax when one relation has multiple children
  • Add closures to constrain what gets loaded — but always include foreign key columns in select()
  • Use ->load() for lazy eager loading on an already-fetched collection
  • All of this is unchanged in Laravel 10, 11, and 12

FAQ

How many queries does nested eager loading fire? One query per distinct relationship loaded. If you load owner.contacts and owner.company, that’s three queries total: one for the projects, one for owners, one for contacts, one for companies. The depth of nesting doesn’t change the count — it’s always one query per relationship.

Does nested eager loading work with polymorphic relationships? Yes. You can eager load morphTo and morphMany relationships using the same with() syntax. Laravel handles the polymorphic type resolution during eager loading.

Can I eager load relationships defined on a pivot model? Yes, using withPivot() and with() together. For belongsToMany relationships with a custom pivot model, define the pivot relationships and include them in your with() call.

What’s the difference between with() and load()? with() is called before the query runs and bundles the relationship queries in the same operation. load() is called after ->get(), on an existing collection. Both prevent N+1 — with() is just more efficient because it’s planned upfront.

Does eager loading work with pagination? Yes. Project::with('owner.contacts')->paginate(20) works correctly. Only the paginated records’ relationships are loaded — not the entire table’s.