Filtering Related Models with Constrained Eager Loading in Laravel
Avoid N+1 query problems by loading only the related records that meet specific conditions using Eloquent's with() method and closures.
16 Jun 2026, 08:21 UTC

The Problem: Over-Fetching Related Data
When using standard eager loading via with('relationship'), Laravel retrieves all related records. If you only need a subset—such as only "approved" comments for a list of blog posts—loading every single record into memory is inefficient. Filtering these records in PHP after they are loaded still results in the database transferring unnecessary data.
The solution is Constrained Eager Loading, which allows you to pass a closure to the with method to apply SQL WHERE clauses directly to the relationship query.
Prerequisites
- Laravel 8.x or later.
- A parent model (e.g.,
Post) with a defined relationship (e.g.,hasMany) to a child model (e.g.,Comment). - A database column on the child model to filter by (e.g., a boolean
approvedcolumn).
Implementation Procedure
To filter a relationship during eager loading, pass an array to the with() method where the key is the relationship name and the value is a closure containing the constraints.
use App\Models\Post;\n\n// Retrieve posts and only their approved comments\n$posts = Post::with(['comments' => function ($query) {\n $query->where('approved', true);\n}])->get();\n\nforeach ($posts as $post) {\n // This collection only contains records where approved = 1\n $approvedCount = $post->comments->count();\n}
Handling Soft Deletes
If the related model uses the SoftDeletes trait, Laravel automatically excludes trashed records. To include them while still applying your custom constraint, call withTrashed() inside the closure:
$posts = Post::with(['comments' => function ($query) {\n $query->where('approved', true)->withTrashed();\n}])->get();
Verification and Diagnostics
Because eager loading happens behind the scenes, it is critical to verify that the constraint is actually being applied at the database level rather than in memory.
1. Query Log Inspection
Run the following in a controller or tinker session to inspect the raw SQL executed by the application:
use Illuminate\Support\Facades\DB;\n\nDB::enableQueryLog();\n\n$posts = Post::with(['comments' => function ($query) {\n $query->where('approved', true);\n}])->get();\n\ndd(DB::getQueryLog());
Expected Result: You should see two queries (one for posts, one for comments). The second query must contain a WHERE clause similar to "approved" = ? with a binding of true or 1.
2. Collection Assertion
If writing a feature test, verify that no unapproved comments leaked into the collection:
$posts = Post::with(['comments' => function ($q) {\n $q->where('approved', true);\n}])->get();\n\n$posts->each(function ($post) {\n $this->assertTrue(\n $post->comments->every(fn($comment) => $comment->approved === true)\n );\n});
Limitations and Risks
- Relationship Scope: Constraints applied in
with()only affect that specific load. If you later call$post->comments()as a method (which returns a Query Builder), the constraints are not applied. - N+1 Risk: If you access a different relationship that wasn't included in the
with()array, Laravel will trigger a new query for every record in the loop. - Memory: While this reduces the number of records loaded, very large result sets can still exhaust PHP memory. Consider using
paginate()instead ofget().
Recovery Options
If the constrained eager load is causing unexpected results or performance bottlenecks due to complex indexes, you can use these alternatives:
Manual Relation Mapping (Fallback)
If you need to load different constraints dynamically per item, you can manually set the relation:
$posts = Post::all();\nforeach ($posts as $post) {\n $post->setRelation('comments', $post->comments()->where('approved', true)->get());\n}
Note: This introduces an N+1 problem and should only be used as a temporary debugging step or for very small datasets.
PHP-Level Filtering
If the database query is too slow due to the WHERE clause on a non-indexed column, load all records and filter using the collection filter() method:
$posts = Post::with('comments')->get();\n$posts->each(function ($post) {\n $post->setRelation('comments', $post->comments->filter(fn($c) => $c->approved));\n});0 replies
A thoughtful contribution can make all the difference. Be the first to share one.