f in x
Advanced Eloquent ORM: Optimized Query Builder, Relationships, Eager Loading and N+1 Problem
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

Advanced Eloquent ORM: Optimized Query Builder, Relationships, Eager Loading and N+1 Problem

[2026-05-29] Author: Ing. Calogero Bono

Every Laravel developer knows Eloquent, but few leverage its full potential in high-concurrency applications. This guide dives deep into advanced techniques for optimizing queries, handling complex relationships, solving the dreaded N+1 problem, and using eager loading intelligently. You will not find basic concepts here, only proven strategies to write performant, maintainable, and scalable code. Whether you are building a REST API, an admin panel, or a real-time application, mastering these aspects of Eloquent ORM is essential to avoid bottlenecks and ensure fast response times.

Optimized Query Builder: Beyond Basic Queries

Using Subqueries to Reduce the Number of Queries

One of the most powerful techniques for performance optimization is using subqueries instead of inefficient loops. Instead of loading all entities and then filtering with PHP, delegate the work to the database.
Example: select users with their last published post.

$users = User::addSelect(['last_post_title' => Post::select('title')
    ->whereColumn('user_id', 'users.id')
    ->latest()
    ->limit(1)
])->get();

This query executes a correlated subquery, returning a virtual attribute without loading complete relationships.

Lazy Loading vs Conscious Eager Loading

Lazy loading is convenient but deadly for performance. In production environments, disable global lazy loading with Model::preventLazyLoading() in a service provider and use explicit eager loading. Use load() only when necessary after the main query, or with() upfront. To optimize, specify exactly the columns needed:

$posts = Post::with(['user:id,name', 'comments:post_id,body'])->get();

Avoid loading unused fields with select() at the relationship level.

Chunking and Cursor for Large Datasets

When working with millions of rows, get() can cause memory overflow. Use chunk to process in batches:

Post::chunk(200, function ($posts) {  // batch processing });

For faster iteration with lower memory, cursor leverages PHP generators:

foreach (Post::cursor() as $post) {  // process one by one }

Complex Relationships and Their Optimization

Polymorphic Relationships: When and How to Use Them

Polymorphic relationships allow a model to belong to multiple other models with a single pivot table. Example: comments associated with posts or videos. Optimize by indexing the commentable_id and commentable_type fields.

class Comment extends Model {    public function commentable()    {        return $this->morphTo();    }}

For eager loading with polymorphics, specify the expected types:

$comments = Comment::with('commentable')->get();

Add composite indexes to avoid full table scans.

Many-to-Many Relationships with Custom Pivot Tables

Pivot tables often hold additional data (e.g., quantity in an order). Use withPivot() to include them and wherePivot() to filter. To optimize, avoid loading all pivot rows when not needed.

$user->roles()->withPivot(['expires_at'])->wherePivot('active', 1)->get();

Has-Many-Through and Remote Relationships

Has-many-through relationships allow accessing posts of an author through their books, but beware: they can generate complex subqueries. Always check the execution plan and, if necessary, replace with explicit joins. Example:

$authors = Author::with('posts')->get(); // hasManyThrough

The N+1 Problem: Detection and Definitive Solutions

What is the N+1 Problem

It occurs when for each parent record (1) you execute a separate query for a relationship (N). Classic example: looping over users and for each one loading their posts. With 100 users, you get 1 + 100 queries = 101 queries. Eager loading with with() reduces to 2 total queries.

Automatic Detection with Laravel Debugbar and Telescope

Use tools like Laravel Debugbar or Laravel Telescope to monitor executed queries. Enable Model::preventLazyLoading() in development environments: you will get an exception every time lazy loading is invoked unintentionally.

Nested Eager Loading and Selective Loading

To avoid N+1 on multiple levels, use nested eager loading with dot notation:

$posts = Post::with('user.profile', 'comments.author')->get();

Also, use lazy eager loading (load()) when the relationship is not known upfront but only after a condition. Be careful: load() inside a loop can also cause N+1; load in batch before the loop:

$posts = Post::all();
$posts->load('comments'); // single additional query

Alternatives to Classic Loading: Lazy Collections and N+1 on Collections

If you need to filter or transform an already loaded collection, use LazyCollection to avoid keeping everything in memory. For N+1 on collections (e.g., accessing derived properties), consider using append with computed attributes or API Resources with preventive eager loading.

Best Practices and Advanced Patterns

Optimized Global and Local Scopes

Global scopes automatically apply conditions to every query on a model. They can be useful for soft deletes or multi-tenancy, but be aware: every query includes them, so ensure they are indexed. Local scopes are preferred for optional filters. Example of a local scope for published posts:

public function scopePublished($query) {    return $query->whereNotNull('published_at');}

Indexing and Query Profiling

Even the best eager loading fails if tables are not indexed. Analyze slow queries with EXPLAIN and add composite indexes on columns used in JOIN, WHERE, and ORDER BY. For belongsTo and hasMany relationships, index the foreign key column.

Conditional Lazy Loading with Dynamic Relationships

In some cases, eager loading is not flexible. In Laravel 11+, you can define dynamic relationships that activate only under conditions, reducing load. Use when() in the query builder to include optional relationships:

$query = Post::query();
if ($request->has('include_comments')) {    $query->with('comments');}

Caching for Read-Heavy Relationships

For frequently read and rarely modified data (e.g., post categories), use caching with remember() or the Cache-Aside pattern. Combine with model touch to automatically invalidate the cache.

Summary and Concrete Next Steps

For a performant Laravel application, mastering advanced Eloquent ORM is crucial. Key takeaways:

  • Disable lazy loading in development with preventLazyLoading().
  • Use explicit eager loading with with() specifying only needed columns.
  • Adopt subqueries to reduce query count when possible.
  • Replace generic loops with chunk() or cursor() for large datasets.
  • Profile queries with Debugbar or Telescope and index appropriately.
  • Consider caching for relationships that change infrequently.

For further reading, check the official Laravel documentation on Eloquent Relationships. For a broader overview of Laravel 11 and 12, visit our Laravel 12 app structure guide and the Modern PHP 8 guide.

Sponsored Protocol

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()