SQL
Interrogare una base di dati relazionale
Il modello relazionale dice come sono organizzati i dati; l'algebra relazionale dice quali operazioni hanno senso su di essi. SQL è il linguaggio con cui quelle operazioni si chiedono davvero a un DBMS. In questo modulo si parte dalla query più semplice e si arriva alle interrogazioni che compaiono agli esami: raggruppamenti, join, subquery correlate.
Tutti gli esempi usano la sintassi MySQL e lo stesso piccolo database, così da poter confrontare una query con l'altra. Dove compare il simbolo del passo-passo puoi far eseguire la query una clausola alla volta e vedere le righe entrare e uscire dal risultato.
SQL e il database di esempio
Un linguaggio dichiarativo
In C++ per trovare i pazienti di Pisa scriveresti un ciclo: scorri una struttura dati, confronti un campo, accumuli i risultati in una lista. Descrivi il procedimento. SQL funziona al contrario: descrivi la proprietà del risultato che vuoi ottenere e lasci al DBMS il compito di trovare il modo più veloce per calcolarlo.
Approccio procedurale
«Parti dalla testa della lista, per ogni elemento confronta il campo citta con "Pisa", se coincide copia l'elemento in una nuova lista, poi avanza al prossimo…»
Approccio dichiarativo
«Voglio cognome e nome dei pazienti la cui città è Pisa.»
Il come (scansione sequenziale, uso di un indice, ordine dei join) lo decide l'ottimizzatore del DBMS.
Questa differenza ha una conseguenza pratica importante: una query si legge come una frase, e il modo più rapido per scriverne una corretta è tradurre la richiesta in italiano pezzo per pezzo, non immaginare cicli.
Il database "Clinica"
Il database di esempio descrive una piccola clinica: ci sono i medici, i pazienti e le visite che i medici effettuano sui pazienti. Tre tabelle, due chiavi esterne.
Questi sono i dati concreti con cui lavoreremo. Tienili a portata: ogni risultato mostrato più avanti si può verificare a mano su queste righe.
Nota una riga anomala: Lorella Nutrie ha Reddito NULL e Ada Bianchi non ha effettuato nessuna visita. Non sono sviste: sono i due casi che fanno sbagliare le query e che useremo per capire NULL e join esterni.
La query di base
SELECT, FROM, WHERE
Ogni interrogazione risponde a tre domande, nell'ordine:
- Da dove prendo i dati? → clausola
FROM: è il "sacco" da cui si pesca. - Quali righe mi interessano? → clausola
WHERE: una condizione controllata riga per riga. - Quali colonne voglio vedere? → clausola
SELECT: la proiezione.
«Indicare cognome e nome dei pazienti con reddito superiore a 1300 euro» diventa:
SELECT Cognome, Nome
FROM Paziente
WHERE Reddito > 1300;
Il punto e virgola chiude l'istruzione. Guarda cosa succede davvero, un passo alla volta:
Se vuoi tutte le colonne, l'asterisco evita di elencarle:
SELECT *
FROM Paziente
WHERE Reddito > 1300;
All'esame conta anche l'ordine. Se la traccia chiede «cognome e nome», proietta Cognome, Nome in quest'ordine: è un dettaglio di coerenza con la richiesta, e i correttori lo notano.
La forma generale, e come si legge
FROM tabelle
WHERE condizioni;
Si scrive in quest'ordine, ma si ragiona in un altro: prima FROM, poi WHERE, infine SELECT. È lo stesso ordine con cui il DBMS la esegue ed è il motivo per cui certe scritture sono vietate (lo vedremo con GROUP BY). Tienilo a mente fin da adesso: la proiezione è l'ultima cosa che accade.
Costruire le condizioni
Nella WHERE si usano i confronti classici (=, <>, <, <=, >, >=) combinati con AND, OR e NOT. Le stringhe vanno tra apici singoli.
-- medici ortopedici di Pisa (due condizioni vere insieme)
SELECT Matricola, Cognome
FROM Medico
WHERE Specializzazione = 'Ortopedia'
AND Citta = 'Pisa';
-- pazienti che stanno a Pisa oppure che si chiamano Rita
SELECT CodFiscale
FROM Paziente
WHERE Citta = 'Pisa'
OR Nome = 'Rita';
Scegliere fra AND e OR è la prima fonte di errori. Il criterio è sempre lo stesso: la condizione viene valutata su una riga per volta. Chiedere «i redditi di Antonella Lepre e Umberto Manzi» con un AND sui nomi non restituisce nulla, perché nessuna singola riga può contenere due persone diverse; serve OR.
Tre abbreviazioni ricorrenti:
| Costrutto | Significato | Esempio |
|---|---|---|
BETWEEN a AND b | intervallo chiuso, estremi inclusi | Parcella BETWEEN 120 AND 150 |
IN (v1, v2, …) | il valore è uno di quelli elencati | Citta IN ('Pisa','Siena') |
LIKE 'Ros%' | confronto su stringhe: % = qualsiasi sequenza, _ = un carattere | Cognome LIKE 'R%' |
BETWEEN funziona su numeri, date e timestamp, ed è solo una scorciatoia: Eta BETWEEN 45 AND 60 equivale a Eta >= 45 AND Eta <= 60.
Duplicati e DISTINCT
La chiave primaria garantisce che non esistano due righe identiche in una tabella, ma nel risultato di una query i duplicati possono comparire: basta proiettare attributi non chiave. Chiedere le specializzazioni dei medici restituisce "Ortopedia" due volte e "Cardiologia" due volte, perché ogni riga corrisponde a un medico diverso.
-- con i duplicati: 5 righe, una per medico
SELECT Specializzazione
FROM Medico;
-- senza duplicati: 3 righe, una per specializzazione
SELECT DISTINCT Specializzazione
FROM Medico;
Con più attributi proiettati, DISTINCT agisce sulla combinazione: due righe sono duplicate solo se coincidono su tutti gli attributi proiettati. SELECT DISTINCT Cognome, Nome elimina solo le persone con stesso cognome e stesso nome.
Quando serve DISTINCT? Quando la query passa da una tabella "figlia" a una "padre" e la richiesta parla di entità distinte. Esempio tipico: «i medici che hanno effettuato almeno una visita» — se un medico ha fatto tre visite comparirebbe tre volte, quindi DISTINCT è necessario.
Valori NULL
Cosa significa NULL
NULL non è zero e non è la stringa vuota: è assenza di valore. Nel database della clinica il reddito di Lorella Nutrie è NULL, e può voler dire cose diverse:
- valore sconosciuto: il reddito esiste, ma non lo conosciamo;
- valore inesistente: la persona non ha redditi, quindi l'attributo non si applica;
- valore non disponibile: esiste e lo conosciamo, ma non possiamo registrarlo.
Il DBMS non distingue i tre casi: li rappresenta tutti con lo stesso NULL. Per questo un attributo pieno di NULL è il sintomo di uno schema progettato male, e per questo la chiave primaria non può mai essere NULL.
IS NULL, IS NOT NULL e la logica a tre valori
Il punto cruciale: qualsiasi confronto con NULL non produce né vero né falso, ma un terzo valore, "sconosciuto". E una riga entra nel risultato solo se la condizione è vera. Conseguenza pratica: le righe con NULL spariscono sia da Reddito > 1300 sia da Reddito <= 1300.
Non funziona
WHERE Reddito = NULL
WHERE Reddito <> NULL
Nessuna riga verrà mai restituita: il confronto vale "sconosciuto", mai vero.
Funziona
WHERE Reddito IS NULL
WHERE Reddito IS NOT NULL
IS NULL tiene solo le righe prive di valore, IS NOT NULL solo quelle che ce l'hanno.
Un esempio classico: in una tabella Studente(Matricola, DataIscrizione, DataLaurea) la data di laurea è NULL per chi non si è ancora laureato. «Gli studenti non ancora laureati» si traduce esattamente in WHERE DataLaurea IS NULL: il NULL qui non è un buco nei dati, è l'informazione stessa.
Lavorare con le date
Tipi e confronti
MySQL rappresenta il tempo con due tipi principali:
| Tipo | Formato | Esempio |
|---|---|---|
DATE | YYYY-MM-DD | '2013-03-01' |
TIMESTAMP | YYYY-MM-DD HH:MM:SS | '2013-03-01 14:30:00' |
Una data si scrive come una stringa fra apici e si confronta con gli operatori normali: Data > '2013-01-01' seleziona le visite successive a quella data, BETWEEN funziona come sui numeri. Il formato è ordinato in modo che il confronto alfabetico coincida con quello cronologico, quindi non ci sono sorprese.
Estrarre e calcolare
| Funzione | Cosa restituisce |
|---|---|
YEAR(d), MONTH(d), DAY(d) | anno, mese e giorno come interi |
CURRENT_DATE | la data di oggi (variabile di sistema, non una funzione da chiamare con le parentesi) |
DATEDIFF(d1, d2) | di quanti giorni d1 è più recente di d2 |
DATE_ADD(d, INTERVAL n unità) | una nuova data spostata in avanti; DATE_SUB all'indietro |
DAYNAME(d), MONTHNAME(d), DAYOFWEEK(d) | nome del giorno, nome del mese, giorno della settimana (1–7) |
Due date non si sottraggono con il meno: per la differenza si usa DATEDIFF. Il meno e il più si usano invece fra una data e un INTERVAL, e il risultato è ancora una data.
-- visite effettuate a marzo 2013
SELECT Medico, Paziente
FROM Visita
WHERE YEAR(Data) = 2013 AND MONTH(Data) = 3;
-- pazienti che hanno compiuto 50 anni prima di oggi
SELECT CodFiscale
FROM Paziente
WHERE CURRENT_DATE > DataNascita + INTERVAL 50 YEAR;
-- quanti giorni sono passati fra due visite dello stesso paziente
SELECT DATEDIFF('2013-02-20', '2013-01-15'); -- 36
"Cinque anni fa" non è una data fissa. Se la traccia dice «i laureati di cinque anni fa», la query deve restare corretta anche eseguita l'anno prossimo: si scrive YEAR(DataLaurea) = YEAR(CURRENT_DATE) - 5, non YEAR(DataLaurea) = 2020.
Operatori di aggregazione
Gli operatori di aggregazione fanno un calcolo su un insieme di righe e restituiscono un singolo valore: il risultato collassa in una riga sola. Sono cinque: COUNT, SUM, AVG, MIN, MAX.
COUNT: contare righe o contare valori
Qui si concentra la maggior parte degli errori, perché COUNT ha tre forme che rispondono a domande diverse:
| Forma | Conta | Domanda a cui risponde |
|---|---|---|
COUNT(*) | le righe del risultato | «quante visite sono state effettuate?» |
COUNT(A) | le righe in cui A non è NULL | «di quanti pazienti conosciamo il reddito?» |
COUNT(DISTINCT A) | i valori diversi di A | «quanti pazienti diversi sono stati visitati?» |
Sul nostro database: le visite del 1° marzo 2013 sono due righe, ma riguardano due pazienti diversi; le visite totali di Rita Gatto sono tre righe ma un solo paziente. Ogni volta che la richiesta contiene la parola «diversi», «distinti» o nomina un'entità che può ripetersi, la risposta è COUNT(DISTINCT …).
-- quante visite sono state fatte il 1° marzo 2013? (righe)
SELECT COUNT(*) AS VisitePrimoMarzo
FROM Visita
WHERE Data = '2013-03-01';
-- quanti pazienti diversi sono stati visitati a marzo 2013? (valori)
SELECT COUNT(DISTINCT Paziente) AS PazientiMarzo
FROM Visita
WHERE YEAR(Data) = 2013 AND MONTH(Data) = 3;
La clausola AS rinomina la colonna del risultato: non è obbligatoria, ma una colonna che si chiama COUNT(*) è illeggibile. Usa nomi parlanti in UpperCamelCase: NumeroCardiologi, ParcellaMediaOrtopedici.
SUM, AVG, MIN, MAX
Funzionano sui valori di un attributo numerico (o confrontabile) all'interno dell'insieme di righe selezionato. I NULL vengono ignorati: la media dei redditi è calcolata solo sui pazienti di cui conosciamo il reddito, non conta la riga di Lorella Nutrie come zero.
SELECT AVG(Reddito) AS RedditoMedio,
MIN(Reddito) AS RedditoMinimo,
MAX(Reddito) AS RedditoMassimo,
COUNT(Reddito) AS RedditiNoti
FROM Paziente;
La trappola del massimo
«Indicare il reddito massimo e chi lo detiene» sembra una riga sola di codice:
Sbagliata
SELECT MAX(Reddito), Nome, Cognome
FROM Paziente;
L'aggregato produce una riga, ma Nome e Cognome ne avrebbero cinque: non c'è modo di sapere quale scegliere. Il DBMS o dà errore o restituisce un nome a caso, che è peggio.
Corretta
SELECT Nome, Cognome, Reddito
FROM Paziente
WHERE Reddito = (SELECT MAX(Reddito)
FROM Paziente);
Prima si calcola il valore massimo, poi si cercano le righe che lo assumono. Con l'uguaglianza si ottengono anche eventuali ex aequo.
Ricordalo come una regola: per ottenere la riga legata a un valore aggregato serve sempre un secondo passaggio (una subquery o un join con una tabella derivata). Non esiste un modo per farlo in una sola SELECT piatta.
Raggruppamento
GROUP BY: aggregare a blocchi
Finora l'aggregazione produceva un solo numero per tutta la tabella. GROUP BY spezza prima le righe in gruppi omogenei, e poi applica l'aggregato a ciascun gruppo. La regola d'oro da cui discende tutto il resto:
«Per ogni specializzazione, la parcella media dei suoi medici»: la tabella MEDICO viene divisa in tre blocchi (Ortopedia, Cardiologia, Dermatologia) e in ciascuno si calcola la media.
SELECT Specializzazione, AVG(Parcella) AS ParcellaMedia
FROM Medico
GROUP BY Specializzazione;
La regola della proiezione
Dentro un gruppo, l'attributo di raggruppamento è costante: tutti i medici del blocco "Ortopedia" hanno Specializzazione = 'Ortopedia'. Gli altri attributi no: nel blocco ci sono cognomi e parcelle diversi. Da qui la regola:
Nella SELECT di una query con raggruppamento possono comparire solo: (a) gli attributi elencati nella GROUP BY, (b) espressioni di aggregazione. Qualsiasi altro attributo è un errore logico, perché non ha un valore unico nel gruppo.
Quindi «per ogni specializzazione, la parcella minima e il cognome del medico che la pratica» non si risolve con SELECT Specializzazione, MIN(Parcella), Cognome … GROUP BY Specializzazione: il cognome non è determinato dal gruppo. Serve, come per il massimo, un secondo passaggio:
SELECT M.Specializzazione, M.Cognome, M.Parcella
FROM Medico M
INNER JOIN
(SELECT Specializzazione, MIN(Parcella) AS ParcellaMinima
FROM Medico
GROUP BY Specializzazione) AS D
ON M.Specializzazione = D.Specializzazione
WHERE M.Parcella = D.ParcellaMinima;
HAVING: condizioni sui gruppi
WHERE sa valutare solo una riga alla volta, quindi non può esprimere «i gruppi con più di un medico»: è una proprietà del gruppo intero. Per questo esiste HAVING, che si applica dopo il raggruppamento e scarta gruppi interi.
WHERE o HAVING?
WHERE
- agisce prima del raggruppamento
- elimina righe
- non può contenere operatori di aggregazione
- esempio:
WHERE Citta = 'Pisa'
HAVING
- agisce dopo il raggruppamento
- elimina gruppi
- si esprime sempre con un aggregato
- esempio:
HAVING COUNT(*) > 2
Regola operativa: se la condizione si può scrivere guardando una riga sola, va nella WHERE; se per verificarla devi contare, sommare o mediare, va nella HAVING. Mettere nella WHERE una condizione che serve al gruppo è l'errore che fa perdere più punti, perché cambia silenziosamente il significato della query.
Gli aggregati non si annidano mai: HAVING MAX(AVG(Parcella)) non esiste. La HAVING contiene sempre un confronto fra due parti (=, >, <…). Per confrontare un gruppo con il massimo fra tutti i gruppi serve una subquery su una tabella derivata.
-- specializzazioni con la parcella media più alta
SELECT M.Specializzazione
FROM Medico M
GROUP BY M.Specializzazione
HAVING AVG(M.Parcella) = (
SELECT MAX(D.MediaParcelle)
FROM (SELECT AVG(M2.Parcella) AS MediaParcelle
FROM Medico M2
GROUP BY M2.Specializzazione) AS D
);
Join: rimettere insieme l'informazione
Nel modello relazionale l'informazione è frammentata in più tabelle, per evitare ridondanze e anomalie. Le domande reali però attraversano quella frammentazione: «quali pazienti ha visitato la dottoressa Rossi?» tocca tutte e tre le tabelle. Il join è l'operazione che le ricompone.
INNER JOIN
Date due tabelle, l'inner join combina ogni riga della prima con tutte le righe della seconda che soddisfano una condizione, detta predicato di join, e affianca le due righe formandone una sola. Le righe che non trovano corrispondenza vengono scartate.
Alias e ambiguità
Dopo un join il risultato può contenere due colonne con lo stesso nome (Medico.Cognome e Paziente.Cognome). Per distinguerle si usano gli alias dichiarati nella FROM: FROM Medico M permette poi di scrivere M.Cognome.
SELECT DISTINCT P.Nome AS NomePaziente, P.Cognome AS CognomePaziente,
M.Nome AS NomeMedico, M.Cognome AS CognomeMedico
FROM Paziente P
INNER JOIN Visita V ON P.CodFiscale = V.Paziente
INNER JOIN Medico M ON V.Medico = M.Matricola
WHERE YEAR(V.Data) = 2013;
Senza alias questa query sarebbe impossibile da scrivere: non si potrebbe dire quale Nome è quello del paziente. Anche quando non sono obbligatori, usali sempre: rendono leggibile la query e ti costringono a sapere da quale tabella viene ogni attributo.
NATURAL JOIN e CROSS JOIN
Il join naturale non ha predicato esplicito: combina le righe che hanno valori uguali su tutti gli attributi con lo stesso nome, e nel risultato quelle colonne compaiono una volta sola. È elegante ma fragile: nel nostro schema non funzionerebbe, perché la colonna si chiama Medico in VISITA e Matricola in MEDICO. E se due tabelle avessero per caso un Citta omonimo, il join userebbe anche quello, cambiando il significato della query.
Il prodotto cartesiano (CROSS JOIN) combina invece ogni riga con ogni riga, senza condizione: 5 medici × 5 pazienti = 25 righe. Serve nei casi in cui il legame non è una chiave esterna ma un confronto qualsiasi:
-- pazienti che hanno lo stesso nome di almeno un medico
SELECT COUNT(DISTINCT P.CodFiscale)
FROM Paziente P CROSS JOIN Medico M
WHERE P.Nome = M.Nome;
Concettualmente un inner join è un prodotto cartesiano seguito da una selezione: è utile saperlo per capire cosa succede, anche se nessun DBMS lo esegue davvero così.
Join esterni
L'inner join butta via le righe che non trovano corrispondenza. A volte sono proprio quelle che ci interessano: i medici che non hanno mai visitato, i pazienti mai visti da nessuno. I join esterni conservano le righe spaiate e riempiono con NULL le colonne mancanti.
| Tipo | Cosa conserva | Dove mette i NULL |
|---|---|---|
LEFT OUTER JOIN | tutte le righe della tabella di sinistra | a destra |
RIGHT OUTER JOIN | tutte le righe della tabella di destra | a sinistra |
INNER JOIN | solo le righe appaiate | nessuno |
Trovare ciò che manca (anti-join)
Il join esterno da solo non risponde a «i medici che non hanno mai visitato»: risponde a «tutti i medici, con le loro visite se ne hanno». Il trucco è aggiungere una WHERE che tenga solo le righe riempite di NULL: sono esattamente quelle che non hanno trovato corrispondenza.
Lo schema mentale: outer join = «tutti quelli di sinistra, più quello che trovo»; outer join + IS NULL sulla chiave destra = «solo quelli di sinistra che non trovano niente». La stessa domanda si può risolvere con NOT IN o NOT EXISTS: sono formulazioni equivalenti, scegli quella che ti risulta più chiara.
SELF JOIN: una tabella con sé stessa
Quando la domanda mette in relazione due righe della stessa tabella — due visite dello stesso paziente, due medici della stessa città — si fa il join di una tabella con sé stessa, dandole due alias diversi. I due alias vanno pensati come due copie indipendenti.
-- pazienti visitati più di una volta dallo stesso medico
SELECT DISTINCT V1.Paziente
FROM Visita V1
INNER JOIN Visita V2
ON V1.Medico = V2.Medico
AND V1.Paziente = V2.Paziente
AND V1.Data <> V2.Data;
La terza condizione (V1.Data <> V2.Data) è indispensabile: senza, ogni visita farebbe join con sé stessa e il risultato conterrebbe tutti i pazienti. È l'errore tipico del self join.
Join su più tabelle
Per rispondere a «nome e cognome dei pazienti visitati dalla dottoressa Rossi» serve attraversare tre tabelle: da PAZIENTE a VISITA con la chiave esterna del paziente, da VISITA a MEDICO con quella del medico.
SELECT DISTINCT P.Nome, P.Cognome
FROM Paziente P
INNER JOIN Visita V ON P.CodFiscale = V.Paziente
INNER JOIN Medico M ON V.Medico = M.Matricola
WHERE M.Cognome = 'Rossi' AND M.Nome = 'Marta';
Il metodo per costruirla è sempre lo stesso: parti dalla tabella che contiene ciò che vuoi proiettare e aggiungi un join per ogni "salto" necessario a raggiungere le informazioni citate nella richiesta.
Tabelle derivate e CTE
Derived table
Una derived table è una query messa dentro la FROM di un'altra query: produce una tabella temporanea, usata al volo e poi dimenticata. Serve quando hai bisogno di un risultato intermedio — tipicamente un aggregato — da confrontare con i dati di partenza. L'alias è obbligatorio.
SELECT AVG(D.ParcellaMedia) AS MediaDelleMedie
FROM (SELECT AVG(Parcella) AS ParcellaMedia
FROM Medico
GROUP BY Specializzazione) AS D;
Nota che nella derived table l'attributo di raggruppamento non è proiettato: qui non serve sapere a quale specializzazione appartiene ogni media, servono solo i valori.
WITH: le Common Table Expression
Le derived table annidate diventano presto illeggibili. Una CTE è la stessa idea, ma scritta prima della query e battezzata con un nome. Più CTE si separano con la virgola, e ciascuna può usare le precedenti.
WITH ortopedici AS (
SELECT M.Matricola
FROM Medico M
WHERE M.Specializzazione = 'Ortopedia'
),
pazienti_ortopedici AS (
SELECT DISTINCT V.Paziente AS CodFiscale
FROM Visita V
INNER JOIN ortopedici O ON V.Medico = O.Matricola
)
SELECT COUNT(*) AS PazientiSenesiMaiDaOrtopedico
FROM Paziente P
LEFT OUTER JOIN pazienti_ortopedici PO ON P.CodFiscale = PO.CodFiscale
WHERE P.Citta = 'Siena' AND PO.CodFiscale IS NULL;
Il vantaggio non è la velocità ma la leggibilità: la query si legge come una sequenza di passaggi con un nome ciascuno, esattamente come scomporresti il problema a parole.
Subquery
Una subquery è una query dentro un'altra query. Serve a due scopi: calcolare un valore da confrontare, oppure calcolare un insieme a cui appartenere (o non appartenere). Per ogni query con subquery esiste una versione equivalente con join, e viceversa: usa la formulazione che rende il ragionamento più evidente.
Subquery non correlate: IN e NOT IN
Una subquery non correlata non dipende dalla query esterna: viene calcolata una volta sola, produce un insieme di valori, e la query esterna controlla l'appartenenza con IN (o la non appartenenza con NOT IN).
-- ortopedici che hanno visitato almeno una volta nel 2013
SELECT M.Nome, M.Cognome, M.Parcella
FROM Medico M
WHERE M.Specializzazione = 'Ortopedia'
AND M.Matricola IN (SELECT V.Medico
FROM Visita V
WHERE YEAR(V.Data) = 2013);
La query insidiosa. «I medici che hanno effettuato visite, ma nessuna il 1° marzo 2013.» Scrivere solo WHERE Matricola NOT IN (SELECT Medico FROM Visita WHERE Data = '2013-03-01') è sbagliato: ci finisce dentro anche chi non ha mai visitato, perché tecnicamente non è nell'insieme. Servono due condizioni:
WHERE M.Matricola IN (SELECT V.Medico FROM Visita V)
AND M.Matricola NOT IN (SELECT V.Medico FROM Visita V
WHERE V.Data = '2013-03-01');
La prima dice «ha visitato almeno una volta», la seconda «mai quel giorno». Ogni volta che leggi «eccetto», «tranne», «ma non», chiediti se l'insieme di partenza va delimitato.
Subquery scalari
Una subquery scalare restituisce una sola riga con un solo attributo: quindi è un valore, e si confronta con =, <, >… È il caso degli aggregati.
-- quanti otorini hanno una parcella sopra la media della loro specializzazione
SELECT COUNT(*)
FROM Medico M1
WHERE M1.Specializzazione = 'Otorinolaringoiatria'
AND M1.Parcella > (SELECT AVG(M2.Parcella)
FROM Medico M2
WHERE M2.Specializzazione = 'Otorinolaringoiatria');
Usa = con una subquery solo se sei sicuro che restituisca una riga sola. «Il medico con la parcella più alta» sembra unico, ma due medici potrebbero avere la stessa parcella massima: in quel caso = provoca un errore di esecuzione e va usato IN.
Subquery correlate
Una subquery correlata nomina al suo interno un attributo della query esterna: non può più essere calcolata una volta sola, perché il suo risultato cambia per ogni riga esaminata. Concettualmente è come una funzione chiamata riga per riga.
-- per ogni paziente maschio, il numero di visite ricevute
SELECT P.CodFiscale,
(SELECT COUNT(*)
FROM Visita V
WHERE V.Paziente = P.CodFiscale) AS TotaleVisite
FROM Paziente P
WHERE P.Sesso = 'M';
Qui la subquery sta nella SELECT: calcola "al volo" un valore da mostrare come colonna, e deve quindi essere scalare. La regola per capire dove va una subquery: se è un'informazione da mostrare, va nella SELECT; se è una condizione, va nella WHERE.
EXISTS e NOT EXISTS
Spesso non interessa quante righe trova la subquery, ma solo se ne trova almeno una. EXISTS risponde proprio a questo, e la sua negazione NOT EXISTS verifica che il risultato sia vuoto.
La divisione: le richieste con "tutti"
«I pazienti visitati da tutti i medici» è la classica interrogazione di divisione. Due strade, entrambe da conoscere.
Con il doppio NOT EXISTS, traducendo letteralmente: «prendi i pazienti per cui non esiste nemmeno un medico che non li abbia visitati».
SELECT P.CodFiscale
FROM Paziente P
WHERE NOT EXISTS (
SELECT *
FROM Medico M
WHERE NOT EXISTS (
SELECT *
FROM Visita V
WHERE V.Medico = M.Matricola
AND V.Paziente = P.CodFiscale)
);
Con il raggruppamento, contando: un paziente va bene se il numero di medici diversi che l'hanno visitato è pari al numero totale di medici.
SELECT V.Paziente
FROM Visita V
GROUP BY V.Paziente
HAVING COUNT(DISTINCT V.Medico) = (SELECT COUNT(*) FROM Medico);
La seconda è più corta e quasi sempre preferibile; la prima è utile quando la condizione "per tutti" è più complessa di un semplice conteggio. In entrambe, il DISTINCT nel COUNT è obbligatorio: senza, due visite dello stesso medico conterebbero come due medici.
Ordine di esecuzione
Le clausole nell'ordine giusto
Le clausole si scrivono in un ordine e si eseguono in un altro. Conoscere quello di esecuzione spiega quasi tutti gli errori di SQL.
| # | Clausola | Cosa fa | Conseguenza |
|---|---|---|---|
| 1 | FROM / JOIN | costruisce l'insieme di righe di partenza | gli alias definiti qui sono visibili ovunque |
| 2 | WHERE | scarta righe | non può usare aggregati né alias della SELECT |
| 3 | GROUP BY | forma i gruppi | da qui in poi si ragiona per gruppi, non per righe |
| 4 | HAVING | scarta gruppi | può (e deve) usare gli aggregati |
| 5 | SELECT | calcola le colonne del risultato | qui nascono gli alias delle colonne |
| 6 | DISTINCT | elimina i duplicati | agisce sulla combinazione proiettata |
| 7 | ORDER BY | ordina il risultato | può usare gli alias della SELECT, perché viene dopo |
| 8 | LIMIT | taglia le prime n righe | ha senso solo insieme a un ordinamento |
ORDER BY e LIMIT
-- i tre medici più costosi, dal più caro
SELECT Cognome, Specializzazione, Parcella
FROM Medico
ORDER BY Parcella DESC
LIMIT 3;
ASC (crescente) è il default, DESC inverte. Si può ordinare su più colonne (ORDER BY Specializzazione, Parcella DESC) e per colonne calcolate usando l'alias. Senza ORDER BY nessun ordine è garantito: se la traccia chiede un elenco ordinato, l'ordinamento va scritto.
Errori tipici
| Sintomo | Causa | Rimedio |
|---|---|---|
| Risultato vuoto quando dovrebbero esserci righe | confronto con NULL, oppure AND al posto di OR su valori diversi dello stesso attributo | IS NULL; ripensare la condizione riga per riga |
| Righe ripetute | join che moltiplica le righe (un medico con più visite) | DISTINCT, oppure raggruppare |
| Conteggi troppo alti | COUNT(*) dopo un join | COUNT(DISTINCT chiave) |
| «non è una colonna del gruppo» | attributo proiettato che non sta nella GROUP BY né in un aggregato | aggiungerlo alla GROUP BY o passare a una derived table |
| Compaiono elementi che non hanno nulla a che fare | NOT IN su un insieme non delimitato | aggiungere la condizione di appartenenza all'insieme di partenza |
| Il self join restituisce tutto | manca la condizione che distingue le due copie | aggiungere V1.Data <> V2.Data o simile |
Checklist prima di consegnare
- Ho proiettato esattamente gli attributi richiesti, nell'ordine della traccia?
- Servono duplicati nel risultato? Se no, ho messo
DISTINCT? - Le condizioni sui gruppi sono nella
HAVINGe quelle sulle righe nellaWHERE? - Le date sono espresse in modo indipendente da quando eseguo la query?
- Ho usato gli alias per ogni tabella e nomi parlanti per le colonne calcolate?
- Con
NOT IN/NOT EXISTS: l'insieme di partenza è quello giusto?
Esercizi svolti
Prova a scriverli prima di aprire la soluzione. Lo schema è quello della clinica: MEDICO(Matricola, Cognome, Nome, Specializzazione, Parcella, Citta), PAZIENTE(CodFiscale, Cognome, Nome, Sesso, Citta, DataNascita, Reddito), VISITA(Medico, Paziente, Data, Mutuata).
Livello base
1. Indicare cognome e specializzazione dei medici che lavorano a Pisa con parcella non superiore a 140 euro.
Soluzione
SELECT Cognome, Specializzazione
FROM Medico
WHERE Citta = 'Pisa' AND Parcella <= 140;
Due condizioni sulla stessa riga: AND. Nessun join, nessun DISTINCT: la matricola è chiave, quindi non ci sono duplicati da temere… ma attenzione, qui proiettiamo cognome e specializzazione, quindi due medici omonimi della stessa specializzazione comparirebbero due volte. Se la traccia chiedesse «le combinazioni distinte», servirebbe DISTINCT.
2. Indicare il codice fiscale dei pazienti di cui non conosciamo il reddito.
Soluzione
SELECT CodFiscale
FROM Paziente
WHERE Reddito IS NULL;
«Non conosciamo» è esattamente la definizione di NULL. Reddito = NULL non restituirebbe niente.
3. Indicare quante visite mutuate sono state effettuate nel 2013.
Soluzione
SELECT COUNT(*) AS VisiteMutuate2013
FROM Visita
WHERE Mutuata = 1 AND YEAR(Data) = 2013;
Si contano righe, non entità distinte: COUNT(*) è corretto.
Livello intermedio
4. Per ogni specializzazione, indicare il numero di medici e la parcella media, considerando solo le specializzazioni con almeno due medici.
Soluzione
SELECT Specializzazione,
COUNT(*) AS NumeroMedici,
AVG(Parcella) AS ParcellaMedia
FROM Medico
GROUP BY Specializzazione
HAVING COUNT(*) >= 2;
La condizione «almeno due medici» riguarda il gruppo, quindi HAVING. Niente DISTINCT: l'attributo di raggruppamento è unico per costruzione.
5. Indicare nome e cognome dei pazienti mai visitati da un ortopedico.
Soluzione
SELECT P.Nome, P.Cognome
FROM Paziente P
WHERE P.CodFiscale NOT IN (
SELECT V.Paziente
FROM Visita V
INNER JOIN Medico M ON V.Medico = M.Matricola
WHERE M.Specializzazione = 'Ortopedia');
Qui NOT IN senza delimitazione è corretto: l'insieme di partenza è «tutti i pazienti», e chi non è mai stato visitato da un ortopedico ci deve stare davvero. Versione equivalente con anti-join:
SELECT P.Nome, P.Cognome
FROM Paziente P
LEFT OUTER JOIN (
SELECT DISTINCT V.Paziente
FROM Visita V INNER JOIN Medico M ON V.Medico = M.Matricola
WHERE M.Specializzazione = 'Ortopedia') AS D
ON P.CodFiscale = D.Paziente
WHERE D.Paziente IS NULL;
6. Indicare l'incasso totale generato dalle visite dei cardiologi (una visita frutta alla clinica la parcella del medico che la effettua).
Soluzione
SELECT SUM(M.Parcella) AS IncassoCardiologia
FROM Visita V
INNER JOIN Medico M ON V.Medico = M.Matricola
WHERE M.Specializzazione = 'Cardiologia';
La somma va fatta sulle visite, non sui medici: ogni riga del join è una visita, quindi la parcella viene sommata tante volte quante sono le visite. Sommare Parcella sulla sola tabella MEDICO risponderebbe a una domanda diversa.
Livello avanzato
7. Per ogni città dei pazienti, indicare il nome della città e il reddito medio dei suoi pazienti, ordinando dal più alto.
Soluzione
SELECT Citta, AVG(Reddito) AS RedditoMedio
FROM Paziente
GROUP BY Citta
ORDER BY RedditoMedio DESC;
I NULL non entrano nella media. Siena, che ha un solo paziente con reddito ignoto, compare con media NULL: se la traccia volesse escluderla basterebbe HAVING AVG(Reddito) IS NOT NULL.
8. Indicare la matricola dei medici che hanno visitato per la prima volta almeno un paziente nel 2013.
Soluzione
SELECT DISTINCT V1.Medico
FROM Visita V1
WHERE YEAR(V1.Data) = 2013
AND NOT EXISTS (SELECT *
FROM Visita V2
WHERE V2.Medico = V1.Medico
AND V2.Paziente = V1.Paziente
AND V2.Data < V1.Data);
«Prima volta» significa che non esiste una visita precedente della stessa coppia medico-paziente: è una condizione che dipende dalla riga esaminata, quindi la subquery è correlata. La stessa cosa si ottiene con un self join esterno e IS NULL.
9. Indicare cognome e nome del paziente più anziano, insieme al numero di visite che ha ricevuto.
Soluzione
SELECT P.Cognome, P.Nome,
(SELECT COUNT(*)
FROM Visita V
WHERE V.Paziente = P.CodFiscale) AS NumeroVisite
FROM Paziente P
WHERE P.DataNascita = (SELECT MIN(DataNascita) FROM Paziente);
Due subquery con ruoli diversi: quella nella WHERE individua la riga (data di nascita minima = più anziano), quella nella SELECT è correlata e calcola un valore da mostrare. L'uguaglianza gestisce anche eventuali coetanei.
10. Indicare le specializzazioni i cui medici hanno visitato, complessivamente, più pazienti diversi della media dei pazienti diversi visitati per specializzazione.
Soluzione
WITH pazienti_per_spec AS (
SELECT M.Specializzazione,
COUNT(DISTINCT V.Paziente) AS PazientiDiversi
FROM Visita V
INNER JOIN Medico M ON V.Medico = M.Matricola
GROUP BY M.Specializzazione
)
SELECT Specializzazione, PazientiDiversi
FROM pazienti_per_spec
WHERE PazientiDiversi > (SELECT AVG(PazientiDiversi)
FROM pazienti_per_spec);
La CTE calcola una volta sola il conteggio per specializzazione e la query finale la usa due volte: come tabella da filtrare e come sorgente della media. Senza CTE servirebbero due derived table identiche.
Prossimo passo. Fin qui abbiamo solo letto dati. Nel modulo successivo si passa a modificarli (INSERT, UPDATE, DELETE) e a far reagire il database da solo con procedure, trigger ed event.