A példákban sokszor találkozunk nem normalizált, vagy egyéb szempontból nem szépen megtervezett táblákkal – a példa könnyű érthetőségének oltárán feláldoztam legszentebb elveinket. :)
A legalapvetőbb példákat pirossal, az eggyel haladóbbakat sárgával jelöltem. A példák és példacsoportok kattintással csukhatók-nyithatók.
SELECT COUNT(*) FROM table1
SELECT COUNT(field1) FROM table1
6 olyan sor volt a táblában, ahol a field1
mező NULL, a COUNT(mezőnév) függvény ezeket nem veszi figyelembe. COUNT(*) mindig az összes
rekordok számát szolgáltatja, COUNT(mezőnév) pedig azokét, ahol a mező nem NULL.
Tanulság: Ha azon sorok számára vagyunk kíváncsiak, ahol egy mező ki van töltve, akkor az itt bemutatott módon is megtehetjük, felesleges a NULL értékek explicit kizárása:
SELECT COUNT(field1) FROM table1 WHERE field1 IS NOT
NULL
Azt szeretnénk megtudni, hogy az alábbi táblában:
csaladinev | utonev | status |
---|---|---|
Buzgó | Mócsing | aktív |
Bágyadt | Béla | passzív |
Ismeretlen | István | NULL |
hány olyan sor van, ahol a status
mező ki van töltve (nem NULL értékű). Ennek a lekérdezésnek:
SELECT COUNT(*) FROM t1 WHERE status <> NULL
mi lesz az eredménye?
status
mező nem NULL. A NULL-t ugyanis bármivel hasonlítjuk össze
bárhogyan, az összehasonlítás eredménye mindig hamis
lesz.(a szabályt ld.
itt.)
Helyesen így néz ki a parancs:
SELECT COUNT(*) FROM t1 WHERE status IS NOT NULL
Most arra kérdezünk rá, hogy a fenti táblában hány olyan sor van, ahol a
status
mező NULL-t tartalmaz. Ennek a lekérdezésnek:
SELECT COUNT(status) FROM t1 WHERE status IS NULL
mi lesz az eredménye?
0 lesz az eredmény, bár van egy olyan sor, ahol a status
mezőben NULL található. A bajt az okozza, hogy a COUNT(mezőnév) függvény
definíciószerűen csak azon sorok számát adja vissza, ahol a mező NULL-tól különbözik – tehát már maga a lekérdezett alap-halmaz:
SELECT COUNT(status) FROM t1
nem tartalmaz olyan sorokat, amelyekben a status mező NULL, ezt aztán hiába próbáljuk a
WHERE status IS NULL
feltétellel tovább szűrni, eddigre a NULL-ok már rég meglógtak. Helyesen így kell írni:
SELECT COUNT(*) FROM t1 WHERE status IS NULL
mert a COUNT(*) függvény viszont mindig a feltételek által kiválasztott összes sor számát adja vissza.
Tudjuk, hogy a SELECT DISTINCT utasítás arra való, hogy megtudjuk, hány különféle érték található egy mezőben vagy mezőcsoportban. De vajon figyelembe veszi-e ez a parancs a NULL tartalmú mezőket? A fenti táblában mutatott adatok esetében ennek a parancsnak:
SELECT DISTINCT status FROM t1
mi lesz a kimenete?
A válasz az, hogy igen, a SELECT DISTINCT a NULL értékeket is figyeli, a kimenet ez lesz:
status |
---|
aktív |
passzív |
NULL |
A fenti táblából már kikerestük, hogy hány különféle status
érték fordul
elő benne. Kíváncsiságunk nem lankad, most már azt is tudni szeretnénk, hogy az egyes értékek hányszor fordulnak elő. Tudjuk, hogy erre a célra szolgál
a kimenet csoportosítása, a GROUP BY záradék, bevetjük tehát ezt a fegyvert. Mi lesz a következő lekérdezés eredménye? Megjelenik-e a NULL-t
tartalmazó sor az eredményben?
SELECT status, COUNT(status) AS N FROM t1 GROUP BY status
Igen, a GROUP BY figyelembe veszi a NULL-t is – ám a kimenet ez lesz:
status | N |
---|---|
NULL | 0 |
aktív | 1 |
passzív | 1 |
Hát ez gyönyörű. Látszik, hogy van olyan rekord, amelyben a status
mező tartalma NULL, különben nem lenne ott az első sor – ám
azt is mondja nekünk az SQL, hogy van ugyan ilyen rekord, de nullaszor (!!) fordul elő.
Miután kidühöngtük magunkat, próbáljunk meg rájönni, miért kaptuk ezt a gúnynak is beillő választ az SQL-től.
A fenti példában már egyszer beleütköztünk a COUNT(mezőnév) függvénynek abba a
tulajdonságába, hogy csak a nem NULL mezőket tartalmazó rekordokat számolja. Itt is ez történik: először csoportosítja az adatbáziskezelő a rekordokat
a status
mező tartalma szerint, kap három csoportot. Ezután elvégzi az egyes csoportokon belül a számolást, felhívja a COUNT(status)
függvényt; ez viszont a NULL-os csoportban egyetlen rekordot sem talál, mert ő arra van betanítva, hogy csak a nem-NULL értékeket számolja.
Helyesen persze így kell írni:
SELECT status, COUNT(*) AS N FROM t1
GROUP BY status
A kimenet így már sokkal szimpatikusabb lesz:
status | N |
---|---|
NULL | 1 |
aktív | 1 |
passzív | 1 |
Arra vagyunk kíváncsiak, hogy az alábbi táblában:
csaladinev | utonev | elso_mhely | mostani_mhely |
---|---|---|---|
Buzgó | Mócsing | SQL kft | Adat BT |
Bágyadt | Béla | SQL kft | SQL kft |
Ismeretlen | István | SQL kft | NULL |
hány olyan személy szerepel, akinek az első munkahelye nem egyezik meg a mostanival.
Ennek a lekérdezésnek:
SELECT csaladinev,utonev FROM t1 WHERE elso_mhely <> mostani_mhely
mi lesz az eredménye?
Buzgó Mócsing lesz az eredmény, tehát egyetlen sornál ítéli úgy meg az SQL, hogy az első munkahely nem egyezik meg a másodikkal — pedig azt vártuk volna, hogy az utolsó sor is ilyen. Hiszen az első munkahely az SQL kft, a második pedig NULL, vagyis nem egyeznek. Dehát akkor mit is mond az SQL? Talán csak nem azt, hogy megegyeznek?
Nem, ő ezt mondja: A harmadik sornál azt kérdezték tőlem, egyenlő-e elso_mhely
és mostani_mhely
? Erre nemmel kell válaszolnom,
mert mostani_mhely
ismeretlen. Persze akkor is nemmel válaszolnék, ha azt kérdezték volna, hogy egyenlőek-e (a szabályt ld.
itt).
Vajon helyesen hogy néz ki a parancs?
Ennek megválaszolásához először is el kell döntenünk, hogy pontosan mi is a kérdés. Ha a főnökünk bízott meg ezzel a feladattal, vissza kell kérdeznünk: Mi legyen a nem ismert adatokkal? Ugyanis ha a precízebben megfogalmazott feladat így hangzik: 'Hány személy van, akinek ismerjük mindkét munkahelyét és ezek nem egyformák?' - akkor jó a fenti megoldás. Ha ellenben ez a követelmény: 'Hány személy van akinél az első és a jelenlegi munkahely bejegyzése különbözik egymástól?' - akkor így kell eljárnunk:
SELECT csaladinev,utonev FROM t1
WHERE elso_mhely <> mostani_mhely OR (elso_mhely IS NULL AND mostani_mhely IS NOT NULL) OR (elso_mhely IS NOT NULL AND mostani_mhely IS NULL)
Vagyis: azok a rekordok kellenek, ahol
Persze ennek gyakorlatilag nem sok értelme van, az első kérdésfeltevés sokkal reálisabb, csak éppen semmit nem tudunk meg azokról, akiknél valamelyik munkahely kitöltetlen. Értékelhető képet akkor kapunk, ha két lekérdezést futtatunk: Az egyik a kérdésben szereplő, a másik pedig azt adja meg, hogy kiknél kitöltetlen legalább az egyik munkahely-mező:
SELECT csaladinev,utonev FROM t1 WHERE elso_mhely IS NULL OR mostani_mhely IS NULL
Láthattuk tehát, hogy az SQL logikusan jár el, amikor az eredményhalmazból kizárja mindazon sorokat, ahol NULL szerepel egy összehasonlításban. Pedig első hallásra nagyon ésszerűtlennek tűnt ez a szabály...
Egy tábla a tavalyi és az idei havi jutalmakat tartalmazza:
csaladinev | utonev | jutalom_tavaly | jutalom_iden |
---|---|---|---|
Buzgó | Mócsing | 220000 | 250000 |
Bágyadt | Béla | 190000 | 170000 |
Ismeretlen | István | 320000 | NULL |
Az idei jutalom még nem mindenkinél ismert, ahol hiányzik ez az érték, ott NULL szerepel a táblában. Kíváncsiak vagyunk az össz-jutalomra, ezért lefuttatjuk ezt a lekérdezést:
SELECT csaladinev,utonev,jutalom_tavaly + jutalom_iden FROM t1
Mi lesz az eredmény?
Azt várnánk, hogy Ismeretlen Istvánnál 320000 lesz az összeg, hiszen csak az előző évi értéket ismerjük, de nem, az eredmény helyesen ez lesz:
csaladinev | utonev | jutalom_tavaly+jutalom_iden |
---|---|---|
Buzgó | Mócsing | 470000 |
Bágyadt | Béla | 360000 |
Ismeretlen | István | NULL |
Azért helyes ez az eredmény, mert
320000 + ismeretlen = ismeretlen
Ha a NULL helyett nullát szeretnénk az összegbe bevinni, akkor be kell vetnünk valamelyik erre szolgáló (és sajnos nem szabványos, adatbáziskezelőnként más és más) függvényt. Ezekről itt olvashatunk.
A fenti tábla adataiból ki akarjuk számítani a tavalyi és az idei jutalom hányadosát:
SELECT csaladinev,utonev,jutalom_tavaly / jutalom_iden FROM t1
de attól félünk, hogy ez így nem lesz jó, hibajelzést fogunk kapni az SQL-től, hiszen az utolsó sornál NULL-lal kellene osztania, ami nyilván lehetetlen, akár csak a nullával való osztás. Vajon mi lesz a lekérdezés eredménye? Csakugyan hibajelzés?
Természetesen nem kapunk hibajelzést, az eredmény ez lesz:
csaladinev | utonev | jutalom_tavaly/jutalom_iden |
---|---|---|
Buzgó | Mócsing | 0.8800 |
Bágyadt | Béla | 1.1176 |
Ismeretlen | István | NULL |
Nem történik NULL-lal osztás, hiszen amikor az SQL azt látja, hogy egy aritmetikai művelet valamelyik operandusa NULL, akkor nem számol semmit, hanem csak közli flegmán, hogy az eredmény definiálatlan, azaz NULL.
És milyen igaza van!
Szeretnénk az alábbi táblából:
csaladinev | utonev | elso_mhely | mostani_mhely |
---|---|---|---|
Buzgó | Mócsing | SQL kft | Adat BT |
Bágyadt | Béla | SQL kft | SQL kft |
Ismeretlen | István | SQL kft | NULL |
kigyűjteni minden sorból a neveket és a munkahelyeket. A két munkahelyet egyetlen sztringbe összefűzve akarjuk kiadni. Erre sajnos nincs egységes megoldás, adatbáziskezelőtől függően kell kialakítanunk a SELECT utasítást, pl. így:
MySQL:
SELECT csaladinev,utonev CONCAT(elso_mhely,' - ', mostani_mhely) AS munkahelyek FROM t1
MS SQL, MS Access:
SELECT csaladinev,utonev elso_mhely + ' - ' + mostani_mhely) AS munkahelyek FROM t1
Oracle:
SELECT csaladinev,utonev CONCAT(elso_mhely,CONCAT(' - ', mostani_mhely)) AS munkahelyek FROM t1
Mi lesz a lekérdezés eredménye?
Ezt fogjuk kapni:
csaladinev | utonev | munkahelyek |
---|---|---|
Buzgó | Mócsing | SQL kft - Adat BT |
Bágyadt | Béla | SQL kft - SQL kft |
Ismeretlen | István | NULL |
Azt vártuk volna, hogy az utolsó sorban ez látható majd:
hiszen itt az első munkahely ki van töltve, a második nem, ahelyett tehát üres sztringet képzeltünk volna el a kimeneten. Ám mivel a sztring-összefűzés egyik operandusa NULL, az eredmény is NULL lesz – akárcsak az aritmetikai műveletek esetében.
Számok átlagát úgy számítjuk ki, hogy az értékeket összeadjuk és elosztjuk az értékek számával. Valaki János azt mondja, hogy egy adott oszlopban lévő
értékek összegét a SUM, a sorok számát a COUNT függvény adja meg, a következő lekérdezés tehát a fizetes
mező átlagát szolgáltatja:
SELECT SUM(fizetes) / COUNT(*) FROM dolgozok
A lekérdezés eredménye 150000 lett. Tóth Gyula bácsi (bádogos és kvízvezetékszerelő) kioktatja Jánost: Felesleges feltalálnod újra a kereket! Erre a célra való a szabványos AVG függvény, ugyanezt az eredményt egyszerűbben is megkaphatod:
SELECT AVG(fizetes) FROM dolgozok
János megköszöni a tanácsot, ám amikor lefuttatják ezt a lekérdezést, legnagyobb meglepetésükre az eredmény 200000 lesz. Vajon miért? És melyikük lekérdezése a helyes? Vagy egyik sem jó?
A SUM függvény összeadja a kitöltött, tehát nem NULL mezők összegét, a COUNT(*) pedig megadja az összes sor számát. Ha van NULL-t tartalmazó sor, akkor az első lekérdezés nyilván hibás eredményt ad, hiszen nem az összegzett értékek számával osztjuk el az összeget, hanem egy nagyobb számmal. Ha nagyon ragaszkodunk a COUNT és a SUM függvényhez, akkor ezt kellene írnunk:
SELECT SUM(fizetes) / COUNT(fizetes) FROM dolgozok
Itt épp kapóra jön a COUNT(mezőnév) azon tulajdonsága, hogy csak a kitöltött mezőket számolja össze.
Tehát Gyula bácsinak volt igaza, hiszen János eredeti megoldása hibás és még a fenti, módosított változat is bonyolultabb az AVG-t alkalmazó lekérdezésnél – márpedig az egyszerűség, áttekinthetőség érték.
De azért hozzá kell tenni, hogy egyik lekérdezés sem szolgáltat érdemi eredményt önmagában. Ha az volt a kérdés, hogy mennyi az átlagfizetés a cégnél, akkor az egyetlen értelmes válasz valahogy így hangzik:
A dolgozók száma 40, ebből 30-nak van az adatbázisban kitöltve a fizetése,
az ő átlaguk 200000 forint.
Ez persze már nem adatbáziskezelési, hanem adatfeldolgozási probléma, de nagyon fontos tanulság: Mindig pontosan fogalmazzuk meg, hogy a statisztikai adataink mire vonatkoznak!
Láttuk, hogy a NULL mindenféle műveleteknél meglepő eredményeket tud produkálni. Nézzük most meg, hogy vajon az IN esetében is így van-e. Készítünk egy nagyon egyszerű táblát és feltöltjük adatokkal:
CREATE TABLE t1 (f1 INTEGER); INSERT INTO t1 VALUES (1),(2),(3),(4);
Keressük ki azokat az elemeket, amelyek nem egyenlők 1-gyel vagy 2-vel:
SELECT * FROM t1 WHERE f1 NOT IN(1,2)
A kimeneten persze 3 és 4 fog megjelenni. De mi történik, ha így:
SELECT * FROM t1 WHERE f1 NOT IN(1,2,NULL)
módosítjuk a lekérdezést? Meg is érkezett a meglepetés a NULL-tól: üres lesz a kimenet. Vajon miért?
A NOT IN művelet tulajdonképpen leegyszerüsített írásmódja annak, hogy a mező tartalma nem egyenlő egyik értékkel sem:
SELECT * FROM t1 WHERE f1 <> 1 AND f1 <> 2 AND f1 <> NULL
Az f1 <> NULL
eredménye NULL, ha ezt AND művelettel összekapcsoljuk a többi feltétellel, akkor vagy NULL-t kapunk (az
összes többi feltétel teljesült), vagy FALSE-ot (valamelyik nem teljesült). TRUE soha sem lesz. Erről
részletesebben itt olvashatunk.
Az Olvasó számára mesterkéltnek tűnhet ez az egész feladat: Miért tennék én az IN művelet operandusai közé egy NULL-t? Valójában ez könnyen előfordulhat, amikor egy program szedi össze az operandusokat és nem vigyáz arra, hogy NULL ne lehessen köztük, vagy egy allekérdezés eredménye szolgáltatja az értékek listáját.
Egy nyelviskolának van egy nyilvántartása a hallgatóiról:
CREATE TABLE hallgatok( hallgato_id INT NOT NULL, lakhely VARCHAR(25) );
aminek a tartalma pillanatnyilag:
hallgato_id | lakhely |
---|---|
101 | Budapest |
102 | Debrecen |
103 | Miskolc |
Van egy nyilvántartás a tanárokról is:
CREATE TABLE tanarok( tanar_id INT NOT NULL, lakhely VARCHAR(25) );
ilyen tartalommal:
tanar_id | lakhely |
---|---|
1 | Budapest |
2 | Debrecen |
3 | Sopron |
4 | NULL |
5 | Budapest |
Ki akarjuk keresni azokat a hallgatókat, akiknek a városában nem lakik egyetlen tanár sem:
SELECT * FROM hallgatok WHERE lakhely NOT IN ( SELECT DISTINCT lakhely FROM tanarok )
Azt várjuk, hogy a lekérdezés a 103-as hallgató adatait fogja kiadni, mert Miskolcon nem lakik egyetlen tanár sem. Ehelyett üres lesz a kimenet. Vajon miért?
Nézzük meg először, hogy az allekérdezés:
SELECT DISTINCT lakhely FROM tanarok
jó-e, a várt eredményt szolgáltatja-e. Azt tapasztaljuk, hogy a kimenet hibátlan:
lakhely |
---|
Budapest |
Debrecen |
Sopron |
NULL |
További ellenőrzésként összerakjuk kézzel a teljes lekérdezést:
SELECT * FROM hallgatok WHERE lakhely NOT IN ( 'Budapest','Debrecen','Sopron',NULL )
Ez is úgy működik, ahogy az eredeti SELECT, nem szolgáltat egyetlen sort sem – eddig rendben, de akkor mégis mi okozhatja a nem várt eredményt?
A tapasztaltabb Olvasók már sejthetik, hol a baj gyökere: ha meglepetés van, akkor szinte biztosak lehetünk benne, hogy egy NULL (Nem Unalmas Lekérdezést Létrehozó) ólálkodik a háttérben. És valóban, az allekérdezés által szolgáltatott listában ott egy NULL, ezáltal létrejött a fentebbi példában bemutatott eset: a NOT IN paraméterlistájába NULL került.
Meg kell jegyezni, hogy az üres kimenet tulajdonképpen jól tükrözi a helyes választ a kérdésre. Nem tudjuk megmondani, hogy van-e olyan hallgató, akinek a városában nincs tanár, mert hiányosak az adataink, márpedig hiányos adatokból csak hiányos vagy hibás válaszokat lehet előállítani. Hiába változtatjuk meg a lekérdezést így:
SELECT * FROM hallgatok WHERE lakhely NOT IN ( SELECT DISTINCT lakhely FROM tanarok WHERE lakhely IS NOT NULL )
Megkapjuk ugyan a 103-as számú hallgatót – csak nem biztos, hogy ez az igazság. Hiszen lehet, hogy a 4-es azonosítójú tanár épp Miskolcon lakik!
A fenti példában az volt a feladat, hogy keressük meg, mely hallgatókhoz nem található egyetlen tanár sem, aki ugyanabban a városban lakik. Az adattervezési tökéletlenségek és az adatbeviteli programok hiányosságai miatt sajnos előfordul olyan rekord a tanárokat leíró táblában, ahol a lakhely nincs kitöltve, ezért teljesen korrekt válasz nem adható. Tekintsünk most el ettől és fogalmazzuk meg JOIN segítségével a lekérdezést!
Ez a megoldás:
SELECT hallgato_id,hallgatok.lakhely FROM hallgatok LEFT JOIN tanarok ON hallgatok.lakhely = tanarok.lakhely WHERE tanar_id IS NULL
Nem lehet elégszer hangsúlyozni: ez nem biztos, hogy érvényes megoldás, mert talán a 4-es azonosítójú tanár épp Miskolcon lakik. Hiányos adatokból csak hiányos választ lehet előállítani. Hiába no, van, amiből lehet várat építeni, van, amiből nem...
Valaki János azt állítja, hogy ha egy mező tartalmazhat NULL-t, de UNIQUE kikötés van rajta:
CREATE TABLE t1(f1 INT UNIQUE NULL)
akkor NULL tartalmú mezőböl legfeljebb egy lehet, különben sérülne a UNIQUE megkötés, tehát ez az INSERT nem fog sikerülni:
INSERT INTO t1 VALUES(1),(NULL),(2),(NULL)
Ne várjuk meg, míg Tóth Gyula bácsi ismét rápirít Jánosra, magyarázzuk mi meg neki, hogy miért nincs igaza!
A UNIQUE megkötés azt jelenti, hogy két kitöltött mező értéke nem lehet egyforma. Ha NULL-t tartalmaz a mező, akkor nincs kitöltve.
...azzal is történik valami. De vajon mi?
Valaki János készít egy táblát és megpróbál beilleszteni egy sort:
CREATE TABLE t1( csaladinev VARCHAR(20), utonev VARCHAR(20), elso_mhely VARCHAR(20) NOT NULL, mostani_mhely VARCHAR(20) NULL ); INSERT INTO t1 (csaladinev,utonev) VALUES('Ismeretlen','István');
Vajon sikerül neki? Hiszen az elso_mhely
mezőbe nem kerülhet NULL, a bemeneten nem adtuk meg, mi legyen ebben a mezőben – akkor
tehát hibajelzést kellene kapnunk! Vagy mégsem? És ha nem, mi lesz ebben a mezőben?
Sikerülni fog az adatbevitel, a tábla így fog kinézni:
csaladinev | utonev | elso_mhely | mostani_mhely |
---|---|---|---|
Ismeretlen | István | NULL |
Hát kérem, ide becsempésztek két értéket! Ki tehette vajon?
Mivel más nem járt erre, csak az adatbáziskezelő lehetett a tettes. Na de honnan találta ki, hogy mit írjon oda? És hogyan tudnánk megmondani neki, hogy mást írjon?
Ha egy bemeneti mező nincs specifikálva, akkor az adatbáziskezelő az alapértelmezett (default) értéket teszi oda. Amennyiben a meződefinícióban külön nem rendelkeztünk róla, akkor a default érték default-ja kerül be, ami
Megadhatunk a mező definíciójában persze más default értéket:
CREATE TABLE t1( csaladinev VARCHAR(20), utonev VARCHAR(20), elso_mhely VARCHAR(20) NOT NULL DEFAULT '[semmi]', mostani_mhely VARCHAR(20) NULL );
Ha ilyen lett volna a tábladefiníció, akkor a fenti INSERT hatására ez került volna be az adatbázisba:
csaladinev | utonev | elso_mhely | mostani_mhely |
---|---|---|---|
Ismeretlen | István | [semmi] | NULL |
Az első fontos tanulság az, hogy akkor is bekerülhet valami az adatbázisba, ha mi külön nem rendelkezünk róla és ez a 'valami' a default érték. A NOT NULL megkötés önmagában csak az ilyen adatbevitel ellen véd meg:
INSERT INTO t1 (csaladinev,utonev,elso_mhely) VALUES('Ismeretlen','István',NULL);
tehát amikor egy program explicit módon NULL-t kísérel meg bevinni. Ha a program nem rendelkezik az illető mezőről, akkor NULL ugyan nem jut be, később esetleg hibákat okozó default érték viszont igen.
Megállapíthatjuk, hogy az adatbeviteli programoknak nagyon nagy szerepe van az adat-integritás megőrzésében, az adatbázis-megkötések egyedül nem képesek erre. A NULL-lal és a default értékekkel vívott hősies küzdelemről részletesebben itt olvashatunk.
A második tanulság az, hogy a default értéknek is van default-ja, ami meglepetéseket okozhat, mivel nem jól látható, a háttérből ugrik elő hirtelen. Célszerű itt is a maximális szájbarágás elvét követni és a meződefinícióban a default értéket explicit módon megadni.
Van egy nagyon egyszerű kis táblánk, csak egy azonosítót és egy nevet tartalmaz. Előírjuk, hogy a névnek is egyedinek kell lennie és megpróbáljuk feltölteni a táblát:
DROP TABLE IF EXISTS t1;
CREATE TABLE t1(
id INT PRIMARY KEY,
nev VARCHAR(25) NOT NULL UNIQUE
);
INSERT INTO t1(id) VALUES(1),(10),(100);
de hibajelzést kapunk. Arról panaszkodik az adatbáziskezelő, hogy a nev
mezőbe többször megpróbáltuk bevinni ugyanazt az értéket.
Vajon miért siránkozik?
A nev
mezőt nem specifikáltuk a bemeneten, ezért oda a DEFAULT értéket próbálta meg beilleszteni az adatbáziskezelő, ami nyilván
mindig ugyanaz, ezért másodszor már nem sikerülhetett a művelet. (PRIMARY KEY esetén nem lehet a mezőben NULL, tehát biztosan nem NULL az
alapértelmezett érték.)
A táblák összekapcsolása szorosan összefügg a referenciális integritás témakörével. Az itt következő példákban található tábladefiníciók (többek közt) azért is 'csúnyák', mert nem tartalmaznak kulcs-definíciókat és megkötéseket. A csúnyaság itt is a jobb áttekinthetőséget szolgálja – azt, hogy az éppen gyakorolni kívánt témára tudjunk maximálisan koncentrálni.
Létrehozunk egy számlákat tartalmazó táblát és beleírunk két sort:
CREATE TABLE szamlak( szamla_id INT NOT NULL, cim VARCHAR(25) ); INSERT INTO szamlak VALUES (1,'tegnapi'), (2,'mai');
Ezután létrehozunk egy számlasorokat tartalmazó táblát és ebbe is beleírunk két sort:
CREATE TABLE szamlasorok( szamla_id INT NOT NULL, sor_id INT NOT NULL, megnevezes VARCHAR(25) ); INSERT INTO szamlasorok VALUES (1,1,'cucc 1'), (1,2,'cucc 2');
A két táblát persze a szamla_id
mező kapcsolja össze és látszik, hogy a 'tegnapi' számlához két sor tartozik, a 'mai' nevűhöz pedig egy sem.
Árat, mértékegységet, stb. szándékosan nem tartalmaznak a számla-sorok, hogy áttekinthetőbb legyen az egész, mivel most csak a JOIN tulajdonságaira
szeretnénk koncentrálni.
Írjunk egy lekérdezést, amelyben az összes számlát megjelenítjük a hozzájuk tartozó sorokkal együtt – de csak azokat, amelyekhez tartozik számlasor.
Ez a megoldás:
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak INNER JOIN szamlasorok ON szamlak.szamla_id = szamlasorok.szamla_id
A kimeneten ezt látjuk:
szamla_id | cim | megnevezes |
---|---|---|
1 | tegnapi | cucc 1 |
1 | tegnapi | cucc 2 |
Valóban azt kaptuk, amit vártunk, csak az első számla tételei jelennek meg, mert a másodikhoz egy sem tartozik.
Valaki János azt mondja, hogy ha a fenti táblákból le akarjuk válogatni az összes számlát a hozzájuk tartozó sorokkal együtt – de csak azokat, amelyekhez tartozik számlasor, akkor nincs is szükségünk INNER JOIN-ra, ez a lekérdezés is megfelel:
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak,szamlasorok WHERE szamlak.szamla_id = szamlasorok.szamla_id
Vajon igaza van-e?
Valóban, a János által javasolt megoldás (amelyet implicit JOIN-nak hívnak egyébként) és az INNER JOIN-nal felépített megoldás:
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak INNER JOIN szamlasorok ON szamlak.szamla_id = szamlasorok.szamla_id
ekvivalensek. Végrehajtási idő szempontjából sincs közöttük különbség; ízlés kérdése, hogy melyiket használjuk. A JOIN-os változat szerintem könnyebben érthető, különösen akkor, amikor a WHERE záradék további bonyolult kifejezéseket tartalmaz. Az INNER JOIN részben külön vannak választva az összekapcsoláshoz szükséges feltételek, ez javítja az olvashatóságot.
A fenti táblákból szeretnénk leválogatni azon számlákat, amelyekhez tartozik 'cucc 2' megnevezésű termékre vonatkozó sor. Az első megoldás az, hogy a feltételt a WHERE záradékba írjuk:
SELECT szamlak.szamla_id,cim
FROM szamlak
INNER JOIN szamlasorok ON
szamlak.szamla_id = szamlasorok.szamla_id
WHERE megnevezes = 'cucc 2'
Vajon írhatjuk-e az ON záradékba is a feltételt? Tehát ez a megoldás:
SELECT szamlak.szamla_id,cim
FROM szamlak
INNER JOIN szamlasorok ON
szamlak.szamla_id = szamlasorok.szamla_id
AND megnevezes = 'cucc 2'
helyes-e?
Igen, a két megoldás ekvivalens, esetleg sebességbeli különbség lehet közöttük, ami persze adatbáziskezelőtől és verziótól függhet, és nagy valószínűséggel ha egyáltalán létezik is ilyen különbség, nem nagy mértékű.
A két megoldás között elsősorban stilisztikai különbség van: szerintem az első jobban olvasható, mert az összekapcsoláshoz szükséges feltétel és a leválogató feltétel élesen szétválik. Bonyolultabb feltételek esetén az olvashatóságbeli különbség sokkal nagyobb lehet, mint ebben az egyszerű példában.
Meg kell jegyezni, hogy MS Access esetében a második megoldásnál a teljes feltételt zárójelbe kell tenni:
(szamlak.szamla_id = szamlasorok.szamla_id AND
megnevezes = 'cucc 2')
különben szintaktikai hibajelzést kapunk. Ha ennek az okát kérdezi az Olvasó, akkor csak azt tudom válaszolni: Azért van ez így, mert így sikerült nekik.
A korábban bemutatott táblákat vesszük elő most is. Írjunk egy lekérdezést, amelyben az összes számlát megjelenítjük a hozzájuk tartozó sorokkal együtt – azokat is, amelyekhez nem tartozik számlasor.
Ez a megoldás (ha a NULL-ok helyett nem akarunk valami mást kiíratni):
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak LEFT JOIN szamlasorok ON szamlak.szamla_id = szamlasorok.szamla_id
A kimeneten ezt látjuk:
szamla_id | cim | megnevezes |
---|---|---|
1 | tegnapi | cucc 1 |
1 | tegnapi | cucc 2 |
2 | mai | NULL |
Valóban azt kaptuk, amit vártunk, az első számlához két tétel tartozik, a másodikhoz egy sem.
A korábban bemutatott, számlákat és számlasorokat tartalmazó táblákat vesszük elő ismét. A számlák nem változtak:
szamla_id | cim |
---|---|
1 | tegnapi |
2 | mai |
de valahogy becsusszant egy olyan számlasor, amelyhez nem tartozik számla:
szamla_id | sor_id | megnevezes |
---|---|---|
1 | 1 | cucc 1 |
1 | 2 | cucc 2 |
1010 | 1 | árva cucc |
Ennek nem lett volna szabad megtörténnie, dehát, mint annyi minden ezen a világon, az adatbeviteli programok sem tökéletesek. Írjunk lekérdezést, amely leválogatja az 'árva' számlasorokat, amelyekhez nem tartozik számla!
Ez a megoldás:
SELECT szamlasorok.szamla_id,sor_id,megnevezes FROM szamlasorok LEFT JOIN szamlak ON szamlak.szamla_id = szamlasorok.szamla_id WHERE szamlak.szamla_id IS NULL
RIGHT JOIN-nal is megfoglamazhatjuk persze (mint mindig):
SELECT szamlasorok.szamla_id,sor_id,megnevezes FROM szamlak RIGHT JOIN szamlasorok ON szamlak.szamla_id = szamlasorok.szamla_id WHERE szamlak.szamla_id IS NULL
Hát persze, hogy ismerem, mondja az Olvasó. Hát persze, hogy ismerem, mondta ezen sorok írója is, aztán beleszaladt az itt leegyszerüsítve bemutatandó szituációkba és lett meglepetés.
Figyelem! Az alább következő lekérdezések értelmetlenek, kizárólag az Olvasó bosszantására szolgálnak, kereskedelmi forgalomban nem kaphatók, gyerekek elől elzárva tartandók. Volt olyan Olvasó, aki szerint felnőttek elől is, továbbá engem kellene száraz, hűvös helyen tárolni fénytől elzárva, de ezt a véleményt nem osztom.
1. Vegyük a már jól ismert két táblát a számlákkal és a számlasorokkal. Mi lesz a kimenete ennek a lekérdezésnek – és MIÉRT?
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak LEFT JOIN szamlasorok ON 1 = 1
Az összekapcsolási feltétel konstans IGAZ-at szolgáltat.
2. És ennek a lekérdezésnek mi lesz az eredménye?
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak LEFT JOIN szamlasorok ON 1 = 0
Az összekapcsolási feltétel itt viszont konstans HAMIS-at szolgáltat, soha nem teljesül.
1. eset. Az eredmény ez lesz:
szamla_id | cim | megnevezes |
---|---|---|
1 | tegnapi | cucc 1 |
1 | tegnapi | cucc 2 |
2 | mai | cucc 1 |
2 | mai | cucc 2 |
Egy egyszerű keresztszorzat jött létre, az egyik tábla összes sora összekombinálódott a másik tábla összes sorával. A LEFT JOIN a definíciója szerint kiadja a baloldali tábla összes sorát és
Most minden sorra teljesült a feltétel, ezért kapjuk a baloldali tábla összes sorát kombinálva a jobboldali tábla összes sorával.
2. eset. Az eredmény ez lesz:
szamla_id | cim | megnevezes |
---|---|---|
1 | tegnapi | NULL |
2 | mai | NULL |
Most sehol sem teljesült a feltétel, ezért kapjuk a baloldali tábla összes sorát egy-egy NULL kíséretében.
Ismét a már megszokott két táblát fogjuk elővenni a számlákkal és a számlasorokkal:
szamla_id | cim |
---|---|
1 | tegnapi |
2 | mai |
szamla_id | sor_id | megnevezes |
---|---|---|
1 | 1 | cucc 1 |
1 | 2 | cucc 2 |
Van egy lekérdezésünk, mellyel az összes számlát megjelenítjük a hozzájuk tartozó sorokkal együtt, azokat is, amelyekhez nem tartozik számlasor:
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak LEFT JOIN szamlasorok ON szamlak.szamla_id = szamlasorok.szamla_id
Kiegészítjük egy WHERE feltétellel ezt a lekérdezést úgy, hogy csak a 'cucc 2' megnevezésű termékre vonatkozó sorok jelenjenek meg:
SELECT szamlak.szamla_id,cim,megnevezes
FROM szamlak
LEFT JOIN szamlasorok ON
szamlak.szamla_id = szamlasorok.szamla_id
WHERE megnevezes = 'cucc 2'
Ezt az eredményt kapjuk:
szamla_id | cim | megnevezes |
---|---|---|
1 | tegnapi | cucc 2 |
Valaki János azt mondja, hogy ha a feltételt az ON záradékban helyezzük el:
SELECT szamlak.szamla_id,cim,megnevezes
FROM szamlak
LEFT JOIN szamlasorok ON
szamlak.szamla_id = szamlasorok.szamla_id
AND megnevezes = 'cucc 2'
akkor ugyanezt az eredményt fogjuk kapni, éppen úgy, mint az INNER JOIN esetében (ld. itt). Ám Tóth Gyula bácsi (bádogos és kvízvezetékszerelő) ráripakodik: János, te még mindig nem érted, hogy miként működik az OUTER JOIN! A második esetben ez lesz az eredmény:
szamla_id | cim | megnevezes |
---|---|---|
1 | tegnapi | cucc 2 |
2 | mai | NULL |
Persze Gyula bácsinak igaza van, valóban ez lesz a lekérdezés kimenete – de vajon miért olyan a kimenet a két esetben, amilyen? És melyik megoldás a helyes?
Mivel ugyebár tudjuk, hogy a LEFT JOIN hogy működik, ezért könnyű a válasz.
Az első esetben a WHERE feltétel megszűri a kimenetet, csak azon rekordok maradnak meg, melyeknél 'cucc 2' áll a számlasor megnevezésében.
A második esetben nincs WHERE feltétel, ezért a lekérdezés kiadja a szamlak
tábla ÖSSZES sorát. Mivel csak a 'tegnapi' számlához
tartozik 'cucc 2', ezért fog a 'mai' számlánál NULL állni a harmadik oszlopban.
És akkor most térjünk rá arra a kérdésre, hogy melyik megoldás a helyes. Ez nyilván attól függ, hogy mire vagyunk kíváncsiak, akarjuk-e látni azon számlákat is, amelyekhez nem tartozik 'cucc 2' megnevezésű számlasor. Ha LEFT JOIN-t írtunk, akkor feltehetőleg igenis látni akarjuk ezeket is, tehát az ON-ba írt feltétel a helyes. Az első megoldás igazából félrevezető, mert azokat a rekordokat adja ki, mint egy INNER JOIN. Ha ezekre vagyunk kíváncsiak, írjunk INNER JOIN-t a félreértések elkerülése érdekében!
Egy cégnél sok autót bérelnek, az autók adatait egy táblában tárolják:
CREATE TABLE autok( auto_id INT NOT NULL, marka VARCHAR(25), tipus_id VARCHAR(25) );
A tábla tartalma pl. így néz ki:
auto_id | marka | tipus_id |
---|---|---|
1 | MERCEDES | MER1001 |
2 | MERCEDES | MERC1001A |
3 | VOLVO | VOL5050 |
A típusazonosító (tipus_id
) sajnos két különböző adatforrásból származik, ugyanazon típust néha más-más jellel látják el a két
adatszolgáltatónál. Például az itt szereplő MER1001 jelölés az egyik adatforrásból származik, a MERC1001A a másiktól – és mindkettő
ugyanazt a típust jelöli.
Joggal mondhatjuk, hogy ez nem ügyesen kialakított megoldás, de változtatni nem tudunk rajta, együtt kell élnünk vele.
A típusok leírása egy másik táblában található:
CREATE TABLE tipusok( id_egyik VARCHAR(25), id_masik VARCHAR(25), tipusnev VARCHAR(50) );
A tábla tartalma így néz ki:
id_egyik | id_masik | tipusnev |
---|---|---|
MER1001 | MERC1001A | E 63 AMG 4Matic |
VOL5050 | VOLV5050B | V40 Kinetic, T2 |
Írjunk lekérdezést, amely kiadja az autók azonosítóját, márkáját, típusazonosítóját és típusnevét!
Erre a lekérdezésre van szükségünk:
SELECT auto_id,marka,tipus_id,tipusnev FROM autok LEFT JOIN tipusok ON tipus_id = id_egyik OR tipus_id = id_masik
Nyilvánvalóan nem INNER JOIN-t kell használnunk, mert akkor eltűnnének azok az autók a kimenetről, amelyeknek a típuskódja nem található meg a
tipusok
táblában. A kimenet:
auto_id | marka | tipus_id | tipusnev |
---|---|---|---|
1 | MERCEDES | MER1001 | E 63 AMG 4Matic |
2 | MERCEDES | MERC1001A | E 63 AMG 4Matic |
3 | VOLVO | VOL5050 | V40 Kinetic, T2 |
Ismét a számlákat és számlasorokat tartalmazó két táblát vesszük elő:
CREATE TABLE szamlak( szamla_id INT NOT NULL, cim VARCHAR(25) ); CREATE TABLE szamlasorok( szamla_id INT NOT NULL, sor_id INT NOT NULL, megnevezes VARCHAR(25) );
és lefuttatjuk ezt a lekérdezést:
SELECT szamlak.szamla_id,cim,megnevezes FROM szamlak LEFT JOIN szamlasorok ON szamlak.szamla_id = szamlasorok.szamla_id
A kimeneten ezt látjuk:
szamla_id | cim | megnevezes |
---|---|---|
101 | első számla | egyik áru |
101 | első számla | másik áru |
202 | másik számla | NULL |
Valaki János ezt látva magabiztosan kijelenti: A 202 azonosítójú, másik számla című számlához nem tartozik egyetlen sor sem a számlasorokat tartalmazó táblában. Tóth Gyula bácsi (bádogos...) azonban azt állítja, hogy ez nem biztos, lehet, hogy van annak a számlának is egy sora. Vajon mire gondolhatott az öreg?
A szamlasorok
táblában ez a tartalom is lehetett:
szamla_id | sor_id | megnevezes |
---|---|---|
101 | 1 | egyik áru |
101 | 2 | másik áru |
202 | 1 | NULL |
Van egy számlasor, ami a 202-es számlához tartozik, csak éppen NULL a megnevezése. Ez persze arra mutat, hogy nem tökéletes az adatbázisszerkezet és az adatbeviteli program sem áll a helyzet magaslatán – ami a valóságban sajnos nem egyszer előfordul.
Írtam egy lekérdezést és véletlenül két T-vel írtam le a LEFT kulcsszót:
SELECT f1,f2 FROM t1 LEFTT JOIN t2 ON f1 = f2
A lekérdezés hibajelzés nélkül lefutott és azt az eredményt szolgáltatta, mintha nem tettem volna múltidőbe szegény LEFT-et, hanem INNER-t írtam volna helyette. Vajon miért?
A táblanevekhez rendelhetünk helyettesítő nevet (alias), amit az AS kulcsszóval vagy anélkül odaírhatunk a táblanév után. A LEFTT-et úgy vette az SQL fordító, mintha a t1 neveként definiáltam volna, a JOIN-nak pedig az INNER az alapértelmezett változata, ezt nem kell kiírni. Ilyennek tekintette a SELECT parancsomat:
SELECT f1,f2 FROM t1 AS LEFTT INNER JOIN t2 ON f1 = f2