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:
- 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
- evitate di lanciare la funzione unset() tante volte quanti sono i doppioni
- 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
- 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.