23 Ottobre 2025

Velocizzare un sito PrestaShop ottimizzando una singola query SQL: un caso reale.

Come abbiamo ridotto i tempi di risposta da 1,8 secondi a 3 millisecondi su inventivashop.com e risolto i problemi Google Core web Vitals.

Ottimizzazione-Query-SQL-PrestaShop

Nel panorama dell’e-commerce moderno, ogni millisecondo conta. Quando si parla di performance web e posizionamento sui motori di ricerca, la velocità del sito non è più un optional ma un requisito fondamentale. Questo è particolarmente vero per siti ad alto traffico come inventivashop.com, che domina la prima posizione su Google per parole chiave competitive come “Insegne”.
Il sito in questione rappresenta un caso studio eccellente di come un’infrastruttura già ampiamente ottimizzata possa comunque nascondere colli di bottiglia critici che, una volta identificati e risolti, portano a miglioramenti prestazionali straordinari. In questo articolo tecnico condivideremo il nostro approccio metodico all’ottimizzazione di una query SQL particolarmente problematica, dimostrando come sia stato possibile ottenere un miglioramento delle performance del 99,83% — passando da 1,8 secondi a soli 3 millisecondi.

Posizionamento-parola-chiave-Insegne-Google

Il contesto: un’infrastruttura già al top

Prima di addentrarci nel caso specifico, è importante sottolineare che inventivashop.com non partiva certo da zero in termini di ottimizzazione. Il sito beneficiava già di un arsenale completo di tecnologie e best practices:

Stack tecnologico all’avanguardia

  • HTTP/3: Protocollo di nuova generazione per connessioni più veloci e resilienti
  • ZSTD Compression: Compressione avanzata per ridurre il payload delle risposte
  • NGINX ottimizzato: Tuning a livello webserver con configurazioni personalizzate per massime performance
  • Certificati TLS compressi: Riduzione dell’overhead SSL/TLS
  • Supporto WebP: Immagini di nuova generazione con dimensioni ridotte fino al 30%
  • Core Web Vitals ottimizzati: Tutte le best practices implementate per LCP, FID, CLS
  • Percona Server 5.7: Database server ottimizzato al posto di MySQL vanilla

Quest’ultimo punto merita particolare attenzione. La scelta di Percona Server 5.7, pur essendo in End of Life da novembre 2024, non è stata casuale ma il risultato di benchmark approfonditi che hanno evidenziato performance superiori rispetto a MariaDB per le specifiche query del sito.

La scelta controversa: Percona Server 5.7 in EOL

Durante i test preliminari, avevamo identificato una query critica che mostrava comportamenti drasticamente diversi tra i vari database engine:

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

La differenza era così marcata (circa 9x più veloce) che abbiamo dovuto prendere una decisione difficile: utilizzare una versione EOL ma con performance nettamente superiori, implementando naturalmente tutte le misure di sicurezza necessarie (isolamento di rete, hardening, monitoring continuo).

Il problema: quando un singolo modulo mette in ginocchio il sito

Nonostante tutte queste ottimizzazioni, il monitoraggio delle performance continuava a evidenziare un problema ricorrente e significativo. Le metriche mostravano picchi di latenza inspiegabili, e il Time To First Byte (TTFB) risultava ben al di sopra dei nostri standard in determinate pagine.

Il modulo BlockLayered: indispensabile ma pesante

Il colpevole si è rivelato essere il modulo blocklayered di PrestaShop (nelle versioni più recenti rinominato “Faceted Search” o “Ricerca a strati”). Questo modulo è fondamentale per qualsiasi e-commerce moderno perché gestisce:

  • Filtri di categoria (prezzo, colore, taglia, marca, ecc.)
  • Navigazione sfaccettata nei prodotti
  • Conteggi dinamici dei prodotti per ogni filtro
  • Aggregazioni e ordinamenti complessi

Il modulo viene invocato praticamente in ogni pagina di categoria, ricerca o listing prodotti — ovvero nelle pagine più visitate di un e-commerce. Una sua inefficienza si ripercuote quindi sull’intera esperienza utente e, di conseguenza, sui tassi di conversione.

La scoperta: 1,8 secondi per una singola query

Attraverso l’analisi sistematica delle slow query log e l’utilizzo di Percona Toolkit (pt-query-digest), abbiamo identificato la query problematica. I numeri erano allarmanti:

  • Tempo medio di esecuzione: 1.700-1.800 ms
  • Frequenza: Migliaia di volte al giorno
  • Impatto totale: Decine di minuti di tempo CPU sprecato ogni ora
  • Pattern: Presente in ogni navigazione di categoria con filtri

Per contestualizzare: in un’architettura ben ottimizzata, ci aspettiamo che le query di lettura su cataloghi prodotti richiedano 10-50ms. Una query da quasi 2 secondi è circa 40-180 volte più lenta del dovuto.

L’approccio diagnostico

Avremmo potuto utilizzare soluzioni APM (Application Performance Monitoring) come New Relic, che avrebbero fornito una visione olistica delle performance applicative. Tuttavia, data la natura concentrata del problema — una singola query molto frequente e molto lenta — abbiamo optato per un approccio più chirurgico:

  1. Abilitazione slow query log su Percona Server
  2. Raccolta dati per 24-48 ore
  3. Analisi con pt-query-digest per identificare pattern e normalizzare le query
  4. EXPLAIN ANALYZE per comprendere il piano di esecuzione
  5. Profiling della query specifica per identificare i colli di bottiglia

Questo approccio ci ha permesso di concentrare tutte le risorse sulla risoluzione del problema reale senza dispersioni.

La query problematica: anatomia di un disastro prestazionale

Ecco la query originale generata dal modulo blocklayered. Il modulo blocklayered di PrestaShop — oggi conosciuto nelle versioni più recenti come “Faceted Search” o “Ricerca a strati” — è uno dei moduli fondamentali per la navigazione filtrata dei prodotti all’interno di una categoria, una pagina di ricerca o una selezione di prodotti (es. produttore, attributo, tag, ecc.).

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)

Cosa rende questa query così lenta?

Analizzando l’`EXPLAIN` della query, emergevano diversi problemi critici:

1. GROUP BY su una tabella con molte righe : L’operazione di raggruppamento su `product_shop.id_product` richiede l’ordinamento di tutte le righe intermedie generate dalle JOIN, un’operazione O(n log n) su dataset potenzialmente grandi.

2. Funzioni aggregate MAX() su JOIN : I due `MAX()` — su `product_attribute_shop.id_product_attribute` e `image_shop.id_image` — costringono il database a:

  • Eseguire tutte le JOIN
  • Raggruppare per prodotto
  • Calcolare il massimo per ogni gruppo

Questo pattern è particolarmente inefficiente quando un prodotto ha molte varianti o molte immagini.

3. Cascade di LEFT JOIN : La cascata di 11 LEFT JOIN crea un prodotto cartesiano molto ampio. Per un prodotto con:

  • 10 varianti (product_attribute)
  • 5 immagini
  • Dati multilingua

Si generano potenzialmente 50+ righe intermedie che poi devono essere raggruppate.

4. Assenza di filtri nelle subquery : Non c’è pre-filtraggio. Il database deve processare TUTTE le varianti di TUTTI i prodotti prima di applicare i filtri.

5. Temporary table e filesort : L’`EXPLAIN` mostrava: Using temporary; Using filesort

Due segnali che il database deve creare tabelle temporanee e ordinarle su disco — operazioni molto costose.

Il costo nascosto del GROUP BY

Il GROUP BY in questa query è particolarmente insidioso perché:

  • Non può utilizzare indici in modo efficiente (l’ordinamento è diverso dal raggruppamento)
  • Richiede buffer di memoria significativi (o spill su disco)
  • Deve processare tutte le righe prima di poter restituire anche solo il primo risultato
  • Impedisce ottimizzazioni come “loose index scan”

Con un catalogo di 10.000 prodotti e 50.000 varianti, questa query potrebbe generare temporaneamente milioni di righe intermedie.

La soluzione: riscrivere la query eliminando gli antipattern

Dopo giorni di analisi, benchmark e iterazioni, siamo arrivati a questa query ottimizzata ma molto, molto molto, molto, molto, molto, molto lunga:

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)

Anatomia delle ottimizzazioni

Analizziamo nel dettaglio cosa rende questa query così più efficiente:

1. Eliminazione completa del GROUP BY

Prima: GROUP BY product_shop.id_product
Dopo: Nessun GROUP BY

Questa è la modifica più impattante. Eliminando il GROUP BY, evitiamo:

  • La creazione di tabelle temporanee
  • L’ordinamento intermedio di potenzialmente milioni di righe
  • Lo spill su disco quando i buffer di memoria sono insufficienti

2. Sostituzione di MAX() con subquery mirate

Per gli attributi prodotto:

— PRIMA: MAX(product_attribute_shop.id_product_attribute)
— Con JOIN che genera N righe per prodotto

— DOPO: Subquery che restituisce direttamente l’attributo default

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

Vantaggi:

  • La subquery è eseguita una sola volta e materializzata
  • Restituisce esattamente una riga per prodotto (grazie al filtro default_on = 1)
  • MySQL può utilizzare indici sulla colonna default_on
  • Nessuna funzione aggregata da calcolare

Per le immagini di copertina:

— PRIMA: MAX(image_shop.id_image)
— Con LEFT JOIN su tutte le immagini

— DOPO: Subquery pre-filtrata

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

Vantaggi:

  • Filtro cover = 1 seleziona già solo l’immagine di copertina
  • Pre-filtro sui prodotti della categoria riduce drasticamente le righe processate
  • La subquery viene eseguita prima e il suo risultato è riutilizzato
  • Restituisce esattamente una riga per prodotto

3. Pre-filtraggio strategico

La subquery per le immagini include questo filtro preliminare:

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

Con un catalogo di 10.000 prodotti di cui solo 150 nella categoria 66:

  • Prima: Processava immagini di tutti i 10.000 prodotti (50.000+ immagini)
  • Dopo: Processa solo immagini dei 150 prodotti rilevanti (750 immagini)

Riduzione del 98,5% delle righe processate in questa fase.

4. Ordine ottimizzato delle JOIN

Prima: Partiva da ps_category_product con una LEFT JOIN su ps_product

Dopo:

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

Utilizzando INNER JOIN per le tabelle fondamentali, forziamo il query optimizer a:

  • Applicare prima i filtri sulla categoria
  • Considerare solo prodotti attivi e visibili
  • Ridurre drasticamente il dataset prima di eseguire le LEFT JOIN opzionali

5. Materializzazione delle subquery

MySQL/Percona può materializzare le subquery nella clausola FROM, creando temporanee molto piccole e indicizzate. Nel nostro caso:

Subquery pa_default : ~500 righe (una per prodotto con varianti)
Subquery img_cover : ~150 righe (solo prodotti della categoria con immagini)

Queste tabelle temporanee sono così piccole che restano in memoria (InnoDB buffer pool) e le JOIN successive diventano operazioni O(1) tramite hash join o index lookup.

Il paradosso delle subquery: più semplici, più veloci

Esiste un preconcetto diffuso secondo cui le subquery sarebbero sempre più lente delle JOIN dirette. Questo era parzialmente vero nei database engine più datati (MySQL 5.1 e precedenti), ma non è più valido nei motori moderni.

Perché le subquery ben scritte sono più veloci:

1. Scope ridotto: Processano solo i dati necessari
2. Materializzazione automatica: Il query optimizer crea temporanee ottimizzate
3. Riutilizzo: La subquery è eseguita una volta, il risultato riutilizzato
4. Migliore utilizzo degli indici: Query più semplici permettono strategie di indicizzazione più efficaci
5. Prevenzione prodotto cartesiano: Evitano l’esplosione combinatoria delle righe

Nel nostro caso specifico, abbiamo sostituito una query con:

0 subquery
11 LEFT JOIN
2 MAX()
1 GROUP BY

Con una query che ha:

3 subquery (di cui una annidata)
8 LEFT JOIN
2 INNER JOIN
0 MAX()
0 GROUP BY

Eppure quest’ultima è 600 volte più veloce.

I risultati: da 1.800 ms a 3 ms

I benchmark sono stati condotti in condizioni reali, su database di produzione (replicato in ambiente di staging), ottenendo un miglioramento da 1,8 secondi a 0,003 secondi (3 millisecondi) come dal video seguente in cui illustriamo tutto il processo di testing.

L’implementazione: dal database al codice PHP

Ottimizzare la query SQL era solo metà del lavoro. La vera sfida è stata integrare questa query nel modulo PrestaShop esistente, mantenendo la completa compatibilità con tutte le funzionalità del modulo blocklayered.

Il modulo blocklayered si trova in: /modules/blocklayered/blocklayered.php

La query originale era nel metodo getProductByFilters() della classe BlockLayered, circa alla riga 1996.

Adattamento del codice PHP

La modifica più delicata è stata gestire il fatto che la query ottimizzata restituisce le stesse colonne ma attraverso subquery con alias diversi. Abbiamo dovuto:

  1. Aggiornare la costruzione della query:
    $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. Verificare la compatibilità dei campi restituiti: Abbiamo dovuto testare che tutti i campi utilizzati successivamente nel codice fossero presenti con gli stessi nomi e tipi.
  3. Gestire i casi edge: Prodotti senza varianti, prodotti senza immagini, ecc. La query ottimizzata con LEFT JOIN ben posizionate gestisce già questi casi, ma abbiamo aggiunto test specifici.
  4. Mantenere la paginazione e l’ordinamento: La clausola LIMIT e ORDER BY dovevano continuare a funzionare correttamente con i parametri dinamici di PrestaShop.

Riflessioni e considerazioni finali

L’eccellenza richiede tempo e metodo

Questo intervento rappresenta uno dei casi più interessanti di ottimizzazione “chirurgica” su un sistema in piena produzione. È stato un lavoro di precisione, svolto passo dopo passo, che ha richiesto tempo, competenze e una grande attenzione ai dettagli. L’intero processo – dall’analisi iniziale alla validazione finale – non è qualcosa che si possa improvvisare: anche per un team esperto, un’attività di questo tipo può richiedere almeno due giornate lavorative piene, spesso molto di più, considerando la necessità di diagnosticare, sperimentare e verificare ogni singolo cambiamento.

La parte più complessa non è stata tanto l’intervento tecnico in sé, quanto la fase di studio preliminare: capire dove si annidava il collo di bottiglia, isolare la query problematica e comprendere l’impatto reale di ogni possibile modifica. Da lì è iniziato un lungo lavoro di rifinitura, in cui la competenza in SQL, la conoscenza approfondita del funzionamento interno di MySQL/Percona e la familiarità con l’architettura di PrestaShop sono risultate essenziali.

Non si tratta solo di scrivere una query più efficiente: è necessario capire come questa interagisce con il codice PHP, con il modello dei dati e con le logiche di business dell’applicazione. Ogni modifica deve essere testata in modo rigoroso, sia per verificarne le prestazioni, sia per assicurarsi che non introduca regressioni o comportamenti inattesi. Anche una piccola svista può causare problemi in produzione, per cui il testing e la qualità del codice diventano fasi fondamentali, al pari della scrittura stessa della query.

Quando vale davvero la pena ?

Un intervento così profondo non è sempre giustificato. Diventa sensato solo quando esistono condizioni precise: un traffico consistente, un impatto economico concreto legato alle performance, e un bottleneck reale e misurabile che incide sull’esperienza utente o sulle conversioni. In pratica, ha senso agire in questo modo quando le ottimizzazioni più comuni sono già state applicate e si è raggiunto un punto in cui l’unico margine di miglioramento passa dal cuore del database e dal codice applicativo.

PageSpeed-InventivaShop

Nel caso di inventivashop.com queste condizioni erano tutte presenti. Il sito gestiva un volume di traffico notevole, con un alto valore per singola conversione. Il problema era stato identificato in modo preciso grazie al monitoraggio, e l’infrastruttura era già stata ottimizzata sotto ogni altro aspetto. In questo contesto, riscrivere la query più critica non era solo una scelta sensata, ma l’unica strada per ottenere un miglioramento tangibile e duraturo.

Le alternative e le lezioni apprese

Prima di arrivare alla riscrittura, sono state considerate diverse strade, ma tutte presentavano limiti evidenti. Soluzioni più superficiali avrebbero potuto mascherare il problema senza risolverlo davvero, mentre un intervento più profondo sul database prometteva risultati strutturali, capaci di migliorare le prestazioni a lungo termine.

La lezione più importante è che non si può ottimizzare alla cieca: ogni decisione deve basarsi su dati concreti, raccolti con strumenti di profilazione e analisi delle query lente. In molti casi, una sola query è responsabile della maggior parte del carico CPU o del rallentamento generale, e individuare quel punto critico fa la differenza tra un sistema efficiente e uno in costante affanno.

Abbiamo anche imparato che le subquery, spesso demonizzate, se scritte con criterio e filtrate correttamente, possono risultare più performanti di JOIN complesse. Gli indici, poi, restano uno degli elementi più determinanti: la query migliore del mondo diventa lenta se il database non è in grado di accedere ai dati nel modo giusto.

Infine, nessun risultato di questo tipo è sostenibile senza un’attenta attività di documentazione. Nel nostro caso, tutto il lavoro è stato tracciato e commentato in oltre venti pagine di documentazione tecnica, per garantire che ogni modifica fosse comprensibile e replicabile anche in futuro.

La sostenibilità nel tempo

Una volta raggiunto il risultato, il lavoro non si ferma. Un’ottimizzazione di questo livello deve essere monitorata costantemente per individuare tempestivamente eventuali regressioni o variazioni nelle performance. La query ottimizzata è stata versionata e documentata nel repository del progetto, e prima di ogni aggiornamento di PrestaShop o del database viene rieseguito un processo di verifica completo. Inoltre, il team di sviluppo interno è stato formato per comprendere a fondo il funzionamento della nuova implementazione, così da poterla mantenere e adattare nel tempo.

Conclusioni

L’ottimizzazione di una singola query SQL su inventivashop.com ha prodotto un miglioramento straordinario, riducendo i tempi di esecuzione da 1.800 millisecondi a soli 3 millisecondi: un incremento di efficienza del 99,83%. Ma più dei numeri, ciò che conta è l’effetto sull’esperienza utente: caricamenti istantanei, Core Web Vitals pienamente nella zona “Good”, e un utilizzo delle risorse server notevolmente ridotto.

Questo tipo di risultato non arriva per caso. Richiede tempo, metodo e competenze molto specifiche, ma soprattutto una mentalità orientata ai dati e alla precisione. Non è un approccio da applicare a ogni progetto, né la prima opzione quando emergono problemi di performance. Tuttavia, quando tutte le altre ottimizzazioni hanno già dato il massimo e resta un singolo collo di bottiglia a rallentare tutto il sistema, un’analisi profonda delle query può fare la differenza tra un sito “sufficiente” e uno “eccezionale”.

Nel nostro caso, l’investimento di tempo e risorse è stato pienamente ripagato: inventivashop.com ha consolidato la sua posizione sui motori di ricerca per keyword competitive, ha migliorato l’esperienza d’uso per migliaia di clienti e ha visto un impatto diretto sulle conversioni. La vera chiave del successo è stata l’unione di un approccio data-driven, una forte competenza tecnica e la volontà di dedicare il tempo necessario per fare le cose nel modo giusto.

Nota: Questo caso studio rappresenta un intervento reale su un sistema in produzione. I risultati sono stati misurati e validati in ambiente reale, ma le performance possono variare in base a configurazione hardware, versione del software, dimensione del database e pattern di utilizzo specifici.

Hai dei dubbi? Non sai da dove iniziare? Contattaci !

Abbiamo tutte le risposte alle tue domande per aiutarti nella giusta scelta.

Chatta con noi

Chatta direttamente con il nostro supporto prevendita.

0256569681

Contattaci telefonicamente negli orari d’ufficio 9:30 – 19:30

Contattaci online

Apri una richiesta direttamente nell’area dei contatti.

DISCLAIMER, Note Legali e Copyright. Red Hat, Inc. detiene i diritti su Red Hat®, RHEL®, RedHat Linux®, e CentOS®; AlmaLinux™ è un marchio di AlmaLinux OS Foundation; Rocky Linux® è un marchio registrato di Rocky Linux Foundation; SUSE® è un marchio registrato di SUSE LLC; Canonical Ltd. detiene i diritti su Ubuntu®; Software in the Public Interest, Inc. detiene i diritti su Debian®; Linus Torvalds detiene i diritti su Linux®; FreeBSD® è un marchio registrato di The FreeBSD Foundation; NetBSD® è un marchio registrato di The NetBSD Foundation; OpenBSD® è un marchio registrato di Theo de Raadt; Oracle Corporation detiene i diritti su Oracle®, MySQL®, MyRocks®, VirtualBox® e ZFS®; Percona® è un marchio registrato di Percona LLC; MariaDB® è un marchio registrato di MariaDB Corporation Ab; PostgreSQL® è un marchio registrato di PostgreSQL Global Development Group; SQLite® è un marchio registrato di Hipp, Wyrick & Company, Inc.; KeyDB® è un marchio registrato di EQ Alpha Technology Ltd.; Typesense® è un marchio registrato di Typesense Inc.; REDIS® è un marchio registrato di Redis Labs Ltd; F5 Networks, Inc. detiene i diritti su NGINX® e NGINX Plus®; Varnish® è un marchio registrato di Varnish Software AB; HAProxy® è un marchio registrato di HAProxy Technologies LLC; Traefik® è un marchio registrato di Traefik Labs; Envoy® è un marchio registrato di CNCF; Adobe Inc. detiene i diritti su Magento®; PrestaShop® è un marchio registrato di PrestaShop SA; OpenCart® è un marchio registrato di OpenCart Limited; Automattic Inc. detiene i diritti su WordPress®, WooCommerce®, e JetPack®; Open Source Matters, Inc. detiene i diritti su Joomla®; Dries Buytaert detiene i diritti su Drupal®; Shopify® è un marchio registrato di Shopify Inc.; BigCommerce® è un marchio registrato di BigCommerce Pty. Ltd.; TYPO3® è un marchio registrato di TYPO3 Association; Ghost® è un marchio registrato di Ghost Foundation; Amazon Web Services, Inc. detiene i diritti su AWS® e Amazon SES®; Google LLC detiene i diritti su Google Cloud™, Chrome™, e Google Kubernetes Engine™; Alibaba Cloud® è un marchio registrato di Alibaba Group Holding Limited; DigitalOcean® è un marchio registrato di DigitalOcean, LLC; Linode® è un marchio registrato di Linode, LLC; Vultr® è un marchio registrato di The Constant Company, LLC; Akamai® è un marchio registrato di Akamai Technologies, Inc.; Fastly® è un marchio registrato di Fastly, Inc.; Let’s Encrypt® è un marchio registrato di Internet Security Research Group; Microsoft Corporation detiene i diritti su Microsoft®, Azure®, Windows®, Office®, e Internet Explorer®; Mozilla Foundation detiene i diritti su Firefox®; Apache® è un marchio registrato di The Apache Software Foundation; Apache Tomcat® è un marchio registrato di The Apache Software Foundation; PHP® è un marchio registrato del PHP Group; Docker® è un marchio registrato di Docker, Inc.; Kubernetes® è un marchio registrato di The Linux Foundation; OpenShift® è un marchio registrato di Red Hat, Inc.; Podman® è un marchio registrato di Red Hat, Inc.; Proxmox® è un marchio registrato di Proxmox Server Solutions GmbH; VMware® è un marchio registrato di Broadcom Inc.; CloudFlare® è un marchio registrato di Cloudflare, Inc.; NETSCOUT® è un marchio registrato di NETSCOUT Systems Inc.; ElasticSearch®, LogStash®, e Kibana® sono marchi registrati di Elastic N.V.; Grafana® è un marchio registrato di Grafana Labs; Prometheus® è un marchio registrato di The Linux Foundation; Zabbix® è un marchio registrato di Zabbix LLC; Datadog® è un marchio registrato di Datadog, Inc.; Ceph® è un marchio registrato di Red Hat, Inc.; MinIO® è un marchio registrato di MinIO, Inc.; Mailgun® è un marchio registrato di Mailgun Technologies, Inc.; SendGrid® è un marchio registrato di Twilio Inc.; Postmark® è un marchio registrato di ActiveCampaign, LLC; cPanel®, L.L.C. detiene i diritti su cPanel®; Plesk® è un marchio registrato di Plesk International GmbH; Hetzner® è un marchio registrato di Hetzner Online GmbH; OVHcloud® è un marchio registrato di OVH Groupe SAS; Terraform® è un marchio registrato di HashiCorp, Inc.; Ansible® è un marchio registrato di Red Hat, Inc.; cURL® è un marchio registrato di Daniel Stenberg; Facebook®, Inc. detiene i diritti su Facebook®, Messenger® e Instagram®. Questo sito non è affiliato, sponsorizzato o altrimenti associato a nessuna delle entità sopra menzionate e non rappresenta nessuna di queste entità in alcun modo. Tutti i diritti sui marchi e sui nomi di prodotto menzionati sono di proprietà dei rispettivi detentori di copyright. Ogni altro marchio citato appartiene ai propri registranti. MANAGED SERVER® è un marchio registrato a livello europeo da MANAGED SERVER SRL, con sede legale in Via Flavio Gioia, 6, 62012 Civitanova Marche (MC), Italia e sede operativa in Via Enzo Ferrari, 9, 62012 Civitanova Marche (MC), Italia.

SOLO UN ATTIMO !

Ti sei mai chiesto se il tuo Hosting faccia schifo ?

Scopri subito se il tuo hosting provider ti sta danneggiando con un sito lento degno del 1990 ! Risultato immediato.

Close the CTA
Torna in alto