SQL Optimization
Supponiamo abbiate due relazioni di questo tipo
test (id, descrizione, soglia) storico_test (id, test, studente, punteggio)
La prima relazione raggruppa tutti i dati relativi ai test (stiamo parlando per esempio di un’applicazione di e-learning), la seconda i dati relativi alle esecuzioni dei test da parte degli studenti. Supponiamo che il nostro scopo sia quello di ottenere un report che indichi per ogni tipo di test quanti studenti lo hanno sostenuto, quanti studenti lo hanno superato e quanti no. Da dove iniziamo? Sicuramente il join naturale fra le due relazione deve essere fatto
select * from test, storico_test where test.id = storico_test.test;
Il fatto stesso che ci chiedano “per ogni tipo di test” ci fa subito venire in mente un raggruppamento
select test.id as test
from test, storico_test
where test.id = storico_test.test
group by test.id
Quanti studenti lo hanno sostenuto? Anche questo è piuttosto facile
select test.id as test, count(*) as sostenuto
from test, storico_test
where test.id = storico_test.test
group by test.id
Adesso viene la parte più succosa, ho visto affrontare il problema in diversi modi, uno dei quali è l’utilizzo di subquery nella select
select test.id as test, count(*) as sostenuto,
(select count(*) from storico_test as nested
where nested.test = test.id and nested.risultato >= test.soglia) as passato,
(select count(*) from storico_test as nested
where nested.test = test.id and nested.risultato < test.soglia) as rifiutato
from test, storico_test
where test.id = storico_test.test
group by test.id, test.soglia
Corretto, ma ripetere per ben tre volte la stessa join può essere critico per le performance se le tabelle contengono molti dati. Ho visto risolvere lo stesso problema in modi più fantasiosi (ad esempio utilizzando l'operatore insiemistico intersect), ma tutti più o meno con lo stesso peso computazionale. La mia proposta è quella di utilizzare il costrutto (per i database che lo supportano) case
select test.id as test, count(*) as sostenuto,
count(case when storico_test.risultato >= test.soglia then 1 else null end) as passato,
count(case when storico_test.risultato < test.soglia then 1 else null end) as rifiutato
from test, storico_test
where test.id = storico_test.test
group by test.id
Grazie all'utilizzo del costrutto case e al fatto che l'operatore count non conta i null, riusciamo a risolvere il problema con una sola join :-)
P.S. Questa ottimizzazione (più altre che tralascio) ha portato una query in produzione in una banca da più di 6 ore a pochi secondi :-)
Write a comment