PrestaShop debug profiling : activer et analyser performances SQL

Guide pratique pour activer le debug profiling en sécurité, repérer N+1, convertir les requêtes en EXPLAIN ANALYZE et compléter par le slow query log.

Trois écrans avec du code et des schémas de bases de données pour le débogage SQL.

Table des matières :

  1. Activer le debug profiling PrestaShop (sans exposer votre boutique)
  2. Ce que mesure réellement le profiler (et ce qu’il ne mesure pas)
  3. Passer du tableau PrestaShop à une analyse SQL exploitable (EXPLAIN / EXPLAIN ANALYZE)
  4. Trouver l’origine applicative : hooks, modules, overrides et N+1
  5. Compléter le profiling par des traces serveur : slow query log, verrous et contention
  6. Check-list opérationnelle : une session de debug profiling SQL en conditions maîtrisées

Activer le debug profiling PrestaShop (sans exposer votre boutique)

Le « debug profiling » de PrestaShop est un mode d’instrumentation qui injecte, en bas des pages (front et parfois back), un tableau récapitulatif : nombre de requêtes SQL, temps cumulé, mémoire, hooks exécutés, fichiers inclus. C’est brutal mais efficace, parce que ça pointe immédiatement les pages qui font 300 requêtes et 1,8 s de SQL avant même de regarder PHP-FPM ou le réseau. Dans la suite, je pars sur des shops PrestaShop 8.1/8.2 et 9.x (cœur Symfony 6.4 côté BO) avec PHP 8.1 à 8.3 et MySQL 8.0+ ou MariaDB 10.6+.

Le point critique : ne l’activez pas sur un front public, ou alors derrière une restriction IP / Basic Auth / environnement staging. Le profiler ajoute du HTML, peut révéler des infos d’architecture (chemins, hooks, modules), et surtout augmente la charge (collecte + rendu). Si vous êtes déjà proche du mur (timeouts, 503), commencez par stabiliser l’infra et les logs côté serveur ; la checklist “serveur/logs/ressources” de l’article Erreur HTTP 503 : diagnostic serveur, logs et ressources est plus adaptée qu’un profilage “à chaud”.

Techniquement, l’activation la plus propre se fait via config/defines_custom.inc.php (persiste aux mises à jour, contrairement à l’édition sauvage de defines.inc.php). Exemple :

<?php
// config/defines_custom.inc.php

define('_PS_MODE_DEV_', true);
define('_PS_DEBUG_PROFILING_', true);

// Optionnel : affiche davantage de détails SQL dans certains messages d'erreur.
// À activer uniquement en environnement cloisonné.
// define('_PS_DEBUG_SQL_', true);

Quelques précautions “terrain” qui évitent de fausser les mesures (ou de se faire surprendre) :

  • PSMODEDEV ne sert pas qu’au profiler : affichage d’erreurs, désactivation partielle de caches, comportement plus verbeux. En prod, le simple fait de le laisser activé peut exposer des détails sensibles.
  • Pensez aux pages mises en cache : si vous avez un reverse proxy (Varnish/NGINX cache) ou un cache full-page, le profiler peut ne pas apparaître… ou apparaître sur une page mise en cache si vous avez mal cloisonné. Dans le doute, testez en contournant le cache (cookie admin, header Cache-Control, URL de staging).
  • Restreindre l’accès sans bricoler PrestaShop : faites-le au niveau proxy/webserver (allowlist IP, Basic Auth). Exemple minimal NGINX (à adapter) :
location / {
  allow 203.0.113.10; # IP bureau / VPN
  deny all;
  try_files $uri $uri/ /index.php?$args;
}

Ensuite : videz le cache (var/cache/* en 8/9) et désactivez tout cache applicatif le temps du diagnostic (Smarty cache, CCC) pour éviter des mesures incohérentes. Pour cadrer la démarche perf de bout en bout (quoi mesurer, où, et dans quel ordre), vous pouvez vous appuyer sur Audit performance PrestaShop : méthode en 6 étapes reproductibles.

Enfin, fixez-vous une règle simple de sécurité opérationnelle : dès que vous avez fini, repassez _PS_DEBUG_PROFILING_ à false et supprimez/neutralisez la restriction temporaire (ou, mieux, faites tout sur une préprod dédiée). Beaucoup de “fuites” d’info viennent d’un mode debug oublié après un dépannage.

Ce que mesure réellement le profiler (et ce qu’il ne mesure pas)

Le profiler PrestaShop agrège des métriques in-process : durée PHP, mémoire, et surtout temps de requêtes SQL vus par la couche Db. Attention au vocabulaire : “temps SQL” ici = temps que MySQL/MariaDB met à exécuter les requêtes plus la latence réseau DB et la sérialisation des résultats. Ça ne vous dit rien sur le CDN, le TLS, le rendu navigateur, ni les requêtes XHR. Le lien avec l’UX reste indirect : si vous observez un TTFB élevé, le SQL est souvent un suspect, mais pas le seul (OPcache, contention CPU, lock InnoDB, saturation FPM). Pour le cadrage TTFB et les cibles, voir TTFB PrestaShop : réduire le Time To First Byte sous 200 ms.

Ce que le tableau vous donne très bien :

  • Un ordre de grandeur (ex. : “catégorie = 220 requêtes, produit = 80 requêtes”).
  • Des dérives nettes (ex. : après l’ajout d’un module, une page prend +120 requêtes).
  • Une signature d’anomalie : répétition, requêtes identiques, ou requêtes “très chères” (grosses jointures, tri, pagination).

Ce qu’il ne faut pas lui demander :

  • d’expliquer pourquoi MySQL a mis 700 ms (IO ? lock ? buffer pool ?)
  • d’isoler l’impact des appels externes (API paiement, ERP, tracking) si ça ne passe pas par SQL
  • de donner un résultat “stable” à la milliseconde : le warm-up et la charge de fond influencent fortement.

Sur une page front typique (catégorie / recherche / produit), le tableau de profiling devient vite lisible si vous cherchez des patterns, pas “la requête la plus lente”. Les signaux forts : (1) explosion du nombre de requêtes (souvent un N+1), (2) quelques requêtes lourdes (JOIN + ORDER BY + pagination), (3) requêtes répétées à l’identique (cache absent ou contourné), (4) temps SQL cumulé disproportionné par rapport au temps PHP.

Le N+1 se voit très bien : même SQL (ou SQL quasi identique) exécuté N fois, typiquement sur les déclinaisons/attributs, les prix spécifiques, ou les images. Exemple réaliste : un module qui “enrichit” un listing produit en déclinaisons peut déclencher un N+1 si l’enrichissement est fait produit par produit ; c’est exactement le genre de zone à auditer quand vous implémentez des fonctionnalités comme celles décrites dans Module PrestaShop attributs : afficher les déclinaisons en liste produits.

Ne sur-interprétez pas une capture isolée : le profilage est sensible au warm-up (cache MySQL, cache filesystem, OPcache déjà chaud), au contenu de session (client logué vs anonyme), et au contexte catalogue (nombre de produits dans catégorie, règles de prix, multi-boutique). Une méthode simple pour rendre les comparaisons crédibles :

  • faites 3 passages sur la même URL (le premier “chauffe”),
  • notez médiane (pas le meilleur temps),
  • comparez sur un scénario figé (mêmes cookies, même devise/langue, même panier).

Côté BO (routes Symfony), si vous êtes en mode debug et que les bundles sont actifs, vous pouvez aussi croiser avec le profiler Symfony ; la doc officielle rappelle bien le rôle : “The Symfony Profiler collects data about each request to help you debug problems in your application” (Symfony Profiler). C’est utile quand la lenteur est dans une page back-office (catalogue, commandes) et que vous voulez séparer “SQL du legacy PrestaShop” et “SQL/Doctrine côté Symfony”.

Passer du tableau PrestaShop à une analyse SQL exploitable (EXPLAIN / EXPLAIN ANALYZE)

Le profiler vous donne un SQL “brut”. L’étape suivante consiste à le rendre testable : copiez la requête, remplacez les placeholders (ids, lang, shop), et exécutez-la sur une base de staging avec des stats proches (mêmes volumes, mêmes index). Le point clé n’est pas la “beauté” du SQL, mais le plan d’exécution. Sur MySQL 8.0.18+, utilisez EXPLAIN ANALYZE : vous obtenez des timings réels par opérateur, ce qui évite de deviner.

Pour éviter les faux positifs, gardez une mini check-list avant de modifier quoi que ce soit :

  • La requête est-elle vraiment celle qui consomme (temps cumulé, répétition) ou juste la plus visible ?
  • Est-elle lente tout le temps ou seulement quand il y a charge / locks ?
  • Le problème vient-il d’un WHERE/JOIN (index) ou d’un ORDER BY/pagination (tri + filesort) ?
  • Est-ce une requête “fonctionnelle” (inévitable) ou un appel “inutile” (appel en boucle, hook trop tôt, cache manquant) ?

Dans PrestaShop, les requêtes critiques combinent souvent ps_product, ps_product_shop, ps_product_lang, ps_category_product, ps_specific_price et la couche stock. Les antipatterns classiques à repérer dans EXPLAIN : type=ALL (full scan), Using temporary, Using filesort, ou un rows énorme sur une table de liaison (catégorie → produit) faute d’index composite. Corriger ça revient rarement à “optimiser PHP” : c’est surtout des index (composites, couvrants), ou un changement de clause ORDER BY pour coller à l’index (et parfois un refactoring fonctionnel : arrêter de trier sur un champ non indexable à grande échelle). Pour la démarche index/EXPLAIN, voir Index MySQL : optimiser WHERE, JOIN et ORDER BY avec EXPLAIN.

Un mini-scénario typique en boutique FR/EU (multi-langue, multi-boutique, règles de prix par groupes) : une catégorie “Soldes” avec 10 000 produits, tri “prix croissant”, et plusieurs règles de prix spécifiques. Même si votre SQL “passe”, le plan peut exploser si :

  • l’index ne tient pas compte de id_shop / id_lang,
  • le tri s’appuie sur une expression (prix final calculé) => filesort,
  • la pagination force MySQL à “scanner puis jeter” beaucoup de lignes.

Dans ce cas, deux leviers reviennent souvent :

1) Réduire le volume de lignes évaluées (index adaptés + filtres plus sélectifs)
2) Éviter le tri coûteux (tri sur un champ indexable, ou pré-calcul)

Un exemple de micro-stratégie qui marche bien en e-commerce : déplacer le coût. Si le profiler montre une requête de listing qui fait ORDER BY sur un champ calculé (prix final avec taxes, règles, devises…), vous allez vous battre contre le moteur. À l’inverse, si vous pré-calculer/denormaliser certaines données dans une table dédiée (ou via un indexation moteur de recherche), vous remplacez un tri coûteux par un accès indexé. Ce n’est pas gratuit : vous échangez du coût en lecture contre du coût en écriture (mise à jour prix/stock), donc ça se décide en fonction du ratio trafic / fréquence de mise à jour.

Dernier point pratique : quand vous testez un index, mesurez aussi l’impact sur l’écriture (imports, mise à jour stock/prix, commandes). Un index “parfait” pour les listings peut ralentir un flux d’ERP ou un gros import catalogue si vous en ajoutez 5 sur des tables très écrites.

Trouver l’origine applicative : hooks, modules, overrides et N+1

Quand le SQL explose, c’est rarement “le core” seul. Le plus souvent, c’est un module qui s’accroche à des hooks très tôt (header, product list, cart) et reconstruit de l’état à coups de requêtes. Le profiling vous aide à corréler : (1) l’ordre des hooks exécutés, (2) la dérive SQL juste après un hook, (3) parfois le fichier/stack si le mode debug est suffisamment bavard. Si vous devez identifier où un hook est appelé (y compris hooks dynamiques), l’article Hooks PrestaShop : rechercher et identifier les hooks dynamiques vous fait gagner du temps, surtout quand vous êtes dans une base legacy + thème custom.

Le N+1, en PrestaShop, se cache souvent derrière des boucles innocentes : foreach ($products as $product) { loadSomething($product['id_product']); }. Sur un listing de 48 produits, vous venez d’ajouter 48 requêtes (voire 96 si vous touchez stock + prix). La correction standard : batcher (requête IN (...)), ou précharger via un repository/adapter, puis mapper en mémoire.

Deux heuristiques rapides pour repérer un N+1 dans le tableau :

  • même requête répétée avec uniquement id_product = ? qui change,
  • le temps total SQL augmente quasi linéairement avec le nombre d’éléments affichés (24 → 48 produits = temps SQL ~×2).

Si vous êtes sur un module Symfony/Doctrine, le problème est encore plus fréquent via l’ORM : chargement lazy + itération = avalanche. Pour un angle “détection N+1 et bloat mémoire” (même si ce n’est pas spécifique PrestaShop), voir Scout Monitoring Symfony : détection N+1 Doctrine et memory bloat.

N’oubliez pas deux sources “silencieuses” de surcoût dans des shops legacy :

  • Overrides (override/ et surcharge de classes) : une surcharge peut ajouter des requêtes dans une méthode appelée partout (chargement produit, panier, client). Si vous suspectez un override, cherchez des ajouts de Db::getInstance()->executeS() dans des méthodes très sollicitées.
  • Templates et helpers qui déclenchent des appels : certains thèmes/modules appellent des méthodes dans les templates (Smarty) qui finissent en requêtes (souvent via “helper” ou “getXXX”). Le profiler aide à corréler : même page, même rendu, mais SQL qui explose après une modification de thème.

Pour isoler proprement, évitez de “désactiver 30 modules au hasard” sur la prod. Faites un A/B sur staging : clone base + fichiers, active profiling, puis désactivez par lots. Vous cherchez une diff nette : -40% de requêtes ou -300 ms SQL sur une URL précise. Ensuite seulement vous ouvrez le module : cache (par ID shop/lang/currency/customer group), réduction des requêtes, ou ajout d’index. Sur le volet hygiène (désinstallations propres, impact perf et sécurité), la méthode est bien cadrée dans Modules PrestaShop : désinstallation propre, performances et sécurité en production.

Compléter le profiling par des traces serveur : slow query log, verrous et contention

Le profiler donne une photo côté PHP. Mais dès que vous passez en charge (pics de trafic, jobs cron, import, indexation), la réalité est côté base : verrous, contention, I/O, buffer pool saturé. Le bon chaînage, c’est : profiler PrestaShop → identifier 3–10 requêtes suspectes → vérifier côté DB si elles apparaissent en “lentes”. L’outil standard reste le slow query log (MySQL/MariaDB). Pour l’activer proprement et l’exploiter dans un contexte PrestaShop, suivez Requêtes MySQL lentes PrestaShop : activer slow query log.

Deux conseils pratiques pour que le slow log soit exploitable (et pas un “dump” inutilisable) :

  • démarrez avec un long_query_time raisonnable (souvent entre 0,2 s et 1 s selon la taille de la boutique) puis ajustez ; si vous le mettez trop bas tout de suite, vous allez noyer le signal.
  • activez la journalisation des requêtes non indexées avec prudence : utile en audit, mais peut produire énormément d’entrées sur des shops bavards.

Ne sous-estimez pas les symptômes qui ressemblent à du “SQL lent” mais qui sont en fait des waits : Waiting for table metadata lock, contention sur ps_cart, transactions longues, ou backlog de connexions. Le profiler voit “une requête à 900 ms” ; MySQL, lui, peut vous dire “900 ms dont 850 ms d’attente”. Selon votre SGBD, regardez performance_schema (MySQL), INFORMATION_SCHEMA.INNODB_TRX, les métriques InnoDB (log file syncs, buffer pool), et les événements de verrouillage.

Pour analyser rapidement un slow log volumineux, un outil classique côté MySQL est pt-query-digest (Percona Toolkit) : il regroupe les requêtes par “fingerprint” et vous sort un top par temps total, temps moyen, etc. Doc : Percona Toolkit — pt-query-digest

Enfin, quand vous avez confirmé que le SQL est le goulet, traitez aussi la couche cache, sinon vous optimisez des requêtes qui ne devraient même pas exister à chaque hit. PrestaShop n’a pas un cache magique “tout terrain” : il faut souvent combiner cache applicatif + cache objet + cache HTTP. En pratique, Redis (cache, sessions, verrous), Varnish (HTTP), et OPcache (bytecode) sont les briques les plus rentables. Pour les choix et les pièges, voir Cache PrestaShop : Varnish, Redis, Memcached et OPcache côté serveur et, côté mise en œuvre, Redis PrestaShop : configurer le cache sur VPS ou serveur dédié.

Check-list opérationnelle : une session de debug profiling SQL en conditions maîtrisées

Première règle : verrouillez le périmètre. Faites ça sur staging quand c’est possible, sinon mettez un contrôle d’accès (IP allowlist au niveau reverse proxy, ou Basic Auth). Ensuite, fixez un scénario reproductible : URL exacte, langue, devise, état logué, panier vide/plein, et désactivez les “bruits” (cron en parallèle, indexation, imports). C’est la différence entre “j’ai vu une requête lente” et “j’ai une régression mesurable”.

Deuxième règle : collectez et classez. Sur 3 à 5 pages critiques (home, catégorie lourde, produit, panier, checkout), notez : nombre de requêtes, temps SQL cumulé, top 5 requêtes les plus coûteuses, et requêtes répétées.

Un format simple (copiable dans un tableur) :

Page Contexte # requêtes SQL cumulé Requête la plus chère Répétitions ? Hypothèse
Catégorie X anonyme, FR/EUR, tri prix 240 1,6 s SELECT ... ORDER BY ... oui (N+1) module listing

Puis, pour chaque requête candidate :

  • EXPLAIN ANALYZE (ou EXPLAIN si non dispo)
  • vérification index (SHOW INDEX, index composite aligné sur WHERE + JOIN + ORDER BY)
  • vérification cardinalité (est-ce que la clause filtre vraiment ?)
  • validation que la requête n’est pas déclenchée en boucle (hook/module/template)

Si vous devez déboguer finement le chemin PHP qui déclenche la requête (notamment en module custom), passez sur un débogueur ; l’article Xdebug VS Code : configurer le débogage PHP en local est un bon point de départ pour remonter de la requête au caller.

Troisième règle : fermez la boucle par la prod. Une fois le correctif appliqué (index, batch, cache, refactor module), mesurez sous charge : tests de montée (k6/JMeter), métriques DB (latence, locks, QPS), métriques PHP-FPM (busy workers), et TTFB. Sans ça, vous risquez un “gain” local qui se transforme en contention globale (ex. index trop large qui ralentit les écritures).

Deux réflexes de fin de session qui évitent les “effets de bord” :

  • purgez et régénérez proprement les caches après modification (application + opcode + éventuels caches externes),
  • désactivez le mode profiling et vérifiez qu’aucune page (front/BO) n’expose encore le tableau.

Pour industrialiser la surveillance et éviter de retomber dans le diagnostic au feeling, posez une stack d’observabilité (Prometheus/Grafana/Netdata) et des seuils actionnables ; voir Surveillance PrestaShop : tableau de bord, seuils et réduction des fausses alertes et PrestaShop performance : monitoring, tests de charge et runbooks soldes.


À lire aussi