1. ismétlő óra: JOIN-ok — táblák összekapcsolása

60 perc
Ismétlés + gyakorlás
Eszközök: DBeaver
SQL szintaxis: INNER JOIN, LEFT JOIN, ON, WHERE, IS NULL, NOT EXISTS

Miről szól ez az óra?

Ez a két részes ismétlés első fele. Ma magukra a JOIN-okra koncentrálunk: végigmegyünk minden kapcsolattípuson (1:1, 1:N, N:M), mindegyiket megnézzük INNER JOIN-nal és LEFT JOIN-nal is, és minden esetben megszámoljuk, hány sor jött vissza és miért.

Ehhez jön két gyakorlati kérdés, amivel a mindennapi munkában lépten-nyomon találkozol:

  1. Ha van egy plusz feltételem az összekapcsoláshoz, azt a JOIN ... ON mellé írjam, vagy a WHERE-be?
  2. Hogyan keresem meg azokat a sorokat, amelyekhez nincs adat a másik táblában?

A 2. ismétlő órán ugyanezekre a lekérdezésekre — változatlan FROM/JOIN résszel — rátesszük a GROUP BY-t. Ennek a sorrendnek egyetlen oka van:

Fontos

A GROUP BY nem a táblákon dolgozik, hanem az eredményen.

Mire a GROUP BY sorra kerül, a JOIN-ok már lefutottak, és egyetlen új, ideiglenes táblát gyártottak. A GROUP BY ezt látja — azt sem tudja, hogy két, három vagy öt táblából állt-e össze.

Ezért ha a mai órán pontosan megérted, hány sorod van és mit jelent egy sor, akkor a következő óra magától értetődő lesz. Ha nem, akkor az aggregált riportod hibás lesz — méghozzá hibaüzenet nélkül.

Az óra nem magyarázza el újra, hogy a WHERE-alapú (implicit) összekötéshez képest miért más az explicit JOIN — azt már vettük. Itt magukra a JOIN-okra és az eredményükre koncentrálunk.


Az óra felépítése

Blokk Idő Téma
1 14’ Két tábla: INNER és LEFT (1:1 és 1:N)
2 13’ Több tábla láncban: INNER és LEFT
3 9’ Több a többhöz (N:M) kapcsolat
4 12’ Plusz feltétel: ON vagy WHERE?
5 12’ A „hiány” keresése — amihez nincs adat a másik táblában
Megjegyzés

Az adatbázis. Minden példa az adatbazis_setup.sql szkripttel felállított webshop-adatbázison fut, és minden eredménytábla valós futási eredmény. Ha együtt akarsz dolgozni, futtasd le a szkriptet, mielőtt elkezdjük.


1. blokk: Két tábla — INNER és LEFT

A modell egy mondatban

A JOIN egy új, ideiglenes táblát gyárt két tábla soraiból. Az ON feltétel mondja meg, mely sorok tartoznak össze; a JOIN típusa pedig azt, hogy mi történjen a párt nem találó sorokkal.

JOIN típus Mi történik a párt nem találó bal oldali sorral?
INNER JOIN Kiesik. Csak a párok maradnak.
LEFT JOIN Megmarad — a jobb oldali oszlopok helyére NULL kerül.

Ez az egész. Minden más ebből következik.

Megjegyzés

Létezik RIGHT JOIN és FULL OUTER JOIN is. A RIGHT JOIN ugyanaz, mint a LEFT, csak a másik oldalról nézve — a gyakorlatban ritkán használjuk, mert olvashatóbb megfordítani a táblák sorrendjét és LEFT-et írni. A FULL OUTER JOIN mindkét oldal árváit megtartja. Ma az INNER-re és a LEFT-re koncentrálunk, mert a valós munka 95%-a ez a kettő.

1.1. Egy az egyhez (1:1): ügyfelek és adatlapjaik

A 24 ügyfelünkből csak 5-nek van kitöltött adatlapja az ugyfel_adatok táblában.

INNER JOIN — csak a párok:

SELECT u.id, u.nev, ua.telefonszam
FROM ugyfelek u
INNER JOIN ugyfel_adatok ua ON ua.ugyfel_id = u.id
ORDER BY u.id;
id nev telefonszam
1 Kiss Péter +36 30 111 2222
2 Nagy Anna +36 20 999 8888
3 Horváth Bence +36 70 555 4444
4 Varga Éva +36 30 777 6666
5 Tóth Gábor +36 20 333 4444

5 sor. A 24 ügyfélből 19 csendben eltűnt — nem hiba, nem figyelmeztetés, egyszerűen nincsenek ott.

LEFT JOIN — minden ügyfél megmarad:

SELECT u.id, u.nev, ua.telefonszam
FROM ugyfelek u
LEFT JOIN ugyfel_adatok ua ON ua.ugyfel_id = u.id
WHERE u.id <= 8
ORDER BY u.id;
id nev telefonszam
1 Kiss Péter +36 30 111 2222
… … …
5 Tóth Gábor +36 20 333 4444
6 Fekete Lilla (NULL)
7 Szabó Roland (NULL)
8 Molnár Zsuzsa (NULL)

WHERE nélkül 24 sor jönne vissza — pontosan annyi, ahány ügyfél van. Ez az 1:1 kapcsolat jellemzője: a LEFT JOIN nem szaporítja a sorokat, csak kiegészíti őket.

Fontos

A NULL itt információ, nem hiba. Azt jelenti: „ehhez az ügyfélhez nem találtam adatlapot.” Tesztelőként pont ezek a sorok érdekesek — a LEFT JOIN teszi őket láthatóvá.

1.2. Egy a többhöz (1:N): kategóriák és termékek

Itt már szaporodnak a sorok: egy kategóriához több termék tartozik.

INNER JOIN:

SELECT k.nev AS kategoria, t.nev AS termek, t.ar
FROM kategoriak k
INNER JOIN termekek t ON t.kategoria_id = k.id
ORDER BY k.id, t.id;

25 sor. Nem 6 (ahány kategória) és nem 26 (ahány termék) — hanem annyi, ahány párosítás létrejött.

LEFT JOIN — a kategóriák felől:

SELECT k.nev AS kategoria, t.nev AS termek, t.ar
FROM kategoriak k
LEFT JOIN termekek t ON t.kategoria_id = k.id
WHERE k.id IN (4, 5, 6)          -- Sport, Könyv, Játék
ORDER BY k.id, t.id;
kategoria termek ar
Sport Yoga szőnyeg 8 990
Sport Kettlebell 16kg 19 990
Sport Ugrókötel 3 990
Sport Stopperóra Garmin 49 990
Sport Bokszzsák 39 990
Könyv Clean Code – Robert Martin 8 990
Könyv The Pragmatic Programmer 9 490
Könyv SQL in 10 Minutes 5 990
Könyv Design Patterns – Gang of Four 11 990
Könyv Agile Testing – Lisa Crispin 9 990
Játék (NULL) (NULL)

WHERE nélkül 26 sor: a 25 párosítás plusz egy sor a „Játék” kategóriának, amelyhez még nincs termék.

1.3. „És ha megfordítjuk?” — melyik oldal a bal oldal?

Ez a kérdés dönti el, mit akarsz látni. Írjuk meg ugyanezt a kapcsolatot a másik irányból:

SELECT t.id, t.nev AS termek, k.nev AS kategoria
FROM termekek t
LEFT JOIN kategoriak k ON k.id = t.kategoria_id     -- most a TERMÉK a bal oldal
WHERE t.id >= 23
ORDER BY t.id;
id termek kategoria
23 SQL in 10 Minutes Könyv
24 Design Patterns – Gang of Four Könyv
25 Agile Testing – Lisa Crispin Könyv
26 Ajándékutalvány (NULL)

Ez is 26 sor — de teljesen más okból! Itt nem a termék nélküli kategória a plusz sor, hanem az Ajándékutalvány, amelynek a kategoria_id-ja NULL, tehát nincs kategóriája.

Tipp

A kérdés, amit fel kell tenned, mielőtt JOIN-t írsz: „Melyik táblából akarok minden sort látni, akkor is, ha nincs párja?“ Az a tábla megy a FROM után (az lesz a bal oldal), a másik a LEFT JOIN után.

  • „Minden kategóriát akarok látni” → FROM kategoriak LEFT JOIN termekek
  • „Minden terméket akarok látni” → FROM termekek LEFT JOIN kategoriak

1.4. Sorszám-összefoglaló — ezt érdemes fejben tartani

Lekérdezés Sorok Miért?
kategoriak önmagában 6 6 kategória
termekek önmagában 26 26 termék
kategoriak INNER JOIN termekek 25 25 párosítás (a Játékhoz nincs termék, az Ajándékutalványnak nincs kategóriája)
kategoriak LEFT JOIN termekek 26 25 párosítás + 1 sor a Játéknak
termekek LEFT JOIN kategoriak 26 25 párosítás + 1 sor az Ajándékutalványnak
ugyfelek INNER JOIN ugyfel_adatok 5 csak akinek van adatlapja
ugyfelek LEFT JOIN ugyfel_adatok 24 minden ügyfél (1:1 — nem szaporít)
Figyelmeztetés

Tesztelői reflex: minden JOIN után tedd fel a kérdést, hogy hány sort vártál. Ha 24 ügyfélre vagy kíváncsi, és 5 sort kapsz, azonnal látod a bajt. Ha nem gondolod végig előre, a lekérdezés hallgat.


2. blokk: Több tábla láncban — INNER és LEFT

Három vagy több tábla összekötése nem új szabály, csak ugyanaz a szabály többször egymás után. A lánc: ugyfelek → rendelesek → rendeles_tetelek.

2.1. INNER JOIN lánc — lépésenként

SELECT u.nev, r.id AS rendeles_id, r.allapot, rt.id AS tetel_id, rt.mennyiseg
FROM ugyfelek u
INNER JOIN rendelesek r        ON r.ugyfel_id   = u.id
INNER JOIN rendeles_tetelek rt ON rt.rendeles_id = r.id
ORDER BY u.id, r.id, rt.id;

Nézzük meg, hogyan változik a sorszám minden egyes JOIN után:

Meddig? Sorok Mi történt?
FROM ugyfelek 24 minden ügyfél
+ INNER JOIN rendelesek 15 ⬇ csökkent! 14 ügyfél kiesett (nincs rendelése); a maradék 10 ügyfélre összesen 15 rendelés jut
+ INNER JOIN rendeles_tetelek 25 ⬆ nőtt! minden rendeléshez 1–2 tétel tartozik
+ INNER JOIN termekek 25 változatlan — minden tételhez pontosan 1 termék tartozik
Fontos

A sorszám mindkét irányba mozoghat, és ez a lényeg: az INNER JOIN kidobja a pár nélküli sorokat (24 → 15), ugyanakkor felszorozza azokat, amelyeknek több párjuk van (15 → 25).

A lánc végén egy sor jelentése már nem „egy ügyfél”, hanem „egy rendelési tétel”. Ezt hívjuk a lekérdezés szemcsézettségének (angolul grain). A 6. blokkban ez lesz a mindent eldöntő kérdés.

2.2. LEFT JOIN lánc — ugyanaz, senkit nem hagyva el

SELECT u.id, u.nev, r.id AS rendeles_id, r.allapot, rt.id AS tetel_id, rt.mennyiseg
FROM ugyfelek u
LEFT JOIN rendelesek r        ON r.ugyfel_id   = u.id
LEFT JOIN rendeles_tetelek rt ON rt.rendeles_id = r.id
ORDER BY u.id, r.id, rt.id;
Meddig? INNER lánc LEFT lánc
FROM ugyfelek 24 24
+ … rendelesek 15 29 (15 rendelés + 14 rendelés nélküli ügyfél)
+ … rendeles_tetelek 25 39 (25 tétel + 14 árva sor)

Nézzünk bele három konkrét ügyfél sorába:

id nev rend allapot tetel mennyiseg
5 Tóth Gábor 6 lemondva 11 1
6 Fekete Lilla 7 folyamatban 12 3
6 Fekete Lilla 7 folyamatban 13 1
11 Antal Mária (NULL) (NULL) (NULL) (NULL)

Antal Máriának nincs rendelése. A LEFT JOIN mégis legyártott neki egy sort, csupa NULL jobb oldallal — és a lánc második LEFT JOIN-ja is NULL-t hagyott, hiszen rt.rendeles_id = NULL sem talál semmit.

Fekete Lilla ezzel szemben kétszer szerepel, mert a 7-es rendelésének két tétele van. Ugyanaz az ügyfél, ugyanaz a rendelés, két sor. Jegyezd meg ezt a sort — a 6. blokkban visszatérünk rá.

2.3. ⚠️ A lánc-csapda: INNER a LEFT után

-- ❌ HIBÁS: a második JOIN INNER
SELECT COUNT(*)
FROM ugyfelek u
LEFT  JOIN rendelesek r        ON r.ugyfel_id   = u.id
INNER JOIN rendeles_tetelek rt ON rt.rendeles_id = r.id;
-- Eredmény: 25

25 sor — pontosan annyi, mint a tiszta INNER láncnál. A LEFT JOIN gondosan megtartotta Antal Mária NULL-os sorát, aztán a következő INNER JOIN kidobta, mert NULL-hoz nem talált tételt.

Figyelmeztetés

LEFT JOIN-láncban minden további JOIN-nak is LEFT-nek kell lennie. Egyetlen INNER a lánc bármely pontján visszamenőleg megsemmisíti az összes előtte lévő LEFT hatását. Ez az egyik leggyakoribb csendes hiba — nincs hibaüzenet, csak kevesebb sor.


3. blokk: Több a többhöz (N:M) kapcsolat

Az N:M kapcsolat nem külön JOIN-fajta — csak két 1:N kapcsolat egy kapcsolótáblán keresztül. Egy ügyfélnek több kedvenc terméke lehet, és egy terméket több ügyfél is megjelölhet: közöttük a kedvencek tábla áll.

ugyfelek  ──1:N──►  kedvencek  ◄──N:1──  termekek
                  (kapcsolótábla)

3.1. INNER JOIN — a tényleges kedvencek

SELECT
    u.nev AS ugyfel,
    t.nev AS termek,
    kd.hozzaadas_datuma,
    kd.ar_akkor,
    t.ar  AS mai_ar
FROM ugyfelek u
INNER JOIN kedvencek kd ON kd.ugyfel_id = u.id
INNER JOIN termekek t   ON t.id = kd.termek_id
ORDER BY u.id, t.id;
ugyfel termek hozzaadas_datuma ar_akkor mai_ar
Kiss Péter Samsung Galaxy A55 2024-02-15 119 990 129 990
Kiss Péter Apple AirPods Pro 2024-03-01 89 990 89 990
Kiss Péter Clean Code – Robert Martin 2024-04-10 8 990 8 990
Nagy Anna Apple AirPods Pro 2024-01-20 89 990 89 990
Nagy Anna Nike Air Max 90 2024-05-02 39 990 44 990
Horváth Bence Yoga szőnyeg 2024-03-12 8 990 8 990
Horváth Bence Kettlebell 16kg 2024-04-01 17 990 19 990
Varga Éva Clean Code – Robert Martin 2024-02-18 8 990 8 990
Tóth Gábor Samsung Galaxy A55 2024-05-15 129 990 129 990

Pontosan 9 sor — annyi, ahány sor a kedvencek táblában van. Ez az N:M szabálya: a kapcsolótábla sorainak száma határozza meg az eredményt, nem a két szélső táblájé.

Figyeld meg azt is, hogy az Apple AirPods Pro kétszer szerepel (Kiss Péter és Nagy Anna listáján), és Kiss Péter háromszor. Ez normális: az N:M kapcsolat mindkét irányban szaporít.

Tipp

A kapcsolótábla saját oszlopai értékes adatok. Az ar_akkor mező megőrizte, mennyibe került a termék a megjelölés pillanatában — ezért tudjuk egyetlen pillantással megmondani, hogy Kiss Péter Samsungja azóta 10 000 forinttal drágult. Ha kihagynád a kapcsolótáblát a SELECT-ből, ez az információ elveszne.

3.2. LEFT JOIN — mindenki, kedvenc nélkül is

SELECT u.id, u.nev AS ugyfel, t.nev AS termek
FROM ugyfelek u
LEFT JOIN kedvencek kd ON kd.ugyfel_id = u.id
LEFT JOIN termekek t   ON t.id = kd.termek_id
WHERE u.id BETWEEN 4 AND 8
ORDER BY u.id, t.id;
id ugyfel termek
4 Varga Éva Clean Code – Robert Martin
5 Tóth Gábor Samsung Galaxy A55
6 Fekete Lilla (NULL)
7 Szabó Roland (NULL)
8 Molnár Zsuzsa (NULL)

WHERE nélkül 28 sor: a 9 kedvenc + 19 ügyfél, akinek egyet sem jelölt meg.

És a másik irányból, ha minden terméket akarunk látni:

SELECT COUNT(*)
FROM termekek t
LEFT JOIN kedvencek kd ON kd.termek_id = t.id
LEFT JOIN ugyfelek u   ON u.id = kd.ugyfel_id;
-- Eredmény: 29

29 sor: a 9 kedvenc + 20 termék, amit senki nem jelölt meg. (26 termékből 6 szerepel valakinek a listáján.)

3.3. Sorszám-összefoglaló az N:M-hez

Lekérdezés Sorok Miért?
kedvencek önmagában 9 9 megjelölés
ugyfelek INNER JOIN kedvencek INNER JOIN termekek 9 a kapcsolótábla sorainak száma
ugyfelek LEFT JOIN kedvencek LEFT JOIN termekek 28 9 + 19 kedvenc nélküli ügyfél
termekek LEFT JOIN kedvencek LEFT JOIN ugyfelek 29 9 + 20 nem kedvelt termék
Megjegyzés

A másik N:M kapcsolatunk a rendelesek ↔︎ termekek a rendeles_tetelek kapcsolótáblán keresztül — pontosan ugyanez a minta, csak ott a kapcsolótáblának mennyiseg és egysegar oszlopa is van. Ott a 2. blokkban látott 25 sor ugyanígy a kapcsolótábla sorainak száma.


4. blokk: Plusz feltétel az összekapcsoláshoz — ON vagy WHERE?

Eddig az ON-ban mindig csak a kapcsolómezők egyenlősége szerepelt. De mi van, ha plusz feltételem is van? A feladat: „Kössük össze az ügyfeleket a rendeléseikkel — de csak a teljesített rendelések érdekelnek.”

Két helyre írhatod. Nézzük meg mindkettőt.

4.1. INNER JOIN esetén nincs különbség

-- A) a feltétel az ON mellett
SELECT COUNT(*) FROM ugyfelek u
INNER JOIN rendelesek r ON r.ugyfel_id = u.id AND r.allapot = 'teljesítve';
-- Eredmény: 8

-- B) a feltétel a WHERE-ben
SELECT COUNT(*) FROM ugyfelek u
INNER JOIN rendelesek r ON r.ugyfel_id = u.id
WHERE r.allapot = 'teljesítve';
-- Eredmény: 8

Ugyanaz a nyolc sor. Logikus: az INNER JOIN amúgy is csak a párokat tartja meg, tehát mindegy, hogy a szűrés a párosításkor vagy utána történik.

Ha csak INNER JOIN-t használnál, ez a kérdés soha nem jönne elő. A különbség a LEFT JOIN-nál robban.

4.2. LEFT JOIN esetén óriási a különbség

A) verzió — a feltétel az ON mellett:

SELECT u.id, u.nev, r.id AS rendeles_id, r.allapot
FROM ugyfelek u
LEFT JOIN rendelesek r
       ON r.ugyfel_id = u.id
      AND r.allapot = 'teljesítve'      -- ⬅ ITT
WHERE u.id <= 8
ORDER BY u.id, r.id;
id nev rendeles_id allapot
1 Kiss Péter 1 teljesítve
1 Kiss Péter 2 teljesítve
2 Nagy Anna 12 teljesítve
3 Horváth Bence 14 teljesítve
4 Varga Éva 5 teljesítve
5 Tóth Gábor (NULL) (NULL)
6 Fekete Lilla (NULL) (NULL)
7 Szabó Roland 8 teljesítve
7 Szabó Roland 15 teljesítve
8 Molnár Zsuzsa (NULL) (NULL)

B) verzió — a feltétel a WHERE-ben:

SELECT u.id, u.nev, r.id AS rendeles_id, r.allapot
FROM ugyfelek u
LEFT JOIN rendelesek r ON r.ugyfel_id = u.id
WHERE r.allapot = 'teljesítve'          -- ⬅ ITT
  AND u.id <= 8
ORDER BY u.id, r.id;
id nev rendeles_id allapot
1 Kiss Péter 1 teljesítve
1 Kiss Péter 2 teljesítve
2 Nagy Anna 12 teljesítve
3 Horváth Bence 14 teljesítve
4 Varga Éva 5 teljesítve
7 Szabó Roland 8 teljesítve
7 Szabó Roland 15 teljesítve

Tóth Gábor, Fekete Lilla és Molnár Zsuzsa eltűnt. Pedig ők léteznek, és van rendelésük — csak nem teljesített (lemondva, illetve folyamatban).

4.3. Miért? — a végrehajtási sorrend adja a választ

Sorrend Lépés Mit csinál?
1 FROM + JOIN … ON Összeállítja az eredménytáblát
2 WHERE Az eredménytábla sorait szűri
3 GROUP BY A megmaradt sorokat csoportokba rendezi
4 HAVING A csoportokat szűri
5 SELECT Kiszámolja az oszlopokat és az aggregátumokat
6 ORDER BY Sorba rendez

Az ON az 1. lépésben fut, a WHERE a 2.-ban. Ebből minden következik:

ON-ba írt feltétel WHERE-be írt feltétel
Mikor fut? Az eredménytábla összeállítása közben Az összeállított eredménytáblán, utána
Mit csinál? Eldönti, hogy mit kapcsoljon hozzá a bal oldali sorhoz Eldönti, hogy melyik sor maradjon meg
Ha nincs találat? A bal oldali sor megmarad, NULL jobb oldallal A NULL-os sor megbukik a feltételen → kiesik
Hatás A LEFT JOIN LEFT JOIN marad A LEFT JOIN-ból csendben INNER JOIN lesz

A B) verzióban a LEFT JOIN először tényleg legyártja Tóth Gábor sorát NULL állapottal. Aztán jön a WHERE r.allapot = 'teljesítve', ami a NULL = 'teljesítve' kifejezést értékeli ki — az eredmény pedig nem TRUE, hanem NULL, ami a WHERE szemében ugyanolyan bukás, mint a FALSE. A sor kiesik.

Figyelmeztetés

A LEFT JOIN jobb oldalára vonatkozó WHERE feltétel visszaalakítja a lekérdezést INNER JOIN-ná. Hibaüzenetet nem kapsz, csak kevesebb sort. Ez a 2.3-as lánc-csapda testvére: mindkettő ugyanúgy „öli meg” a LEFT JOIN-t.

4.4. A döntési szabály — tanuld meg ezt a három sort

Hova írjam? Mikor? Példa
ON mellé A feltétel a jobb oldali (LEFT JOIN-nal hozzávett) táblára vonatkozik, és a bal oldali sorokat meg akarod tartani akkor is, ha nincs illeszkedő pár LEFT JOIN rendelesek r ON r.ugyfel_id = u.id AND r.allapot = 'teljesítve'
WHERE-be A feltétel a bal oldali (megtartandó) táblára vonatkozik, vagy tényleg ki akarod dobni a sorokat WHERE u.varos = 'Budapest'
HAVING-be A feltétel a csoport egészére, azaz egy aggregátumra vonatkozik (6. blokk) HAVING COUNT(r.id) >= 2
Megjegyzés

Ha a WHERE-be írod a jobb oldali feltételt, az nem feltétlenül hiba — csak szinte biztosan nem az történik, amit akartál. Ha tényleg csak azok az ügyfelek kellenek, akiknek van teljesített rendelésük, akkor írj egyszerűen INNER JOIN-t: legalább a kód is azt mondja, amit csinál.


5. blokk: A „hiány” keresése — amihez nincs adat a másik táblában

Ez a tesztelő egyik legfontosabb kérdéstípusa, mert az adatintegritási hibák szinte mindig így néznek ki: „van egy sor, amihez lennie kellene párnak, de nincs.”

  • Melyik ügyfél nem rendelt még soha? (marketinglista)
  • Melyik terméket nem adtuk el egyszer sem? (kifutó termékek)
  • Melyik ügyfélnek nincs kitöltve az adatlapja? (hiányos migráció)
  • Melyik rendeléshez nem tartozik egyetlen tétel sem? (sérült adat!)

5.1. Az alapminta: LEFT JOIN + IS NULL

A trükk két lépésből áll, és pontosan azt használja ki, amit az 1. és 4. blokkban megtanultunk:

  1. LEFT JOIN-nal megtartunk minden bal oldali sort — a pár nélkülieket is, NULL jobb oldallal.
  2. Aztán a WHERE-ben pont ezekre a NULL-okra szűrünk — így csak a pár nélküli sorok maradnak.
SELECT u.id, u.nev, u.varos
FROM ugyfelek u
LEFT JOIN rendelesek r ON r.ugyfel_id = u.id
WHERE r.id IS NULL              -- ⬅ „nem talált párt"
ORDER BY u.id;
id nev varos
11 Antal Mária Szombathely
12 Ábrahám László Kaposvár
13 Erdős Katalin Debrecen
… … …
24 Űrhajós Borbála Debrecen

14 ügyfél a 24-ből még soha nem rendelt. (Emlékszel? A 2.2-es táblázatban pont ez a 14 sor volt a különbség a 15 és a 29 között.)

Fontos

Melyik oszlopra írjam az IS NULL-t? Mindig a jobb oldali tábla olyan oszlopára, ami soha nem lehet NULL, ha a sor létezik — tipikusan az elsődleges kulcsára (r.id) vagy a kapcsolómezőre (r.ugyfel_id).

Ha egy NULL-ozható oszlopra írnád (pl. WHERE r.megjegyzes IS NULL), akkor nem azt kapnád, hogy „nincs rendelése”, hanem azt, hogy „nincs rendelése vagy van, de üres a megjegyzése”. Két teljesen különböző kérdés.

Ugyanez a minta az N:M kapcsolatra — melyik terméket nem adtuk el soha?

SELECT t.id, t.nev, t.ar
FROM termekek t
LEFT JOIN rendeles_tetelek rt ON rt.termek_id = t.id
WHERE rt.id IS NULL
ORDER BY t.id;
id nev ar
4 Lenovo IdeaPad 3 249 990
10 Zara nyári ruha 19 990
13 Siemens porszívó 79 990
15 Moulinex turmix 18 990
20 Bokszzsák 39 990
26 Ajándékutalvány 5 000

5.2. Ugyanez NOT EXISTS-szel

SELECT u.id, u.nev, u.varos
FROM ugyfelek u
WHERE NOT EXISTS (
    SELECT 1 FROM rendelesek r WHERE r.ugyfel_id = u.id
)
ORDER BY u.id;

Szintén 14 sor, ugyanazok. Ez a változat gyakran olvashatóbb, mert szó szerint azt mondja, amit gondolsz: „nem létezik hozzá rendelés”. Nincs benne a LEFT JOIN + IS NULL kétlépéses trükk, és nincs veszélye a sorok felszorzódásának sem.

Mindkettőt ismerd fel — éles kódban mindkettővel találkozni fogsz.

5.3. ⚠️ A NOT IN csapdája

Van egy harmadik, kézenfekvőnek tűnő megoldás, ami NULL jelenlétében csendben rossz eredményt ad. Keressük meg azokat a kategóriákat, amelyekhez nem tartozik termék:

-- ❌ HIBÁS
SELECT k.nev
FROM kategoriak k
WHERE k.id NOT IN (SELECT t.kategoria_id FROM termekek t);

Eredmény: 0 sor. Pedig az 1.2-es példából tudjuk, hogy a „Játék” kategóriához nincs termék!

Mi történt? Emlékezz az 1.3-as példára: az Ajándékutalvány termék kategoria_id-ja NULL. Az allekérdezés tehát ilyesmit ad vissza: (1, 1, 1, …, 5, NULL). Az SQL pedig a 6 NOT IN (1, 2, 3, 4, 5, NULL) kifejezést így értékeli ki: 6 <> 1 AND 6 <> 2 AND … AND 6 <> NULL. Az utolsó tag NULL — és TRUE AND NULL = NULL, ami nem TRUE. Egyetlen sor sem tud átmenni a feltételen.

-- ✅ JAVÍTVA: kizárjuk a NULL-okat az allekérdezésből
SELECT k.nev
FROM kategoriak k
WHERE k.id NOT IN (
    SELECT t.kategoria_id FROM termekek t
    WHERE t.kategoria_id IS NOT NULL
);

Eredmény: Játék. Most már jó.

Figyelmeztetés

Ez a hiba a valóságban is előfordul, és nagyon alattomos: ma működik a lekérdezés, holnap valaki felvisz egy NULL idegen kulcsú sort, és a riport üres lesz — hibaüzenet nélkül. Ezért ha teheted, NOT IN helyett használj NOT EXISTS-t vagy LEFT JOIN + IS NULL-t: azok NULL-lal is helyesen működnek.

5.4. A haladó eset: „nincs ilyen párja” (a 4. és 5. blokk találkozása)

Kérdés: „Melyek azok a termékek, amelyekre nem érkezett 5 csillagos vélemény?“

Ez nem sima anti-join, mert nem az a kérdés, hogy „nincs véleménye”, hanem hogy „nincs ilyen véleménye”. Sokan itt esnek el.

1. próbálkozás — „szűrjünk arra, hogy nem 5” (HIBÁS):

-- ❌
SELECT DISTINCT t.id, t.nev
FROM termekek t
INNER JOIN velemenyek v ON v.termek_id = t.id
WHERE v.ertekeles <> 5;
id nev
2 Apple AirPods Pro
6 Nike Air Max 90

Két sor. Ez több szempontból is rossz:

  • Az INNER JOIN miatt a vélemény nélküli termékek eltűntek — pedig azokra végképp nem érkezett 5 csillagos.
  • A NULL értékelésű vélemények is kiestek, mert NULL <> 5 nem TRUE.
  • És ami a legrosszabb: ha egy terméknek van 5 csillagos és van 3 csillagos véleménye is, akkor a 3 csillagos sor miatt bekerülne a listába — hibásan.

2. próbálkozás — ON-szűrés + anti-join (HELYES):

-- ✅
SELECT t.id, t.nev
FROM termekek t
LEFT JOIN velemenyek v
       ON v.termek_id = t.id
      AND v.ertekeles = 5          -- ⬅ csak az 5 csillagosakat kapcsoljuk hozzá (4. blokk)
WHERE v.id IS NULL                 -- ⬅ és megtartjuk azokat, amikhez nem jött ilyen (5.1)
ORDER BY t.id;
id nev
2 Apple AirPods Pro
4 Lenovo IdeaPad 3
5 TP-Link WiFi adapter
6 Nike Air Max 90
7 Adidas póló (fehér)
… …

24 termék a 26-ból. Csak a Samsung Galaxy A55 és a Logitech MX Master 3 esik ki — pontosan ez a két termék kapott 5 csillagot.

Olvasd el a lekérdezést hangosan, két lépésben:

  1. Az ON … AND v.ertekeles = 5 azt mondja: „minden termékhez kapcsold hozzá az 5 csillagos véleményeit — ha nincs ilyen, hagyd üresen a helyét.”
  2. A WHERE v.id IS NULL azt mondja: „és most add vissza azokat, ahol a hely üres maradt.”
Fontos

Miért nem mehet a feltétel a WHERE-be? Mert akkor a LEFT JOIN-ból INNER JOIN lenne (4. blokk), és a WHERE v.ertekeles = 5 AND v.id IS NULL egyszerre követelné meg, hogy legyen 5 csillagos vélemény, és hogy ne legyen vélemény. Mindig 0 sort adna.

5.5. Anti-join minták összefoglalva

Minta Mikor használd?
LEFT JOIN … WHERE jobb.id IS NULL Alapeset; ha a jobb oldali tábla oszlopait is látni akarod
WHERE NOT EXISTS (SELECT 1 …) Olvashatóbb; NULL-biztos; nem szoroz fel sorokat
LEFT JOIN … ON … AND feltétel … WHERE jobb.id IS NULL „Nincs ilyen párja” — a plusz feltétel az ON mellé megy
GROUP BY … HAVING COUNT(jobb.id) = 0 Ha számokat is akarsz mellé (lásd 6.2)
WHERE … NOT IN (…) ⚠️ Csak akkor, ha biztosan nincs NULL az allekérdezésben

Összefoglalás

A JOIN-ok egy táblázatban

Kapcsolat Példa INNER LEFT Mi a plusz sor?
1:1 ugyfelek + ugyfel_adatok 5 24 19 ügyfél adatlap nélkül
1:N kategoriak + termekek 25 26 a termék nélküli „Játék”
1:N (fordítva) termekek + kategoriak 25 26 a kategória nélküli Ajándékutalvány
Lánc (3 tábla) ugyfelek→rendelesek→tetelek 25 39 14 rendelés nélküli ügyfél
N:M ugyfelek↔︎kedvencek↔︎termekek 9 28 19 ügyfél kedvenc nélkül
N:M (fordítva) termekek↔︎kedvencek↔︎ugyfelek 9 29 20 nem kedvelt termék

A két nagy csapda, ami megöli a LEFT JOIN-t — mindkettő hibaüzenet nélkül:

  1. INNER a LEFT után a láncban (2.3) → a második JOIN kidobja a NULL-os sorokat.
  2. A jobb oldali tábla szűrése a WHERE-ben (4.2) → ugyanaz történik, csak a WHERE teszi.

A négy ökölszabály

  1. Mielőtt JOIN-t írsz, döntsd el, melyik táblából akarsz minden sort látni — az megy a FROM után.
  2. LEFT JOIN-láncban minden további JOIN is LEFT legyen — egyetlen INNER mindent elront.
  3. A jobb oldali tábla szűrése az ON mellé megy, nem a WHERE-be.
  4. Hiány kereséséhez LEFT JOIN … WHERE jobb.id IS NULL vagy NOT EXISTS — a NOT IN NULL-lal becsap.

Tesztelői ellenőrző lista JOIN-okhoz

  • Hány sort vártál? (24 ügyfél? 6 kategória? 26 termék?) Ennyi jött vissza?
  • Melyik táblából akartál minden sort látni? Az van a FROM után?
  • LEFT JOIN-láncban mindegyik JOIN LEFT?
  • A jobb oldali táblára vonatkozó szűrés az ON mellett van?
  • Ha NOT IN-t használtál: lehet-e NULL az allekérdezés eredményében?
Tipp

Következő óra: ugyanezeket a lekérdezéseket vesszük elő, és rátesszük a GROUP BY-t. Érdemes a mai sorszámokat (5, 24, 25, 26, 9, 28, 39) fejben tartani — a következő órán mindegyik vissza fog köszönni.


Házi feladatok az önálló gyakorláshoz

1. feladat — JOIN irány

Írj egy lekérdezést, amely minden terméket kilistáz a kategóriája nevével együtt! Azok a termékek is szerepeljenek, amelyeknek nincs kategóriájuk. Hány sor lesz az eredmény, és melyik termékhez lesz NULL a kategória?

💡 Megoldás (kattints ide)
SELECT t.id, t.nev AS termek, k.nev AS kategoria
FROM termekek t
LEFT JOIN kategoriak k ON k.id = t.kategoria_id
ORDER BY t.id;

26 sor — pontosan annyi, ahány termék van. A 26-os Ajándékutalvány sorában lesz NULL a kategória, mert a kategoria_id-ja NULL.

A kulcs a JOIN iránya: mivel minden terméket akarunk látni, a termekek kell a FROM után. Ha megfordítanád (FROM kategoriak LEFT JOIN termekek), szintén 26 sort kapnál — de akkor az Ajándékutalvány hiányozna, és helyette a „Játék” kategória jelenne meg üres termékkel. Ugyanaz a szám, teljesen más tartalom.



2. feladat — ON vagy WHERE? (aggregálás nélkül)

Listázd ki minden ügyfél nevét és a 2024-ben leadott rendeléseinek azonosítóját és dátumát! Azok az ügyfelek is szerepeljenek egy sorral, akiknek 2024-ben nem volt rendelésük — beleértve azt is, akinek egyáltalán nincs rendelése.

Utána írd meg úgy is, hogy a dátumszűrés a WHERE-be kerül. Hány sort ad a két változat?

💡 Megoldás (kattints ide)
-- ✅ A feltétel az ON mellett
SELECT u.id, u.nev, r.id AS rendeles_id, r.rendeles_datum
FROM ugyfelek u
LEFT JOIN rendelesek r
       ON r.ugyfel_id = u.id
      AND r.rendeles_datum >= DATE '2024-01-01'
ORDER BY u.id, r.id;

28 sor. Részlet:

id nev rendeles_id rendeles_datum
4 Varga Éva (NULL) (NULL)
5 Tóth Gábor 6 2024-05-03
6 Fekete Lilla 7 2024-05-10
7 Szabó Roland 8 2024-01-28
7 Szabó Roland 15 2024-02-14
8 Molnár Zsuzsa 9 2024-03-11
9 Balogh Tamás (NULL) (NULL)
-- ❌ A feltétel a WHERE-ben
SELECT u.id, u.nev, r.id AS rendeles_id, r.rendeles_datum
FROM ugyfelek u
LEFT JOIN rendelesek r ON r.ugyfel_id = u.id
WHERE r.rendeles_datum >= DATE '2024-01-01'
ORDER BY u.id, r.id;

Csak 12 sor — pontosan annyi, ahány 2024-es rendelés van. A 24 ügyfélből 16 teljesen eltűnt.

A tanulság Varga Éva és Balogh Tamás sorában van: nekik van rendelésük, de 2023-as. Az ON-os változatban látszanak (NULL rendeléssel), a WHERE-esben nem. Pedig egy „ki nem rendelt idén?” riportnak pont ők az érdekes sorai.


3. feladat — Anti-join kétféleképpen

Melyik ügyfeleknek nincs kitöltve az adatlapja (nincs soruk az ugyfel_adatok táblában)? Írd meg a lekérdezést kétféleképpen: LEFT JOIN-nal és NOT EXISTS-szel!

💡 Megoldás
-- A) LEFT JOIN + IS NULL
SELECT u.id, u.nev, u.email
FROM ugyfelek u
LEFT JOIN ugyfel_adatok ua ON ua.ugyfel_id = u.id
WHERE ua.ugyfel_id IS NULL
ORDER BY u.id;

-- B) NOT EXISTS
SELECT u.id, u.nev, u.email
FROM ugyfelek u
WHERE NOT EXISTS (
    SELECT 1 FROM ugyfel_adatok ua WHERE ua.ugyfel_id = u.id
)
ORDER BY u.id;

Mindkettő 19 sort ad vissza — pontosan az a 19, amit a 6.1-es lekérdezés nincs_adatlap oszlopa is mutatott.

Ez tipikus migrációs teszteset: ha a specifikáció szerint minden ügyfélnek kellene adatlappal rendelkeznie, akkor ez a 19 sor mind hiba. Ha viszont az adatlap opcionális, akkor ez normális működés — a lekérdezés ugyanaz, csak az értelmezése más.



4. feladat — Anti-join N:M kapcsolaton

Melyik termékeket nem jelölte meg senki kedvencként? Hány ilyen termék van?

💡 Megoldás
SELECT t.id, t.nev, t.ar
FROM termekek t
LEFT JOIN kedvencek kd ON kd.termek_id = t.id
WHERE kd.termek_id IS NULL
ORDER BY t.id;

20 sor. Az első néhány:

id nev
3 Logitech MX Master 3
4 Lenovo IdeaPad 3
5 TP-Link WiFi adapter
7 Adidas póló (fehér)
8 Levi’s 501 farmer
… …

Ellenőrzés a 3.3-as táblázattal: ott azt láttuk, hogy a termekek LEFT JOIN kedvencek LEFT JOIN ugyfelek 29 sort ad: 9 kedvenc + 20 nem kedvelt termék. Pont ezt a 20-at kaptuk meg.

Két részlet:

  1. N:M kapcsolatnál elég a kapcsolótábláig elmenni — a termekek LEFT JOIN kedvencek már megválaszolja a kérdést, az ugyfelek táblára nincs is szükség.
  2. Az IS NULL a kapcsolótábla oszlopára megy (kd.termek_id). A kedvencek táblának összetett elsődleges kulcsa van (ugyfel_id, termek_id), tehát nincs egyetlen id oszlopa — bármelyik kulcsoszlop jó, mert egyik sem lehet NULL, ha a sor létezik.