Note dal lavoro · Backend
Query N+1 in Laravel: come riconoscerle e toglierle
Se una pagina Laravel diventa lenta man mano che i dati crescono, la prima cosa da controllare sono le query N+1: una query per caricare un elenco e poi una in più per ogni elemento, per recuperare una relazione. Con 20 righe sono 21 query, con 500 sono 501.
Come si presenta
// 1 query per gli ordini, poi 1 per ogni cliente
$orders = Order::all();
foreach ($orders as $order) {
echo $order->customer->name;
}
Il codice è corretto e funziona, ed è proprio per questo che passa inosservato: il costo si vede solo con molti dati.
Come riconoscerle
- Debugbar o Telescope mostrano il numero di query per richiesta e quelle duplicate.
DB::listenpermette di registrare ogni query in sviluppo.- Dal Laravel 8.43 puoi far fallire i lazy load accidentali:
Model::preventLazyLoading(! app()->isProduction());nel metodobootdelAppServiceProvider. In sviluppo ricevi un’eccezione appena una relazione viene caricata in modo pigro, in produzione l’app non si rompe.
Come risolverle
// 2 query in totale
$orders = Order::with('customer')->get();
// solo le colonne che servono (includi la chiave esterna)
$orders = Order::with('customer:id,name')->get();
// un conteggio senza caricare le righe
$orders = Order::withCount('items')->get();
with()carica le relazioni insieme alla query principale, in una query aggiuntiva per relazione.withCount()e simili calcolano conteggi e somme direttamente nel database.load()eloadMissing()fanno lo stesso su una collezione già caricata.
Attenzione ai due estremi
L’eager loading carica tutto ciò che chiedi: con liste lunghe e relazioni pesanti rischi di riempire la memoria. In quel caso paginare, limitare le colonne o elaborare a blocchi (chunk, lazy). Il contrario non va meglio: caricare relazioni che la vista non usa è lavoro sprecato.
Una regola pratica
Ogni volta che un ciclo tocca $modello->relazione, chiediti dove viene caricata. Abilita preventLazyLoading in sviluppo e il problema smette di essere una sorpresa in produzione.