Une page peut être lente sans une seule requête lente. Vingt produits, et pour chacun une requête pour sa catégorie, son prix et son image : soixante et une requêtes là où deux suffiraient. Chacune prend deux millisecondes et la page prend une seconde et demie.
Comptez-les d'abord
// at the end of the page
echo count($pdo->query('SHOW SESSION STATUS LIKE "Questions"')->fetchAll());
Ou lisez le compte dans la barre de débogage du framework, ou dans le journal lent avec long_query_time à 0 le temps d'une requête. Le nombre compte plus que n'importe quel chronométrage isolé : en dessous de 20 c'est normal, au-dessus de 100 c'est le bug.
À quoi cela ressemble dans le code
// 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);
}
Le remède est une jointure, ou une requête pour tous les identifiants
$ids = array_column($products, 'category_id');
$cats = query('SELECT * FROM categories WHERE id IN (' . placeholders($ids) . ')', $ids);
// then match them up in PHP
Deux requêtes au lieu de vingt et une, et cela n'empire pas quand la page affiche cent produits - or c'est justement cette propriété qui compte.
Dans un ORM
C'est à cela que sert le chargement anticipé. Le défaut paresseux est commode et produit exactement cette forme, et le remède tient d'ordinaire en un mot.
$products = Product::with('category', 'images')->limit(20)->get();