🔍 Le Subquery in SQL

Guida completa per MariaDB - Dal semplice al complesso

🗄️ Database di esempio usato in questa guida

Useremo queste tabelle per tutti gli esempi:


clienti (id(PK), nome, citta) prodotti(id(PK), nome, categoria, prezzo)

ordini (id(PK), cliente_id(FK), importo, data_ordine)

dipendenti (id(PK), nome, reparto_id(FK), stipendio, data_assunzione)

reparti (id(PK), nome, budget)

vendite(id(PK), prodotto_id(FK), quantita, data_vendita)

1. Cos'è una Subquery?

Una subquery (o "query annidata") è semplicemente una query dentro un'altra query. È racchiusa tra parentesi tonde ( ) e viene eseguita per prima.

📌 Definizione semplice:
Una subquery è una SELECT che sta dentro un'altra istruzione SQL e fornisce un risultato che viene usato dalla query esterna.

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.

La struttura base

SELECT colonne
FROM tabella
WHERE colonna operatore (SELECT ... FROM ... WHERE ...);
                          ↑ questa è la SUBQUERY

Analogia per capire meglio

Pensa alla subquery come a una domanda preparatoria:

Domanda complessa: "Chi guadagna più della media?"

Si scompone in:
  1. Prima calcolo: qual è la media? → questa è la subquery
  2. Poi confronto: chi supera quel valore? → questa è la query principale
-- 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

2. Quando servono le Subquery?

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

REGOLA D'ORO per capire se serve una subquery

Fatti questa domanda: "Per rispondere, devo prima calcolare/trovare qualcos'altro?"

  • → Probabilmente ti serve una subquery
  • NO → Probabilmente basta una query semplice o un JOIN

3. Subquery nella clausola WHERE

È il caso più comune. La subquery calcola un valore (o un insieme di valori) che poi viene usato per filtrare.

Caso A: Subquery che restituisce UN SOLO valore (scalare)

Usa quando: la subquery restituisce esattamente un numero o un valore.
Operatori: =, >, <, >=, <=, <>
Esempio 1: Confronto con la media
-- 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
Esempio 2: Trovare chi ha il valore massimo
-- 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)
Esempio 3: Confronto con un'altra tabella
-- 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'
);
⚠️ Attenzione: Se la subquery può restituire più di un valore e usi =, ottieni un errore! In quel caso devi usare IN o EXISTS (vedi sezione successiva).

Caso B: Subquery che restituisce PIÙ valori (lista)

Usa quando: la subquery può restituire zero, uno o più valori.
Operatore principale: IN
Esempio 4: Appartenenza a una lista
-- 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)
Esempio 5: NOT IN (esclusione)
-- 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

Quando usare = vs IN

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)

4. Operatori speciali: IN, ANY, ALL, EXISTS

4.1 - Operatore IN

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

4.2 - Operatori ANY e ALL

Permettono confronti con tutti o almeno uno dei valori restituiti.

ANY (almeno uno) - Query completa

-- 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

ALL (tutti quanti) - Query completa

-- 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

4.3 - Operatore EXISTS

EXISTS verifica semplicemente se la subquery restituisce almeno una riga. Non importa cosa restituisce, solo SE restituisce qualcosa.

📌 Usa EXISTS quando:
  • Vuoi sapere "esiste almeno un..." senza bisogno dei dati specifici
  • Spesso più efficiente di IN per grandi dataset
Esempio: Clienti con almeno un ordine
-- 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"
Esempio: NOT EXISTS
-- 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
);

5. Subquery nella clausola FROM (Derived Tables)

Puoi usare una subquery come se fosse una tabella temporanea. La subquery va nella clausola FROM e DEVE avere un alias.

📌 Usa quando:
  • Devi fare calcoli su dati già aggregati
  • Vuoi "preparare" i dati prima di elaborarli ulteriormente
  • Hai bisogno di fare un GROUP BY su un GROUP BY
Esempio 1: Media delle medie
-- 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!
Esempio 2: Filtro su dati aggregati complessi
-- 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);
⚠️ Errore comune: Dimenticare l'alias dopo la parentesi chiusa della subquery.
-- SBAGLIATO (manca alias):
SELECT * FROM (SELECT ... FROM ...);

-- CORRETTO:
SELECT * FROM (SELECT ... FROM ...) AS nome_alias;

6. Subquery nella clausola SELECT (Scalar Subquery)

Puoi inserire una subquery come "colonna calcolata". DEVE restituire esattamente un valore per ogni riga.

📌 Usa quando:
  • Vuoi mostrare un valore di riferimento accanto ai dati
  • Calcoli percentuali rispetto a un totale
  • Mostri dati da un'altra tabella senza JOIN
Esempio 1: Confronto con la media
-- 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;
Esempio 2: Percentuale sul totale
-- 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;

7. 🔗 Subquery Correlate

Questa sezione è fondamentale perché le subquery correlate sono uno degli strumenti più potenti (e meno compresi) di SQL.

Cos'è una Subquery Correlata?

Una subquery correlata è una subquery che fa riferimento a una colonna della query esterna. Questo significa che:

  • La subquery non può funzionare da sola - ha bisogno di un valore dalla query esterna
  • Viene rieseguita per ogni riga della query esterna
  • Il risultato della subquery cambia a seconda della riga che sta elaborando

7.1 - La differenza fondamentale

Visualizzare nome dipendenti con stipendio superiore alla media degli stipendi di tutta l'azienda:NON correlata (indipendente)

-- Subquery eseguita UNA sola volta
SELECT nome
FROM dipendenti
WHERE stipendio > (
    SELECT AVG(stipendio) 
    FROM dipendenti
);

Funzionamento:

  1. Calcola la media: 35000
  2. Poi cerca tutti con stipendio > 35000

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).

Visualizzare nome dipendenti con stipendio superiore alla media degli stipendi del reparto a cui appartengono: CORRELATA (dipendente)

-- 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:

  1. Per Mario (rep.1): calcola media rep.1
  2. Per Lucia (rep.2): calcola media rep.2
  3. ... ricalcola per ogni dipendente

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 .

7.2 - Data una subquery, come riconoscere se è una subquery correlata?

La regola del "riferimento esterno"

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

7.3 - QUANDO usare le subquery correlate

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

7.4 - ESEMPI CONCRETI E DETTAGLIATI

📌 Esempio 1: Dipendenti sopra la media DEL PROPRIO reparto

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.

Come funziona passo per passo:

Supponiamo questi dati:

nomereparto_idstipendio
Mario140000
Lucia130000
Paolo250000
Anna245000
Passo 1 - Elabora Mario (reparto 1):
La subquery calcola: AVG dei dipendenti con reparto_id = 1 → (40000+30000)/2 = 35000
Mario ha 40000 > 35000? → Mario entra nel risultato
Passo 2 - Elabora Lucia (reparto 1):
La subquery calcola: AVG dei dipendenti con reparto_id = 1 → 35000
Lucia ha 30000 > 35000? NO → Lucia esclusa
Passo 3 - Elabora Paolo (reparto 2):
La subquery calcola: AVG dei dipendenti con reparto_id = 2 → (50000+45000)/2 = 47500
Paolo ha 50000 > 47500? → Paolo entra nel risultato
Passo 4 - Elabora Anna (reparto 2):
La subquery calcola: AVG dei dipendenti con reparto_id = 2 → 47500
Anna ha 45000 > 47500? NO → Anna esclusa

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

📌 Esempio 2: Il prodotto più costoso DI OGNI categoria

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
💡 Nota: Se due prodotti hanno lo stesso prezzo massimo nella stessa categoria, questa query li restituisce entrambi!

📌 Esempio 3: Clienti che hanno fatto ordini (con EXISTS)

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

📌 Esempio 4: Prodotti MAI venduti (NOT EXISTS)

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

📌 Esempio 5: Percentuale stipendio rispetto al proprio reparto

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%

7.5 - Subquery vs Alternativa con JOIN

Spesso una subquery può essere riscritta con un JOIN . Ecco quando preferire l'una o l'altra:

Subquery

-- 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

Alternativa con JOIN

-- 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

Subquery

-- 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

Alternativa con JOIN

-- 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

Quando la correlata è INDISPENSABILE

Ci sono casi dove non puoi evitare la subquery correlata:

  • Confronto con aggregato del gruppo: "stipendio > media del proprio reparto"
  • Top N per gruppo: "i primi 3 venditori per ogni regione"
  • Valore massimo/minimo per gruppo: "il prodotto più caro di ogni categoria"
  • Calcoli percentuali sul gruppo: "% sul totale del proprio reparto"

In questi casi il valore di confronto dipende dal contesto della riga e non può essere pre-calcolato.

7.6 - Performance delle subquery correlate

⚠️ Attenzione alle prestazioni

Le subquery correlate vengono eseguite per ogni riga della query esterna. Questo significa:

  • 100 righe nella tabella esterna = 100 esecuzioni della subquery
  • 10.000 righe = 10.000 esecuzioni

Per dataset grandi, valuta se puoi ottenere lo stesso risultato con:

  • JOIN + GROUP BY
  • Window functions (ROW_NUMBER, RANK, ecc. - se disponibili)
  • Subquery nel FROM (derived table) + JOIN
Esempio: Alternativa performante con JOIN
-- 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;

7.7 - Riepilogo: quando usare le correlate

✅ 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

8. 💡 I Trucchi per Riconoscere quando servono le Subquery

Ecco le parole chiave e le situazioni tipiche che indicano la necessità di una subquery:

TRUCCO #1: Parole "trigger" nella richiesta

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...)

TRUCCO #2: Il test "cambia per ogni riga?"

Per capire se serve una subquery correlata chiediti:

"Il valore di confronto cambia a seconda della riga che sto valutando?"

  • NO (stesso valore per tutti) → Subquery semplice (non correlata)
  • (valore diverso per ogni riga/gruppo) → Subquery CORRELATA

Valore FISSO per tutti

"Chi guadagna più della media aziendale?"

La media aziendale è UNA sola → non correlata

Valore VARIABILE per gruppo

"Chi guadagna più della media del proprio reparto?"

Ogni reparto ha la sua media → CORRELATA

TRUCCO #3: Costruisci dall'interno verso l'esterno

Quando scrivi una subquery:

  1. Scrivi PRIMA la subquery da sola (se non correlata) e verificala
  2. Per le correlate: scrivi prima la struttura esterna, poi aggiungi la subquery con il riferimento
-- 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!
);

TRUCCO #4: Subquery vs JOIN - Quale scegliere?

Usa SUBQUERY quando:devi confrontare con aggregati (AVG, MAX, MIN, SUM)

Usa JOIN quando devi visualizzare dati da più tabelle

9. ⚠️ Errori comuni da evitare

❌ ERRORE: Usare funzioni di aggregazione nel WHERE

-- NON FUNZIONA!
SELECT nome
FROM dipendenti
WHERE stipendio > AVG(stipendio);

✓ CORRETTO: Subquery per l'aggregato

-- FUNZIONA!
SELECT nome
FROM dipendenti
WHERE stipendio > 
  (SELECT AVG(stipendio) FROM dipendenti);

❌ ERRORE: = con subquery multi-riga

-- ERRORE se ci sono più reparti!
SELECT nome
FROM dipendenti
WHERE reparto_id = (
  SELECT id FROM reparti 
  WHERE budget > 50000
);

✓ CORRETTO: IN per liste

-- FUNZIONA con qualsiasi numero!
SELECT nome
FROM dipendenti
WHERE reparto_id IN (
  SELECT id FROM reparti 
  WHERE budget > 50000
);

❌ ERRORE: Dimenticare alias nel FROM

-- ERRORE di sintassi!
SELECT *
FROM (
  SELECT reparto_id, AVG(stipendio)
  FROM dipendenti
  GROUP BY reparto_id
);

✓ CORRETTO: Sempre l'alias

-- FUNZIONA!
SELECT *
FROM (
  SELECT reparto_id, AVG(stipendio) as media
  FROM dipendenti
  GROUP BY reparto_id
) AS t;

❌ ERRORE: Correlata senza alias esterni

-- AMBIGUO: quale tabella?
SELECT nome
FROM dipendenti
WHERE stipendio > (
    SELECT AVG(stipendio) 
    FROM dipendenti
    WHERE reparto_id = reparto_id
);

✓ CORRETTO: Alias chiari

-- 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
);

⚠️ Attenzione a NULL con NOT IN

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
);

10. 🎯 Esercizi guidati

Prova a risolvere questi esercizi. Per ogni esercizio, prima identifica se serve una subquery e di che tipo.

Esercizio 1: Livello Base

Richiesta: Trova tutti i dipendenti che guadagnano più di 40000€.
📝 Serve una subquery?

NO! È un semplice confronto con un valore fisso.

SELECT nome, stipendio 
FROM dipendenti 
WHERE stipendio > 40000;

Esercizio 2: Livello Base - Subquery semplice

Richiesta: Trova il dipendente con lo stipendio più alto.
📝 Serve una subquery?

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);

Esercizio 3: Livello Intermedio - IN

Richiesta: Trova i clienti che hanno fatto ordini superiori a 1000€.
📝 Serve una subquery?

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
);

Esercizio 4: Livello Intermedio - NOT EXISTS

Richiesta: Trova i clienti che non hanno mai fatto ordini.
📝 Serve una subquery?

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
);

Esercizio 5: Livello Avanzato - CORRELATA

Richiesta: Trova i dipendenti che guadagnano più della media del proprio reparto.
📝 Serve una subquery?

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!
);

Esercizio 6: Livello Avanzato - CORRELATA per MAX per gruppo

Richiesta: Per ogni categoria, trova il prodotto più costoso.
📝 Serve una subquery?

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!
);

Esercizio 7: Livello Avanzato - CORRELATA nel SELECT

Richiesta: Mostra ogni dipendente con il numero di colleghi nel suo stesso reparto.
📝 Serve una subquery?

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;

Esercizio 8: Livello Esperto - Doppia condizione correlata

Richiesta: Trova i dipendenti che sono i più pagati nel loro reparto E sono stati assunti prima della media di assunzione del reparto.
📝 Serve una subquery?

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
);

📋 Riepilogo Finale

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

✨ Consiglio finale per le CORRELATE

Quando vedi frasi come:

  • "...del proprio reparto/categoria/gruppo...""...relativamente a..."
  • "...rispetto al suo..."

→ Pensa SUBITO a una subquery CORRELATA!