SQL Optimization

1 April, 2007 (18:57) | programming, sql

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