Eliminating the N+1 Problem in Laravel Applications

Understanding the N+1 Query Trap

The N+1 problem occurs when your application executes one query to fetch a parent record and then executes a separate query for every child record associated with it. In a

application, this usually happens when you iterate over a collection of models and access a relationship that hasn't been loaded. For instance, fetching three products and then querying the database for each product's prices results in four total queries. While negligible at a small scale, this pattern scales poorly; a hundred products will trigger a hundred and one queries, drastically slowing down your application.

Prerequisites

To follow this guide, you should have a baseline understanding of

and the
Laravel
framework. You should be familiar with
Eloquent ORM
relationships and how to define them within your models.

Key Libraries & Tools

  • Laravel
    : A robust PHP framework providing the Eloquent ORM.
  • Eloquent ORM
    : The database mapper used to interact with your data via PHP classes.
  • DB Facade: A tool used here to enable query logging for performance monitoring.

Solving the Issue with Eager Loading

The most direct solution is using the with() method. This instructs

to fetch all related records in a single additional query using a WHERE IN statement.

// Instead of lazy loading, we eager load the relationship
$products = Product::with('prices')->get();

If you find yourself always needing a relationship, you can automate this by adding the $with property to your

model. This ensures the relationship is always present without manual intervention in your controllers.

class Product extends Model {
    protected $with = ['prices', 'variants'];
}

Granular Control: Without and WithOnly

Sometimes, global eager loading is overkill. If you have the $with property set but want to exclude a specific relation for a single request, use the without() method. Conversely, withOnly() overrides the model's defaults entirely, ensuring you only pull exactly what you need for that specific execution context.

Enforcing Best Practices with PreventLazyLoading

introduced a powerful safety net: Model::preventLazyLoading(). By placing this in your AppServiceProvider, the framework will throw a LazyLoadingViolationException whenever you accidentally trigger an N+1 query.

public function boot()
{
    Model::preventLazyLoading(! app()->isProduction());
}

This configuration is a developer's best friend. It forces you to write clean, eager-loaded code during development while ensuring your production environment remains stable for end users.

Syntax Notes & Tips

  • Query Logging: Use DB::enableQueryLog() and DB::getQueryLog() to verify exactly how many queries your code triggers.
  • Array Syntax: The $with property and the with() method both accept arrays, allowing you to load multiple relationships simultaneously.
  • Nested Loading: You can eager load nested relations using dot notation, such as with('prices.currency').
3 min read