Taming the N+1 Problem in Yii2 with Eager Loading
Eager loading with Yii2’s <code>with()</code> solves the N+1 query problem. Learn how to apply it, the trade‑offs, and a concrete example with articles, comments, and authors.
12 Nov 2025, 00:04 UTC

Why the N+1 Problem Matters
When a PHP application uses Yii2 Active Record, a common pitfall is the N+1 query pattern. You query a parent model once, then loop over its children, and for each child you fire another query to fetch related data. The result is a linear increase in database round-trips, which can kill performance on large result sets.
Thesis: Use with() for Eager Loading
The with() method tells Yii2 to pull related models in a separate query right after the parent query. This shifts the work from the application layer to the database layer, drastically reducing the number of SQL statements and overall latency.
Understanding with() Syntax
- Basic eager loading:
Post::find()->with('author')->all(); - Nested relations:
Post::find()->with('comments.author')->all(); - Conditional eager loading – filter, sort, or limit the related data:
$posts = Post::find()->with(["comments" => function($q){ $q->orderBy(['created_at' => SORT_DESC])->limit(5); }])->all();
Concrete Example: Articles, Comments, and Authors
Assume we have three tables: article, comment, and author. The following Active Record relations are defined:
class Article extends ActiveRecord
{
public function getComments()
{
return $this->hasMany(Comment::class, ['article_id' => 'id']);
}
public function getAuthor()
{
return $this->hasOne(Author::class, ['id' => 'author_id']);
}
}
Lazy-loading code (N+1 in action):
$articles = Article::find()->all();
foreach ($articles as $article) {
echo $article->title;
foreach ($article->comments as $comment) {
echo $comment->content;
echo $comment->author->name; // another query per comment
}
}
With eager loading, the same logic only triggers three queries:
$articles = Article::find()
->with(["comments.author"])
->all();
foreach ($articles as $article) {
echo $article->title;
foreach ($article->comments as $comment) {
echo $comment->content;
echo $comment->author->name; // already loaded
}
}
Verify the query count with the Yii Debug Toolbar: you should see one query for articles, one for comments, and one for authors, regardless of how many articles or comments you iterate over.
Trade‑offs and Limitations
- Memory consumption: Eager loading pulls all related rows into memory as Active Record objects. If you load 10,000 comments, the process may exhaust PHP memory. Use
limit()oroffset()on the eager relation to constrain the data set. - Unnecessary data: Eager loading a relation that the view never uses wastes a database round‑trip. Keep eager loading focused on the data you actually need.
- Complex joins: Deeply nested eager loading can generate large SQL statements. Monitor query execution time and consider using
joinWith()for join‑based eager loading if you need to filter on related columns.
Actionable Checklist
- Identify the N+1 hotspot by running the app with the Debug Toolbar and counting queries.
- Replace the lazy‑loading relation call with
with()orjoinWith()as appropriate. - Apply filters or limits inside the eager‑loading closure to avoid over‑fetching.
- Re‑run the Debug Toolbar; the number of queries should drop to the number of top‑level tables accessed.
- Profile memory usage with
memory_get_usage()before and after eager loading to ensure the application stays within limits. - Document the change and update unit tests to assert the query count.
By carefully applying eager loading, you can eliminate the N+1 problem, reduce latency, and keep your Yii2 application responsive even as data volumes grow.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.