SQL JOIN prakticky: INNER, LEFT, RIGHT a FULL na příkladech
Pochopte SQL JOINy na jednoduchých tabulkách zákazníků a objednávek. INNER, LEFT, FULL OUTER, self join a nejčastější chyby s duplicitami.
JOIN je operace, která z relační databáze dělá relační databázi. Data máte rozdělená do více tabulek a JOIN je při dotazu zase spojí. Ukážeme si všechny běžné typy na jednom malém příkladu, který si můžete rovnou spustit v PostgreSQL, MySQL/MariaDB i SQLite.
Ukázková data
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
total DECIMAL(10,2) NOT NULL
);
INSERT INTO customers (id, name) VALUES
(1, 'Alena'), (2, 'Bohdan'), (3, 'Cyril');
INSERT INTO orders (id, customer_id, total) VALUES
(10, 1, 450.00), (11, 1, 120.00), (12, 2, 990.00), (13, NULL, 75.00);
Všimněte si dvou „okrajových“ případů: Cyril nemá žádnou objednávku a objednávka 13 nemá zákazníka. Právě na nich se typy JOINů liší.
INNER JOIN – jen shody
SELECT c.name, o.id AS order_id, o.total
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;
| name | order_id | total |
|---|---|---|
| Alena | 10 | 450.00 |
| Alena | 11 | 120.00 |
| Bohdan | 12 | 990.00 |
Vrátí jen řádky, které mají protějšek v obou tabulkách. Cyril ani objednávka 13 ve výsledku nejsou. Slovo INNER je nepovinné – samotné JOIN znamená totéž.
LEFT JOIN – vše z levé tabulky
SELECT c.name, o.id AS order_id, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
Výsledek obsahuje navíc řádek Cyril | NULL | NULL. LEFT JOIN použijete, když chcete „všechny zákazníky a k nim objednávky, pokud nějaké mají“.
Hledání řádků bez protějšku
Klasický trik: LEFT JOIN a filtrování na NULL v pravé tabulce.
SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
Vrátí zákazníky bez objednávky (Cyril). Alternativou je NOT EXISTS, který bývá stejně rychlý a často čitelnější:
SELECT c.name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
Pozor na NOT IN (SELECT customer_id FROM orders) – protože sloupec obsahuje NULL, vrátí tento dotaz prázdný výsledek. Je to jedna z nejzáludnějších chyb v SQL.
RIGHT JOIN a FULL OUTER JOIN
RIGHT JOIN je zrcadlo LEFT JOINu – zachová všechny řádky z pravé tabulky. V praxi se moc nepoužívá, protože stačí prohodit pořadí tabulek a napsat LEFT JOIN.
FULL OUTER JOIN zachová řádky z obou stran:
SELECT c.name, o.id AS order_id
FROM customers c
FULL OUTER JOIN orders o ON o.customer_id = c.id;
Ve výsledku bude Cyril bez objednávky i objednávka 13 bez zákazníka. PostgreSQL a SQLite (od verze 3.39) ho podporují, MySQL a MariaDB ne. Tam ho nahradíte spojením LEFT a RIGHT JOINu přes UNION:
SELECT c.name, o.id FROM customers c LEFT JOIN orders o ON o.customer_id = c.id
UNION
SELECT c.name, o.id FROM customers c RIGHT JOIN orders o ON o.customer_id = c.id;
Agregace přes JOIN
Typický úkol: součet objednávek za zákazníka, včetně těch s nulou.
SELECT c.name,
COUNT(o.id) AS orders_count,
COALESCE(SUM(o.total), 0) AS revenue
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY revenue DESC;
Dvě důležité drobnosti:
COUNT(o.id)počítá jen ne-NULL hodnoty, takže Cyril má 0.COUNT(*)by mu dal 1.COALESCEpřevedeNULLsoučet na 0.
Pozor na podmínku v ON vs. WHERE
U LEFT JOINu záleží, kam podmínku napíšete:
-- Všichni zákazníci, objednávky jen nad 200 Kč
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.total > 200;
-- Tohle z LEFT JOINu potichu udělá INNER JOIN
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.total > 200;
Ve druhém dotazu WHERE vyřadí řádky, kde je o.total NULL, a Cyril zmizí.
Self join
Tabulku můžete spojit samu se sebou, například zaměstnance s nadřízeným:
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;
Aliasy (e, m) jsou tady povinné, jinak databáze nerozliší, o kterou „kopii“ tabulky jde.
Nejčastější chyby
- Násobení řádků. Když spojíte zákazníka s objednávkami a zároveň s adresami, každá objednávka se zopakuje pro každou adresu. Součty pak vycházejí špatně. Řešení: agregovat v poddotazu nebo CTE a spojovat až výsledky.
- Chybějící podmínka ON.
CROSS JOIN(kartézský součin) vznikne i omylem, třeba ve staré syntaxiFROM a, bbezWHERE. Z 10 000 × 10 000 řádků je rázem 100 milionů. - JOIN přes neindexovaný sloupec. Cizí klíče jako
orders.customer_idby měly mít index. PostgreSQL ho pro cizí klíč automaticky nevytváří, InnoDB v MySQL ano.
Jak si ověřit, že dotaz dělá, co má
- Spočítejte řádky před JOINem a po něm (
SELECT COUNT(*) ...). Nečekaný nárůst ukazuje na násobení. - Ručně si projděte okrajové případy – záznamy bez protějšku a NULL hodnoty.
- U pomalých dotazů použijte
EXPLAIN, viz článek Indexy a pomalé dotazy.
Pokud si nejste jistí, proč dotaz vrací víc řádků, než má, vložte schéma i dotaz do ColdHost AI v režimu Výuka – vysvětlí krok po kroku, kde se řádky násobí.