Reducing N+1 Queries in Yii 2 with ActiveRecord's with() Method
Learn how Yii 2’s ActiveRecord with() method eager‑loads relations to turn N+1 query patterns into a predictable, small number of SQL statements, with a concrete paginated blog example and guidance on trade‑offs.
02 Aug 2026, 08:01 UTC

Problem: Loading a list of posts with their comments triggers many queries
When you display a list of blog posts and want to show each post’s comments, a naive approach loads the posts first and then, inside a loop, queries the comments for each post. This pattern creates an N+1 query problem: one query for the posts plus one additional query for every post to fetch its comments. As the list grows, the number of round‑trips to the database can become a performance bottleneck.
Thesis: Yii 2’s ActiveRecord with() method eager‑loads relations in a predictable way, cutting the query count to a small, known number while keeping the code readable.
How eager loading works in Yii 2
The with() method tells Yii’s query builder to prefetch related data when the primary query is executed. You pass an array of relation names that match those defined in the model’s relations() method. Yii then generates either a single SQL JOIN (when possible) or a second query that fetches all related rows and maps them back to the primary models.
Important behaviors to know:
- Simple eager loading:
Post::find()->with(['comments'])->all()results in two queries: one for posts, one for all comments of those posts. - Nested relations: You can dot‑separate relation names, e.g.
with(['comments.author'])to load the author of each comment. Yii automatically aliases tables to avoid column conflicts. - Pagination interaction: When you add
limit()oroffset()to the primary query, Yii splits the work: first a query for the primary models (respecting pagination), then a second query that fetches the related rows for those specific models. This preserves correct pagination counts.
Worked example: a blog index with paginated posts and comments
Assume you have a Post model with a relation comments and a Comment model with a relation author. The goal is to show 10 posts per page, each with its comments and the comment author’s name.
// In a controller action, e.g. PostController::actionIndex()
use yii\data\Pagination;
$query = Post::find()
->with([
'comments', // load comments for each post
'comments.author' // load the author of each comment
])
->orderBy(['created_at' => SORT_DESC]);
$count = $query->count();
$pagination = new Pagination(['totalCount' => $count, 'pageSize' => 10]);
$posts = $query->offset($pagination->offset)
->limit($pagination->limit)
->all();
// In the view file (index.php)
foreach ($posts as $post):
echo ''.htmlspecialchars($post->title).'
';
echo ''.htmlspecialchars($post->content).'
';
echo '';
foreach ($post->comments as $comment):
echo '- '.
htmlspecialchars($comment->text).' — '.
htmlspecialchars($comment->author->username).
'
';
endforeach;
echo '
';
endforeach;
Where to run: Place the controller code in controllers/PostController.php and the view in views/post/index.php. No special permissions are required beyond the usual web‑server access to the application.
Expected checks: Enable Yii’s debug toolbar or add a temporary log line Yii::getLogger()->log($query->createCommand()->getRawSql(), Logger::LEVEL_INFO) before executing the query. You should see two SQL statements: one selecting the paginated posts, and a second selecting comments (and authors) where the post ID is in the set returned by the first query.
Risks: The second query may return a large number of rows if the posts have many comments. This increases memory usage because all related rows are hydrated into PHP objects. For very large datasets consider:
- Limiting the depth of eager loading (e.g., only load comments, not authors).
- Using lazy loading inside the view and accepting the N+1 cost when the dataset is small.
- Batching the primary query with
each()or processing posts in chunks.
Trade‑off and limitation
Eager loading reduces query count but can increase the amount of data transferred from the database and the memory footprint of the PHP process. If a relation is defined with a complex join table or additional conditions, the generated SQL may become less efficient than a series of targeted queries. Always profile with the debug panel or Yii::getDb()->getSchema()->getTableSchema() to verify that the actual execution time meets your requirements.
Actionable closing
To adopt eager loading safely:
- Identify loops that access a relation on each model.
- Add the relation name (or nested dot‑separated path) to a
with()call on the base query. - Run the page with the debug toolbar enabled and confirm that the number of queries dropped as expected.
- Monitor memory usage (e.g., via
memory_get_usage()) under realistic loads; if it spikes, consider limiting the eager‑loaded depth or reverting to lazy loading for that relation.
By following these steps you keep the code clean, reduce database round‑trips, and maintain visibility into the performance impact of your data‑fetching strategy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.