Eloquent performance: 10 pattern che rallentano le tue query senza che tu lo sappia
Il 15 settembre 2025 mi ha contattato il socio tecnico di un'agenzia web milanese che sviluppa piattaforme e-commerce custom per un portafoglio di 23 clienti finali, prevalentemente PMI italiane del settore arredamento, moda B2B e ricambistica. Il problema era grave e ricorrente: tre dei loro clienti più grandi avevano aperto nello stesso mese ticket di reclamo con lo stesso contenuto - "il sito diventa lentissimo quando abbiamo traffico". Il socio tecnico aveva già provato di tutto sui suoi cinque server Hetzner: upgrade di RAM su due macchine, aggiunta di Redis come object cache, introduzione di OPcache con tuning aggressivo, persino la migrazione di un cliente a un server più potente. I miglioramenti erano marginali e puntuali, i clienti restavano insoddisfatti, e il socio tecnico iniziava a temere che l'agenzia avesse un problema architetturale strutturale che li avrebbe costretti a una riscrittura sistematica del codebase condiviso fra tutti i progetti Laravel.
Per 48 ore consecutive, dal 17 al 19 settembre, abbiamo installato Laravel Telescope in modalità read-only su tutti e tre gli ambienti di produzione dei clienti in sofferenza, con configurazione minimale che catturava solo le query SQL e i tempi di risposta senza impattare le performance del sistema osservato. Alla fine delle 48 ore il dump di analisi era impietoso: la pagina di listing prodotti di un cliente medio emetteva mediamente 340 query SQL per richiesta, di cui 280 erano N+1 classici generati da accessi non ottimizzati a relazioni Eloquent; la dashboard amministrativa di un altro cliente caricava 2.300 record completi solo per mostrarne 20 in prima pagina, saturando 80 MB di memoria PHP a richiesta; il modulo di reportistica di un terzo cliente faceva eager loading di sette relazioni quando ne usava effettivamente due, caricando decine di migliaia di record inutili in RAM. Il problema architetturale non esisteva: il problema era la disciplina di scrittura di query Eloquent nel codebase quotidiano del team.
In sei giornate di lavoro distribuite in tre settimane, affiancandomi al team interno durante il loro normale ciclo di sviluppo, ho identificato dieci pattern ricorrenti di errore nel loro modo di scrivere codice Eloquent, ho documentato ciascuno con esempi concreti dal loro codice e con il fix corrispondente, e ho insegnato loro come prevenire questi pattern in fase di code review prima che raggiungano produzione. Il tempo medio di risposta delle pagine critiche sui tre clienti è migliorato di un fattore che va da 6x a 22x. Nessun upgrade hardware, nessuna riscrittura architetturale, nessun cambiamento di framework: solo la disciplina di scrivere Eloquent in modo consapevole delle sue trappole. Questo articolo è la lista dettagliata di quei dieci pattern, esattamente come li ho presentati al team milanese durante le sessioni formative, con l'obiettivo che chiunque legga possa riconoscerli nel proprio codice e fissarli prima che diventino un ticket di reclamo da parte del cliente finale.
Perché Laravel Telescope è lo strumento di profiling più sottovalutato dalle software house PMI?
Il primo pilastro metodologico per qualunque intervento serio di ottimizzazione Eloquent è misurare con strumenti giusti cosa fa davvero la tua applicazione quando gira con traffico reale. L'errore più comune che vedo nelle software house PMI è fare ottimizzazione a sentimento - il senior developer apre un file, guarda il codice del controller "che sembra lento", e riscrive cose che ipotizza siano il collo di bottiglia. Senza misurazione reale, il risultato è quasi sempre ottimizzare cose che contano per il 3% del tempo totale ignorando il collo di bottiglia vero che conta per il 75%. La documentazione ufficiale di Laravel Telescope descrive nel dettaglio i watcher disponibili e la loro configurazione per ambienti di produzione, e va letta attentamente prima di qualunque deploy - Telescope non è un tool di sviluppo, è un tool di profiling di produzione quando configurato correttamente.
Il setup che applico sempre in contesti PMI è questo. In ambiente di produzione, Telescope viene configurato con un sampling rate basso (1% delle richieste nei miei progetti tipici), con i watcher abilitati limitati a Queries, Requests e Slow Queries, con retention dei dati a 72 ore. L'accesso alla dashboard è protetto da middleware di autorizzazione che consente solo a IP specifici o utenti con ruolo superadmin, mai aperto a tutto il mondo. La configurazione specifica si fa nel file config/telescope.php con queste direttive. Primo, il filtering delle richieste tramite closure Telescope::filter() che accetta solo l'1% delle richieste casuali più il 100% delle richieste con codice HTTP 5xx o con durata superiore ai 2 secondi. Secondo, il pruning automatico ogni notte via scheduler che elimina i dati vecchi oltre le 72 ore. Terzo, l'isolamento del database Telescope su una connessione dedicata verso un database separato da quello applicativo - perché i watcher stessi generano scritture, e non vuoi che Telescope inquini le metriche di quello che sta misurando.
Con questo setup, dopo 48 ore di raccolta dati in produzione hai un database Telescope popolato con decine di migliaia di request campionate, e puoi estrarre ordinamenti molto precisi: le 20 pagine che generano più query medie, le 20 pagine con la varianza più alta (spesso indicatrice di N+1 latenti), le singole query che superano i 500 ms di media, i controller che richiedono più memoria PHP. Questo è il punto di partenza di qualunque intervento serio di ottimizzazione Eloquent - fare diagnosi prima di fare terapia. La stessa metodologia di profiling preliminare è alla base dell'approccio che ho descritto nel mio articolo sul tuning Doctrine ORM per applicazioni Symfony in produzione con query builder e DQL ottimizzati - lo stesso pattern metodologico si applica a entrambi gli ORM PHP più diffusi nel 2026.
Se stai gestendo un'applicazione Laravel in produzione con problemi di performance che hai difficoltà a localizzare, o se vuoi un'analisi indipendente dei reali colli di bottiglia prima di investire in upgrade hardware o riscritture, nel mio profilo professionale trovi il dettaglio degli interventi di tuning Eloquent e profiling di produzione che ho condotto in contesti PMI italiane, sempre con metodologia misurabile prima di qualunque intervento correttivo.
Pattern 1-4: relazioni Eloquent non ottimizzate, il 70% del problema tipico
Il primo blocco di pattern problematici riguarda la gestione delle relazioni tra model Eloquent, ed è statisticamente il gruppo responsabile del 60-75% del degrado di performance nelle applicazioni Laravel PMI. Sono tutti cugini dello stesso problema architetturale: Eloquent di default carica le relazioni in modo lazy, una query per ogni relazione accessa, e il codice applicativo che non si rende conto di questo comportamento emette decine o centinaia di query senza rendersene conto.
Pattern 1: lazy loading non dichiarato in cicli e template. Il caso canonico è $orders = Order::all(); seguito da foreach ($orders as $order) { echo $order->customer->name; }. Ogni accesso a $order->customer genera una query separata - 100 ordini producono 101 query totali. Il fix è dichiarare l'eager loading esplicito: Order::with('customer')->get(). Nei template Blade lo stesso pattern emerge con accessi a relazioni nidificate: {{ $order->customer->company->vatNumber }} genera tre query se tutte e tre le relazioni non sono state caricate. Il fix è eager loading annidato: Order::with('customer.company')->get().
Pattern 2: eager loading eccessivo. L'opposto del problema precedente, altrettanto dannoso: il codice carica 12 relazioni "nel caso servano", ma ne usa effettivamente solo 3. Il tempo per query non cambia molto, ma il consumo di memoria PHP e di banda di rete versus database esplode. La pratica che insegno al team è caricare solo le relazioni che l'intera request userà realmente, non quelle che potrebbe usare. Se la stessa query serve pagine diverse con bisogni diversi, costruire metodi di repository distinti (listWithCustomer(), listWithCustomerAndProducts()) invece di un unico metodo che carica tutto sempre.
Pattern 3: constrained eager loading mancante. Quando carichi una relazione hasMany che ha potenzialmente migliaia di record figli, caricarli tutti è uno spreco. Eloquent supporta il constrained eager loading che filtra e limita i record caricati: Order::with(['items' => fn ($q) => $q->where('active', true)->limit(10)])->get(). Questo pattern è particolarmente importante per model con relazioni che crescono monotonicamente nel tempo (log, movimenti, attività, commenti) dove il default di caricare tutto produce eager loading che consuma memoria non limitata.
Pattern 4: uso di load() tardivo quando si poteva fare upfront. Vedo spesso codice che fa $orders = Order::all(); if ($user->isAdmin()) { $orders->load('privateNotes'); }. Questa seconda query è spesso accettabile, ma in alcuni casi (quando stai dentro un ciclo o un job schedulato) generare multiple caricamenti sequenziali produce race condition e complessità non necessaria. Quando sai in anticipo che servirà la relazione, preferire sempre il with() iniziale.
Sul cliente milanese, questi quattro pattern messi insieme coprivano il 280 delle 340 query totali della pagina di listing. Applicando with() corretto su tre relazioni (customer, category, primaryImage) e constrained eager loading su una quarta (latestMovements limitato agli ultimi 5), le query sono scese da 340 a 18, e il tempo della pagina è passato da 4,2 secondi a 380 ms.
Pattern 5-7: cast automatici, eventi nascosti e scope inefficienti
Pattern 5: $casts pesanti eseguiti a ogni idratazione. Eloquent supporta casting automatico di attributi - date, decimal, json, encrypted, enum. Ogni cast è eseguito a ogni lettura del modello, e alcuni cast sono computazionalmente pesanti (encryption/decryption con OpenSSL, deserializzazione JSON complessa). Se un modello ha 15 attributi castati e ne leggi 10.000 istanze in un report, stai eseguendo 150.000 operazioni di cast. Il pattern corretto è tenere i cast minimali sui modelli usati in query massive o usare DB::table() per report dove l'oggetto Eloquent non serve. I cast costosi (encryption, json complesso) vanno usati solo sui modelli realmente acceduti come entità singole, non nelle liste massive.
Pattern 6: model events silenziosi che rallentano ogni save(). I model observer e gli event listener agganciati a saving, saved, creating, created vengono eseguiti sempre a ogni operazione, anche quando non sai che esistono. Vedo regolarmente codebase dove Order::save() fa scattare sei observer (logging, audit trail, notification, cache invalidation, search reindex, webhook esterni), e ogni save() impiega 200 ms non per la scrittura SQL ma per l'orchestrazione degli observer. Il fix è spostare gli observer pesanti su queue asincrone e lasciare nel ciclo sincrono solo gli observer che devono assolutamente essere atomici con la scrittura. Per batch massivi usare Model::withoutEvents(fn () => ...) per bypassare completamente gli observer.
Pattern 7: scope Eloquent che nascondono query non ottimizzate. Il pattern $user->posts()->published()->popular()->get() è elegante e leggibile, ma ogni scope aggiunge complessità alla query finale, e gli scope che fanno subquery correlate possono trasformare una query semplice in un piano di esecuzione disastroso. Ho visto casi in cui un singolo popular() scope aggiungeva una subquery correlata che trasformava una query da 40 ms a 4 secondi. La disciplina che insegno è: ogni scope Eloquent deve essere ispezionato con toRawSql() e EXPLAIN del MySQL per verificare che il piano finale sia sensato. Quando uno scope è lento, riscriverlo come metodo di query builder esplicito è sempre un'opzione.
Pattern 8-10: aggregazioni, chunking e idratazione non necessaria
Pattern 8: aggregazioni fatte in PHP invece che in SQL. Il pattern $orders->sum('total') eseguito su una Collection PHP richiede il caricamento di tutti gli ordini in memoria. Su 50.000 ordini può essere 200 MB di RAM e 6 secondi di tempo. La versione SQL pura Order::sum('total') fa la stessa operazione in 40 ms senza idratare un solo modello PHP. Lo stesso vale per count() (preferire count() SQL a count($collection)), per avg() e max(). La regola: qualunque aggregazione su dataset superiore a 1.000 record va fatta in SQL, non in PHP. Lo stesso principio si applica a withCount() per conteggi di relazioni documentato ufficialmente: User::withCount('posts')->get() è enormemente più efficiente di iterare sugli user e contare i post.
Pattern 9: iterazione massiva senza chunking. Il pattern User::all()->each(fn ($u) => $u->doSomething()) su 100.000 utenti carica tutti i 100.000 in RAM prima di iniziare a processarli. Il fix è User::chunkById(500, fn ($users) => $users->each(fn ($u) => $u->doSomething())), che carica 500 utenti alla volta con memoria costante. Il variant chunkById è da preferire al chunk classico perché evita i bug di paginazione quando il dataset viene modificato durante l'iterazione stessa (classico in job che eliminano o modificano i record che stanno iterando). Questi pattern di elaborazione batch efficiente sono complementari alle tecniche di caching multilivello Laravel per strategie ad alto traffico che ho documentato in un articolo dedicato, dove il caching e il chunking si intersecano per pattern di job asincroni di ricostruzione periodica di view materializzate.
Pattern 10: idratazione Eloquent quando serve solo l'array raw. Eloquent idrata ogni riga del risultato SQL in un oggetto PHP completo con cast, relazioni accessorie, observer hook - utile quando serve lavorare con entità di dominio, superfluo quando serve solo leggere qualche colonna per un dashboard JSON. DB::table('orders')->select('id', 'total', 'customer_name')->get() è 5-8 volte più veloce di Order::select('id', 'total', 'customer_name')->get()->map(fn ($o) => ['id' => $o->id, ...]) per dataset medi. Il suggerimento operativo: per API dashboard, report aggregati, endpoint di ricerca a basso nesting relazionale, usare DB::table() raw invece di Eloquent. La perdita di pulizia semantica è compensata dal guadagno di performance su dataset non banali.
Il risultato finale dell'intervento sul cliente milanese, al termine delle sei giornate di lavoro distribuite in tre settimane, è stato il seguente. Tre clienti su cui era stato eseguito il tuning hanno visto miglioramenti rispettivamente di 7x, 14x e 22x sui tempi medi di risposta delle pagine critiche - con il caso 22x su un listing prodotti che è passato da 9,8 secondi a 440 millisecondi. Numero totale di query SQL per request media sui tre clienti sceso in aggregato dell'84%, misurato su traffico reale di produzione nei 14 giorni successivi al deploy. Memoria PHP massima per request scesa del 68% medio. Nessun upgrade hardware, nessun contratto di licenza aggiuntivo, nessuna introduzione di tecnologie nuove - solo disciplina di scrittura su Eloquent applicata in modo sistematico. Il team interno dell'agenzia ha interiorizzato i dieci pattern durante le sessioni formative, e nei mesi successivi ha iniziato a identificarli autonomamente in code review prima del merge nelle sue nuove feature. La lezione strategica per il socio tecnico è stata una: quando tre clienti diversi hanno lo stesso problema, il problema non è del cliente - è del metodo con cui il team sta scrivendo codice.
Se gestisci una software house PMI o guidi un team di sviluppo Laravel e riconosci nel tuo lavoro quotidiano i sintomi descritti in questo articolo - pagine che si fanno lente nel tempo senza cause evidenti, memory peaks imprevedibili in job asincroni, clienti finali che si lamentano della velocità senza che tu riesca a localizzare la causa - il problema quasi sempre non è il framework, l'hardware o la dimensione del dataset: è la disciplina di scrittura delle query Eloquent all'interno del team. Una sessione strutturata di audit Telescope su 48-72 ore di produzione più una giornata di review disciplinata dei pattern emersi produce in quasi tutti i casi guadagni di performance 5-20x senza alcun investimento infrastrutturale aggiuntivo. Se vuoi confrontarti su una valutazione tecnica del tuo caso specifico, contattami per una consulenza iniziale: in una mezza giornata di analisi guidata installiamo Telescope in modalità read-only sul tuo ambiente di produzione, raccogliamo i dati reali del tuo traffico, identifichiamo i tre pattern Eloquent più costosi specifici del tuo codebase e costruiamo insieme un piano di remediation calibrato sulla capacità reale del tuo team di implementarlo senza interrompere la roadmap produttiva.