Una pagina può essere lenta senza una sola query lenta. Venti prodotti, e per ciascuno una query per la categoria, una per il prezzo e una per l'immagine: sessantuno query dove ne bastavano due. Ognuna impiega due millisecondi e la pagina impiega un secondo e mezzo.

Prima le conti

// at the end of the page
echo count($pdo->query('SHOW SESSION STATUS LIKE "Questions"')->fetchAll());

Oppure legga il numero dalla barra di debug del framework, o dallo slow log con long_query_time a 0 per una sola richiesta. Il numero conta più di qualsiasi tempo preso da solo: sotto 20 è normale, sopra 100 è il difetto.

Che aspetto ha nel codice

// N+1: one query, then one more per row
$products = query('SELECT * FROM products LIMIT 20');
foreach ($products as $p) {
    $p->category = query('SELECT * FROM categories WHERE id = ?', $p->category_id);
}

Il rimedio è una join, o una query per tutti gli id

$ids = array_column($products, 'category_id');
$cats = query('SELECT * FROM categories WHERE id IN (' . placeholders($ids) . ')', $ids);
// then match them up in PHP

Due query invece di ventuno, e non peggiora quando la pagina mostra cento prodotti - ed è proprio questa la proprietà che conta.

Dentro un ORM

L'eager loading serve a questo. Il default pigro è comodo e produce esattamente questa forma, e il rimedio è di solito una parola sola.

$products = Product::with('category', 'images')->limit(20)->get();
Mettere in cache una pagina così la nasconde finché la cache non si raffredda, e il primo visitatore dopo ogni deploy si prende tutte e 400 le query. Prima sistemi il conteggio, poi metta in cache.
Tenga d'occhio il numero di query mentre la pagina cresce. Una pagina che ne lancia 3 per 1 riga e 300 per 100 righe ha questa forma, qualunque cosa dicano i tempi di oggi.