← Torna agli articoli
Performance e database

Non è il tuo database a essere lento, è la tua query: le basi dell'ottimizzazione MySQL in Laravel

Una guida pratica all'ottimizzazione MySQL in Laravel: correggere le query N+1, aggiungere gli indici giusti, evitare i cicli e trasformare una pagina da 3 secondi in 150 ms senza aggiornare l'hardware.

✦
Articolo in evidenza ↗

Non è il tuo database a essere lento, è la tua query: le basi dell'ottimizzazione MySQL in Laravel che ogni sviluppatore dovrebbe conoscere

Ogni sviluppatore Laravel ci è passato. Una pagina che si caricava in 200 ms improvvisamente impiega 3 secondi. Il primo istinto è incolpare il database: «MySQL è lento», «ci serve un server più potente», «passiamo a PostgreSQL». Ma ecco la verità scomoda — il tuo database non è quasi mai il problema. Sono le tue query.

MySQL è uno dei software più collaudati che esistano. Può gestire milioni di righe e migliaia di connessioni simultanee senza battere ciglio. Quello che non può fare è correggere magicamente query scritte male, indici mancanti e il famigerato problema N+1.

In questo articolo esamineremo le tecniche essenziali di ottimizzazione MySQL che ogni sviluppatore Laravel dovrebbe conoscere — quelle che trasformano una pagina da 3 secondi in una da 150 ms senza aggiornare un solo pezzo di hardware.

1. Il problema N+1: il killer silenzioso di Laravel

Questa è la causa più comune di applicazioni Laravel lente, ed è quasi sempre invisibile finché non la cerchi deliberatamente.

Considera questo codice dall'aspetto innocente:

<?php $posts = Post::all(); foreach ($posts as $post) { echo $post->author->name; }

Sembra a posto, vero? Tranne che questo genera N+1 query. Una query per recuperare tutti i post, poi una query aggiuntiva per ogni post per recuperare il suo autore. Con 100 post, sono 101 query. Con 1.000 post, sono 1.001 query.

La soluzione è l'eager loading:

<?php $posts = Post::with('author')->get(); foreach ($posts as $post) { echo $post->author->name; }

Ora sono solo 2 query, indipendentemente da quanti post hai.

Come rilevare le query N+1

Laravel ha strumenti eccellenti per questo:

  • Laravel Debugbar — mostra ogni query ed evidenzia i duplicati.
  • Laravel Telescope — un compagno di debug completo con profilazione delle query.
  • DB::listen() — registra manualmente ogni query.
<?php DB::listen(function ($query) { Log::info($query->sql, $query->bindings); });

Puoi anche abilitare la modalità strict in sviluppo per lanciare eccezioni sul lazy loading:

<?php // In AppServiceProvider::boot() Model::preventLazyLoading(!app()->isProduction());

Questo ti costringe a individuare i problemi N+1 durante lo sviluppo invece che in produzione.

2. Gli indici: il più grande guadagno singolo

Se fai solo una cosa dopo aver letto questo articolo, falla: aggiungi indici alle tue chiavi esterne e alle colonne interrogate frequentemente.

Laravel lo rende facile con le migrazioni:

<?php Schema::table('posts', function (Blueprint $table) { $table->index('user_id'); $table->index('status'); $table->index(['status', 'created_at']); // indice composto });

Una query che scansiona 1.000.000 di righe può scendere a scansionarne 100 con l'indice giusto. È un'accelerazione di 10.000 volte grazie a una singola riga di codice.

Come sapere quali indici ti servono

Usa EXPLAIN per vedere cosa sta facendo MySQL:

<?php $posts = Post::where('status', 'published') ->where('user_id', 42) ->orderBy('created_at', 'desc') ->get(); // Vedi il piano di esecuzione DB::select('EXPLAIN ' . $posts->toSql(), $posts->getBindings());

Cerca questi segnali d'allarme:

  • type: ALL — scansione completa della tabella, nessun indice utilizzato.
  • rows: 500000 — MySQL si aspetta di scansionare mezzo milione di righe.
  • Extra: Using filesort — ordinamento in memoria o su disco invece di usare un indice.
  • Extra: Using temporary — creazione di una tabella temporanea, di solito per GROUP BY.

La soluzione? Aggiungi un indice che corrisponda al pattern della query — di solito le colonne in WHERE, poi le colonne in ORDER BY.

3. Seleziona solo ciò che ti serve

Un altro errore classico:

<?php $users = User::all(); // recupera ogni colonna per ogni utente

Se ti servono solo nomi ed email, dillo:

<?php $users = User::select('id', 'name', 'email')->get();

Questo conta più di quanto pensi. Recuperare grandi colonne TEXT o JSON che non usi può moltiplicare sia l'uso della memoria che il tempo della query. E in Laravel, ogni modello Eloquent ha un overhead — costruire centinaia di modelli completi quando ti servono solo pochi campi è uno spreco.

Per dati veramente di sola lettura, considera di saltare completamente Eloquent:

<?php $stats = DB::table('orders') ->select('status', DB::raw('COUNT(*) as total')) ->groupBy('status') ->get();

Query Builder è significativamente più veloce di Eloquent quando non hai bisogno di modelli.

4. Usa chunk() e cursor() per grandi set di dati

Caricare un milione di righe in memoria farà crashare la tua applicazione — anche se la query stessa è veloce. Laravel fornisce due strumenti per lo streaming di grandi set di dati:

<?php // chunk() — carica in batch User::chunk(1000, function ($users) { foreach ($users as $user) { // elabora } }); // cursor() — usa un generatore, una riga alla volta foreach (User::cursor() as $user) { // elabora }

cursor() usa i generatori PHP ed è efficiente in memoria, ma mantiene una singola connessione al database aperta per tutta l'iterazione. chunk() esegue più query, il che è spesso più sicuro in produzione.

5. Evita le query nei cicli

Se ti ritrovi a scrivere questo:

<?php foreach ($userIds as $id) { $user = User::find($id); // ... }

Fermati. Sostituiscilo con una singola query:

<?php $users = User::whereIn('id', $userIds)->get();

Questo vale anche per update e delete:

<?php // Male foreach ($userIds as $id) { User::where('id', $id)->update(['active' => false]); } // Bene User::whereIn('id', $userIds)->update(['active' => false]);

La seconda versione è una query invece di centinaia — spesso la differenza tra 500 ms e 5 ms.

6. Attenzione con whereHas e withCount

whereHas() è comodo ma può generare costose sottoquery correlate:

<?php $users = User::whereHas('orders', function ($q) { $q->where('total', '>', 1000); })->get();

Per semplici verifiche di esistenza, preferisci whereIn con una sottoquery:

<?php $userIds = Order::where('total', '>', 1000) ->distinct() ->pluck('user_id'); $users = User::whereIn('id', $userIds)->get();

Allo stesso modo, withCount() esegue una sottoquery per ogni relazione. Va bene per un paio di relazioni, ma se stai contando cinque relazioni nella stessa query, considera di ristrutturare.

7. Usa la cache in modo aggressivo

A volte la query più veloce è quella che non esegui affatto. Il livello di cache di Laravel è uno degli strumenti di performance più sottoutilizzati del framework.

<?php $topPosts = Cache::remember('top_posts', 3600, function () { return Post::orderByDesc('views')->limit(10)->get(); });

Per dati che cambiano raramente — categorie, impostazioni, liste statiche — la cache è essenzialmente performance gratuita. Usa Redis o Memcached in produzione e ricorda di invalidare le chiavi quando i dati cambiano.

8. Analizza le query lente in produzione

MySQL ha uno strumento integrato per trovare query lente:

-- Abilita il log delle query lente SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

Dopo un giorno in produzione, esamina il log. Troverai i veri colpevoli — le query che non avresti mai immaginato essere lente, eseguite migliaia di volte al minuto.

In Laravel, puoi anche registrare le query lente per richiesta tramite un service provider:

<?php // In AppServiceProvider::boot() if (app()->isProduction()) { DB::listen(function ($query) { if ($query->time > 1000) { // ms Log::warning('Slow query', [ 'sql' => $query->sql, 'time' => $query->time, ]); } }); }

9. Usa i tipi di dati corretti

I piccoli dettagli contano più di quanto pensi:

  • Usa INT UNSIGNED per gli ID, non VARCHAR.
  • Usa DECIMAL per il denaro, mai FLOAT.
  • Usa ENUM o TINYINT per gli stati, non VARCHAR(255).
  • Usa CHAR(36) per gli UUID, non VARCHAR(255).
  • Preferisci DATETIME o TIMESTAMP alle date come stringhe.

Colonne più piccole significano indici più piccoli, che significano ricerche più veloci. Si accumula.

10. Ottimizza relazioni e vincoli

Le migrazioni di Laravel rendono facile definire chiavi esterne:

<?php Schema::table('posts', function (Blueprint $table) { $table->foreignId('user_id')->constrained()->cascadeOnDelete(); });

Le chiavi esterne non solo preservano l'integrità dei dati — creano anche automaticamente un indice, che è spesso esattamente ciò che ti serve per le performance dei JOIN.

Un flusso di lavoro pratico di debug

Quando una pagina è lenta, segui questa sequenza:

  1. Installa Laravel Debugbar o Telescope. Vedi quante query vengono eseguite.
  2. Cerca pattern N+1. Aggiungi with() ovunque una relazione venga acceduta in un ciclo.
  3. Controlla il numero di query. Una pagina dovrebbe tipicamente eseguire meno di 20 query. Oltre 100 è un campanello d'allarme.
  4. Esegui EXPLAIN sulle query più lente. Cerca scansioni complete della tabella.
  5. Aggiungi indici. Fai corrispondere l'indice alle colonne usate in WHERE e ORDER BY.
  6. Metti in cache ciò che non cambia. Soprattutto aggregati e dati di configurazione.
  7. Misura di nuovo. Ripeti finché la pagina non è veloce.

Conclusione

Nove volte su dieci, «il database è lento» significa in realtà «le query sono lente». Prima di rivolgerti a un server più potente, a un motore di database diverso o a un servizio cloud gestito, passa un pomeriggio a ottimizzare le tue query. Troverai quasi sempre che i guadagni sono già lì — aspettano nel codice.

Eager loading, indici corretti, evitare i cicli e la cache risolveranno la maggior parte dei problemi di performance nelle applicazioni Laravel. Nessuna di queste tecniche richiede nuova infrastruttura. Richiedono solo consapevolezza.

Inizia dalla tua pagina più lenta. Lancia un log delle query. Correggi l'N+1. Aggiungi un indice mancante. Misura di nuovo. Poi ripeti — e guarda il tuo database diventare magicamente «più veloce» senza cambiare nulla di esso.

Tecnologie e argomenti

Tag dell'articolo

Nessun articolo corrisponde a questi filtri.

Hai un progetto o un'idea da discutere?

Parliamone ↗