Skip to main content

Join in SQL: una guida completa ai tipi, alle differenze e a quando utilizzarli

I join SQL sono una delle funzionalità più potenti — e più fraintese — dei database relazionali. Questa guida illustra in modo approfondito ogni tipo di join, con esempi concreti, tabelle comparative e indicazioni chiare su quando ricorrere a ciascuno di essi.

16 min read
Join in SQL: una guida completa ai tipi, alle differenze e a quando utilizzarli

I join sono il meccanismo che dà il nome ai database relazionali. Lo scopo principale della separazione dei dati in tabelle distinte e normalizzate è proprio quello di poterle combinare all’occorrenza, estraendo esattamente la porzione di dati correlati di cui si ha bisogno, nella forma desiderata. Comprendere a fondo i join è indispensabile per chiunque lavori seriamente con SQL. È la competenza che distingue le query che funzionano da quelle che funzionano correttamente.

Questa guida tratta tutti i tipi di join standard — INNER, LEFT, RIGHT, FULL OUTER, CROSS e SELF — con esempi pratici, tabelle dei risultati e indicazioni chiare su quando ciascuno di essi è appropriato. Al termine della lettura, dovresti essere in grado di analizzare un problema relativo ai dati e capire immediatamente quale join, o combinazione di join, ti fornirà la risposta giusta.

Lo schema di esempio

Tutti gli esempi in questa guida utilizzano le stesse due tabelle, il che rende i confronti chiari e direttamente comparabili.

customers

customer_id nome città
1 Alice Nguyen Sydney
2 Bob Martins Melbourne
3 Carol White Brisbane
4 David Kim Sydney

ordini

id_ordine id_cliente prodotto importo
101 1 Computer portatile 1200,00
102 1 Mouse 45,00
103 2 Tastiera 89,00
104 5 Monitor 399,00

Alcune cose da notare appositamente: il cliente 3 (Carol) e il cliente 4 (David) non hanno ordini. L'ordine 104 fa riferimento al customer_id 5, che non esiste nella tabella dei clienti. Sono proprio questi casi limite a rendere significative le differenze tra i vari tipi di join.

INNER JOIN

Un INNER JOIN restituisce solo le righe in cui è presente un valore corrispondente in entrambe le tabelle. Se una riga nella tabella di sinistra non ha corrispondenza nella tabella di destra, viene esclusa. Se una riga nella tabella di destra non ha corrispondenza nella tabella di sinistra, viene anch’essa esclusa.

SELECT
c.customer_id,
c.name,
o.order_id,
o.product,
o.amount
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id;

Risultato:

customer_id name order_id product amount
1 Alice Nguyen 101 Computer portatile 1200,00
1 Alice Nguyen 102 Mouse 45,00
2 Bob Martins 103 Tastiera 89,00

Notate cosa manca: Carol e David (clienti 3 e 4) non compaiono perché non hanno ordini corrispondenti. L'ordine 104 non compare perché il customer_id 5 non esiste nella tabella dei clienti. L'INNER JOIN rappresenta l'intersezione: rimangono solo le righe con una corrispondenza su entrambi i lati.

Quando utilizzarlo: l’INNER JOIN è la scelta giusta quando ti interessano solo i record che presentano una relazione in entrambe le tabelle. Report sui clienti che hanno effettuato ordini, sui prodotti venduti, sui dipendenti assegnati a progetti — qualsiasi scenario in cui le righe non corrispondenti siano irrilevanti ai fini della domanda.

La trappola: se si utilizza un INNER JOIN quando in realtà servono tutti i record di una delle due tabelle, si perderanno dati in modo impercettibile. La query viene eseguita senza errori, ma i risultati sono incompleti. Questa è una delle cause più comuni di report errati in SQL.


LEFT JOIN (LEFT OUTER JOIN)

Un LEFT JOIN restituisce tutte le righe della tabella di sinistra, più eventuali righe corrispondenti della tabella di destra. Laddove non vi sia alcuna corrispondenza nella tabella di destra, le colonne di destra vengono riempite con NULL.

SELECT
c.customer_id,
c.name,
o.order_id,
o.product,
o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;

Risultato:

customer_id name order_id product amount
1 Alice Nguyen 101 Computer portatile 1200,00
1 Alice Nguyen 102 Mouse 45,00
2 Bob Martins 103 Tastiera 89,00
3 Carol White NULL NULL NULL
4 David Kim NULL NULL NULL

Ora compaiono Carol e David, con valori NULL nelle colonne relative agli ordini. L’ordine 104 continua a non comparire: non ha un cliente corrispondente e la tabella di sinistra in questo caso è quella dei clienti, non quella degli ordini.

Quando utilizzarlo: LEFT JOIN è il join più comunemente utilizzato dopo INNER JOIN. Utilizzatelo ogni volta che desiderate tutti i record della tabella primaria (sinistra), indipendentemente dal fatto che abbiano una corrispondenza nella tabella secondaria (destra). Elenchi di clienti con il loro ordine più recente (o NULL se non hanno mai effettuato un ordine), prodotti con il loro livello di scorte attuale (o NULL se non sono ancora disponibili), utenti con i dati del loro profilo (o NULL se il profilo non è stato ancora creato).

Ricerca di righe senza corrispondenza: LEFT JOIN combinato con una clausola WHERE che filtra i valori NULL è il modello standard per individuare i record senza corrispondenza:

-- Trova i clienti che non hanno mai effettuato un ordine
SELECT c.customer_id, c.name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;

Risultato:

customer_id name
3 Carol White
4 David Kim

Si tratta di un approccio più pulito e spesso più performante rispetto all’uso di sottoquery NOT IN o NOT EXISTS per lo stesso compito.


RIGHT JOIN (RIGHT OUTER JOIN)

Un RIGHT JOIN è l’immagine speculare di un LEFT JOIN. Restituisce tutte le righe della tabella di destra, più eventuali righe corrispondenti della tabella di sinistra. Laddove non vi sia alcuna corrispondenza nella tabella di sinistra, le colonne del lato sinistro risultano NULL.

SELECT
c.customer_id,
c.name,
o.order_id,
o.product,
o.amount
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;

Risultato:

customer_id name order_id product amount
1 Alice Nguyen 101 Computer portatile 1200,00
1 Alice Nguyen 102 Mouse 45,00
2 Bob Martins 103 Tastiera 89,00
NULL NULL 104 Monitor 399,00

Ora compare l'ordine 104, con valori NULL nelle colonne relative al cliente poiché l'ID cliente 5 non esiste. Carol e David non compaiono più: non hanno ordini e la tabella a destra (ordini) determina il risultato.

Quando utilizzarlo: il RIGHT JOIN è meno comune nella pratica, per un semplice motivo: qualsiasi RIGHT JOIN può essere riscritto come LEFT JOIN invertendo l’ordine delle tabelle. La maggior parte degli sviluppatori ritiene che il LEFT JOIN sia più leggibile perché segue la direzione naturale di lettura: «dammi tutto da questa tabella, più tutto ciò che corrisponde da quella».

-- Queste due query restituiscono risultati identici:

-- RIGHT JOIN
SELECT c.name, o.product
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;

-- LEFT JOIN equivalente (stile preferito)
SELECT c.name, o.product
FROM orders o
LEFT JOIN customers c ON c.customer_id = o.customer_id;

Utilizza il RIGHT JOIN quando la query risulta più leggibile in questo modo, oppure quando lavori su un codice che lo utilizza in modo coerente. Non utilizzarlo mai solo perché hai imparato che esiste.


FULL OUTER JOIN

Un FULL OUTER JOIN restituisce tutte le righe di entrambe le tabelle. Laddove vi è una corrispondenza, vengono popolate le colonne di entrambe le parti. Laddove non vi è corrispondenza in nessuna delle due parti, le colonne della parte non corrispondente sono NULL.

SELECT
c.customer_id,
c.name,
o.order_id,
o.product,
o.amount
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id;

Risultato:

customer_id name order_id product amount
1 Alice Nguyen 101 Computer portatile 1200,00
1 Alice Nguyen 102 Mouse 45,00
2 Bob Martins 103 Tastiera 89,00
3 Carol White NULL NULL NULL
4 David Kim NULL NULL NULL
NULL NULL 104 Monitor 399,00

Vengono visualizzate tutte le righe di entrambe le tabelle. Le righe corrispondenti vengono unite. Le righe non corrispondenti di una delle due tabelle vengono visualizzate con valori NULL nell’altra tabella.

Quando utilizzarlo: FULL OUTER JOIN è la scelta giusta quando è necessario visualizzare contemporaneamente tutti i dati — corrispondenti e non corrispondenti — di entrambe le tabelle. Riconciliazione dei dati tra due sistemi, individuazione di discrepanze tra una fonte e una destinazione, verifica di ciò che è presente in una tabella ma non nell’altra. È meno comune rispetto a INNER JOIN o LEFT JOIN, ma insostituibile quando serve.

Nota su MySQL: MySQL non supporta nativamente la sintassi FULL OUTER JOIN. La soluzione standard consiste nell’utilizzare UNION con un LEFT JOIN e un RIGHT JOIN:

SELECT c.customer_id, c.name, o.order_id, o.product
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id

UNION

SELECT c.customer_id, c.name, o.order_id, o.product
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;

PostgreSQL, SQL Server e la maggior parte degli altri database supportano direttamente il FULL OUTER JOIN.

Extreme close-up of programming code on a screen showing various technical elements


CROSS JOIN

Un CROSS JOIN restituisce il prodotto cartesiano delle due tabelle: ogni riga della tabella di sinistra combinata con ogni riga della tabella di destra. Non è presente alcuna condizione ON, poiché viene restituita ogni combinazione indipendentemente da qualsiasi relazione.

SELECT
c.name,
o.product
FROM customers c
CROSS JOIN orders o;

Con 4 clienti e 4 ordini, questa query restituisce 4 × 4 = 16 righe:

name product
Alice Nguyen Portatile
Alice Nguyen Mouse
Alice Nguyen Tastiera
Alice Nguyen Monitor
Bob Martins Computer portatile
Bob Martins Mouse
... ...

Il risultato completo è di 16 righe. Con 100 righe × 100 righe, il totale è 10.000. Con 1.000 × 1.000, è 1.000.000. I CROSS JOIN crescono in modo esponenziale, quindi devono essere utilizzati con cautela.

Quando utilizzarlo: il CROSS JOIN ha casi d’uso concreti. Generare tutte le combinazioni di taglie e colori per un catalogo prodotti. Creare una tabella di dimensione delle date incrociando un elenco di anni con un elenco di giorni. Costruire dati di test che richiedono ogni permutazione. Abbinare ogni rappresentante di vendita a ogni regione per una matrice di copertura.

Il CROSS JOIN accidentale: Uno degli errori SQL più pericolosi è un CROSS JOIN accidentale causato da una condizione JOIN mancante o errata. Se si dimentica la clausola ON o si esegue accidentalmente un join su una condizione che restituisce sempre vero, si possono ottenere milioni di righe da due tabelle relativamente piccole. Verificate sempre il numero di righe quando lavorate con join multipli.


SELF JOIN

Un SELF JOIN non è un tipo di join distinto nella sintassi SQL: si tratta di qualsiasi join in cui una tabella viene unita a se stessa. Si utilizza un normale join INNER, LEFT o di altro tipo, ma entrambi i lati del join fanno riferimento alla stessa tabella con alias diversi.

Il caso d’uso classico riguarda i dati gerarchici o ricorsivi: una tabella employees in cui ogni riga ha un manager_id che fa riferimento a un altro employee_id nella stessa tabella.

Si consideri questa tabella employees:

employees

employee_id name manager_id
1 Sarah Chen NULL
2 James Okafor 1
3 Maria Lopez 1
4 Tom Brennan 2
5 Yuki Tanaka 2

Sarah è l’amministratore delegato e non ha un responsabile. James e Maria fanno capo a Sarah. Tom e Yuki fanno capo a James.

SELECT
e.name AS dipendente,
m.name AS responsabile
FROM dipendenti e
LEFT JOIN dipendenti m ON e.manager_id = m.employee_id;

Risultato:

dipendente responsabile
Sarah Chen NULL
James Okafor Sarah Chen
Maria Lopez Sarah Chen
Tom Brennan James Okafor
Yuki Tanaka James Okafor

Utilizziamo LEFT JOIN in modo che Sarah (che non ha un responsabile) compaia comunque nei risultati con un responsabile NULL, anziché essere esclusa da un INNER JOIN.

Altri casi d’uso del self join: individuare record duplicati in una tabella, identificare righe che condividono un valore con un’altra riga, confrontare una riga con quelle adiacenti in una serie temporale, individuare tutte le coppie di elementi che condividono una categoria.


Confronto tra tutti i tipi di join

Tipo di join Righe non abbinate a sinistra Righe non abbinate a destra Quando utilizzarlo
INNER JOIN Escluse Escluse Si desiderano solo righe con una corrispondenza su entrambi i lati
LEFT JOIN Incluse (NULL) Escluse Si desiderano tutte le righe di sinistra, con o senza corrispondenza
RIGHT JOIN Escluse Incluse (NULL) Si desiderano tutte le righe di destra, con o senza corrispondenza
FULL OUTER JOIN Incluse (NULL) Incluse (NULL) Si desiderano tutte le righe di entrambe le tabelle
CROSS JOIN N/A (nessuna condizione) N/A (nessuna condizione) Ti servono tutte le combinazioni di righe di entrambe le tabelle
SELF JOIN Dipende dal tipo di join utilizzato Dipende dal tipo di join utilizzato Devi mettere in relazione righe all’interno della stessa tabella

Unione di più di due tabelle

Le query reali spesso uniscono tre, quattro o più tabelle. Ogni unione aggiuntiva estende il set di risultati dell’unione precedente. L’ordine è importante: ogni JOIN opera sul risultato di tutto ciò che si trova alla sua sinistra.

SELECT
c.name,
o.order_id,
o.product,
p.category,
p.supplier
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN products p ON o.product_id = p.product_id
LEFT JOIN suppliers s ON p.supplier_id = s.supplier_id;

Alcuni principi per mantenere gestibili i join su più tabelle:

Inizia con il join più selettivo. Se un join riduce drasticamente il numero di righe, inserirlo per primo fa sì che i join successivi operino su un numero minore di righe. Ciò può avere implicazioni significative sulle prestazioni in presenza di tabelle di grandi dimensioni.

Scegli con attenzione i tipi di join. Ogni join in una catena è una decisione indipendente. Potresti eseguire un INNER JOIN tra customers e orders (vuoi solo i clienti che hanno effettuato un ordine), ma un LEFT JOIN tra orders e shipments (vuoi tutti gli ordini, compresi quelli non ancora spediti).

Usa gli alias in modo coerente. Con tre o più tabelle, i nomi delle colonne possono entrare in conflitto. Assegna sempre un alias alle tabelle e anteponi un prefisso a ogni riferimento a una colonna. c.name è univoco; name in un join a quattro tabelle rappresenta un rischio per la manutenzione.

Verificate il numero di righe man mano che procedete. Quando create una query di join complessa, aggiungete ogni tabella in modo incrementale e controllate il numero di righe a ogni passaggio. Un aumento improvviso e inaspettato del numero di righe di solito indica che una condizione di join è errata o mancante.


Considerazioni sulle prestazioni dei JOIN

I join sono il punto in cui le prestazioni SQL si mantengono o crollano. Vale la pena comprendere alcuni principi indipendentemente dal motore di database utilizzato.

Gli indici sulle colonne di join sono essenziali. Quando si esegue un join su customer_id, il database deve cercare le righe corrispondenti. Senza un indice, per ogni ricerca viene eseguita una scansione dell’intera tabella. Con un indice, invece, si accede direttamente alle righe corrispondenti. Per qualsiasi colonna su cui si eseguono join regolarmente, assicurarsi che sia indicizzata: questa è la singola misura di ottimizzazione delle prestazioni più efficace per le query che prevedono un uso intensivo di join.

Filtrare in anticipo riduce il costo del join. Spostare le condizioni WHERE il prima possibile — oppure utilizzare sottoquery e CTE per pre-filtrare — significa che il join opera su un numero minore di righe. Un join tra 10.000 righe e 500 righe è di gran lunga meno oneroso di un join tra 10.000 righe e 50.000 righe, anche se il risultato finale filtrato ha le stesse dimensioni.

EXPLAIN è il tuo alleato. Tutti i principali motori di database supportano EXPLAIN (o EXPLAIN ANALYZE in PostgreSQL) per mostrare il piano di esecuzione della query. Se una query di join è lenta, esegui prima EXPLAIN. Ti dirà se vengono utilizzati gli indici, dove avvengono le scansioni sequenziali e quali sono i conteggi stimati delle righe in ogni fase. Imparare a leggere l’output di EXPLAIN è un fattore che moltiplica significativamente le tue competenze nel lavoro sulle prestazioni SQL.

Fai attenzione al fan-out. Se il lato destro di un join presenta più righe corrispondenti a ciascuna riga del lato sinistro, il set di risultati conterrà più righe rispetto a entrambe le tabelle di input. Si tratta di un comportamento corretto, ma può generare aggregati gonfiati se non si presta attenzione. COUNT(*) su un insieme di risultati unito conta le righe unite, non le righe originali del lato sinistro. Utilizzate COUNT(DISTINCT customer_id) o eseguite l’aggregazione prima dell’unione quando ciò è rilevante.


Errori comuni e come evitarli

Utilizzo di INNER JOIN quando è necessario un LEFT JOIN. La query produce risultati che sembrano plausibili, ma alcune righe mancano senza che venga segnalato. Chiedetevi sempre: le righe senza corrispondenza dovrebbero comunque apparire nei miei risultati? Se sì, utilizzate LEFT JOIN.

Filtraggio su una colonna NULL dopo un LEFT JOIN. Se si esegue un LEFT JOIN e poi si aggiunge WHERE o.status = 'shipped', si è di fatto convertito il LEFT JOIN in un INNER JOIN: le righe in cui o.status è NULL (cioè le righe di sinistra senza corrispondenza) saranno escluse. Filtra sulle colonne del lato destro nella clausola ON, non nella clausola WHERE, se vuoi conservare le righe di sinistra senza corrispondenza.

-- SBAGLIATO: converte il LEFT JOIN in un comportamento da INNER JOIN
SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'shipped';  -- esclude le righe NULL

-- CORRETTO: conserva i clienti senza corrispondenza
SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
AND o.status = 'shipped';

Effettuare un join su una colonna non univoca senza comprenderne le conseguenze. Se si esegue un join su una colonna che presenta valori duplicati sul lato destro, ogni riga di sinistra corrisponderà a più righe di destra e il set di risultati si moltiplicherà. A volte questo è proprio ciò che si desidera; spesso, invece, non lo è. È importante conoscere la cardinalità dei dati prima di dare per scontato che un join sia 1:1.

Dimenticare di gestire i valori NULL negli aggregati. SUM(o.amount) su un risultato di LEFT JOIN ignorerà correttamente i valori NULL, ma anche AVG(o.amount) li ignorerà — il che significa che la media viene calcolata sui clienti che hanno ordini, non su tutti i clienti. COALESCE(o.amount, 0) è spesso l’approccio corretto quando i valori NULL devono essere trattati come zero.


Guida rapida: scegliere il join giusto

Quando ti appresti a scrivere una query con join, poniti queste domande in ordine:

  1. Ho bisogno delle righe della tabella di sinistra anche se non hanno corrispondenze nella tabella di destra? Se sì → LEFT JOIN.
  2. Ho bisogno delle righe della tabella di destra anche se non hanno corrispondenze nella tabella di sinistra? Se sì → RIGHT JOIN (oppure scambia le tabelle e usa LEFT JOIN).
  3. Ho bisogno delle righe senza corrispondenza da entrambe le parti? Se sì → FULL OUTER JOIN.
  4. Mi interessano solo le righe che hanno una corrispondenza su entrambi i lati? Se sì → INNER JOIN.
  5. Ho bisogno di ogni combinazione di righe indipendentemente dalla relazione? Se sì → CROSS JOIN (con cautela).
  6. Sto mettendo in relazione righe all’interno della stessa tabella? Se sì → SELF JOIN utilizzando uno dei tipi sopra indicati.

I join premiano una riflessione ponderata. Il tipo che scegli è una dichiarazione su ciò che vuoi che accada alle righe non corrispondenti. Prendi la decisione giusta e il resto della query seguirà naturalmente.