1. ismétlő óra: JOIN-ok — táblák összekapcsolása
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:
- Ha van egy plusz feltételem az összekapcsoláshoz, azt a
JOIN ... ONmellé írjam, vagy aWHERE-be? - 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:
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 |
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.
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.
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.
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) |
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 |
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.
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.
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 |
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.
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 |
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:
LEFT JOIN-nal megtartunk minden bal oldali sort — a pár nélkülieket is,NULLjobb oldallal.- Aztán a
WHERE-ben pont ezekre aNULL-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.)
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ó.
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 JOINmiatt 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, mertNULL <> 5nemTRUE. - É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:
- Az
ON … AND v.ertekeles = 5azt mondja: „minden termékhez kapcsold hozzá az 5 csillagos véleményeit — ha nincs ilyen, hagyd üresen a helyét.” - A
WHERE v.id IS NULLazt mondja: „és most add vissza azokat, ahol a hely üres maradt.”
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:
INNERaLEFTután a láncban (2.3) → a második JOIN kidobja aNULL-os sorokat.- A jobb oldali tábla szűrése a
WHERE-ben (4.2) → ugyanaz történik, csak aWHEREteszi.
A négy ökölszabály
- Mielőtt JOIN-t írsz, döntsd el, melyik táblából akarsz minden sort látni — az megy a
FROMután. LEFT JOIN-láncban minden további JOIN isLEFTlegyen — egyetlenINNERmindent elront.- A jobb oldali tábla szűrése az
ONmellé megy, nem aWHERE-be. - Hiány kereséséhez
LEFT JOIN … WHERE jobb.id IS NULLvagyNOT EXISTS— aNOT INNULL-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
FROMután? LEFT JOIN-láncban mindegyik JOINLEFT?- A jobb oldali táblára vonatkozó szűrés az
ONmellett van? - Ha
NOT IN-t használtál: lehet-eNULLaz allekérdezés eredményében?
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:
- N:M kapcsolatnál elég a kapcsolótábláig elmenni — a
termekek LEFT JOIN kedvencekmár megválaszolja a kérdést, azugyfelektáblára nincs is szükség. - Az
IS NULLa kapcsolótábla oszlopára megy (kd.termek_id). Akedvencektáblának összetett elsődleges kulcsa van (ugyfel_id, termek_id), tehát nincs egyetlenidoszlopa — bármelyik kulcsoszlop jó, mert egyik sem lehetNULL, ha a sor létezik.