▦ Dati e database

GROUP BY in SQL: sei errori comuni che falsano i totali

WHERE o HAVING, colonne non raggruppate, medie di medie, conteggi gonfiati e date con l'orario: gli errori tipici delle aggregazioni SQL e i rimedi.

GROUP BY è una delle prime clausole che si imparano in SQL e una di quelle che producono più report sbagliati. Il motivo è che una query di aggregazione restituisce quasi sempre un risultato dall'aspetto credibile: una tabella ordinata di numeri. Non ci sono messaggi di errore che avvertono che quei numeri non rispondono alla domanda posta. Ecco gli errori che si incontrano più spesso, con il modo per riconoscerli.

Come funziona, in breve

Una query con GROUP BY viene elaborata in un ordine logico preciso, diverso dall'ordine in cui è scritta:

  1. FROM e JOIN: si costruisce l'insieme delle righe;
  2. WHERE: si filtrano le singole righe;
  3. GROUP BY: le righe rimaste si dividono in gruppi;
  4. HAVING: si filtrano i gruppi;
  5. SELECT: si calcolano le colonne del risultato;
  6. ORDER BY: si ordina.

Tenere a mente questa sequenza spiega la maggior parte degli errori che seguono.

Errore 1: confondere WHERE e HAVING

WHERE agisce sulle righe prima del raggruppamento, HAVING sui gruppi dopo. Per trovare i clienti che hanno speso più di 1000 euro nel 2025:

SELECT id_cliente, SUM(totale) AS speso
FROM ordini
WHERE data_ordine >= '2025-01-01' AND data_ordine < '2026-01-01'
GROUP BY id_cliente
HAVING SUM(totale) > 1000;

Il filtro sulla data va nel WHERE, perché riguarda i singoli ordini. Il filtro sulla somma va nell'HAVING, perché la somma esiste solo dopo il raggruppamento. Mettere nell'HAVING una condizione che potrebbe stare nel WHERE di solito funziona, ma costringe il database a raggruppare righe che poi scarterà.

Errore 2: colonne non raggruppate nel SELECT

SELECT id_cliente, citta, SUM(totale)
FROM ordini
GROUP BY id_cliente;

Se citta non è nel GROUP BY e non è dentro una funzione di aggregazione, quale valore dovrebbe mostrare il database, se un cliente ha ordini spediti in città diverse? Lo standard SQL vieta questa query, a meno che la colonna dipenda funzionalmente dalla chiave raggruppata, e PostgreSQL e le versioni recenti di MySQL la rifiutano con un errore. Le versioni di MySQL configurate senza l'impostazione ONLY_FULL_GROUP_BY, invece, la accettano e restituiscono un valore qualsiasi tra quelli del gruppo. Il risultato può cambiare da un'esecuzione all'altra senza che nessuno se ne accorga.

La correzione è aggiungere la colonna al GROUP BY, se si vuole un gruppo per ogni combinazione, oppure scegliere esplicitamente quale valore mostrare, per esempio con MAX(citta).

Errore 3: contare righe invece di entità

Una tabella righe_ordine ha una riga per ogni prodotto in ogni ordine. La domanda «quanti ordini contengono prodotti della categoria Libri?»:

SELECT COUNT(*) FROM righe_ordine WHERE categoria = 'Libri';

conta le righe, non gli ordini: un ordine con tre libri vale tre. La versione corretta è:

SELECT COUNT(DISTINCT id_ordine) FROM righe_ordine WHERE categoria = 'Libri';

Prima di scrivere un conteggio conviene chiedersi sempre: che cosa rappresenta una riga di questa tabella?

Errore 4: la media delle medie

Si vuole il valore medio di un ordine. Esiste già una tabella di riepilogo con la media per ogni negozio:

negozio ordini media
A 1000 40
B 10 100

Fare la media delle due medie dà 70, ma il valore medio reale di un ordine è (1000 × 40 + 10 × 100) / 1010, cioè circa 40,6. Il negozio B, con pochissimi ordini, pesa quanto il negozio A. La regola è: le medie si ricalcolano dai dati di dettaglio, oppure da somme e conteggi, mai facendo la media di altre medie. Lo stesso vale per le percentuali.

Errore 5: totali gonfiati dalle JOIN

Si vuole il fatturato per cliente insieme al numero di ticket di assistenza aperti:

SELECT c.id, SUM(o.totale), COUNT(t.id)
FROM clienti c
LEFT JOIN ordini o ON o.id_cliente = c.id
LEFT JOIN ticket t ON t.id_cliente = c.id
GROUP BY c.id;

Un cliente con 4 ordini e 3 ticket genera 12 righe prima del raggruppamento: il fatturato risulta triplicato e i ticket quadruplicati. La soluzione è aggregare ogni tabella separatamente e poi unire i risultati:

WITH f AS (SELECT id_cliente, SUM(totale) AS fatturato FROM ordini GROUP BY id_cliente),
     t AS (SELECT id_cliente, COUNT(*) AS ticket FROM ticket GROUP BY id_cliente)
SELECT c.id, COALESCE(f.fatturato, 0), COALESCE(t.ticket, 0)
FROM clienti c
LEFT JOIN f ON f.id_cliente = c.id
LEFT JOIN t ON t.id_cliente = c.id;

Un buon controllo: il totale generale del report deve coincidere con SELECT SUM(totale) FROM ordini.

Errore 6: raggruppare per data una colonna con l'orario

Se data_ordine contiene anche l'ora, GROUP BY data_ordine crea un gruppo per ogni istante diverso, non per ogni giorno. Il report «vendite per giorno» diventa un elenco di migliaia di righe. Bisogna troncare il valore al giorno, con la funzione disponibile nel proprio database, come CAST(data_ordine AS DATE) o date_trunc('day', data_ordine) in PostgreSQL, tenendo conto del fuso orario in cui si vuole definire il giorno.

Un problema collegato: i giorni senza vendite non compaiono affatto, perché non ci sono righe da raggruppare. In un grafico l'andamento sembra continuo anche quando non lo è. Per mostrare anche gli zeri serve una tabella o una serie di date da unire con una LEFT JOIN.

In sintesi

  • WHERE filtra le righe prima del raggruppamento, HAVING filtra i gruppi dopo.
  • Ogni colonna del SELECT deve essere raggruppata o aggregata.
  • COUNT(DISTINCT ...) conta le entità quando una riga non coincide con l'entità.
  • Le medie si ricalcolano da somme e conteggi, mai come media di medie.
  • Più JOIN uno-a-molti gonfiano i totali; le date con orario vanno troncate al giorno.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica e in psicologia, unisce l'analisi tecnica alla comprensione del fattore umano.

Leggi anche