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.

A NULL által okozott galibák
Hány sor van?
Le akarjuk kérdezni, hogy egy táblában hány sor van. Erre a célra közismerten a következő lekérdezés alkalmas:
SELECT COUNT(*) FROM table1
Valaki János azt mondta nekem, hogy így is lehet csinálni:
SELECT COUNT(field1) FROM table1
és mutatott példát rá, amikor valóban igaz is volt az állítása. Erre én készítettem egy táblát, feltöltöttem adatokkal és az első lekérdezés eredménye 15, a másodiké pedig 9 lett (és közben nyilván nem változtattam meg a táblát). Mi lehetett az oka ennek, milyenek voltak az adataim?

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
Hány nem-NULL van?

Azt szeretnénk megtudni, hogy az alábbi táblában:

csaladinevutonevstatus
BuzgóMócsingaktív
BágyadtBélapasszív
IsmeretlenIstvánNULL

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?

0 lesz az eredmény, bár két olyan sor is van, ahol a 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
és így az eredmény persze a várt 2 lesz.
Hány NULL van?

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.

Hány különféle érték van?

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
Mennyi van az értékekből?

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:

statusN
NULL0
aktív1
passzív1

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:

statusN
NULL1
aktív1
passzív1
Mi nem egyenlő mivel?

Arra vagyunk kíváncsiak, hogy az alábbi táblában:

csaladinevutonevelso_mhelymostani_mhely
BuzgóMócsingSQL kftAdat BT
BágyadtBélaSQL kftSQL kft
IsmeretlenIstvánSQL kftNULL

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

  • mindkét mező ki van töltve és nem egyformák vagy
  • az első mező NULL, a második nem vagy
  • a második mező NULL, az első nem.

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...

Mennyi az összjutalom?

Egy tábla a tavalyi és az idei havi jutalmakat tartalmazza:

csaladinevutonevjutalom_tavalyjutalom_iden
BuzgóMócsing220000250000
BágyadtBéla190000170000
IsmeretlenIstván320000NULL

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:

csaladinevutonevjutalom_tavaly+jutalom_iden
BuzgóMócsing470000
BágyadtBéla360000
IsmeretlenIstvánNULL

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.

NULL-lal osztunk

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:

csaladinevutonevjutalom_tavaly/jutalom_iden
BuzgóMócsing0.8800
BágyadtBéla1.1176
IsmeretlenIstvánNULL

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!

Munkahelyek összefűzve

Szeretnénk az alábbi táblából:

csaladinevutonevelso_mhelymostani_mhely
BuzgóMócsingSQL kftAdat BT
BágyadtBélaSQL kftSQL kft
IsmeretlenIstvánSQL kftNULL

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:

csaladinevutonevmunkahelyek
BuzgóMócsingSQL kft - Adat BT
BágyadtBélaSQL kft - SQL kft
IsmeretlenIstvánNULL

Azt vártuk volna, hogy az utolsó sorban ez látható majd:

SQL kft -

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.

Átlagolunk

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!

Mi nincs közte?

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.

Tanárok és hallgatók

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_idlakhely
101Budapest
102Debrecen
103Miskolc

Van egy nyilvántartás a tanárokról is:

CREATE TABLE tanarok(
  tanar_id INT NOT NULL,
  lakhely  VARCHAR(25)
);

ilyen tartalommal:

tanar_idlakhely
1Budapest
2Debrecen
3Sopron
4NULL
5Budapest

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!

Páratlan hallgatók

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...

Egyedi a semmi?

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.

Adatbeviteli kalandok
Amiről nem beszélünk...

...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:

csaladinevutonevelso_mhelymostani_mhely
IsmeretlenIstvánNULL

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

  • NULL-t megengedő mezőknél NULL
  • a NULL-t tiltó szöveges mezőknél a MySQL esetében üres sztring, INT mezőnél nulla, stb.
  • és figyelem: default értékek default-ja adatbáziskezelő-, sőt, verziófüggő is lehet!

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:

csaladinevutonevelso_mhelymostani_mhely
IsmeretlenIstvá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.

Itt mi nem egyedi?

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.)

Több tábla, több gond... JOIN és vidéke

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.

INNER JOIN bemelegítés

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_idcimmegnevezes
1tegnapicucc 1
1tegnapicucc 2

Valóban azt kaptuk, amit vártunk, csak az első számla tételei jelennek meg, mert a másodikhoz egy sem tartozik.

Kell-e az INNER JOIN?

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.

INNER JOIN - WHERE vagy ON?

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.

OUTER JOIN bemelegítés

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_idcimmegnevezes
1tegnapicucc 1
1tegnapicucc 2
2maiNULL

Valóban azt kaptuk, amit vártunk, az első számlához két tétel tartozik, a másodikhoz egy sem.

Az OUTER JOIN és az árvák

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_idcim
1tegnapi
2mai

de valahogy becsusszant egy olyan számlasor, amelyhez nem tartozik számla:

szamla_idsor_idmegnevezes
11cucc 1
12cucc 2
10101á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
Ismeri Ön az OUTER JOIN-t?

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_idcimmegnevezes
1tegnapicucc 1
1tegnapicucc 2
2maicucc 1
2maicucc 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

  • ezekhez párosítja a másik táblának azon sorait, amelyekre teljesül az összekapcsolási feltétel;
  • ha pedig a jobboldali táblából egyetlen sor sem felel meg az ON feltételnek, akkor a jobboldali tábla mezői helyett NULL fog állni a kimeneten.

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_idcimmegnevezes
1tegnapiNULL
2maiNULL

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.

OUTER JOIN - WHERE vagy ON?

Ismét a már megszokott két táblát fogjuk elővenni a számlákkal és a számlasorokkal:

szamla_idcim
1tegnapi
2mai
szamla_idsor_idmegnevezes
11cucc 1
12cucc 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_idcimmegnevezes
1tegnapicucc 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_idcimmegnevezes
1tegnapicucc 2
2maiNULL

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!

JOIN ide vagy oda

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_idmarkatipus_id
1MERCEDESMER1001
2MERCEDESMERC1001A
3VOLVOVOL5050

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_egyikid_masiktipusnev
MER1001MERC1001AE 63 AMG 4Matic
VOL5050VOLV5050BV40 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_idmarkatipus_idtipusnev
1MERCEDESMER1001E 63 AMG 4Matic
2MERCEDESMERC1001AE 63 AMG 4Matic
3VOLVOVOL5050V40 Kinetic, T2
Van-e párja?

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_idcimmegnevezes
101első számlaegyik áru
101első számlamásik áru
202másik számlaNULL

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_idsor_idmegnevezes
1011egyik áru
1012másik áru
2021NULL

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.

Hogy mer működni?

Í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