23 octobre 2025

Accélérer un site PrestaShop en optimisant une seule requête SQL : une étude de cas réelle.

Comment nous avons réduit les temps de réponse de 1,8 seconde à 3 millisecondes sur inventivashop.com et résolu les problèmes de Google Core Web Vitals.

Optimisation des requêtes SQL PrestaShop

Dans le paysage moderne du e-commerce, chaque milliseconde compte. En matière de performances web et de positionnement dans les moteurs de recherche, la vitesse d'un site n'est plus une option, mais une exigence fondamentale. C'est particulièrement vrai pour les sites à fort trafic comme inventivashop.com, qui domine la première place sur Google pour des mots clés compétitifs tels que « Signs ».
Le site en question constitue un excellent exemple de la façon dont une infrastructure déjà largement optimisée peut masquer des goulots d'étranglement critiques qui, une fois identifiés et résolus, entraînent des améliorations de performances spectaculaires. Dans cet article technique, nous partagerons notre approche méthodique pour optimiser une requête SQL particulièrement problématique, en illustrant comment nous y sommes parvenus. une amélioration des performances de 99,83 % — de 1,8 seconde à seulement 3 millisecondes.

Positionnement des mots-clés Google Sign

Le contexte : une infrastructure déjà de premier ordre

Avant d'aborder le cas spécifique, il est important de noter qu'inventivashop.com n'est pas parti de zéro en matière d'optimisation. Le site bénéficiait déjà d'un arsenal complet de technologies et de bonnes pratiques :

Pile technologique de pointe

  • HTTP / 3: Protocole de nouvelle génération pour des connexions plus rapides et plus résilientes
  • Compression ZSTD: Compression avancée pour réduire la charge utile de la réponse
  • Optimisé NGINX:Réglage au niveau du serveur Web avec des configurations personnalisées pour des performances maximales
  • Certificats TLS compressés:Réduction des frais généraux SSL/TLS
  • Prise en charge WebP: Images de nouvelle génération avec une taille de fichier jusqu'à 30 % plus petite
  • Vitaux Web de base optimisé:Toutes les bonnes pratiques mises en œuvre pour LCP, FID, CLS
  • Serveur Percona 5.7: Serveur de base de données optimisé au lieu de MySQL classique

Ce dernier point mérite une attention particulière. Le choix de Percona Server 5.7, bien qu'il soit en fin de vie depuis novembre 2024, n'était pas une coïncidence mais le résultat d'analyses approfondies qui ont mis en évidence des performances supérieures par rapport à MariaDB pour les requêtes spécifiques du site.

Le choix controversé : Percona Server 5.7 en fin de vie

Au cours des tests préliminaires, nous avons identifié une requête critique qui présentait un comportement radicalement différent selon les moteurs de base de données :

  • MariaDB 10.x: 1.700-1.800 ms
  • MySQL 8.0: 1.500-1.600 ms
  • Serveur Percona 5.7: 200 ms

La différence était si marquée (environ 9 fois plus rapide) que nous avons dû prendre une décision difficile : utiliser une version EOL mais avec des performances nettement supérieures, en mettant naturellement en œuvre toutes les mesures de sécurité nécessaires (isolation du réseau, durcissement, surveillance continue).

Le problème : quand un seul module met le site à genoux

Malgré toutes ces optimisations, la surveillance des performances a continué de mettre en évidence un problème récurrent et important. Les indicateurs affichaient des pics de latence inexpliqués, et le temps de réponse au premier octet (TTFB) était largement supérieur à nos normes sur certaines pages.

Le module BlockLayered : essentiel mais lourd

Le coupable s'est avéré être le module superposé par blocs PrestaShop (renommé « Recherche à facettes » ou « Recherche par couches » dans les versions plus récentes). Ce module est essentiel à tout site e-commerce moderne, car il gère :

  • Filtres de catégorie (prix, couleur, taille, marque, etc.)
  • Navigation produit à facettes
  • Nombre de produits dynamiques pour chaque filtre
  • Agrégations et arrangements complexes

Le module est appelé sur pratiquement toutes les pages de catégorie, de recherche ou de fiche produit, c'est-à-dire sur les pages les plus visitées d'un site e-commerce. Son inefficacité impacte donc l'expérience utilisateur dans son ensemble et, par conséquent, les taux de conversion.

La découverte : 1,8 seconde pour une seule requête

Grâce à l'analyse systématique des journaux de requêtes lentes et à l'utilisation de Boîte à outils Percona (pt-query-digest), nous avons identifié la requête problématique. Les chiffres étaient alarmants :

  • Temps d'exécution moyen: 1.700-1.800 ms
  • Frequenza:Des milliers de fois par jour
  • Impact total:Des dizaines de minutes de temps CPU gaspillé chaque heure
  • Patron de Couture:Présent dans chaque catégorie de navigation avec filtres

Pour le contexte : dans une architecture bien optimisée, nous prévoyons que les requêtes de lecture du catalogue de produits prennent entre 10 et 50 ms. Une requête de près de 2 secondes représente environ 40 à 180 fois plus lent de ce qui est dû.

L'approche diagnostique

Nous aurions pu utiliser des solutions de surveillance des performances applicatives (APM) comme New Relic, qui nous auraient fourni une vue globale des performances applicatives. Cependant, compte tenu de la nature concentrée du problème (une requête unique, très fréquente et très lente), nous avons opté pour une approche plus ciblée :

  1. Activation du journal des requêtes lentes sur le serveur Percona
  2. Collecte de données pendant 24-48 heures
  3. Analyse avec pt-query-digest pour identifier les modèles et normaliser les requêtes
  4. EXPLIQUER ANALYSER pour comprendre le plan d'exécution
  5. Profilage de la requête spécifique pour identifier les goulots d'étranglement

Cette approche nous a permis de concentrer toutes les ressources sur la résolution du vrai problème sans gaspillage.

La question problématique : anatomie d'un désastre de performance

Voici la requête originale générée par le module blocklayered. Le module blocklayered di PrestaShop — maintenant connu dans des versions plus récentes sous le nom de « Recherche à facettes » o « Recherche par couches » — est l’un des modules fondamentaux pour le navigation produit filtrée au sein d'une catégorie, d'une page de recherche ou d'une sélection de produits (par exemple, fabricant, attribut, balise, etc.).

SELECT
    p.*,
    product_shop.*,
    stock.out_of_stock,
    IFNULL(stock.quantity, 0) AS quantity,
    MAX(product_attribute_shop.id_product_attribute) AS id_product_attribute,
    product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity,
    pl.`description`,
    pl.`description_short`,
    pl.`available_now`,
    pl.`available_later`,
    pl.`link_rewrite`,
    pl.`meta_description`,
    pl.`meta_keywords`,
    pl.`meta_title`,
    pl.`name`,
    MAX(image_shop.`id_image`) AS id_image,
    isecond.`id_image` AS id_image_second,
    il.`legend`,
    m.`name` AS manufacturer_name,
    cl.`name` AS category_default,
    DATEDIFF(product_shop.`date_add`, DATE_SUB(NOW(), INTERVAL 20 DAY)) > 0 AS new,
    product_shop.price AS orderprice,
    cp.position

FROM
    `ps_category_product` cp  -- Tabella di join Categoria-Prodotto
LEFT JOIN
    `ps_product` p ON p.`id_product` = cp.`id_product`  -- Informazioni base del Prodotto
INNER JOIN
    ps_product_shop product_shop
    ON (
        product_shop.id_product = p.id_product
        AND product_shop.id_shop = 1
    )  -- Informazioni specifiche del Prodotto per lo Shop (necessario)
LEFT JOIN
    `ps_product_attribute` pa ON (p.`id_product` = pa.`id_product`)  -- Attributi del Prodotto (combinazioni)
LEFT JOIN
    ps_product_attribute_shop product_attribute_shop
    ON (
        product_attribute_shop.id_product_attribute = pa.id_product_attribute
        AND product_attribute_shop.id_shop = 1
        AND product_attribute_shop.`default_on` = 1
    )  -- Attributi dello Shop (solo default)
LEFT JOIN
    ps_stock_available stock
    ON (
        stock.id_product = p.id_product
        AND stock.id_product_attribute = IFNULL(`product_attribute_shop`.id_product_attribute, 0)
        AND stock.id_shop = 1
    )  -- Stock disponibile
LEFT JOIN
    `ps_category_lang` cl
    ON (
        product_shop.`id_category_default` = cl.`id_category`
        AND cl.`id_lang` = 6
        AND cl.id_shop = 1
    )  -- Nome della Categoria di default (lingua 6)
LEFT JOIN
    `ps_product_lang` pl
    ON (
        p.`id_product` = pl.`id_product`
        AND pl.`id_lang` = 6
        AND pl.id_shop = 1
    )  -- Dati descrittivi del Prodotto (lingua 6)
LEFT JOIN
    `ps_image` i ON (i.`id_product` = p.`id_product`)  -- Tutte le immagini
LEFT JOIN
    ps_image_shop image_shop
    ON (
        image_shop.id_image = i.id_image
        AND image_shop.id_shop = 1
        AND image_shop.cover = 1
    )  -- Immagine di copertina per lo Shop
LEFT JOIN
    `ps_image` isecond
    ON (
        isecond.`id_product` = p.`id_product`
        AND isecond.position = 2
    )  -- Immagine in seconda posizione
LEFT JOIN
    `ps_image_lang` il
    ON (
        image_shop.`id_image` = il.`id_image`
        AND il.`id_lang` = 6
    )  -- Legenda dell'immagine di copertina (lingua 6)
LEFT JOIN
    `ps_manufacturer` m ON m.`id_manufacturer` = p.`id_manufacturer`  -- Nome del Produttore

WHERE
    product_shop.`id_shop` = 1  -- Filtro per lo Shop
    AND cp.`id_category` = 66  -- Filtro per la Categoria specifica
    AND product_shop.`active` = 1  -- Solo prodotti attivi
    AND product_shop.`visibility` IN ("both", "catalog")  -- Solo prodotti visibili in catalogo o ovunque

GROUP BY
    product_shop.id_product

ORDER BY
    cp.`position` ASC

LIMIT 0, 50;  -- Limite di risultati (offset 0, 50 righe)

Qu'est-ce qui rend cette requête si lente ?

En analysant la requête « EXPLAIN », plusieurs problèmes critiques sont apparus :

1. GROUP BY sur une table avec plusieurs lignes :L'opération de regroupement sur `product_shop.id_product` nécessite le tri de toutes les lignes intermédiaires générées par les JOIN, une opération O(n log n) sur des ensembles de données potentiellement volumineux.

2. Fonctions d'agrégation MAX() sur JOIN :Les deux `MAX()` — sur `product_attribute_shop.id_product_attribute` et `image_shop.id_image` — forcent la base de données à :

  • Effectuer toutes les jointures
  • Regrouper par produit
  • Calculer le maximum pour chaque groupe

Ce modèle est particulièrement inefficace lorsqu’un produit comporte de nombreuses variantes ou de nombreuses images.

3. Cascade de LEFT JOIN La cascade de 11 jointures gauches crée un produit cartésien de grande taille. Pour un produit avec :

  • 10 variantes (product_attribute)
  • images 5
  • Données multilingues

Potentiellement, plus de 50 lignes intermédiaires sont générées et doivent ensuite être regroupées.

4. Manque de filtres dans les sous-requêtes Il n'y a pas de préfiltrage. La base de données doit traiter TOUTES les variantes de TOUS les produits avant d'appliquer les filtres.

5. Table temporaire et tri de fichiers : L'option « EXPLAIN » a montré : Utilisation de temporaire ; Utilisation de filesort

Deux signes indiquant que la base de données doit créer des tables temporaires et les trier sur le disque — des opérations très coûteuses.

Le coût caché de GROUP BY

Il GROUP BY dans cette requête, c'est particulièrement délicat car :

  • Impossible d'utiliser les index efficacement (le tri est différent du regroupement)
  • Nécessite des tampons de mémoire importants (ou des débordements de disque)
  • Il doit traiter toutes les lignes avant de pouvoir renvoyer même le premier résultat.
  • Empêche les optimisations telles que « l'analyse d'index lâche »

Avec un catalogue de 10 000 produits et 50 000 variantes, cette requête pourrait générer temporairement des millions de lignes intermédiaires.

La solution : réécrire la requête pour éliminer les anti-modèles

Après des jours d'analyse, de benchmarking et d'itérations, nous sommes arrivés à cette requête optimisée mais très, très, très, très, très, très, très longue :

SELECT
    p.*,
    product_shop.*,
    stock.out_of_stock,
    IFNULL(stock.quantity, 0) AS quantity,
    pa_default.id_product_attribute,
    pa_default.minimal_quantity AS product_attribute_minimal_quantity,
    pl.`description`,
    pl.`description_short`,
    pl.`available_now`,
    pl.`available_later`,
    pl.`link_rewrite`,
    pl.`meta_description`,
    pl.`meta_keywords`,
    pl.`meta_title`,
    pl.`name`,
    img_cover.id_image AS id_image,
    isecond.`id_image` AS id_image_second,
    il.`legend`,
    m.`name` AS manufacturer_name,
    cl.`name` AS category_default,
    DATEDIFF(product_shop.`date_add`, DATE_SUB(NOW(), INTERVAL 20 DAY)) > 0 AS new,
    product_shop.price AS orderprice,
    cp.position

FROM
    `ps_category_product` cp  -- Tabella di join Categoria-Prodotto
INNER JOIN
    `ps_product_shop` product_shop
    ON (
        product_shop.id_product = cp.id_product
        AND product_shop.id_shop = 1
    )  -- Informazioni specifiche del Prodotto per lo Shop
INNER JOIN
    `ps_product` p
    ON p.`id_product` = cp.`id_product`  -- Informazioni base del Prodotto
LEFT JOIN
    (
        -- Subquery per l'attributo di prodotto predefinito (default)
        SELECT
            pa.id_product,
            pa.id_product_attribute,
            pas.minimal_quantity
        FROM
            ps_product_attribute pa
        INNER JOIN
            ps_product_attribute_shop pas
            ON (
                pas.id_product_attribute = pa.id_product_attribute
                AND pas.id_shop = 1
                AND pas.`default_on` = 1
            )
    ) pa_default ON pa_default.id_product = p.id_product
LEFT JOIN
    ps_stock_available stock
    ON (
        stock.id_product = p.id_product
        AND stock.id_product_attribute = IFNULL(pa_default.id_product_attribute, 0)
        AND stock.id_shop = 1
    )  -- Stock disponibile (collegato all'attributo di default)
LEFT JOIN
    `ps_category_lang` cl
    ON (
        product_shop.`id_category_default` = cl.`id_category`
        AND cl.`id_lang` = 6
        AND cl.id_shop = 1
    )  -- Nome della Categoria di default (lingua 6)
LEFT JOIN
    `ps_product_lang` pl
    ON (
        p.`id_product` = pl.`id_product`
        AND pl.`id_lang` = 6
        AND pl.id_shop = 1
    )  -- Dati descrittivi del Prodotto (lingua 6)
LEFT JOIN
    (
        -- Subquery per l'immagine di copertina (cover)
        SELECT
            i.id_product,
            i.id_image
        FROM
            ps_image i
        INNER JOIN
            ps_image_shop image_shop
            ON (
                image_shop.id_image = i.id_image
                AND image_shop.id_shop = 1
                AND image_shop.cover = 1
            )
        WHERE
            i.id_product IN (
                SELECT
                    cp2.id_product
                FROM
                    ps_category_product cp2
                WHERE
                    cp2.id_category = 66
            )  -- Filtra le immagini solo per i prodotti della categoria 66
    ) img_cover ON img_cover.id_product = p.id_product
LEFT JOIN
    `ps_image` isecond
    ON (
        isecond.`id_product` = p.`id_product`
        AND isecond.position = 2
    )  -- Immagine in seconda posizione
LEFT JOIN
    `ps_image_lang` il
    ON (
        img_cover.`id_image` = il.`id_image`
        AND il.`id_lang` = 6
    )  -- Legenda dell'immagine di copertina (lingua 6)
LEFT JOIN
    `ps_manufacturer` m
    ON m.`id_manufacturer` = p.`id_manufacturer`  -- Nome del Produttore

WHERE
    cp.`id_category` = 66  -- Filtro per la Categoria specifica
    AND product_shop.`active` = 1  -- Solo prodotti attivi
    AND product_shop.`visibility` IN ("both", "catalog")  -- Solo prodotti visibili in catalogo o ovunque

ORDER BY
    cp.`position` ASC

LIMIT 50;  -- Limite di risultati (inizia da 0 per default)

Anatomie des optimisations

Examinons de plus près ce qui rend cette requête si efficace :

1. Éliminez complètement GROUP BY

Prima: GROUP BY product_shop.id_product
Après: Pas de GROUP BY

Il s'agit du changement le plus impactant. En supprimant GROUP BY, nous évitons :

  • Création de tables temporaires
  • Tri intermédiaire de millions de lignes potentielles
  • Déversement de disque lorsque les tampons de mémoire sont insuffisants

2. Remplacement de MAX() par des sous-requêtes ciblées

Pour les attributs du produit :

— AVANT : MAX(product_attribute_shop.id_product_attribute)
— Avec JOIN qui génère N lignes par produit

— AFTER : Sous-requête qui renvoie directement l'attribut par défaut

LEFT JOIN
    (
        SELECT
            pa.id_product,
            pa.id_product_attribute,
            pas.minimal_quantity
        FROM
            ps_product_attribute pa
        INNER JOIN
            ps_product_attribute_shop pas
            ON (
                pas.id_product_attribute = pa.id_product_attribute
                AND pas.id_shop = 1
                AND pas.`default_on` = 1
            )
    ) pa_default
ON
    pa_default.id_product = p.id_product

avantages:

  • La sous-requête est exécutée seulement une fois et matérialisé
  • Renvoie exactement une ligne par produit (merci au filtre default_on = 1)
  • MySQL peut utiliser des index sur la colonne default_on
  • Aucune fonction d'agrégation à calculer

Pour les images de couverture :

— AVANT : MAX(image_shop.id_image)
— Avec LEFT JOIN sur toutes les images

— APRES : Sous-requête pré-filtrée

LEFT JOIN
    (
        SELECT
            i.id_product,
            i.id_image
        FROM
            ps_image i
        INNER JOIN
            ps_image_shop image_shop
            ON (
                image_shop.id_image = i.id_image
                AND image_shop.id_shop = 1
                AND image_shop.cover = 1  -- Solo cover
            )
        WHERE
            i.id_product IN (
                SELECT
                    cp2.id_product
                FROM
                    ps_category_product cp2
                WHERE
                    cp2.id_category = 66  -- Pre-filtro sulla categoria
            )
    ) img_cover
ON
    img_cover.id_product = p.id_product

avantages:

  • Filtrer cover = 1 sélectionnez uniquement l'image de couverture déjà
  • Le pré-filtrage sur les produits de catégorie réduit considérablement les lignes traitées
  • La sous-requête est exécutée en premier et son résultat est réutilisé
  • Renvoie exactement une ligne par produit

3. Pré-filtrage stratégique

La sous-requête d'image inclut ce filtre préliminaire :

WHERE
    i.id_product IN (
        SELECT
            cp2.id_product
        FROM
            ps_category_product cp2
        WHERE
            cp2.id_category = 66
    )

Avec un catalogue de 10 000 produits dont seulement 150 sont dans la catégorie 66 :

  • Prima: Images traitées de tous les 10 000 produits (plus de 50 000 images)
  • Après: Traiter uniquement les images des 150 produits pertinents (750 images)

Réduction de 98,5% des lignes traitées dans cette phase.

4. Ordre JOIN optimisé

Prima:Ça a commencé à partir de ps_category_product avec un LEFT JOIN sur ps_product

Après:

FROM `ps_category_product` cp
INNER JOIN `ps_product_shop` product_shop ON ...
INNER JOIN `ps_product` p ON ...

En utilisant INNER JOIN pour les tables clés, nous forçons l'optimiseur de requêtes à :

  • Appliquer d'abord les filtres à la catégorie
  • Considérez uniquement les produits actifs et visibles
  • Réduisez considérablement l'ensemble de données avant d'effectuer des jointures gauches facultatives

5. Matérialisation des sous-requêtes

MySQL/Percona peut matérialiser des sous-requêtes dans la clause FROM, créant ainsi de très petites requêtes temporaires indexées. Dans notre cas :

Sous-requête pa_default : ~500 lignes (une par produit avec variantes)
Sous-requête img_cover : ~150 lignes (uniquement les produits de catégorie avec images)

Ces tables temporaires sont si petites qu'elles restent en mémoire (pool de mémoire tampon InnoDB) et les jointures ultérieures deviennent des opérations O(1) via une jointure de hachage ou une recherche d'index.

Le paradoxe des sous-requêtes : plus simple, plus rapide

Il existe une idée fausse répandue selon laquelle les sous-requêtes sont toujours plus lentes que les jointures directes. C'était en partie vrai dans les anciens moteurs de base de données (MySQL 5.1 et versions antérieures), mais ce n'est plus le cas dans les moteurs modernes.

Pourquoi les sous-requêtes bien écrites sont plus rapides :

1. Portée réduiteIls ne traitent que les données nécessaires
2. Matérialisation automatique: L'optimiseur de requêtes crée des requêtes optimisées temporaires
3. Réutiliser:La sous-requête est exécutée une fois, le résultat est réutilisé
4. Meilleure utilisation des indexDes requêtes plus simples permettent des stratégies d'indexation plus efficaces
5. Prévention du produit cartésien:Ils évitent l'explosion combinatoire des lignes

Dans notre cas spécifique, nous avons remplacé une requête par :

0 sous-requêtes
11 JOINT GAUCHE
2 MAX()
1 GROUPER PAR

Avec une requête qui a :

3 sous-requêtes (dont une imbriquée)
8 JOINT GAUCHE
2 JOINTURE INTERNE
0 MAX()
0 GROUPER PAR

Pourtant, ce dernier est 600 fois plus vite.

Les résultats : 1 800 ms à 3 ms

Les tests ont été réalisés dans des conditions réelles, sur une base de données de production (répliquée dans un environnement de test), obtenant une amélioration de 1,8 seconde à 0,003 seconde (3 millisecondes) comme le montre la vidéo suivante où nous illustrons l'ensemble du processus de test.

L'implémentation : de la base de données au code PHP

L'optimisation de la requête SQL n'était que la moitié du travail. Le véritable défi consistait à intégrer cette requête au module PrestaShop existant tout en maintenant une compatibilité totale avec toutes les fonctionnalités du module à blocs.

Le module blocklayered est situé dans : /modules/superposé par blocs/superposé par blocs.php

La requête d'origine était dans la méthode getProductByFilters() de la classe BlockLayered, environ à la ligne 1996.

Adaptation du code PHP

Le changement le plus difficile a été de gérer le fait que la requête optimisée renvoie les mêmes colonnes, mais via des sous-requêtes avec des alias différents. Nous avons dû :

  1. Mettre à jour la construction de la requête:
    $id_shop = (int)Context::getContext()->shop->id;
    $this->products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
        SELECT
            p.*,
            ' . ($alias_where == 'p' ? '' : 'product_shop.*,') . '
            pl.`description`,
            pl.`description_short`,
            // … altri campi espliciti invece di pl.*
            img_cover.id_image, // Invece di MAX(image_shop.id_image)
            pa_default.id_product_attribute, // Invece di MAX(…)
            // … resto della query
    ');
    
  2. Vérifiez la compatibilité des champs renvoyésNous avons dû tester que tous les champs utilisés plus tard dans le code étaient présents avec les mêmes noms et types.
  3. Gestion des cas limites: Produits sans variantes, produits sans images, etc. La requête optimisée avec des LEFT JOINs bien placés gère déjà ces cas, mais nous avons ajouté des tests spécifiques.
  4. Maintenir la pagination et le tri: La clause LIMIT e ORDER BY devrait continuer à fonctionner correctement avec les paramètres dynamiques de PrestaShop.

Réflexions et considérations finales

L'excellence exige du temps et de la méthode

Cette intervention représente l'un des cas les plus intéressants d'optimisation « chirurgicale » sur un système en pleine production. Il s'agissait d'un processus précis, étape par étape, qui a nécessité du temps, de l'expertise et une grande minutie. L'ensemble du processus, de l'analyse initiale à la validation finale, ne s'improvise pas : même pour une équipe expérimentée, une telle activité peut nécessiter au moins deux jours ouvrables complets, souvent beaucoup plus, compte tenu de la nécessité de diagnostiquer, de tester et de vérifier chaque changement.

La partie la plus complexe n'était pas l'intervention technique elle-même, mais plutôt la phase d'étude préliminaire : identifier le goulot d'étranglement, isoler la requête problématique et comprendre l'impact réel de chaque modification possible. À partir de là, un long processus d'affinement a commencé, au cours duquel une expertise en SQL, une connaissance approfondie du fonctionnement interne de MySQL/Percona et familiarité avec l'architecture de PrestaShop ils se sont avérés essentiels.

Il ne s'agit pas seulement d'écrire une requête plus efficace : il faut comprendre comment elle interagit avec le code PHP, le modèle de données et la logique métier de l'application. Chaque modification doit être rigoureusement testée, à la fois pour vérifier les performances et pour s'assurer qu'elle n'entraîne pas de régressions ou de comportements inattendus. Même un petit oubli peut entraîner des problèmes en production. vers les tests et la qualité du code deviennent des phases fondamentales, au même titre que la rédaction de la requête elle-même.

Quand est-ce que ça vaut vraiment le coup ?

Une intervention aussi profonde n'est pas toujours justifiée. Elle n'a de sens que lorsque des conditions spécifiques sont réunies : un trafic important, un impact économique concret lié à la performance et une goulot d'étranglement réel et mesurable Cela impacte l'expérience utilisateur ou les conversions. En pratique, cette approche est pertinente lorsque les optimisations les plus courantes ont déjà été appliquées et que les seules améliorations possibles se situent au cœur de la base de données et du code de l'application.

PageSpeed-InventivaShop

Dans le cas d' inventivashop.com Toutes ces conditions étaient réunies. Le site gérait un volume de trafic important, avec une valeur par conversion élevée. Le problème avait été précisément identifié grâce au monitoring, et l'infrastructure avait déjà été optimisée sur tous les autres aspects. Dans ce contexte, réécrire la requête la plus critique était non seulement un choix judicieux, mais aussi le seul moyen d'obtenir une amélioration tangible et durable.

Alternatives et leçons apprises

Avant d'en arriver à la réécriture, plusieurs approches ont été envisagées, mais toutes présentaient des limites évidentes. Des solutions plus superficielles auraient pu masquer le problème sans le résoudre, tandis qu'une intervention plus approfondie sur la base de données promettait des résultats structurels, capables d'améliorer les performances à long terme.

La leçon la plus importante est que on ne peut pas optimiser à l'aveugletteChaque décision doit s'appuyer sur des données concrètes, collectées grâce à des outils de profilage et d'analyse des requêtes lentes. Dans de nombreux cas, une seule requête est responsable de la majeure partie de la charge CPU ou du ralentissement global, et l'identification de ce point critique fait toute la différence entre un système performant et un système constamment en difficulté.

Nous avons également appris que le sous-requête, souvent diabolisé, s'il est écrit avec soin et filtré correctement, peut être plus efficace que les JOIN complexes. indices, restent alors l’un des éléments les plus déterminants : la meilleure requête du monde devient lente si la base de données n’est pas capable d’accéder aux données de la bonne manière.

Enfin, aucun résultat de ce type n'est durable sans une documentation rigoureuse. Dans notre cas, l'ensemble du travail a été suivi et commenté dans plus de vingt pages de documentation technique, garantissant que chaque modification était compréhensible et reproductible ultérieurement.

La durabilité dans le temps

Une fois le résultat obtenu, le travail ne s'arrête pas. Une optimisation de ce niveau est indispensable. surveillé en permanence afin d'identifier rapidement toute régression ou variation de performances. La requête optimisée a été versionnée et documentée dans le référentiel du projet, et un processus de tests complet est effectué avant toute mise à jour de PrestaShop ou de la base de données. De plus, l'équipe de développement interne a été formée pour maîtriser parfaitement la nouvelle implémentation et pouvoir la maintenir et l'adapter au fil du temps.

Conclusions

Optimisation d'une seule requête SQL sur inventivashop.com a produit une amélioration spectaculaire, réduisant les temps d’exécution de 1 800 millisecondes à seulement 3 millisecondes – un gain d’efficacité significatif. 99,83%Mais plus que les chiffres, ce qui compte, c'est l'effet sur l'expérience utilisateur : chargement instantané, Vitaux Web de base entièrement dans la zone « Bon » et une utilisation des ressources du serveur considérablement réduite.

Ce genre de résultat n'est pas le fruit du hasard. Il nécessite du temps, de la méthode et des compétences très spécifiques, mais surtout, une approche axée sur les données et la précision. Cette approche n'est pas applicable à tous les projets, ni la première option en cas de problèmes de performance. Cependant, lorsque toutes les autres optimisations ont été épuisées et qu'un seul goulot d'étranglement ralentit encore l'ensemble du système, une analyse approfondie des requêtes peut faire la différence entre un site « bon » et un site « exceptionnel ».

Dans notre cas, l’investissement en temps et en ressources a été pleinement rentabilisé : inventivashop.com Elle a consolidé sa position sur les moteurs de recherche pour les mots clés compétitifs, amélioré l'expérience utilisateur de milliers de clients et constaté un impact direct sur les conversions. La véritable clé du succès a été la combinaison d’une approche data-driven, une solide expertise technique et la volonté de consacrer le temps nécessaire pour faire les choses correctement.

Remarque : Cette étude de cas représente une intervention concrète sur un système de production. Les résultats ont été mesurés et validés en environnement réel, mais les performances peuvent varier en fonction de la configuration matérielle, de la version logicielle, de la taille de la base de données et des habitudes d'utilisation.

Vous avez des doutes ? Vous ne savez pas par où commencer ? Contactez-nous !

Nous avons toutes les réponses à vos questions pour vous aider à faire le bon choix.

Discute avec nous

Discutez directement avec notre support avant-vente.

0256569681

Contactez-nous par téléphone pendant les heures de bureau 9h30 - 19h30

Contactez-nous en ligne

Ouvrez une demande directement dans l'espace contact.

AVIS DE NON-RESPONSABILITÉ, Mentions légales et droits d'auteur. Red Hat, Inc. détient les droits sur Red Hat®, RHEL®, RedHat Linux® et CentOS® ; AlmaLinux™ est une marque commerciale de la AlmaLinux OS Foundation ; Rocky Linux® est une marque déposée de la Rocky Linux Foundation ; SUSE® est une marque déposée de SUSE LLC ; Canonical Ltd. détient les droits sur Ubuntu® ; Software in the Public Interest, Inc. détient les droits sur Debian® ; Linus Torvalds détient les droits sur Linux® ; FreeBSD® est une marque déposée de la Fondation FreeBSD ; NetBSD® est une marque déposée de la Fondation NetBSD ; OpenBSD® est une marque déposée de Theo de Raadt ; Oracle Corporation détient les droits sur Oracle®, MySQL®, MyRocks®, VirtualBox® et ZFS® ; Percona® est une marque déposée de Percona LLC ; MariaDB® est une marque déposée de MariaDB Corporation Ab ; PostgreSQL® est une marque déposée de PostgreSQL Global Development Group ; SQLite® est une marque déposée de Hipp, Wyrick & Company, Inc. ; KeyDB® est une marque déposée d'EQ Alpha Technology Ltd. ; Typesense® est une marque déposée de Typesense Inc. ; REDIS® est une marque déposée de Redis Labs Ltd ; F5 Networks, Inc. détient les droits sur NGINX® et NGINX Plus® ; Varnish® est une marque déposée de Varnish Software AB ; HAProxy® est une marque déposée de HAProxy Technologies LLC ; Traefik® est une marque déposée de Traefik Labs ; Envoy® est une marque déposée de CNCF ; Adobe Inc. détient les droits sur Magento® ; PrestaShop® est une marque déposée de PrestaShop SA ; OpenCart® est une marque déposée d'OpenCart Limited ; Automattic Inc. détient les droits sur WordPress®, WooCommerce® et JetPack® ; Open Source Matters, Inc. détient les droits sur Joomla® ; Dries Buytaert détient les droits sur Drupal® ; Shopify® est une marque déposée de Shopify Inc. ; BigCommerce® est une marque déposée de BigCommerce Pty. Ltd.; TYPO3® est une marque déposée de la TYPO3 Association; Ghost® est une marque déposée de la Ghost Foundation; Amazon Web Services, Inc. détient les droits sur AWS® et Amazon SES® ; Google LLC détient les droits sur Google Cloud™, Chrome™ et Google Kubernetes Engine™ ; Alibaba Cloud® est une marque déposée d'Alibaba Group Holding Limited ; DigitalOcean® est une marque déposée de DigitalOcean, LLC ; Linode® est une marque déposée de Linode, LLC ; Vultr® est une marque déposée de The Constant Company, LLC ; Akamai® est une marque déposée d'Akamai Technologies, Inc. ; Fastly® est une marque déposée de Fastly, Inc. ; Let's Encrypt® est une marque déposée d'Internet Security Research Group ; Microsoft Corporation détient les droits sur Microsoft®, Azure®, Windows®, Office® et Internet Explorer® ; Mozilla Foundation détient les droits sur Firefox® ; Apache® est une marque déposée de The Apache Software Foundation ; Apache Tomcat® est une marque déposée de The Apache Software Foundation ; PHP® est une marque déposée de PHP Group ; Docker® est une marque déposée de Docker, Inc. Kubernetes® est une marque déposée de The Linux Foundation ; OpenShift® est une marque déposée de Red Hat, Inc. ; Podman® est une marque déposée de Red Hat, Inc. ; Proxmox® est une marque déposée de Proxmox Server Solutions GmbH ; VMware® est une marque déposée de Broadcom Inc. ; CloudFlare® est une marque déposée de Cloudflare, Inc. ; NETSCOUT® est une marque déposée de NETSCOUT Systems Inc. ; ElasticSearch®, LogStash® et Kibana® sont des marques déposées d'Elastic NV ; Grafana® est une marque déposée de Grafana Labs ; Prometheus® est une marque déposée de The Linux Foundation ; Zabbix® est une marque déposée de Zabbix LLC ; Datadog® est une marque déposée de Datadog, Inc. ; Ceph® est une marque déposée de Red Hat, Inc. ; MinIO® est une marque déposée de MinIO, Inc. ; Mailgun® est une marque déposée de Mailgun Technologies, Inc. ; SendGrid® est une marque déposée de Twilio Inc. Postmark® est une marque déposée d'ActiveCampaign, LLC ; cPanel®, LLC détient les droits sur cPanel® ; Plesk® est une marque déposée de Plesk International GmbH ; Hetzner® est une marque déposée de Hetzner Online GmbH ; OVHcloud® est une marque déposée d'OVH Groupe SAS ; Terraform® est une marque déposée de HashiCorp, Inc. ; Ansible® est une marque déposée de Red Hat, Inc. ; cURL® est une marque déposée de Daniel Stenberg ; Facebook®, Inc. détient les droits sur Facebook®, Messenger® et Instagram®. Ce site n'est pas affilié, sponsorisé ou autrement associé à l'une des entités mentionnées ci-dessus et ne représente aucune de ces entités de quelque manière que ce soit. Tous les droits sur les marques et noms de produits mentionnés sont la propriété de leurs titulaires respectifs des droits d'auteur. Toutes les autres marques mentionnées sont la propriété de leurs titulaires respectifs. MANAGED SERVER® est une marque déposée européenne de MANAGED SERVER SRL, dont le siège social est situé Via Flavio Gioia, 6, 62012 Civitanova Marche (MC), Italie et le siège opérationnel Via Enzo Ferrari, 9, 62012 Civitanova Marche (MC), Italie.

JUSTE UN MOMENT !

Vous êtes-vous déjà demandé si votre hébergement était nul ?

Découvrez dès maintenant si votre hébergeur vous pénalise avec un site web lent digne des années 1990 ! Résultats immédiats.

Fermer le CTA
Retour en haut de page