Guida completa per MariaDB - Dal semplice al complesso
Useremo queste tabelle per tutti gli esempi:
Una subquery (o "query annidata") è semplicemente una query dentro un'altra query. È racchiusa tra parentesi tonde ( ) e viene eseguita per prima.
Una Subquery è quindi una query "aiutante" scritta dentro una query principale. L'aiutante trova un dato (o una lista di dati) che serve al "capo" (la query esterna) per completare il filtro o il calcolo.
SELECT colonne
FROM tabella
WHERE colonna operatore (SELECT ... FROM ... WHERE ...);
↑ questa è la SUBQUERY
Pensa alla subquery come a una domanda preparatoria:
-- La domanda "Chi guadagna più della media?" diventa:
SELECT nome, stipendio
FROM dipendenti
WHERE stipendio > (SELECT AVG(stipendio) FROM dipendenti);
↑ calcola prima la media (es: 35000)
poi cerca chi ha stipendio > 35000
Le subquery risolvono problemi che non puoi risolvere in un solo passaggio. Ecco i casi tipici:
| Situazione | Esempio di domanda | Perché serve subquery? |
|---|---|---|
| Confronto con un valore calcolato con le funzioni di aggregazione (min(...) ,max(...), avg(...), count(...),sum(...)) | "Chi guadagna più della media?" | Non puoi usare AVG() direttamente nel WHERE |
| Trovare elementi che appartengono ad un gruppo specificato con una condizione | "Dipendenti dei reparti con budget > 100000" | Devi prima trovare quali sono quei reparti |
| Trovare istanze di un'entità (soggetti) che sono collegate ad almeno un'istanza di un'altra entità | "Clienti che hanno fatto almeno un ordine" | Devi verificare se esistono ordini per ogni cliente |
| Trovare istanze di un'entità (soggetti) che non sono collegate ad istanze di un'altra entità | "Prodotti mai ordinati" | Devi verificare assenza in un'altra tabella |
Fatti questa domanda: "Per rispondere, devo prima calcolare/trovare qualcos'altro?"
È il caso più comune. La subquery calcola un valore (o un insieme di valori) che poi viene usato per filtrare.
=, >, <, >=, <=, <>
-- Chi guadagna più della media aziendale?
SELECT nome, stipendio
FROM dipendenti
WHERE stipendio > (SELECT AVG(stipendio) FROM dipendenti);
-- Come funziona:
-- 1. La subquery calcola: AVG(stipendio) = 35000
-- 2. La query diventa: SELECT nome, stipendio FROM dipendenti WHERE stipendio > 35000
-- Chi ha lo stipendio più alto?
SELECT nome, stipendio
FROM dipendenti
WHERE stipendio = (SELECT MAX(stipendio) FROM dipendenti);
-- ATTENZIONE: questa trova TUTTI quelli con lo stipendio massimo
-- (potrebbero essere più di uno se ci sono più persone con lo stipendio massimo)
-- Dipendenti del reparto "Vendite" (senza usare JOIN ma se si chiedono di visualizzare dati di più tabelle il JOIN è necessario)
SELECT nome, stipendio
FROM dipendenti
WHERE reparto_id = (
SELECT id
FROM reparti
WHERE nome = 'Vendite'
);
=, ottieni un errore! In quel caso devi usare IN o EXISTS (vedi sezione successiva).
IN
-- Dipendenti che lavorano in reparti con budget > 100000
SELECT nome, stipendio
FROM dipendenti
WHERE reparto_id IN (
SELECT id
FROM reparti
WHERE budget > 100000
);
-- Come funziona:
-- 1. La subquery trova gli id dei reparti ricchi: (1, 3, 7)
-- 2. La query diventa: WHERE reparto_id IN (1, 3, 7)
-- Clienti che NON hanno mai fatto ordini
SELECT nome
FROM clienti
WHERE id NOT IN (
SELECT DISTINCT cliente_id
FROM ordini
);
-- Trova tutti i cliente_id presenti in ordini
-- Poi restituisce i clienti che NON sono in quella lista
Usa = |
Usa IN |
|---|---|
| Subquery con funzioni aggregate (MAX, MIN, AVG, SUM, COUNT) |
Subquery che può dare più risultati (lista di ID, lista di valori) |
| Subquery con LIMIT 1 | Quando il filtro è su una chiave primaria/esterna |
| Sei SICURO che dia un solo valore | Nel dubbio, usa IN (funziona anche con un solo valore) |
Già visto sopra. Verifica se un valore è presente in una lista.
WHERE colonna IN (subquery) -- valore presente nella lista
WHERE colonna NOT IN (subquery) -- valore NON presente nella lista
Permettono confronti con tutti o almeno uno dei valori restituiti.
-- Dipendenti con stipendio maggiore
-- di ALMENO UN manager
SELECT nome, stipendio
FROM dipendenti
WHERE stipendio > ANY (
SELECT stipendio
FROM dipendenti
WHERE ruolo = 'manager'
)
= stipendio maggiore del minimo stipendio manager
-- Dipendenti con stipendio superiore
-- a TUTTI i manager
SELECT nome, ruolo
FROM dipendenti
WHERE stipendio > ALL (
SELECT stipendio
FROM dipendenti
WHERE ruolo = 'manager'
)
= stipendio maggiore del massimo stipendio manager
EXISTS verifica semplicemente se la subquery restituisce almeno una riga. Non importa cosa restituisce, solo SE restituisce qualcosa.
-- Trova i clienti che hanno fatto almeno un ordine
SELECT nome
FROM clienti c
WHERE EXISTS (
SELECT 1
FROM ordini o
WHERE o.cliente_id = c.id
);
-- SELECT 1 è una convenzione: significa "non mi interessa
-- cosa restituisci, solo SE restituisci qualcosa"
-- Clienti che NON hanno mai ordinato nulla
SELECT nome
FROM clienti c
WHERE NOT EXISTS (
SELECT 1
FROM ordini o
WHERE o.cliente_id = c.id
);
Puoi usare una subquery come se fosse una tabella temporanea. La subquery va nella clausola FROM e DEVE avere un alias.
-- Qual è lo stipendio medio per reparto, e poi la media di queste medie?
-- ERRORE: non puoi fare AVG(AVG(stipendio)) direttamente!
-- SOLUZIONE: subquery nel FROM
SELECT AVG(media_reparto) AS media_delle_medie
FROM (
SELECT reparto_id, AVG(stipendio) AS media_reparto
FROM dipendenti
GROUP BY reparto_id
) AS medie_per_reparto;
↑ OBBLIGATORIO dare un alias alla subquery!
-- Reparti dove la media stipendi è sopra la media aziendale
SELECT reparto_id, media_stipendio
FROM (
SELECT reparto_id, AVG(stipendio) AS media_stipendio
FROM dipendenti
GROUP BY reparto_id
) AS stats_reparto
WHERE media_stipendio > (SELECT AVG(stipendio) FROM dipendenti);
-- SBAGLIATO (manca alias):
SELECT * FROM (SELECT ... FROM ...);
-- CORRETTO:
SELECT * FROM (SELECT ... FROM ...) AS nome_alias;
Puoi inserire una subquery come "colonna calcolata". DEVE restituire esattamente un valore per ogni riga.
-- Mostra ogni dipendente e quanto guadagna rispetto alla media
SELECT
nome,
stipendio,
(SELECT AVG(stipendio) FROM dipendenti) AS media_aziendale,
stipendio - (SELECT AVG(stipendio) FROM dipendenti) AS differenza
FROM dipendenti;
-- Ogni ordine con la sua percentuale sul totale vendite
SELECT
id,
importo,
ROUND(
importo * 100.0 / (SELECT SUM(importo) FROM ordini),
2
) AS percentuale_totale
FROM ordini;
Questa sezione è fondamentale perché le subquery correlate sono uno degli strumenti più potenti (e meno compresi) di SQL.
Una subquery correlata è una subquery che fa riferimento a una colonna della query esterna. Questo significa che:
-- Subquery eseguita UNA sola volta
SELECT nome
FROM dipendenti
WHERE stipendio > (
SELECT AVG(stipendio)
FROM dipendenti
);
Funzionamento:
La subquery produce un solo valore e la query visualizza i nomi dei dipendenti che hanno stipendio superiore all'unico valore prodotto dalla subquery(media dell'azienda).
-- Subquery eseguita PER OGNI dipendente
SELECT nome
FROM dipendenti d1
WHERE stipendio > (
SELECT AVG(d2.stipendio)
FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id
);
Funzionamento:
La subquery produce tanti valori quanti sono i dipendenti (per ogni dipendente la media del reparto a cui appartiene ) e la query visualizza i nomi dei dipendenti che hanno stipendio superiore alla media del reparto a cui appartengono .
Cerca nella subquery un alias definito FUORI dalla subquery stessa:
SELECT *
FROM tabella1 t1 ← t1 definito QUI (nella query esterna)
WHERE colonna > (
SELECT ...
FROM tabella2 t2
WHERE t2.x = t1.y ← t1 USATO QUI (dentro la subquery)
); ↑ QUESTA È UNA CORRELAZIONE!
Se trovi un riferimento esterno → È CORRELATA
Le subquery correlate sono necessarie (non opzionali) in queste situazioni:
| Situazione | Frase tipica nella richiesta | Perché serve correlata? |
|---|---|---|
| Confronto con funzione di aggregazione "del proprio gruppo" | "...più della media del proprio reparto" | La media cambia per ogni reparto |
| Trovare il massimo/minimo "per categoria" | "Il prodotto più caro di ogni categoria" | Il MAX cambia per ogni categoria |
| Contare/sommare "relativamente a" | "Dipendenti con più di 3 progetti assegnati" | Il conteggio dipende dal dipendente |
| Trovare il "top N" per gruppo | "I primi 3 per ogni categoria" | Il ranking è relativo al gruppo |
Problema: "Trova i dipendenti che guadagnano più della media del loro reparto."
Perché serve correlata? Perché la media da confrontare è diversa per ogni dipendente - dipende dal suo reparto.
Supponiamo questi dati:
| nome | reparto_id | stipendio |
|---|---|---|
| Mario | 1 | 40000 |
| Lucia | 1 | 30000 |
| Paolo | 2 | 50000 |
| Anna | 2 | 45000 |
Risultato finale: Mario, Paolo
-- La query completa:
SELECT d1.nome, d1.stipendio, d1.reparto_id
FROM dipendenti d1
WHERE d1.stipendio > (
SELECT AVG(d2.stipendio)
FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id -- CORRELAZIONE!
);
-- NOTA: d1 e d2 sono alias della STESSA tabella!
-- d1 = la riga che stiamo valutando
-- d2 = tutte le righe per calcolare la media del gruppo
Problema: "Per ogni categoria, mostra il prodotto con il prezzo più alto."
SELECT p1.nome, p1.categoria, p1.prezzo
FROM prodotti p1
WHERE p1.prezzo = (
SELECT MAX(p2.prezzo)
FROM prodotti p2
WHERE p2.categoria = p1.categoria -- CORRELAZIONE!
);
-- Per ogni prodotto p1:
-- - Calcola il MAX prezzo tra i prodotti della STESSA categoria
-- - Se p1.prezzo = quel MAX, includi p1 nel risultato
Problema: "Trova i clienti che hanno effettuato almeno un ordine."
SELECT c.nome, c.citta
FROM clienti c
WHERE EXISTS (
SELECT 1
FROM ordini o
WHERE o.cliente_id = c.id
);
-- Per ogni cliente c:
-- - Cerca se esiste almeno un ordine con cliente_id = c.id
-- - Se esiste → includi il cliente nel risultato
Problema: "Trova i prodotti che non hanno mai avuto vendite."
SELECT p.nome, p.categoria
FROM prodotti p
WHERE NOT EXISTS (
SELECT 1
FROM vendite v
WHERE v.prodotto_id = p.id
);
-- Per ogni prodotto p:
-- - Cerca se NON esiste nessuna vendita con prodotto_id = p.id
-- - Se non esiste → includi il prodotto nel risultato
Problema: "Per ogni dipendente, mostra quanto rappresenta il suo stipendio in percentuale rispetto al totale del suo reparto."
SELECT
d1.nome,
d1.reparto_id,
d1.stipendio,
ROUND(
d1.stipendio * 100.0 / (
SELECT SUM(d2.stipendio)
FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id
), 2
) AS percentuale_reparto
FROM dipendenti d1;
-- Risultato esempio:
-- nome | reparto_id | stipendio | percentuale_reparto
-- ------|------------|-----------|--------------------
-- Mario | 1 | 40000 | 57.14%
-- Lucia | 1 | 30000 | 42.86%
-- Paolo | 2 | 50000 | 52.63%
-- Anna | 2 | 45000 | 47.37%
Spesso una subquery può essere riscritta con un JOIN . Ecco quando preferire l'una o l'altra:
-- Clienti con almeno un ordine
SELECT nome
FROM clienti c
WHERE EXISTS (
SELECT 1 FROM ordini o
WHERE o.cliente_id = c.id
);
✅ Più leggibile per "esiste/non esiste"
✅ Migliore per verifiche di esistenza
-- Stesso risultato con JOIN
SELECT DISTINCT c.nome
FROM clienti c
JOIN ordini o
ON c.id = o.cliente_id;
✅ Spesso più performante
✅ Più familiare per molti
-- Clienti che non hanno mai effettuato ordini
SELECT nome
FROM clienti c
WHERE NOT EXISTS (
SELECT 1 FROM ordini o
WHERE o.cliente_id = c.id
);
✅ Più leggibile per "esiste/non esiste"
✅ Migliore per verifiche di esistenza
-- Stesso risultato con JOIN
SELECT DISTINCT c.nome
FROM clienti c
LEFT JOIN ordini o
WHERE o.id=NULL
ON c.id = o.cliente_id;
✅ Spesso più performante
✅ Più familiare per molti
Ci sono casi dove non puoi evitare la subquery correlata:
In questi casi il valore di confronto dipende dal contesto della riga e non può essere pre-calcolato.
Le subquery correlate vengono eseguite per ogni riga della query esterna. Questo significa:
Per dataset grandi, valuta se puoi ottenere lo stesso risultato con:
-- INVECE DI (subquery correlata - più lenta):
SELECT d1.nome, d1.stipendio
FROM dipendenti d1
WHERE d1.stipendio > (
SELECT AVG(d2.stipendio) FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id
);
-- PROVA QUESTA (JOIN con subquery nel FROM - spesso più veloce):
SELECT d.nome, d.stipendio
FROM dipendenti d
join (
SELECT reparto_id, AVG(stipendio) AS media_rep
FROM dipendenti
GROUP BY reparto_id
) AS medie ON d.reparto_id = medie.reparto_id
WHERE d.stipendio > medie.media_rep;
| ✅ USA subquery correlata quando... | ❌ EVITA subquery correlata quando... |
|---|---|
| Il confronto dipende dal "gruppo" della riga corrente | Il valore di confronto è lo stesso per tutte le righe |
| Calcoli un valore relativo al contesto della riga | Puoi pre-calcolare l'aggregato una volta sola |
| Dataset piccolo/medio | Dataset molto grande (valuta alternative) |
| La logica richiede "per ogni X, trova Y relativo a X" | Un semplice JOIN risolve il problema |
Ecco le parole chiave e le situazioni tipiche che indicano la necessità di una subquery:
Quando leggi o senti queste parole, pensa subito a una subquery:
| Parola/Frase | Tipo di subquery | Esempio |
|---|---|---|
| "più della media" "meno della media" |
WHERE + AVG() - non correlata | WHERE x > (SELECT AVG(x)...) |
| "il ... con ... massimo" "il... con ... minimo" |
WHERE + MAX/MIN() - non correlata | WHERE x = (SELECT MAX(x)...) |
| "del proprio reparto" "della propria categoria" |
CORRELATA! | WHERE x > (SELECT AVG... WHERE rep = t.rep) |
| "per ogni categoria" "in ciascun gruppo" |
CORRELATA! | WHERE x = (SELECT MAX... WHERE cat = t.cat) |
| "che hanno almeno un..." | EXISTS | WHERE EXISTS (SELECT...) |
| "che NON hanno mai ..." |
NOT EXISTS / NOT IN | WHERE NOT EXISTS (SELECT...) |
Per capire se serve una subquery correlata chiediti:
"Il valore di confronto cambia a seconda della riga che sto valutando?"
"Chi guadagna più della media aziendale?"
La media aziendale è UNA sola → non correlata
"Chi guadagna più della media del proprio reparto?"
Ogni reparto ha la sua media → CORRELATA
Quando scrivi una subquery:
-- Per subquery NON correlata:
-- PASSO 1: Testa la subquery da sola
SELECT AVG(stipendio) FROM dipendenti;
-- Risultato: 35000 ✓
-- PASSO 2: Inseriscila nella query principale
SELECT nome FROM dipendenti
WHERE stipendio > (SELECT AVG(stipendio) FROM dipendenti);
-- Per subquery CORRELATA:
-- PASSO 1: Scrivi la struttura esterna
SELECT d1.nome FROM dipendenti d1 WHERE d1.stipendio > (...);
-- PASSO 2: Aggiungi la subquery CON il riferimento a d1
SELECT d1.nome
FROM dipendenti d1
WHERE d1.stipendio > (
SELECT AVG(d2.stipendio) FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id -- riferimento a d1!
);
Usa SUBQUERY quando:devi confrontare con aggregati (AVG, MAX, MIN, SUM)
Usa JOIN quando devi visualizzare dati da più tabelle
-- NON FUNZIONA!
SELECT nome
FROM dipendenti
WHERE stipendio > AVG(stipendio);
-- FUNZIONA!
SELECT nome
FROM dipendenti
WHERE stipendio >
(SELECT AVG(stipendio) FROM dipendenti);
-- ERRORE se ci sono più reparti!
SELECT nome
FROM dipendenti
WHERE reparto_id = (
SELECT id FROM reparti
WHERE budget > 50000
);
-- FUNZIONA con qualsiasi numero!
SELECT nome
FROM dipendenti
WHERE reparto_id IN (
SELECT id FROM reparti
WHERE budget > 50000
);
-- ERRORE di sintassi!
SELECT *
FROM (
SELECT reparto_id, AVG(stipendio)
FROM dipendenti
GROUP BY reparto_id
);
-- FUNZIONA!
SELECT *
FROM (
SELECT reparto_id, AVG(stipendio) as media
FROM dipendenti
GROUP BY reparto_id
) AS t;
-- AMBIGUO: quale tabella?
SELECT nome
FROM dipendenti
WHERE stipendio > (
SELECT AVG(stipendio)
FROM dipendenti
WHERE reparto_id = reparto_id
);
-- CHIARO: d1 è esterno, d2 interno
SELECT nome
FROM dipendenti d1
WHERE d1.stipendio > (
SELECT AVG(d2.stipendio)
FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id
);
Se la subquery può restituire NULL, NOT IN potrebbe non funzionare come ti aspetti!
-- Se ordini.cliente_id contiene NULL, questo potrebbe dare 0 risultati!
SELECT * FROM clienti
WHERE id NOT IN (SELECT cliente_id FROM ordini);
-- SOLUZIONE 1: escludi i NULL
SELECT * FROM clienti
WHERE id NOT IN (
SELECT cliente_id FROM ordini
WHERE cliente_id IS NOT NULL
);
-- SOLUZIONE 2: usa NOT EXISTS (gestisce meglio i NULL e generalmente è più veloce)
SELECT * FROM clienti c
WHERE NOT EXISTS (
SELECT 1 FROM ordini o
WHERE o.cliente_id = c.id
);
Prova a risolvere questi esercizi. Per ogni esercizio, prima identifica se serve una subquery e di che tipo.
NO! È un semplice confronto con un valore fisso.
SELECT nome, stipendio
FROM dipendenti
WHERE stipendio > 40000;
SÌ! Subquery NON correlata. Devi prima trovare il MAX, poi cercare chi lo ha.
SELECT nome, stipendio
FROM dipendenti
WHERE stipendio = (SELECT MAX(stipendio) FROM dipendenti);
SÌ! Subquery con IN (lista di ID). Prima trovi gli ID dei clienti con ordini > 1000, poi cerchi i loro nomi.
SELECT nome
FROM clienti
WHERE id IN (
SELECT DISTINCT cliente_id
FROM ordini
WHERE importo > 1000
);
SÌ! Subquery con NOT EXISTS (o NOT IN ma è sempre preferibile NOT EXISTS).
-- Con NOT EXISTS :
SELECT nome
FROM clienti c
WHERE NOT EXISTS (
SELECT 1 FROM ordini o
WHERE o.cliente_id = c.id
);
SÌ! Subquery CORRELATA perché la media cambia per ogni reparto.
Come riconoscerla? "del proprio reparto" indica che il confronto è relativo al gruppo del dipendente.
SELECT d1.nome, d1.stipendio, d1.reparto_id
FROM dipendenti d1
WHERE d1.stipendio > (
SELECT AVG(d2.stipendio)
FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id -- CORRELAZIONE!
);
SÌ! Subquery CORRELATA perché il MAX dipende dalla categoria.
Come riconoscerla? "per ogni categoria" indica confronto relativo al gruppo.
SELECT p1.nome, p1.categoria, p1.prezzo
FROM prodotti p1
WHERE p1.prezzo = (
SELECT MAX(p2.prezzo)
FROM prodotti p2
WHERE p2.categoria = p1.categoria -- CORRELAZIONE!
);
SÌ! Subquery CORRELATA nel SELECT perché il conteggio dipende dal reparto del dipendente.
SELECT
d1.nome,
d1.reparto_id,
(
SELECT COUNT(*) - 1 -- -1 per escludere se stesso
FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id
) AS numero_colleghi
FROM dipendenti d1;
SÌ! Due subquery CORRELATE con AND.
SELECT d1.nome, d1.reparto_id, d1.stipendio, d1.data_assunzione
FROM dipendenti d1
WHERE d1.stipendio = (
SELECT MAX(d2.stipendio)
FROM dipendenti d2
WHERE d2.reparto_id = d1.reparto_id
)
AND d1.data_assunzione < (
SELECT AVG(d3.data_assunzione)
FROM dipendenti d3
WHERE d3.reparto_id = d1.reparto_id
);
| Tipo | Quando usarla | Esempio pattern |
|---|---|---|
| Non correlata semplice | Confronto con valore unico (media, max, min globale) | WHERE x > (SELECT AVG...) |
| Non correlata con IN | Appartenenza a una lista di valori | WHERE x IN (SELECT id...) |
| CORRELATA nel WHERE | Confronto relativo al gruppo/categoria della riga | WHERE x > (SELECT AVG... WHERE cat = t.cat) |
| CORRELATA nel SELECT | Calcolare un valore per ogni riga (conteggi, aggregati) | SELECT (SELECT COUNT... WHERE fk = t.id) |
| Nel FROM (derived table) | Pre-elaborare dati, aggregare su aggregati | FROM (SELECT... GROUP BY) AS t |
Quando vedi frasi come:
→ Pensa SUBITO a una subquery CORRELATA!