Query MySQL con ORDER BY,GROUP BY per ordinare e rimuovere righe doppie




Apparentemente questo è un dilemma piuttosto discusso; molti “niubbi” di MySQL (me compreso) vogliono usare un SELECT su righe di una tabella, dove sanno che il campo ‘a’ ha valori ripetuti più volte, ma vogliono estrarre una sola riga per ogni valore, al tempo stesso facendo in modo tale che la riga estratta, tra tutte quelle disponibili, sia quella che ha il campo ‘b’ col valore più basso, o alto, o lungo, o corto tra tutte le possibili… bene, il primo tentativo che fanno questi niubbi è una query come questa:

SELECT a, b, c FROM table WHERE <conditions> ORDER BY b DESC GROUP BY a

e ottenendo un errore si rendono conto che non è possibile, perché la sintassi MySQL vuole che la clausola GROUP BY venga prima di quella ORDER BY, come nel seguente esempio:

SELECT a, b, c FROM table WHERE <conditions> GROUP BY a ORDER BY b DESC

In questo caso però, le righe prima vengono sfoltite rimuovendo i doppioni del campo ‘a’ (con un criterio che non possiamo scegliere noi), e solo dopo vengono ordinate per ‘b’, e questo non ci serve.

Quindi come si fa per ordinare prima le righe così come ci servono, e poi rimuovere quelle di troppo?

Per essere brevi, ecco come:

SELECT t.a, t.b, t.c
  FROM table t
  JOIN (SELECT MIN(b) as b
    FROM table
    GROUP BY a) t2 ON t2.b = t.b
  WHERE <condizioni>
  GROUP BY a
  ORDER BY NULL

Quello che fa questo “scheletro di query”, è ordinare le righe per il campo ‘b’ (in senso crescente, cioè ASC, siccome è stata usata la  funzione MIN(), altrimenti potreste usare MAX() per un ordinamento decrescente, e/o altre funzioni in base al tipo di ordinamento che vi serve) all’interno dell’operatore JOIN, e quindi raggruppa per valori unici del campo ‘a’ le righe nella query esterna. L’operatore ORDER BY NULL serve a evitare che l’interprete MySQL perda tempo a ordinare le righe risultanti, siccome un ordinamento viene già fatto con l’operatore GROUP BY. La query è efficace, ma poco efficiente, siccome risulta molto lenta.

All’inizio pensavo che mi sarei dovuto accontentare, tuttavia non soddisfatto, siccome imparando PHP e MySQL sono col tempo diventato ossessionato con l’ottimizzazione per le prestazioni, ho pensato di provare un approccio alternativo, cioè richiedere via SQL le righe “grezze”, e poi rielaborarle successivamente in PHP.

Esaminiamo questo approccio:

$results=doquery("
  SELECT a, b, c
    FROM table
    WHERE <condizioni>
  ");
$datagrid=array();
while ($slot=mysql_fetch_array($results)) $datagrid[]=$slot;
sort($datagrid);
$compare="";
foreach ($datagrid as $k=>$row)
  if ($row[0]==$compare)
    unset($datagrid[$k]);
  else
    $compare=$row[0];

Alla fine di questas routine, $datagrid contiene tutti i dati di cui avete bisogno, e solo quelli; spieghiamo:

  • la query SQL restituisce le righe non processate che corrispondono alle condizioni di WHERE, quindi con i doppioni del campo ‘a’, e senza nessun ordinamento particolare
  • dopo aver inserito i campi delle righe in un array multidimensionale, la funzione PHP sort() si occupa dell’ordinamento dell’array in base agli elementi dei sotto-array, prima per subarray[0], poi per subarray[1] e così via (apriamo una parentesi su questo discorso: se cercate su internet “ordinamento di un array multimensionale in PHP” -meglio se in inglese-, troverete ogni tipo di script artigianale, come se la sola funzoine sort() funzionasse solo sugli array monodimensionali; beh, io ho PHP5 sul mio hosting, ed ho provato a passare il suddetto array multimendionale a sort(), sorpresa delle sorprese l’ha messo in ordine esattamente come serviva a me; se però per voi non dovesse funzionare, recatevi sulla pagina di php.net dedicata alla funzione sort, e nella sezione dei commenti troverete molti esempi già compilati di script “casalinghi”, anche se non saranno mai veloci come sort() che è una funzione propria di PHP)
  • la stringa $compare è inizializzata, e usata nel successivo ciclo foreach per fare un unset() di tutti i subarray dove l’elemento [0] (il nostro campo ‘a’) è uguale a quello del precedente subarray, quindi rimuovendo i duplicati oltre la prima riga

Dopo di ciò potete passare l’array risultate ad un’altra routine per processare i dati così come vi servono.

Per portare la performance ancora oltre, nel caso abbiate bisogno di eseguire una procedura semplice sui dati ottenuti, potete modificare il ciclo foreach dell’esempio come segue:

foreach ($datagrid as $row)
  if ($row[0]!=$compare) {
    $compare=$row[0];
    <routine di elaborazione in questo spazio>
  }

Chiarendo, questo ciclo foreach non si occupa di eliminare i subarray duplicati, ma si limita ad eseguire la routine di elaborazione solo sulla prima istanza di un subarray, ignorando le copie successive; in questo modo, tagliate diverse volte sui tempi di esecuzione:

  1. risparmiate il tempo di un secondo foreach per scorrere l’array ed eseguire le operazioni di elaborazione, siccome queste vengono lanciate direttamente dal primo foreach
  2. evitate di lanciare la funzione unset() tante volte quanti sono i doppioni
  3. risparmiate i cicli di CPU e la memoria necessari a definire un secondo array per contenere i dati sfoltiti, siccome state lavorando direttamente sul primo array costruito sui risultati della query
  4. semplificate il foreach perché non richiedete più un elemento associativo ($k=>$row) ma solo il valore dell’elemento

Con i miei benchmark (su un set di dati MOLTO ristretto, lo ammetto), ho misurato un incredibile aumento di prestazioni di cento volte (già, 100 volte più veloce) per l’ordinamento/sfoltimento in PHP, paragonato al “doppio GROUP BY” in MySQL (0.08 secondi per MySQL/PHP, contro 8 secondi per la query complessa, su 100 iterazioni). I vostri risultati potrebbero essere diversi, ma partendo da un vantaggio di cento volte, c’è molto spazio di manovra.




4 commenti su “Query MySQL con ORDER BY,GROUP BY per ordinare e rimuovere righe doppie”

  1. Ciao,
    Con la seguente variante si ottiene la stessa cosa vero?
    (ho verificato di persona in mysql ma vorrei conferma)

    SELECT t.a, min(t.b), t.c
    FROM dbesercizi.t
    GROUP BY a

    1. Vorrei tanto darti una risposta definitiva, ma non lo so!
      Sono pur sempre un niubbo autodidatta, però di logica il min(t.b) viene eseguito nel SELECT solo dopo il GROUP BY, quindi prima GROUP BY esegue il suo raggruppamento secondo come sta comodo, senza riguardo per il valore del campo b, e poi il min(t.b) una volta applicato ai risultati sarà sempre uguale a t.b.
      Sempre secondo me eh!
      Poi siccome tu hai una tabella, prova a fare nella stessa query il SELECT sia del valore minimo che del campo nudo e crudo, e se sono sempre uguali tra di loro allora è come dico io 😉

  2. Allora… la tabella è semplice:

    INSERT INTO dbesercizi.t(x,a,b,c)
    values(
    a,5,3,1
    ),
    (
    b,5,6,2
    )

    se faccio

    SELECT t.a, min(t.b), t.c
    FROM dbesercizi.t
    GROUP BY a;

    allora ottengo

    a=5 min(t.b)=3 c=1

    se faccio

    SELECT t.a, max(t.b), t.c
    FROM dbesercizi.t
    GROUP BY a;

    allora ottengo
    a=5 max(t.b)=6 c=1

    invece (se ho capito bene quello che intendi dire tu)
    senza alcun aggregato
    SELECT t.a, t.b, t.c
    FROM dbesercizi.t
    GROUP BY a;

    ottengo
    a=5 b=3 c=1

    Da qui ho dedotto che funziona….
    Fammi sapere la tua opinione a riguardo e se sei disponibile magari se mi potresti spiegare meglio il meccanismo del GROUP by, nel senso che viene prima fatto il GROUP BY e dopo la SELECT o il contrario?

    1. Non sono così esperto, ma se per te ha funzionato nel modo in cui riporti, l’unico altro test da fare sarebbe espandere la tabella di prova per vedere se i risultati sono coerenti.
      E infine fare un benchmark per vedere se GROUP BY è più o meno veloce di min()/max()

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *