Model::all() è spesso il primo metodo Eloquent che si impara: “dammi tutti i record di una tabella”.
Funziona, è leggibile, e per dataset piccoli va benissimo. Ma in produzione diventa rapidamente un punto di
accumulo di problemi: query più pesanti del necessario, consumo di memoria, tempi di risposta più lunghi e,
soprattutto, una catena di query aggiuntive quando si accede a relazioni (il classico N+1).
In questo articolo partiamo da Model::all() e vediamo tecniche pratiche per:
- ridurre il numero di query SQL eseguite;
- ridurre la quantità di dati letti dal database;
- ridurre il costo di idratazione dei Model Eloquent quando non serve;
- mantenere il codice pulito e misurabile (profilazione e guardrail).
1) Cosa fa davvero Model::all()
Model::all() esegue una singola query SELECT * FROM ..., idrata tutti i record in istanze Eloquent,
e restituisce una Collection in memoria. Semplificando:
use App\Models\User;
$users = User::all(); // SELECT * FROM users
Il problema non è “una query”: il problema è quanto carichi e cosa succede dopo. Tre scenari tipici:
- Dataset grande: porti in memoria migliaia di righe e colonne che non userai.
- Accesso a relazioni: ogni accesso lazy a una relazione può generare query aggiuntive (N+1).
- Lettura-only: stai solo mostrando dati, ma stai pagando il costo del Model (cast, accessors, eventi).
2) Primo taglio: non usare *, seleziona solo le colonne necessarie
Il modo più immediato per ridurre tempo e I/O è limitare le colonne.
use App\Models\User;
$users = User::query()
->select(['id', 'name', 'email'])
->get();
Se ti serve una singola colonna o un valore:
use App\Models\User;
$emails = User::query()->pluck('email'); // array/collection di email
$firstId = User::query()->value('id'); // un singolo valore
$count = User::query()->count(); // aggregate senza idratare model
Regola pratica: se nel codice non usi una colonna, non leggerla dal database.
3) Ridurre il numero di query: evitare N+1 con eager loading mirato
Il punto dolente è spesso questo: carichi i model e poi, in un loop, accedi a una relazione.
use App\Models\Post;
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // ogni accesso può generare una query
}
Qui la query iniziale è una, ma ogni $post->author può diventare un’altra query. La soluzione è
caricare le relazioni in anticipo:
use App\Models\Post;
$posts = Post::query()
->with(['author:id,name']) // eager loading + select sulle colonne della relazione
->select(['id', 'title', 'author_id'])
->get();
foreach ($posts as $post) {
echo $post->author->name;
}
Eager loading condizionale e “a richiesta”
Se in alcuni casi ti serve la relazione e in altri no, caricala in modo condizionale.
use App\Models\Post;
$query = Post::query()->select(['id','title','author_id']);
if (request()->boolean('include_author')) {
$query->with('author:id,name');
}
$posts = $query->get();
Se hai già una collection e vuoi evitare duplicazioni:
use App\Models\Post;
$posts = Post::query()->select(['id','title','author_id'])->get();
// Carica solo se manca (utile in codice riusato)
$posts->loadMissing('author:id,name');
4) Ridurre “quanti model”: quando Eloquent è troppo, scendi di livello
Eloquent è comodo, ma idratare migliaia di istanze Model ha un costo. Se stai facendo una lettura semplice (liste, export, statistiche), puoi ridurre drasticamente il “peso” passando a strutture più leggere.
Query Builder con DB::table()
use Illuminate\Support\Facades\DB;
$rows = DB::table('users')
->select(['id','name','email'])
->where('active', true)
->get(); // restituisce oggetti stdClass, non Model
Base query senza idratazione Eloquent: toBase()
use App\Models\User;
$rows = User::query()
->select(['id','name','email'])
->toBase() // ritorna Query Builder "base"
->get();
Se ti serve solo un elenco di ID per fare una seconda operazione, pluck evita model e riduce memoria:
use App\Models\Order;
$orderIds = Order::query()
->whereDate('created_at', now())
->pluck('id');
5) Ridurre memoria e query su grandi dataset: chunk, cursor e lazy
Model::all() carica tutto in una volta. Se devi processare molti record, preferisci iterazione a blocchi.
chunk() (batch in più query, memoria costante)
use App\Models\User;
User::query()
->select(['id','email'])
->orderBy('id')
->chunk(500, function ($users) {
foreach ($users as $user) {
// invio email, export, ecc.
}
});
cursor() (streaming, una query, iterazione lazy)
use App\Models\User;
foreach (
User::query()
->select(['id','email'])
->where('active', true)
->orderBy('id')
->cursor()
as $user
) {
// attenzione: con cursor le relazioni lazy possono diventare N+1
}
lazyById() (streaming per ID, robusto su tabelle che cambiano)
use App\Models\User;
User::query()
->select(['id','email'])
->where('active', true)
->lazyById(500)
->each(function ($user) {
// processamento
});
Per processing massivo, chunk() e lazyById() evitano out-of-memory e riducono il rischio di saltare record
quando la tabella è modificata durante l’elaborazione.
6) Ottimizzare relazioni: conta e aggrega senza caricare model correlati
Spesso ti servono solo conteggi o somme. Caricare l’intera relazione è inutile.
withCount()
use App\Models\User;
$users = User::query()
->select(['id','name'])
->withCount('posts') // aggiunge posts_count
->get();
withSum() e simili
use App\Models\Customer;
$customers = Customer::query()
->select(['id','name'])
->withSum('orders', 'total') // orders_sum_total
->get();
Se devi solo verificare esistenza, usa exists() invece di caricare collezioni:
use App\Models\User;
$hasAdmins = User::query()->where('role', 'admin')->exists();
7) Pagina invece di caricare tutto: paginate() e cursorPaginate()
Se stai mostrando una lista in UI, non esiste quasi mai un buon motivo per all(). Paginare riduce la query,
la memoria e stabilizza i tempi di risposta.
use App\Models\Post;
$posts = Post::query()
->select(['id','title','published_at'])
->orderByDesc('published_at')
->paginate(20);
Per dataset grandi dove OFFSET diventa costoso, considera cursorPaginate():
use App\Models\Post;
$posts = Post::query()
->select(['id','title','published_at'])
->orderByDesc('published_at')
->cursorPaginate(20);
8) “Meno model” anche in output: Resource e DTO per evitare accessors costosi
In molte app, i costi non sono solo nel DB ma anche nella serializzazione: accessors, attributi append, cast complessi, e trasformazioni ripetute. Una strategia solida è rendere esplicita la forma dell’output con:
- API Resources (trasformazione controllata);
- DTO/ViewModel (oggetti leggeri per la vista);
- select mirate + map (solo i campi necessari).
use App\Models\User;
$users = User::query()
->select(['id','name','email'])
->get()
->map(fn ($u) => [
'id' => $u->id,
'name' => $u->name,
'email' => $u->email,
]);
9) Caching: ridurre query ripetitive e stabilizzare le performance
Se i dati cambiano poco e vengono richiesti spesso (menu, impostazioni, liste “statiche”), la cache riduce query e latenza.
In Laravel puoi usare Cache::remember() con TTL o invalidazione esplicita.
use App\Models\Category;
use Illuminate\Support\Facades\Cache;
$categories = Cache::remember('categories:public', 600, function () {
return Category::query()
->select(['id','name','slug'])
->orderBy('name')
->get();
});
La cache è efficace quando hai “molte letture” e “poche scritture”. Valuta sempre invalidazione e coerenza.
10) Misurare e prevenire regressioni: logging query e guardrail contro N+1
Ottimizzare senza misurare è un rischio. Alcune buone pratiche:
- abilitare il logging delle query in ambiente di sviluppo;
- usare strumenti come Debugbar/Telescope per vedere query e tempi;
- aggiungere test che falliscono in presenza di N+1 (dove ha senso);
- rendere “esplicite” le relazioni richieste in ogni endpoint.
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
DB::listen(function ($query) {
Log::debug('SQL', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time_ms' => $query->time,
]);
});
11) Una checklist rapida: sostituzioni tipiche a Model::all()
| Obiettivo | Invece di | Usa |
|---|---|---|
| Limitare colonne | Model::all() |
Model::select([...])->get() |
| Evitare N+1 | loop su relazioni lazy | with(), loadMissing(), select sulle relazioni |
| Solo conteggi/somme | caricare relazione | withCount(), withSum(), exists() |
| Liste in UI | caricare tutto | paginate(), cursorPaginate() |
| Processing massivo | caricare tutto in memoria | chunk(), lazyById(), cursor() |
| Lettura-only leggera | model Eloquent | DB::table(), toBase(), pluck() |
| Dati poco variabili | query ripetute | Cache::remember() |
Conclusione
Model::all() è comodo, ma è anche il punto di partenza perfetto per ragionare su performance e qualità:
riduci i dati letti (colonne e righe), riduci il numero di query (eager loading e aggregati), e riduci l’uso di
Model quando ti serve solo una proiezione leggera dei dati (Query Builder, toBase(), pluck()).
Con queste tecniche puoi mantenere Eloquent dove aggiunge valore e “scendere di livello” dove serve efficienza, ottenendo endpoint più rapidi, prevedibili e facili da far crescere.