Solving the N+1 Query Problem in Yii2 ActiveRecord
Learn how to eliminate the N+1 query problem in Yii2 using ActiveRecord's eager loading capabilities to significantly improve database performance.
29 Sept 2025, 20:14 UTC

The Silent Performance Killer: N+1 Queries
You have a page listing 50 recent orders, and for each order, you display the customer's name. You use a simple foreach loop to iterate through the orders and call $order->customer->name. On the surface, the code is clean and readable. Under the hood, however, your application is executing one query to fetch the orders and then 50 separate queries to fetch the customer for each individual order.
This is the N+1 query problem. As your dataset grows, your page load time increases linearly, eventually crashing your database under the weight of redundant requests. The solution in Yii2 is Eager Loading, which collapses those dozens of requests into a few optimized queries.
Lazy Loading vs. Eager Loading
By default, Yii2 ActiveRecord uses Lazy Loading. This means related data is only fetched from the database the moment you access the property for the first time. While this saves memory if you don't always need the related data, it is disastrous inside loops.
Eager Loading tells Yii2 to fetch the primary models and all requested related models in one go. Instead of executing a query for every single row, Yii2 executes one query for the main model and one additional query for each relationship, using an IN clause to fetch all relevant related records at once.
Implementing Eager Loading with with()
To implement eager loading, you use the with() method during the initial query construction. This method accepts the name of the relationship defined in your ActiveRecord model.
Example: Optimized Order Retrieval
Assume you have an Order model with a getCustomer() relationship defined. Here is the comparison between the inefficient approach and the optimized approach.
Inefficient (Lazy Loading):
// This triggers N+1 queries
$orders = Order::find()->all();
foreach ($orders as $order) {
echo $order->customer->name; // Query executed here for every iteration
}
Optimized (Eager Loading):
// This triggers exactly 2 queries regardless of the number of orders
$orders = Order::find()
->with('customer')
->all();
foreach ($orders as $order) {
echo $order->customer->name; // Data is already loaded in memory
}
Handling Nested Relationships
If you need to go deeper—for example, fetching the Order, the Customer, and the Customer's Address—you can pass an array or use dot notation:
$orders = Order::find()
->with('customer.address')
->all();
Trade-offs and Memory Constraints
Eager loading is not a silver bullet. Because it loads all related records into memory at once, it can lead to high RAM usage if you are fetching thousands of records with multiple heavy relationships.
Furthermore, with() performs a separate query (using IN), which is generally more efficient for ActiveRecord. However, if you need to filter your primary results based on a value in the related table (e.g., "Find all orders where the customer is from New York"), with() will not work because it fetches the related data after the primary query. In those cases, you must use joinWith(), which performs a SQL JOIN and allows you to use the related table in the where() clause.
Verifying the Fix
To confirm that eager loading is working and you have eliminated the N+1 problem, you should monitor the actual SQL being executed. In a development environment, you can use the Yii2 Debug Toolbar.
- Open the Database section of the debug toolbar.
- Count the number of queries executed for your specific action.
- If you see a repeating pattern of
SELECT * FROM customer WHERE id = ..., you are still lazy loading. - If you see one
SELECT * FROM orderfollowed by oneSELECT * FROM customer WHERE id IN (1, 2, 3...), the optimization is successful.