I JOIN SQL combinano le righe di due tabelle confrontando una colonna in comune, come l'author_id di un libro e l'id di un autore. Il tipo di join decide cosa succede alle righe che non corrispondono. Gli esempi funzionano in PostgreSQL, MySQL, SQLite e SQL Server.
Come usare i JOIN SQL
Metti la seconda tabella dopo JOIN e indica in ON come le sue righe corrispondono a quelle della prima:
SELECT columns
FROM table_a
JOIN table_b ON table_a.key = table_b.key;
Tutti gli esempi di questa sezione uniscono le stesse due tabelle: Butler ha scritto due dei libri, Asimov nessuno, e nessuno sa chi abbia scritto Beowulf.
INNER JOIN
Un INNER JOIN restituisce solo le righe che corrispondono in entrambe le tabelle. JOIN da solo significa INNER JOIN.
-- Input: authors
-- id | name
-- ---+--------
-- 1 | Le Guin
-- 2 | Butler
-- 3 | Asimov
-- Input: books
-- title | author_id
-- ---------------------+----------
-- The Dispossessed | 1
-- Kindred | 2
-- Parable of the Sower | 2
-- Beowulf | NULL
SELECT a.name, b.title
FROM authors a
INNER JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
Asimov non ha libri e Beowulf non ha un autore, quindi nessuno dei due compare.
LEFT JOIN
Un LEFT JOIN (o LEFT OUTER JOIN) restituisce tutte le righe della tabella di sinistra, quella dopo FROM. Dove la tabella di destra non ha corrispondenze, le sue colonne tornano NULL.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- Asimov | NULL
RIGHT JOIN
Un RIGHT JOIN (o RIGHT OUTER JOIN) è l'immagine speculare: tutte le righe della tabella di destra, con NULL dove la tabella di sinistra non ha corrispondenze.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
RIGHT JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- NULL | Beowulf
FULL OUTER JOIN
Un FULL OUTER JOIN restituisce tutte le righe di entrambe le tabelle, abbinate dove possibile e completate con NULL dove no. MySQL non lo supporta: vedi “Sintassi alternativa” più sotto.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
FULL OUTER JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- NULL | Beowulf
-- Asimov | NULL
CROSS JOIN
Un CROSS JOIN abbina ogni riga di una tabella a ogni riga dell'altra, quindi non ha ON. Due taglie e due colori danno quattro combinazioni; 1.000 righe per 1.000 righe ne danno un milione, quindi usalo su tabelle piccole.
-- Input: sizes
-- size
-- ----
-- S
-- M
-- Input: colors
-- color
-- -----
-- Red
-- Blue
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- Output:
-- size | color
-- -----+------
-- S | Red
-- S | Blue
-- M | Red
-- M | Blue
LEFT ANTI JOIN
Un left anti join restituisce le righe della tabella di sinistra che non hanno corrispondenze nella tabella di destra. Non esiste una parola chiave apposita: fai un LEFT JOIN e tieni le righe in cui il lato destro è tornato NULL.
-- Input: the authors and books tables above
SELECT a.name
FROM authors a
LEFT JOIN books b ON a.id = b.author_id
WHERE b.author_id IS NULL;
-- Output:
-- name
-- ------
-- Asimov
RIGHT ANTI JOIN
Un right anti join restituisce le righe della tabella di destra che non hanno corrispondenze nella tabella di sinistra.
-- Input: the authors and books tables above
SELECT b.title
FROM authors a
RIGHT JOIN books b ON a.id = b.author_id
WHERE a.id IS NULL;
-- Output:
-- title
-- -------
-- Beowulf
Regole da conoscere
-
NULL non corrisponde mai a niente, nemmeno a un altro NULL. L'
author_idNULL di Beowulf non corrisponde a nessun autore, quindi solo i join che mantengono i libri senza corrispondenza lo restituiscono. - Una riga compare una volta per ogni riga a cui corrisponde. Butler compare due volte qui sopra perché corrisponde a due libri. Se poi sommi una colonna della tabella degli autori, i suoi valori vengono contati due volte.
-
Con un outer join, una condizione sull'altra tabella va messa in
ON. InWHEREviene applicata dopo il join e scarta le righe NULL che il join aveva appena mantenuto:
-- Input: the authors and books tables above
-- Only Butler is left: the WHERE drops every author whose title is NULL
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id
WHERE b.title LIKE 'K%';
-- Output:
-- name | title
-- -------+--------
-- Butler | Kindred
-- Every author is kept, with a book only where it starts with K
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id AND b.title LIKE 'K%';
-- Output:
-- name | title
-- --------+--------
-- Le Guin | NULL
-- Butler | Kindred
-- Asimov | NULL
Sintassi alternativa
MySQL non ha FULL OUTER JOIN. Combina invece un LEFT JOIN con le righe senza corrispondenza di un RIGHT JOIN:
-- MySQL
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a LEFT JOIN books b ON a.id = b.author_id
UNION ALL
SELECT a.name, b.title
FROM authors a RIGHT JOIN books b ON a.id = b.author_id
WHERE a.id IS NULL;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Parable of the Sower
-- Butler | Kindred
-- Asimov | NULL
-- NULL | Beowulf
SQLite prima della versione 3.39 non ha RIGHT JOIN né FULL OUTER JOIN: inverti le tabelle e usa un LEFT JOIN.
Un anti join si può scrivere anche con NOT EXISTS. Evita NOT IN in questo caso: se la subquery restituisce anche un solo NULL, come l'author_id di Beowulf qui, NOT IN non restituisce proprio nulla:
-- Input: the authors and books tables above
SELECT a.name
FROM authors a
WHERE NOT EXISTS (SELECT 1 FROM books b WHERE b.author_id = a.id);
-- Output:
-- name
-- ------
-- Asimov
SELECT a.name
FROM authors a
WHERE a.id NOT IN (SELECT author_id FROM books);
-- Output:
-- name
-- ----
-- (0 rows)
Quando le colonne da confrontare hanno lo stesso nome in entrambe le tabelle, puoi scrivere JOIN books USING (author_id) al posto della condizione ON, tranne che in SQL Server.
Il codice più vecchio spesso elenca le tabelle separate da virgole e mette la corrispondenza in WHERE. È un INNER JOIN scritto in un altro modo. Se togli il WHERE, diventa un CROSS JOIN:
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a, books b
WHERE a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
Quando usare i JOIN SQL?
Usa un join ogni volta che i dati che ti servono sono divisi tra più tabelle. Quale join usare dipende da cosa deve succedere alle righe senza corrispondenza, e la maggior parte dei problemi rientra in uno di questi cinque casi.
Recuperare dettagli da una tabella collegata
La riga di un dipendente contiene l'ID del reparto, ma il report ha bisogno del nome del reparto. Un INNER JOIN lo recupera e scarta le righe che non hanno nulla da cercare.
Mantenere le righe senza corrispondenza
Quando i dati collegati sono facoltativi, come un dipendente non ancora assegnato a un reparto, un INNER JOIN lo nasconderebbe senza dirti niente. Un LEFT JOIN mantiene tutte le righe della tabella principale e riempie i vuoti con NULL.
Elencare tutti gli elementi, anche quelli vuoti
Spesso i report devono mostrare ogni reparto, categoria o cliente, compresi quelli ancora vuoti. Fai in modo che quella sia la tabella di cui si mantengono tutte le righe: prima con un LEFT JOIN, oppure seconda con un RIGHT JOIN.
Trovare le righe senza corrispondenza
Clienti che non hanno mai ordinato, prodotti mai venduti, dipendenti senza reparto: un anti join trova ciò che manca, ed è quindi anche un ottimo controllo sulla qualità dei dati.
Generare tutte le combinazioni
Un CROSS JOIN genera righe che ancora non esistono: ogni taglia in ogni colore, ogni dipendente per ogni corso di formazione, oppure ogni negozio per ogni giorno di un report.
Esempi reali di JOIN SQL
Studenti e i loro corsi
Una relazione molti-a-molti richiede due INNER JOIN, passando per la tabella di collegamento che associa gli studenti ai corsi. Mei non è iscritta a nessun corso, quindi non compare:
-- Input: students
-- id | name
-- ---+------
-- 1 | Amara
-- 2 | Lucas
-- 3 | Mei
-- Input: enrollments
-- student_id | course_id | grade
-- -----------+-----------+------
-- 1 | 10 | A
-- 1 | 20 | B
-- 2 | 10 | B
-- Input: courses
-- id | course_name
-- ---+------------
-- 10 | Databases
-- 20 | Statistics
SELECT s.name, c.course_name, e.grade
FROM students s
INNER JOIN enrollments e ON s.id = e.student_id
INNER JOIN courses c ON e.course_id = c.id;
-- Output:
-- name | course_name | grade
-- ------+-------------+------
-- Amara | Databases | A
-- Amara | Statistics | B
-- Lucas | Databases | B
Utenti con profilo facoltativo
Non tutti gli utenti hanno compilato il profilo. Un LEFT JOIN li mantiene tutti:
-- Input: users
-- id | username
-- ---+---------
-- 1 | rosa_m
-- 2 | devon
-- 3 | kai_l
-- Input: user_profiles
-- user_id | bio
-- --------+------------------------
-- 1 | Data engineer in Lisbon
-- 3 | Loves SQL puzzles
SELECT u.username, p.bio
FROM users u
LEFT JOIN user_profiles p ON u.id = p.user_id;
-- Output:
-- username | bio
-- ---------+------------------------
-- rosa_m | Data engineer in Lisbon
-- devon | NULL
-- kai_l | Loves SQL puzzles
Numero di ordini per ogni cliente
Per includere i clienti senza ordini, fai un LEFT JOIN partendo da customers. Poi conta una colonna della tabella degli ordini: COUNT(*) conta la riga creata dal LEFT JOIN per Omar, che quindi avrebbe 1 invece di 0:
-- Input: customers
-- id | name
-- ---+------------
-- 1 | Harper Lee
-- 2 | Omar Haddad
-- 3 | Yuki Tanaka
-- Input: orders
-- id | customer_id | total
-- ----+-------------+-------
-- 101 | 1 | 59.90
-- 102 | 3 | 120.00
-- 103 | 1 | 14.50
-- Wrong: COUNT(*) counts Omar's NULL row
SELECT c.name, COUNT(*) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY c.id;
-- Output:
-- name | order_count
-- ------------+------------
-- Harper Lee | 2
-- Omar Haddad | 1
-- Yuki Tanaka | 1
-- Right: COUNT(o.id) skips it
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY c.id;
-- Output:
-- name | order_count
-- ------------+------------
-- Harper Lee | 2
-- Omar Haddad | 0
-- Yuki Tanaka | 1
Fatture e pagamenti che non tornano
Un FULL OUTER JOIN che mantiene solo le righe senza corrispondenza confronta due elenchi in una sola query: la fattura 502 non è stata pagata e il pagamento 9003 si riferisce a una fattura che non esiste:
-- Input: invoices
-- invoice_id | amount
-- -----------+-------
-- 501 | 120.00
-- 502 | 75.50
-- 503 | 300.00
-- Input: payments
-- payment_id | invoice_id | paid
-- -----------+------------+-------
-- 9001 | 501 | 120.00
-- 9002 | 503 | 300.00
-- 9003 | 504 | 42.00
-- MySQL has no FULL OUTER JOIN: use the UNION ALL form under "Alternative syntax"
SELECT i.invoice_id, i.amount, p.payment_id, p.invoice_id AS paid_invoice
FROM invoices i
FULL OUTER JOIN payments p ON p.invoice_id = i.invoice_id
WHERE i.invoice_id IS NULL OR p.payment_id IS NULL;
-- Output:
-- invoice_id | amount | payment_id | paid_invoice
-- -----------+--------+------------+-------------
-- 502 | 75.50 | NULL | NULL
-- NULL | NULL | 9003 | 504
Incassi giornalieri di ogni negozio, compresi i giorni senza vendite
Fai un CROSS JOIN tra negozi e giorni per ottenere una riga per negozio al giorno, poi fai un LEFT JOIN delle vendite su questa griglia. York non ha venduto nulla il 6 ottobre, ma ha comunque la sua riga:
-- Input: stores
-- store
-- -----
-- Leeds
-- York
-- Input: days
-- day
-- ----------
-- 2026-10-05
-- 2026-10-06
-- Input: sales
-- store | day | amount
-- ------+------------+-------
-- Leeds | 2026-10-05 | 410.00
-- Leeds | 2026-10-06 | 385.50
-- York | 2026-10-05 | 220.00
SELECT st.store, d.day, COALESCE(SUM(s.amount), 0.00) AS takings
FROM stores st
CROSS JOIN days d
LEFT JOIN sales s ON s.store = st.store AND s.day = d.day
GROUP BY st.store, d.day
ORDER BY st.store, d.day;
-- Output:
-- store | day | takings
-- ------+------------+--------
-- Leeds | 2026-10-05 | 410.00
-- Leeds | 2026-10-06 | 385.50
-- York | 2026-10-05 | 220.00
-- York | 2026-10-06 | 0.00
Altre letture sui JOIN SQL
Su questo blog:
- Cheat sheet dei JOIN SQL: tutti i tipi di join e i loro diagrammi in una sola pagina
- FULL OUTER JOIN in MySQL: altri modi per aggirare l'assenza del FULL OUTER JOIN in MySQL
- Guida ai join in MySQL: i join in MySQL, con esempi
- Guida ai join in SQL Server: i join in T-SQL
- CROSS JOIN in PostgreSQL: altri usi delle query che generano tutte le combinazioni
- Guida alle relazioni tra database: come sono collegate tra loro le tabelle che unisci
- Relazioni uno-a-uno, guida alle relazioni uno-a-molti e guida alle relazioni molti-a-molti: il join che serve per ogni relazione
- Guida a COALESCE in SQL: sostituisci i NULL lasciati da un outer join
Documentazione ufficiale:
- PostgreSQL: tabelle unite
- MySQL: clausola JOIN
- SQLite: la clausola FROM e i join
- SQL Server: clausola FROM più JOIN
Beekeeper Studio è una GUI per database gratuita & open source
Il miglior strumento per query & editor SQL che abbia mai usato. Offre tutto ciò che mi serve per gestire il mio database. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio è veloce, intuitivo e facile da usare. Beekeeper supporta tantissimi database e funziona alla grande su Windows, Mac e Linux.
Cosa dicono gli utenti di Beekeeper Studio
"Beekeeper Studio ha sostituito completamente il mio vecchio workflow SQL. È veloce, intuitivo e rende di nuovo piacevole lavorare con i database."
"Ho provato tante GUI per database, ma Beekeeper trova il perfetto equilibrio tra funzionalità e semplicità. Funziona e basta."