Professional Documents
Culture Documents
Adatbazisok
4
1 Bevezetés
Manapság az adatbázisok már a mindennapi életünk részévé váltak. Tudtunkkal vagy anélkül, lépten-
nyomon adatbázisokat használunk, személyes adatainkat adatbázisokban tárolják. Elég, ha lefuttatunk
egy keresést a Google-ben, megnézünk egy videót a YouTube videó megosztón, keresünk és vásárolunk
egy online áruházban, banki átutalást végzünk az interneten, és a lista még hosszan folytatható lenne.
Ami közös a fenti esetekben, hogy mindegyik webes alkalmazás mögött egy adatbázis áll: a weboldalak
rendszerezett gyűjteménye, videók rendszerezett gyűjteménye, termékek és megrendelések
nyilvántartása, bankszámlaadatok nyilvántartása stb. A legtöbb összetett webes alkalmazásról
elmondható, hogy egy adatbázis áll mögötte, amiben a felhasználói és tartalmi adatok tárolódnak.
Természetesen nem csak webes alkalmazások használnak adatbázisokat, hanem éppúgy asztali és
gyakran beágyazott szoftverek is: adatbázisokban tárolják a hallgatói adatokat, amit az
adminisztrátorok egy asztali alkalmazás segítségével érhetnek el, mobil szoftveren keresztül
hozzáférhetünk a banki adatainkhoz, és tranzakciókat indíthatunk, adatbázisban tárolódnak el a
mobiltelefonunkon rögzített naptár események, de ugyanígy valamilyen adatbázisban tárolódnak el a
gépjárművek GPS és diagnosztikai adatai is.
Adatbázis alatt információk olyan rendezett gyűjteményét értjük, amely hosszú időn át – akár évekig
is – megőrzi a tartalmát. Azt a szoftvert, amellyel ezekhez az adatokhoz hozzáférhetünk, adatbázis-
kezelő rendszernek (Database Management System – DBMS) nevezzük. Az adatbázis-kezelő
rendszerek számos funkcióval rendelkeznek, és olyan, gyakran használt szolgáltatásokat valósítanak
meg, amelyek saját implementációja tipikusan egyenként is sok emberhónap vagy év kutatási és
fejlesztési munkát igényelne. A leggyakrabban támogatott szolgáltatások a következők:
5
A tranzakciókezelés (8. fejezet) segítségével biztosított az összetartozó műveletek atomi
végrehajtása, nem fordulhat elő például, hogy egy banki átutalás során az egyik számla még
terhelésre kerül, de a másik számlán egy rendszerhiba miatt a jóváírás már nem következik be.
A tranzakciós naplózás (8.3. fejezet) során az adatbázison elvégzett műveletekről
bejegyzéseket készítünk egy, az erre a célra fenntartott naplóban. Rendszerhiba esetén a nem
teljesen végrehajtott műveletek hatása a napló alapján visszagörgethető, így helyreállítva az
adatbázis konzisztens állapotát.
Az adatbázishoz általában egyszerre több felhasználó is hozzáférhet, azok párhuzamosan
írhatják és olvashatják az abban tárolt adatokat. A konkurens elérés nem megfelelő
implementáció esetén nemkívánatos következményekkel járhat, például a különböző
felhasználói folyamatok felülírhatják egymás módosításainak hatásait, vagy az egyik folyamat
egy köztes állapotából kiindulva egy másik folyamat hibás adatokat állíthat elő. Az adatbázis-
kezelő rendszer a különböző felhasználói folyamatok – ún. tranzakciók (8. fejezet) –
izolációjával biztosítja a nemkívánatos mellékhatások elkerülését. Ehhez különböző zárolási
stratégiákat (8.2.2.3. fejezet) és ugyanazon adat eltérő verzióinak nyilvántartását (8.2.3.
fejezet) alkalmazza.
Lehetőségünk van az adatokhoz történő hozzáférés szabályozására: a DBMS-ek általában
biztosítják az alapszintű felhasználó-kezelést, amelyen keresztül pontosan meghatározhatjuk,
hogy mely felhasználók az adatok mely köréhez férhetnek hozzá, és azon milyen műveleteket
hajthatnak végre.
6
Egyszerű felhasználók,
Adatbázis adminisztrátor
felhasználói programok
Naplókezelő
Ütemező
Erőforrás kezelő (adat, index) és helyreállító
Tárkezelő
Tárterület
Az adatbázis sémáját az ún. adat-definíciós nyelv (Data Definition Language – DDL, ld. 5. fejezet)
segítségével írhatja le és módosíthatja az adatbázis adminisztrátor: meghatározhatja a tárolt adatok
típusát, tulajdonságait, a különböző adatok közti kapcsolatokat és kényszereket. A DDL utasításokat a
séma leírás fordító dolgozza fel, majd az értelmezett utasításokat átadja a végrehajtó motornak,
ami az erőforrás-kezelőn keresztül elvégzi a módosításokat az adatbázis adataiban és
metaadataiban.
7
1.2 Áttekintés
Jegyzetünk célja egy alapszintű betekintést nyújtani az adatbázisok világába: megismerni a relációs
adatbázisok elméleti hátterét, az adatbázis-kezelő rendszerek által nyújtott leggyakoribb
szolgáltatásokat, és azok gyakorlati alkalmazási lehetőségeit. A jegyzet megírása során a Database
Systems: The Complete Book [1] című szakirodalom felépítésére és terminológiájára támaszkodtunk.
8
2 Az adatok relációs modellje
A táblázat egy fejlécből, sorokból és oszlopokból áll. A táblázat egyes sorai reprezentálják a relációban
tárolt egyedeket, az egyes oszlopok pedig az egyedek egyes tulajdonságait. Jelen reláció egy
képzeletbeli bútorboltban forgalmazott termékeket és azok adatait tárolja, tehát a reláció egyes sorai
egy-egy terméknek felelnek meg. Az egyed tulajdonságait a relációs adatmodellben attribútumnak
nevezzük. Az attribútumok neveit a fejléc elnevezései rögzítik: 𝑁é𝑣, Á𝑟, 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎,
𝐺𝑦á𝑟𝑡ó𝑁é𝑣. Mivel ez a reláció öt attribútummal rendelkezik, ezért azt mondjuk, hogy a reláció foka
(aritása) öt. A relációban tárolt sorok számát pedig a reláció számosságának nevezzük, tehát a
𝑇𝑒𝑟𝑚é𝑘 reláció számossága négy. A reláció sorait n-esnek (tupple) szokták nevezni, egy ilyen n-es a
reláció minden egyes attribútumához egy értéket rendel. A példa reláció első n-ese a 𝑁é𝑣
9
attribútumhoz a „Kanapé”, az Ár attribútumhoz a 150.000, a 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡 attribútumhoz a 3 stb.
értékeket rendeli. Az attribútum típusa mindig valamilyen egyszerű típus (szám, szöveg, dátum stb.),
egy attribútum nem tárolhat összetett típusokat, mint egy C-beli struktúra vagy tömb.
A relációt a sémára illeszkedő n-esek halmaza alkotja. Annak, hogy a reláció egy halmaz, lényeges
következményei, hogy
A reláció sémáját úgy kapjuk, meg, ha a nevét követően zárójelek között felsoroljuk a reláció
attribútumait. A séma határozza meg az adatok tárolási struktúráját az adott relációban:
Egy relációban egy típusú egyed (pl. termékek) példányait lehet eltárolni, de természetesen tetszőleges
számú relációt készíthetünk különböző típusú egyedekhez, és a relációk között kapcsolatot is
létesíthetünk. Például nyilvántarthatjuk a termékeket, a gyártóikat, a termékekre leadott
megrendeléseket, a megrendelésekhez kapcsolódó vásárolói és számlainformációt stb. Az egy témakör
köré csoportosuló, összefüggő relációk összessége alkotja az adatbázist, a relációk sémájának
összessége pedig az adatbázis sémáját. Ha az adatbázisban a termékek adatain túl a gyártók adatait
is el szeretnénk tárolni, akkor ez például a
sémájú relációval lehetséges: a gyártókról nyilvántartjuk a nevüket, címüket, e-mail címüket. Erre a
sémára illeszkedő konkrét relációt a 2.2. táblázatban láthatunk.
Felmerülhet a kérdés, hogy miért van szükségünk egy új algebrára, egy új nyelvre, miért nem
használunk helyette egy általános célú programozási nyelvet, mint például a C vagy a JAVA. A válasz
az, hogy azért, mert egy speciális, erre a célra kifejlesztett nyelvvel (i) sokkal hatékonyabban tudunk
10
(ii) implementáció-független műveletsorozatokat leírni relációkon, amelyeket (iii) a végrehajtó
optimalizáltan tud elvégezni. Ahhoz, hogy egy C nyelvű programot tudjunk írni, amely egy bizonyos
műveletet végrehajt egy reláción, ismernünk kell a kezelt adatbázis implementációját, adatábrázolását,
adattárolási módszereit stb., hogy a kódunk illeszkedjen hozzá. Azáltal, hogy egy magas szintű,
kizárólag relációk kezelésére alkalmas formalizmust alkotunk, szűkítjük ugyan a leírható műveletek
körét, de amit így meg tudunk fogalmazni, az jóval tömörebb lesz (nem kell az implementációs
részletekkel foglalkozni), adatbázis-kezelő rendszerek között hordozható (független az
implementációtól), és mivel erre a szűkített eszközkészletre az adatbázis kezelő rendszer magasan
optimalizált végrehajtó motorral rendelkezik, ezért rendkívül hatékonyan hajtható végre.
Meg kell jegyeznünk, hogy magát a reláció algebrát közvetlenül már nem használják lekérdezések
megfogalmazására, erre manapság leginkább a Structured Query Language-et (SQL) [2] (5. fejezet)
alkalmazzák. Tudnunk kell viszont, hogy az SQL nyelv jelentős része nem más, mint egy „szintaktikus
édesítőszer” a relációalgebrai kifejezésekhez, hogy javítsa annak olvashatóságát. Másrészt az SQL
utasítások feldolgozásakor azok relációalgebrai – vagy ahhoz nagyon hasonló – reprezentációira
fordulnak le. A továbbiakban részletesen ismertetjük a relációs algebrai műveleteket.
A vetítés során egy 𝑅 relációból egy olyan másik 𝑅’ relációt hozunk létre, amely az eredeti 𝑅 relációnak
csak bizonyos oszlopait tartalmazza. A műveletet a π operátorral jelöljük: a 𝜋𝐴1 ,𝐴2 ,…,𝐴𝑛 (𝑅) kifejezés
érteke tehát egy 𝑅’ reláció, amely az 𝑅 reláció 𝐴1 , 𝐴2 , … , 𝐴𝑛 oszlopait (attribútumait) tartalmazza. A
létrejött reláció sémája ennek megfelelően 𝑅 ′ (𝐴1 , 𝐴2, … , 𝐴𝑛 ). Az attribútumok pontos megnevezése
helyett alkalmazhatjuk azok 𝑅-relációbeli sorszámát is: 𝜋𝑖1 ,𝑖2 ,…,𝑖𝑛 . A keletkező reláció természetesen
nem lesz üres: oszlopai 𝑅 megfelelő oszlopainak értékeit fogják tartalmazni.
Tekintsük a 2.1. táblázatban ismertetett 𝑇𝑒𝑟𝑚é𝑘 relációt. Tegyük fel, hogy a termék tábla oszlopai
közül csak az egyszerű adatokat (𝑁é𝑣, Á𝑟, 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎) szeretnénk megjeleníteni, a
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 mezőre pedig nincs szükségünk ehhez. Az irreleváns oszlopok kiszűréséhez használhatjuk
az alábbi projekciót:
𝜋𝑁é𝑣,Á𝑟,𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡,𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎 (𝑇𝑒𝑟𝑚é𝑘)
𝜋1,2,3,4 (𝑇𝑒𝑟𝑚é𝑘)
11
A fenti példában a forrás és az eredmény reláció számossága megegyezik, de ez nem feltétlenül lesz
mindig így. Tegyük fel, hogy a projekció során nem az első négy oszlopot akarjuk megtartani, csak a
𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖á𝑡. Vagyis a 𝜋𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎 (𝑇𝑒𝑟𝑚é𝑘) projekciót alkalmazzuk. Azt várhatnánk, hogy az eredmény
relációnak 4 sora lesz, amelyek a 4 terméknek megfelelő 1-1 kategóriát tartalmazzák. Ehelyett a
kategória attribútumra levetített relációnak csak három sora lesz (2.4. táblázat).
Kategória
Nappali
Iroda
Kert
Ennek oka, hogy a művelet eredménye szintén reláció, vagyis teljesítenie kell a relációk halmazoktól
örökölt azon tulajdonságát, hogy ugyanazon elem nem szerepelhet kétszer vagy többször ugyanabban
a halmazban. Következésképpen a „Nappali” kategória érték kettő helyett csak egyszer fog szerepelni
az eredményben.
𝜋𝑁é𝑣,𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡∗Á𝑟→Ö𝑠𝑠𝑧𝑒𝑔 (𝑇𝑒𝑟𝑚é𝑘)
Név Összeg
Kanapé 450.000
Dohányzóasztal 36.000
Szék 330.000
Hintaágy 32.000
Az eredmény relációnak két attribútuma lesz: 𝑁é𝑣 és Ö𝑠𝑠𝑧𝑒𝑔. A 𝑁é𝑣 attribútum értéke minden egyes
n-esre megegyezik a forrás n-esek 𝑁é𝑣 attribútum értékével, az Ö𝑠𝑠𝑧𝑒𝑔 attribútum értéke pedig a
𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡 ∗ Á𝑟 szorzat eredményeképp fog előállni minden egyes sorra.
A kiválasztás operátor 𝑅 reláción történő alkalmazásával egy olyan új 𝑅’ reláció jön létre, amelynek
sémája megegyezik az 𝑅 reláció sémájával, viszont sorainak csak egy részhalmazát fogja tartalmazni.
12
Azt, hogy pontosan melyik sorok fognak megmaradni, azt a kiválasztáshoz rendelt C feltétel fogja
meghatározni. Az operátort a 𝜎𝐶 (𝑅) – kifejezéssel jelöljük.
A C feltétel egy logikai igaz/hamisra kiértékelhető kifejezést kell, hogy tartalmazzon, amely a reláció
egyes sorain értékelődik ki. A feltétel hivatkozhat a reláció egyes sorainak attribútum értékeire,
konstansokra, és ezeket tetszőleges aritmetikai (pl. +, −,∗,/ stb), relációs (>,<…) és logikai (¬,∧,∨)
operátorokkal kapcsolhatja össze. A 𝜎𝐶 (𝑅) művelet alkalmazása során a C feltétel az R reláció minden
egyes sorára kiértékelődik, és az eredmény relációba csak azok a sorok fognak belekerülni, amelyek a
feltételt kielégítik.
Tekintsük a 2.1. táblázatban ismertetett 𝑇𝑒𝑟𝑚é𝑘 relációt. A 𝜎Á𝑟>30000 (𝑇𝑒𝑟𝑚é𝑘) kifejezés a 30.000 Ft-
nál drágább termékekből álló relációt fogja eredményezni: az Ár > 30000 Feltétel a 𝑇𝑒𝑟𝑚é𝑘 reláció
valamennyi sorára kiértékelődik, de csak az első és negyedik n-es fogja azt kielégíteni. A művelet
eredménye a 2.6. táblázatban látható.
A különböző műveleteket egymásba is ágyazhatjuk, hiszen egy relációból egy másik relációt állítanak
elő. Például egy szelektált reláción végrehajthatunk vetítést, vagy egy vetített reláción szelekciót.
Például a 30.000 Ft-nál olcsóbb termékek nevét a közvetkező kifejezés adja meg:
𝜋𝑁é𝑣 (𝜎Á𝑟<30000 (𝑇𝑒𝑟𝑚é𝑘)). Hasonlóképpen alkalmazhatunk akár egymás után több kiválasztás és
szelekció műveletet is.
Fontos azonban megjegyezni, hogy a kiválasztás és vetítés műveletek általában nem felcserélhetők
(nem kommutatívak). A 𝜋𝑁é𝑣 (𝜎Á𝑟<30000 (𝑇𝑒𝑟𝑚é𝑘)) művelet eredménye a 30.000 Ft-nál olcsóbb
termékek neve. A 𝜎Á𝑟<30000 (𝜋𝑁é𝑣 (𝑇𝑒𝑟𝑚é𝑘)) kifejezés viszont hibás, hiszen a 𝜋𝑁é𝑣 (𝑇𝑒𝑟𝑚é𝑘) művelet
eredményének sémája egyetlen Név attribútummal rendelkezik, amelyen az Ár < 30000 feltétel nem
értelmezhető. Két vetítés művelet viszont mindig felcserélhető: a végeredmény szempontjából
indifferens, hogy melyik oszlopokat hagyjuk el előbb, illetve később, végül csak azok az attribútumok
maradnak, amelyeket mindkét vetítés művelet meghagyja. Általánosan:
𝜋𝐴1 ..𝐴𝑛 (𝜋𝐵1 ..𝐵𝑛 (𝑅)) = 𝜋𝐵1 ..𝐵𝑛 (𝜋𝐴1 ..𝐴𝑛 (𝑅)) = 𝜋{𝐴1 ..𝐴𝑛 }∩{𝐵1 ..𝐵𝑛 } (𝑅)
13
Hasonlóképpen két kiválasztás művelet is mindig felcserélhető: az eredmény relációban csak azok a
sorok fognak megmaradni, amelyek mindkét feltételt kielégítik. Általánosan:
2.2.3 Halmazműveletek
14
Név Ár Raktárkészlet Kategória GyártóNév
Kanapé 150.000 3 Nappali Nagy Bútor
Kft
Dohányzóasztal 12.000 3 Nappali Nagy Bútor
Kft
Szék 22.000 15 Iroda Fabric Rt
Termék1 reláció
Termék2 reláció
2.8. táblázat: Termék1 és Termék2 relációk
A Termék1 és Termék2 relációk uniója (𝑇𝑒𝑟𝑚é𝑘1 ∪ 𝑇𝑒𝑟𝑚é𝑘2) a 2.9. táblázatban látható. Az unió
definíciójának megfelelően a 𝑆𝑧é𝑘 termék csak egyszer fog szerepelni az eredményben, bár mind
Termék1-ben, mind Termék2-ben szerepelt.
A két reláció különbségében (Termék1 − Termék2) pedig azok az elemek fognak megjelenni, amelyek
megtalálhatók Termék1-ben, de hiányoznak Termék2-ből (2.11. táblázat).
Tegyük fel, hogy azon termékek listáját szeretnénk megkapni, amelyeket a nappaliba ajánlanak, és
100.000 Ft-nál olcsóbbak. A kívánt eredményt többféleképpen is kiszámolhatjuk:
1) Kiválaszthatjuk a nappaliba szánt termékeket egy szelekció művelettel, egy másik szelekcióval
kiválaszthatjuk a 100.000 Ft-nál olcsóbb terméket, majd képezhetjük a két reláció metszetét:
𝜎𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎=′ 𝑁𝑎𝑝𝑝𝑎𝑙𝑖′ (𝑇𝑒𝑟𝑚é𝑘) ∩ 𝜎Á𝑟<100.000 (𝑇𝑒𝑟𝑚é𝑘)
15
2) Azonos eredményre jutunk akkor is, ha a Termék reláción egy szelekció operátort alkalmazunk,
amelynek a feltétel kifejezése a két kritérium ÉS kapcsolatát (konjugáltját) tartalmazza:
𝜎𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎=′ 𝑁𝑎𝑝𝑝𝑎𝑙𝑖′ ∧Á𝑟<100.000 (𝑇𝑒𝑟𝑚é𝑘)
Számos adatbázis-kezelő rendszer rendelkezik egy, a relációs algebrai kifejezéseihez hasonló lekérdező
nyelvvel. Amint ebből az egyszerű példából is látható, ugyanazt a lekérdezést gyakran nagyon
sokféleképpen tudjuk megfogalmazni. Bár a végeredményük ugyanaz, akár nagyságrendi különbség is
lehet a különböző lekérdezések végrehajtásához szükséges számítási idő között. Az ilyen
rendszerekben található lekérdezés optimalizáló modul feladata a lekérdezést olyan formára hozni,
hogy az hatékonyan végrehajtható legyen.
2.2.4 Keresztszorzat
A és B halmazok keresztszorzata (vagy egyszerűen csak szorzata) alatt egy olyan új halmazt értünk,
amelynek elemei rendezett párosok: a pár első eleme az A, míg második eleme a B halmaz elemei
közül kerül ki. A keresztszorzat az összes ilyen lehetséges párost tartalmazza. A keresztszorzatot az 𝐴 ×
𝐵 kifejezéssel jelöljük.
Amikor általános halmazok helyett relációkat szorzunk, ugyanígy kell eljárnunk: R és S relációk
keresztszorzata szintén rendezett párosokból fog állni, ahol a párok első fele az R reláció n-esei közül
kerül ki, míg a másik fele az S reláció n-esei közül. Mivel az n-esek tipikusan nem egy attribútum értéket
tartalmaznak, hanem egyszerre többet, ezért a párok is tulajdonképpen nem érték-párosok lesznek,
hanem tetszőleges számú attribútum értékből álló n-esek. Az így előálló új reláció sémája R és S
sémájának egymás után illesztésével áll elő: 𝑅(𝐴1 , … , 𝐴𝑛 ), illetve 𝑆(𝐵1 , … , 𝐵𝑛 ) relációs sémák esetén
𝑅 × 𝑆(𝐴1 , … , 𝐴𝑛 , 𝐵1 , … , 𝐵𝑛 ) lesz az eredmény reláció sémája. Amennyiben az eredeti két reláció
attribútumai között található azonos nevű, akkor vagy a 2.2.7-ben ismertetett attribútum átnevezést
használjuk az elnevezések egyértelműsítésére, vagy a forrás relációk neveivel egyértelműsítjük az
attribútumokat, pl 𝑅. 𝐴𝑥 , 𝑆. 𝐴𝑥 .
Tekintsük a 2.1. táblázatban bemutatott termék listát, valamint a 2.2. táblázatban felsorolt gyártói
listát. A 𝑇𝑒𝑟𝑚é𝑘 × 𝐺𝑦á𝑟𝑡ó reláció sémája tehát a két kiindulási reláció attribútumainak összességéből
áll elő:
𝑇𝑒𝑟𝑚é𝑘 × 𝐺𝑦á𝑟𝑡ó(𝑁é𝑣, Á𝑟, 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎, 𝑇𝑒𝑟𝑚é𝑘. 𝐺𝑦á𝑟𝑡ó𝑁é𝑣, 𝐺𝑦á𝑟𝑡ó. 𝐺𝑦á𝑟𝑡ó𝑁é𝑣, 𝐶í𝑚, 𝐸𝑚𝑎𝑖𝑙)
A reláció sorait pedig a két kiindulási reláció n-eseiből álló párosok alkotják (2.12. táblázat).
16
Raktár- Termék. Gyártó.
Név Ár Kategória Cím Email
készlet GyártóNév GyártóNév
Kanapé 150.000 3 Nappali Nagy Bútor Kft Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Dohányzóasztal 12.000 3 Nappali Nagy Bútor Kft Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Szék 22.000 15 Iroda Fabric Rt Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Hintaágy 32.000 1 Kert Bútorgyár Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Kanapé 150.000 3 Nappali Nagy Bútor Kft Fabric Rt 9012 Győr Ág u 15 fabric@example.org
Dohányzóasztal 12.000 3 Nappali Nagy Bútor Kft Fabric Rt 9012 Győr Ág u 15 fabric@example.org
Szék 22.000 15 Iroda Fabric Rt Fabric Rt 9012 Győr Ág u 15 fabric@example.org
Hintaágy 32.000 1 Kert Bútorgyár Fabric Rt 9012 Győr Ág u 15 fabric@example.org
Kanapé 150.000 3 Nappali Nagy Bútor Kft Bútorgyár 1111 Bp. Hős krt 3. xyb@example.org
Dohányzóasztal 12.000 3 Nappali Nagy Bútor Kft Bútorgyár 1111 Bp. Hős krt 3. xyb@example.org
Szék 22.000 15 Iroda Fabric Rt Bútorgyár 1111 Bp. Hős krt 3. xyb@example.org
Hintaágy 32.000 1 Kert Bútorgyár Bútorgyár 1111 Bp. Hős krt 3. xyb@example.org
Bár első ránézésre a keresztszorzat művelet gyakorlati szempontból nem tűnik túl hasznosnak, számos
bonyolultabb illesztési művelet (2.2.5, 2.2.6, 2.2.8) kiindulási alapjául szolgál.
Ahogyan 2.2.4-ben is láttuk, két reláció n-eseit a keresztszorzat művelet segítségével egyszerűen
kapcsolatba tudjuk egymással hozni, bár az eredmény nem sok hasznos információval szolgál, mivel az
egyik reláció minden egyes n-esét a másik reláció minden egyes n-esével összepárosítja. Ezek közül
kellene nekünk kiszűrni azokat a párosításokat, amelyek valamilyen módon összetartoznak. A 2.12.
táblázatban látott relációban például azok az érdekes párosítások, ahol a termék és annak gyártója
található egymás mellett, vagyis a Termék. GyártóNév attribútum értéke megegyezik a
Gyártó. GyártóNév attribútum értékével (2.13. táblázat).
Raktár- Termék. Gyártó.
Név Ár Kategória Cím Email
készlet GyártóNév GyártóNév
Dohányzóasztal 12.000 3 Nappali Nagy Bútor Kft Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Kanapé 150.000 3 Nappali Nagy Bútor Kft Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Szék 22.000 15 Iroda Fabric Rt Fabric Rt 9012 Győr Ág u 15 fabric@example.org
Hintaágy 32.000 1 Kert Bútorgyár Bútorgyár 1111 Bp. Hős krt 3. xyb@example.org
Amikor két reláció n-eseit csak akkor párosítjuk össze, ha azok azonos (nevű és típusú) attribútumai
azonos értékkel rendelkeznek, a párosítást természetes illesztésnek nevezzük, és a ⋈ operátorral
jelöljük: 𝑇𝑒𝑟𝑚é𝑘 ⋈ 𝐺𝑦á𝑟𝑡ó.
17
Raktár-
Név Ár Kategória GyártóNév Cím Email
készlet
Dohányzóasztal 12.000 3 Nappali Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Kanapé 150.000 3 Nappali Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Szék 22.000 15 Iroda Fabric Rt 9012 Győr Ág u 15 fabric@example.org
Hintaágy 32.000 1 Kert Bútorgyár 1111 Bp. Hős krt 3. xyb@example.org
2.2.6 Theta-illesztés
A természetes illesztés esetén két relációt az egyező nevű attribútumok mentén illesztettünk össze. De
sokszor ez túl szigorú megkötés. Gondoljunk csak bele, hogy ha a Gyártó relációban a gyártó nevét
nem GyártóNév-nek neveztük volna el, hanem egyszerűen Névnek (mint ahogy a termék nevét is
egyszerűen egy Név attribútum jelöli), akkor a két reláció természetes illesztése során a Termék. Név
és a Gyártó. Név attribútum értékek mentén párosítottuk volna össze a két reláció n-eseit. Ez jelen
példában egyetlen sort sem eredményezett volna, de általános esetben is hibás eredményt adna.
További probléma, hogy az egyszerű egyenlőség vizsgálat két attribútum között nem feltétlen
elégséges minden esetben. A bemutatott problémákra nyújt megoldást a theta-illesztés, amely
tetszőleges feltétel mentén történő párosítást tesz lehetővé.
A 2.1. táblázatban és 2.2. táblázatban látható Termék és Gyártó relációkat a gyártó neve szerint akár
theta-illesztéssel is összekapcsolhatjuk:
Az eredmény kissé különböző lesz, hiszen a theta-illesztés nem távolítja el az egyező attribútumokat.
18
𝑇𝑒𝑟𝑚é𝑘 ⋈Á𝑟≥𝑀𝑖𝑛Ö𝑠𝑠𝑧𝑒𝑔∧Á𝑟≤𝑀𝑎𝑥Ö𝑠𝑠𝑧𝑒𝑔 𝑆𝑧𝐾ö𝑙𝑡𝑠é𝑔
Vagyis az egyes termékekhez azokat a szállítási költség n-eseket illesztjük hozzá, amelyekre igaz, hogy
a termék ára a szállítási költség minimum és maximum értéke közé esik. Mivel a minimum-maximum
intervallumok diszjunktak, és az utolsó praktikusan nagy, ezért minden egyes termékhez garantáltan
egy illesztett n-es lesz az SzKöltség táblában (2.16. táblázat).
Ha az eredmény túl terjengős, és számunkra például csak a termék neve, ára és szállítási költsége
érdekes, akkor egy projekcióval tovább szűkíthetjük az eredményt:
Ha ezek közül is csak a 100.000 Ft-nál olcsóbb termékeket szeretnénk látni, akkor egy szelekciót kell
alkalmaznunk (jelen esetben mindegy, hogy a projekció előtt vagy után, hiszen a termék ára a projekció
eredményében is szerepel):
Név Ár Költség
Dohányzóasztal 12.000 2.000
Szék 22.000 1.500
Hintaágy 32.000 1.500
2.2.7 Átnevezés
A keresztszorzat (2.2.4) műveletnél már láttuk, hogy azonos nevű attribútumokat tartalmazó relációk
szorzásánál az eredményben nem egyértelmű, hogy melyik attribútum melyik relációból származik (és
ezáltal, hogy pontosan mi a jelentése), ezért név ütközés esetén az attribútumokat a forrás reláció
nevével prefixáltuk (Termék.GyártóNév, Gyártó. GyártóNév). Hasznos lett volna, ha a szorzás előtt
vagy után át tudtuk volna nevezni az ütköző attribútumokat a könnyebb megkülönböztethetőség
érdekében.
Ugyanígy szükségünk lehet átnevezésekre, ha például egy relációt saját magával szeretnénk theta-
illeszteni. Ilyenkor az illesztés operátor két oldalán ugyanaz a reláció szerepel, az illesztési feltételben
nem tudunk köztük különbséget tenni, hiszen azonos a nevük. Adott a 2.18. táblázatban látható
19
Személy reláció. A reláció minden egyes sora egy természetes személy adatait tartalmazza: a személyi
igazolvány számát (SzigSzám), a nevét, illetve az apja és az anyja személyi igazolvány számát
(amennyiben ismert). Tegyük fel, hogy ki akarjuk deríteni, hogy mi a neve a Kovács Máté nevű személy
apjának. Ehhez keresni kell egy olyan személyt, akinek a személyi igazolvány száma egyezik a Kovács
Mátéhoz tartozó n-esben az Apa attribútumhoz tárolt értékkel.
Vagyis a Személy relációt saját magával kell illeszteni úgy, hogy az egyik példány n-eseinek SzigSzám
attribútum értékeit a másik példány n-eseinek Apa attribútum értékeivel egyeztetjük. Természetesen
a 𝑆𝑧𝑖𝑔𝑆𝑧á𝑚 = 𝐴𝑝𝑎 feltétel egy theta-illesztésnél nem egyértelmű, hasonlóképpen a
𝑆𝑧𝑒𝑚é𝑙𝑦. 𝑆𝑧𝑖𝑔𝑆𝑧á𝑚 = 𝑆𝑧𝑒𝑚é𝑙𝑦. 𝐴𝑝𝑎 feltétel sem, mivel az egyenlőség két oldalának a két különböző
relációra kellene hivatkoznia, de mindkettő neve Személy ebben az esetben.
Többek között a fent vázolt problémákra nyújt megoldást az átnevezés operátor, amellyel egy reláció
attribútumait és magát a relációt is átnevezhetjük. Jelölése: 𝜌𝑆(𝐴1 ,…,𝐴𝑛 ) (𝑅). A művelet eredménye egy
olyan új reláció, amelynek a neve S, attribútumai neve 𝐴1 , … , 𝐴𝑛 . Az attribútumok száma megegyezik
az R reláció attribútumainak számával, az új reláció n-esei is ugyanazok, mint az R relációé. Ha a reláció
attribútumait nem, csak magát a relációt szeretnénk átnevezni, akkor használhatjuk az egyszerűsített
𝜌𝑆 (𝑅) jelölést. Ez utóbbi jelölési módot felhasználva a példában említett SzigSzám = Apa illesztést a
következőképpen fogalmazhatjuk meg:
Tehát a Személy relációról készítünk Szülő néven egy másolatot, így a theta-illesztés feltételében már
egyértelműen azonosíthatjuk az operátor két oldalán lévő relációkat. Az illesztés eredménye a 2.19.
táblázatban látható.
2.19. táblázat: Személy és Szülő relációk illesztése az apa személyi igazolvány száma szerint
Vagyis az illesztés után egy szelekcióval kiválasztjuk azt (azokat) a sorokat, ahol a Személy (gyerek)
neve „Kovács Máté” (az első sor), majd egy vetítéssel a talált sorokból kiválasztjuk azt a Név
attribútumot, ami a Szülő relációból származik (2.20. táblázat).
20
Szülő. Név
Kovács Péter
Két reláció illesztése során (akár természetes, akár theta) azok az n-es párok maradnak csak meg,
amelyek „összeillenek” valamilyen feltétel szerint. Azok az n-esek, amelyekhez a másik relációban nem
található egyetlen megfelelő párosítás sem, az eredményben semmilyen módon nem reprezentálják
magukat. Ez sokszor megfelelő számunkra, de vannak esetek, amikor ezekre a n-esekre is szükségünk
van, vagy épp pont csak a párosítatlan n-esekre lenne szükségünk. Például a 2.18. táblázatban
ismertetett reláción egy olyan kérdést, hogy „hogy hívják a rendszerben tárolt személyeket, és
amennyiben ismert, az anyjukat” az eddig tanult illesztésekkel nem tudjuk megválaszolni, hiszen az
eredménybe csak azok a személyek kerülnek bele, akiknek az anyja is szerepel a relációban. A
különböző külső illesztések a fenti problémát célozzák megoldani úgy, hogy az eredmény relációba a
párosításokon túl beleveszik a párosítatlan n-eseket is az egyik/másik/mindkét relációból.
Tekintsük a 2.18. táblázatban ismertetett relációt. Illesszük teljes külső illesztéssel saját magához a
𝑆𝑧𝑒𝑚é𝑙𝑦. 𝐴𝑝𝑎 = 𝑆𝑧ü𝑙ő. 𝑆𝑧𝑖𝑔𝑆𝑧á𝑚 feltétel segítségével 𝜌𝑆𝑧𝑒𝑚é𝑙𝑦 (𝑆𝑧ü𝑙ő) átnevezés után:
A művelet eredménye definíció szerint a theta-illesztés (2.19. táblázat) kiegészítve mindkét reláció
párosítatlan n-eseivel (2.21. táblázat).
2.21. táblázat: Személy és Szülő relációk külső illesztése az apa személyi igazolvány száma szerint
Mivel Kovács Péter és Nagy Béla apja nem szerepel a Személy relációban, ezért az ő n-eseik jobb oldalát
üres szimbólumokkal egészítjük ki teljes párosításokká (a táblázat első két sora). Hasonlóképpen, mivel
21
Kovács Máté, Kovács Éva és Nagy Éva senkinek nem az apja (nincs a Személy relációban olyan n-es,
aminek az Apa attribútum értéke egyezne az ő SzigSzám attribútum értékükkel), azért a kapcsolódó
n-esek bal oldalát töltjük fel üres szimbólumokkal, hogy teljes párosításokat kapjunk (utolsó három
sor).
Egy olyan feladat megválaszolása, hogy „soroljuk fel a Személy relációban tárolt emberek és mellettük
az apjuk adatait (amennyiben ismert)”, a következő bal oldali külső theta-illesztéssel lehetséges:
Így valamennyi személy megjelenik az eredményben akkor is, ha az apa adatai nem elérhetőek (2.22.
táblázat).
2.22. táblázat: Személy és Szülő relációk bal oldali külső illesztése az apa személyi igazolvány száma szerint
∘
A jobb oldali külső theta-illesztés (az illesztés jel után egy R betűvel jelöljük: 𝑅⋈ 𝑅 𝐶 𝑆) a theta-illesztést
a jobb oldali reláció (S) párosítatlan n-eseivel egészíti ki, azok bal oldalát üres jelekkel feltöltve.
22
a 2.4. táblázatban is láttuk, vetítés során előfordulhat, hogy az attribútum szám csökkenés miatt azonos
n-esek keletkeznek, amit halmazok használata esetén meg kell tisztítani az ismétlődésektől. Zsákok
esetében ezzel szemben a vetítést soronként tudjuk elvégezni anélkül, hogy meg kellene vizsgálni, hogy
az így előálló új n-es egyedi-e vagy sem.
A hatékonyságon túl, zsákok használatának másik fontos oka az, hogy bizonyos értékeket csak
zsákokon tudunk kiszámolni. Például ha szeretnénk megállapítani a 2.23. táblázatban látható termékek
raktárkészletének összegét, akkor először kiválasztjuk az Raktárkészlet oszlop értékeit, majd
összegezzük őket (2.3.6).
2.3.1 Vetítés
Ahogyan már utaltunk rá, a vetítés műveletet zsákok esetén az egyes n-eseken egymástól teljesen
függetlenül végezhetjük el. A végeredményből nem kell eltávolítani az esetlegesen keletkező
ismétlődéseket sem akkor, ha az eredeti relációban (mint zsákban) már eleve voltak ismétlődések, sem
akkor, ha a vetítés során, az egyes attribútumok elhagyása miatt keletkeztek.
Raktárkészlet Kategória
3 Nappali
3 Nappali
15 Iroda
1 Kert
2.3.2 Kiválasztás
Kiválasztást zsák-alapú relációk n-esein egymástól függetlenül végezzük el, az egyes sorok vizsgálata
közben nem kell ellenőriznünk, hogy az eredménybe belekerült-e már korábban egy, az aktuálissal
megegyező bejegyzés, hiszen az eredmény tartalmazhat ismétlődéseket.
23
A kiválasztás önmagában nem képes új ismétlődő n-esek létrehozására, mivel az egy már létező n-esről
dönti csak el, hogy az belekerül-e az eredménybe vagy sem, tehát a kiválasztás eredményébe csak úgy
kerülhet duplikáció, ha már a forrás relációban is volt. Tekintsük a 2.3.1-ben ismertetett
𝜋𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡,𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎 (𝑇𝑒𝑟𝑚é𝑘) relációt. Végezzük el ezen a 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡 < 5 szűrést:
Raktárkészlet Kategória
3 Nappali
3 Nappali
1 Kert
2.3.3 Halmazműveletek
Kategória Kategória
Iroda Nappali
Iroda Nappali
Nappali Iroda
Kert Kert
A két reláció uniója mindkét reláció n-eseit tartalmazni fogja, összesen 8-at (2.27. táblázat).
Kategória
Iroda
Iroda
Nappali
Kert
Nappali
Nappali
Iroda
Kert
2.27. táblázat: K1 ∪ K2
24
egyszer szerepel, ezért az eredményben is csak egyszer fog szerepelni, hasonlóképpen a
„Nappali” is csak egyszer kerül bele az eredménybe. A „Kert” mindkét relációban egyszer
szerepel, így értelemszerűen az eredményben is csak egyszer fog (2.28. táblázat).
Kategória
Iroda
Nappali
Kert
2.28. táblázat: K1 ∩ K2
3. 𝑅 − 𝑆 𝑀𝐴𝑋(0, 𝑛 − 𝑚)-szer fogja tartalmazni 𝑟-t. Vagyis ha r többször fordul elő R-ben mint
S-ben, akkor az előfordulások különbsége számúszor fog szerepelni az eredményben, ha
viszont S-ben legalább annyiszor szerepel, mint R-ben, akkor az eredménybe nem fog
belekerülni (természetesen negatív számúszor nem szerepelhet egy n-es).
A 𝐾1 − 𝐾2 műveletet elvégezve tehát 𝐾1 n-esei közül csak azok fognak megmaradni és
annyiszor, amelyek és amennyivel többször szerepelnek 𝐾1 -ben, mint 𝐾2 -ben. Mivel 𝐾1 2
„Irodát” tartalmaz, míg 𝐾1 csak egyet, ezért az eredményben egyszer fog szerepelni. „Nappali”
𝐾2 -ben többször fordul elő, mint 𝐾1 -ben, ezért az eredménybe nem fog belekerülni egyszer
sem. „Kert” mindkét relációban egyszer fordul elő, ezért az eredménybe szintén nem kerül
bele (2.29. táblázat).
Kategória
Iroda
2.29. táblázat: K1 - K2
25
{„Nagy Bútor Kft”, „5000 Szolnok Fa u 2”, „nb@example. org”} n-esét, mindkét párosítást benne is
hagyjuk az eredményben (2.31. táblázat).
Például, ha a 2.23. táblázatban ábrázolt termék reláció alapján meg szeretnénk válaszolni azt a kérdést,
hogy „Melyik gyártóktól van jelenleg termék raktáron?”, akkor a 𝜎𝐺𝑦á𝑟𝑡ó𝑁é𝑣 (𝑇𝑒𝑟𝑚é𝑘) lekérdezés
eredményében a „Nagy Bútor Kft” kétszer is szerepel. A probléma még szembetűnőbb, ha például 5
gyártó 100 fajta termékét árusítjuk, hiszen akkor az 5 gyártó különböző ismétlődésekkel összesen
százszor fog megjelenni az eredményben, ezáltal emberi felhasználásra alkalmatlanná téve azt.
A fenti problémára megoldás a 𝛿(𝑅) művelet, amely az 𝑅 relációból eltávolítja az ismétlődő n-eseket,
mintegy halmazzá alakítva azt. A 𝛿(𝜋𝐺𝑦á𝑟𝑡ó𝑁é𝑣 (𝑇𝑒𝑟𝑚é𝑘)) kifejezést a fenti példában említett Termék
reláción alkalmazva az eredmény tehát 4 helyett 3 sort fog tartalmazni (2.32. táblázat).
26
GyártóNév
Nagy Bútor Kft
Fabric Rt
Bútorgyár
2.3.6 Aggregálás
Az eddig megismert műveletek segítségével egy reláció egyes n-eseit egymástól függetlenül dolgoztuk
fel (eltekintve a halmazok esetén implicit / zsákok esetén explicit alkalmazott duplikáció eltávolítástól).
Gyakori feladat viszont, hogy több n-esről együttesen tudjunk megállapítani dolgokat. Például meg
tudjuk számolni a bizonyos feltételeket kielégítő n-eseket, vagy adott oszlop értékeit összegezni,
átlagolni stb. tudjuk. Ha meg szeretnénk válaszolni azt a kérdést, hogy mennyibe kerül a
nyilvántartásunk szerinti legdrágább termék, akkor az eredmény az Á𝑟 oszlop értékeinek
aggregálásával állítható elő úgy, hogy a maximum értéket keressük meg az összes érték közül. A
leggyakrabban használt aggregációs műveletek a következők:
1. 𝐶𝑂𝑈𝑁𝑇: a 𝐶𝑂𝑈𝑁𝑇(𝐴) művelet megszámolja, hogy hány ürestől különböző, de nem feltétlen
egyedi érték található az A oszlopban.
2. 𝑆𝑈𝑀: a 𝑆𝑈𝑀(𝐴) művelet összegzi az A oszlopban tárolt numerikus értékeket.
3. 𝑀𝐼𝑁, 𝑀𝐴𝑋: a 𝑀𝐼𝑁(𝐴) , illetve 𝑀𝐴𝑋(𝐴) operátorok az A oszlop legkisebb, illetve legnagyobb
értékeit adják vissza. Numerikus értékeket tároló oszlopoknál a működésük egyértelmű,
stringeket tároló oszlopok esetén az alfabetikusan legkisebb, illetve legnagyobb értéket
eredményezik.
4. 𝐴𝑉𝐺: az 𝐴𝑉𝐺(𝐴) művelet az A oszlopban tárolt numerikus értékek átlagát állítja elő.
A fenti műveleteket tipikusan egy projekció vagy egy csoportosítás (2.3.7) művelet részeként
alkalmazzuk. A 2.23. táblázatban elvégzett 𝜋𝑀𝐼𝑁(Á𝑟)→𝑚𝑖𝑛𝑖𝑚𝑢𝑚,𝑀𝐴𝑋(Á𝑟)→𝑚𝑎𝑥𝑖𝑚𝑢𝑚 (𝑇𝑒𝑟𝑚é𝑘)
lekérdezés eredményének sémája két attribútummal fog rendelkezni: 𝑚𝑖𝑛𝑖𝑚𝑢𝑚 és 𝑚𝑎𝑥𝑖𝑚𝑢𝑚, a
reláció pedig egy n-est fog tartalmazni az Á𝑟 oszlopban talált legkisebb és legnagyobb értékkel (2.33.
táblázat).
minimum maximum
12.000 150.000
Fontos, hogy ha aggregálás műveleteket végzünk el projekció közben, akkor az eredménynek mindig
egy sora lesz, amely az aggregált értékeket tartalmazza. Ebben az egy sorban csak aggregált értékek
szerepelhetnek, egyszerű attribútumok nem (mivel nem lenne egyértelmű, hogy az aggregált
értékekhez melyik egyedi n-es attribútumainak értéke társul).
2.3.7 Csoportosítás
A 2.3.6 fejezetben bemutattuk a leggyakrabban használt aggregáló operátorokat, amelyek egy reláció
valamely oszlopának összes értékén végeznek valamilyen típusú összegzést. Sokszor előfordul
27
azonban, hogy az aggregálást nem az összes n-esen egyszerre, hanem az n-eseket csoportosítva, a
csoportokon külön-külön szeretnénk elvégezni. Erre például olyan kérdések megválaszolásához lehet
szükség, mint hogy „a termékek átlag ára kategóriánként”, „a forgalmazott termékek száma
gyártónként” vagy például „az összes raktározott termékszám kategóriánként és gyártónként”. Ehhez
a forrás reláció n-eseit kategóriánként / gyártónként / kategória-gyártó párosonként csoportokra kell
bontani, és az átlagolást / számlálást / összegzést csoportonként kell elvégezni.
𝛾𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎,𝑆𝑈𝑀(𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡)→𝐾é𝑠𝑧𝑙𝑒𝑡 (𝑇𝑒𝑟𝑚é𝑘)
Az operátor a Termék relációt a 2.34. táblázatban látható három képzeletbeli csoportra bontja.
28
Kategória RaktárkészletKészlet
Nappali 6
Iroda 15
Kert 1
Megjegyezzük, hogy a duplikáció eliminálás operátor kifejezhető csoportosítás segítségével úgy, hogy
a reláció valamennyi attribútumát felsoroljuk csoportosító attribútumként, de nem alkalmazunk
aggregálást. Ezáltal az ekvivalens n-esek és csak azok azonos csoportokba fognak kerülni, és
csoportonként csak egy n-es (az összes attribútummal) kerül az eredmény relációba. Vagyis 𝛿(𝑅) ≡
𝛾𝐴1 …𝐴𝑛 (R), ahol 𝐴1 … 𝐴𝑛 𝑅 összes attribútumát jelöli.
2.3.8 Rendezés
Lekérdezések megfogalmazásakor gyakori igény, hogy az eredményt rendezetten kapjuk meg. Például
gyártók a neveik szerint ABC sorrendbe rendezve, vagy a termékek az áraik szerint növekvő sorrendbe
rendezve. Elméleti szempontból a rendezésnek nincs különösebb jelentősége, sokkal inkább az adatok
emberek általi értelmezését könnyíti meg. A rendezést reláció algebrában a 𝜏𝐿 (𝑅) operátorral jelöljük,
ahol 𝐿 a rendező attribútumok listája: 𝐴1 , 𝐴2 , … , 𝐴𝑛 . A rendezés során az n-esek először 𝐴1 szerint
növekvő, azonos 𝐴1 értékekkel rendelkező n-esek 𝐴2 szerint növekvő stb. módon kerülnek rendezésre.
Ha két n-es valamennyi rendező attribútumra ugyanazokkal az értékekkel rendelkezik, akkor ezen n-
esek sorrendje nem definiált.
Ha a 2.23. táblázatban látható termék relációt például raktárkészlet és ár szerint szeretnénk sorba
rendezni, az a 𝜏𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡,Á𝑟 (𝑇𝑒𝑟𝑚é𝑘) kifejezéssel lehetséges. Mivel a 𝐾𝑎𝑛𝑎𝑝é és a
𝐷𝑜ℎá𝑛𝑦𝑧ó𝑎𝑠𝑧𝑡𝑎𝑙 nevű termékekből egyaránt 3 darab van raktáron, ezért kettejük sorrendjét az áruk
fogja meghatározni (2.36. táblázat).
A kulcs azon attribútumok összessége, amelyekhez az n-esek által rendelt értékek együttesen
garantáltan egyediek az összes lehetséges n-esre: nem csak egy konkrét relációban, hanem a reláció
29
összes lehetséges példányában. A kulcs attribútumokhoz rendelt értékek ismeretével tehát
egyértelműen azonosítani tudjuk a reláció egy konkrét n-esét.
A 2.37. táblázatban látható 𝑇𝑒𝑟𝑚é𝑘 relációban egy lehetséges kulcs például a termékek neve, de csak
akkor, ha feltesszük, hogy nem árusítunk két azonos nevű terméket két különböző gyártótól. De
például egy gyártó valószínűleg nem fog két különböző, de teljesen azonos nevű terméket gyártani, a
termék neveket legalább egy kóddal vagy utótaggal meg fogja különböztetni. Tehát a Név és a
GyártóNév együtt már jó kulcsnak ígérkezik. A kulcsot aláhúzással jelöljük a relációs sémában:
A fenti néhány n-es alapján azt is gondolhatnánk, hogy az Á𝑟 attribútum is kulcs, hiszen egyedi mind a
négy nyilvántartott termékre. Ne felejtsük el azonban, hogy a kulcs a reláció nem csak egy konkrét
példányára egyedi, hanem az összes lehetséges változatára, és természetesen nem kizárt két
különböző termék azonos árral.
Bár jelen esetben a termék és a gyártó neve együtt egyedinek tűnik, a gyakorlatban legtöbbször nem
szokták az egyedek ún. természetes attribútumait kulcsként használni, mivel azok idővel változhatnak,
túl hosszúak, összetettek (sok attribútum együttesen kulcs), hanem inkább egy mesterséges azonosítót
rendelnek az egyedekhez, amely rövid és állandó. Ilyen azonosító lehet például egy személy személyi
száma, taj száma , egy könyv ISBN száma, egy termék kód, alvázszám, motorszám stb. Sokszor még
ezeknél is tömörebb és egyszerűbb kulcsot választanak akár az említett azonosítók mellé: például egy
futó sorszámot, ami minden egyes új n-esnél, amit a relációba beszúrunk, eggyel nő.
A kulcsot relációalgebrai kifejezésekkel is ki tudjuk fejezni. Mivel egy kulcs érték egyértelműen
meghatározza egy R reláció pontosan egy n-esét, ebből az következik, hogy a reláció minden egyes n-
esét csak saját magával tudjuk összepárosítani, ha a relációt saját magával theta-illesztjük a kulcs
attribútum mentén. Vagyis a párost előállító két n-es attribútum értékei páronként megegyeznek,
másképp fogalmazva: nincs olyan párosítás, amelyben a megfelelő attribútum értékek a két n-esben
különböznek. A 𝑇𝑒𝑟𝑚é𝑘 reláció esetében ez a következőképp fogalmazható meg:
30
2.4.2 Külső kulcs
A gyártói adatokat tároló 𝐺𝑦á𝑟𝑡ó relációban (2.38. táblázat) a gyártó neve (mivel egy cég név) egyedi
kell, hogy legyen, ezért használhatjuk azt kulcsként. Figyeljük meg, hogy a 𝑇𝑒𝑟𝑚é𝑘 relációban szintén
van egy 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 attribútum, amely azt rögzíti, hogy az egyes termékeket ki gyártja. A 𝐺𝑦á𝑟𝑡ó𝑁é𝑣
attribútum a 𝑇𝑒𝑟𝑚é𝑘 relációban egy külső kulcs, amelynek lehetséges értékei a 𝐺𝑦á𝑟𝑡ó reláció
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 attribútumához (kulcsához) rendelt értékek közül kerülhetnek ki. Vagyis a 𝑇𝑒𝑟𝑚é𝑘
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 attribútumával egy konkrét 𝐺𝑦á𝑟𝑡ó𝑟𝑎 mutatunk rá. Általánosan is igaz, hogy a külső kulcs
célja, hogy különböző relációk egyedei között kapcsolatot létesítsen úgy, hogy az egyik reláció egy n-
eséből rámutat egy másik (vagy akár ugyanazon) reláció egy n-esére úgy, hogy eltárolja a mutatott n-
es kulcs értékét.
Mivel a külső kulcs értékek mind a kulcs értékek közül kell, hogy kikerüljenek, következésképpen a
külső kulcs értékek között nincs olyan, amely nem szerepel a kulcs értékek között. Ez az átfogalmazott
feltétel kifejezhető a különbség művelettel:
Ha a kulcs (és így a külső kulcs is) több attribútumból áll, akkor a vetítés műveleteknél az összes kulcs
attribútumot ki kell választani.
31
nem negatív egész szám, a termék nevét kötelező kitölteni, egy személy neme csak ’f’ (férfi) vagy ’n’
(nő) lehet stb:
Előírhatunk összetettebb, több reláción átívelő kényszert is. Például megkövetelhetjük, hogy csak
olyan gyártók termékeit forgalmazzuk, amelyeknek a postacíme ismert:
A félig strukturált adatmodellek esetén nem válik élesen ketté az adat és a séma, mint a klasszikus
kétdimenziós relációk esetén, hanem a sémainformáció valamilyen címkék formájában megjelenik az
adatreprezentációban is. Az ilyen adatmodellek tipikusan fa vagy gráf-alapú megközelítést
alkalmaznak, az adatmodellek leírására leggyakrabban XML1 vagy JSON2 formátumot használnak. Az
adatstruktúrák szülő és (fák esetén) gyermek csomópontokból épülnek fel, amelyekhez attribútumok
segítségével rendelhetünk értékeket.
A 2.1. táblázatban ismertetett 𝑇𝑒𝑟𝑚é𝑘 reláció egy lehetséges XML reprezentációja a következő:
<Termékek>
<Termék Név=“Kanapé” Raktárkészlet=“3”>
<Ár>150.000</Ár>
<Kategória>Nappali</Kategória>
1
Extensible Markup Language (XML) - http://www.w3.org/XML
2
The JSON Data Interchange Format - http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-
404.pdf
32
<GyártóNév>Nagy Bútor Kft</GyártóNév>
</Termék>
<Termék Név=“Dohányzóasztal” Raktárkészlet=“3”>
<Ár>12.000</Ár>
<Kategória>Nappali</Kategória>
<GyártóNév>Nagy Bútor Kft</GyártóNév>
</Termék>
<Termék Név=“Szék” Raktárkészlet=“15”>
<Ár>22.000</Ár>
<Kategória>Iroda</Kategória>
<GyártóNév>Fabric Rt</GyártóNév>
</Termék>
<Termék Név=“Hintaágy” Raktárkészlet=“1”>
<Ár>32.000</Ár>
<Kategória>Kert</Kategória>
<GyártóNév>Bútorgyár</GyártóNév>
</Termék>
</Termékek>
Ugyanazt az adatot számtalan módon le lehet írni XML-ben, a tervező döntésén múlik, hogy mit
reprezentál attribútumként és mit címkeként. A fenti példában a nevet és raktárkészletet
attribútumként, a többi tulajdonságot címke segítségével írtuk le.
A példában jól látszik, hogy XML dokumentumok esetén a séma információ (címke és attribútum
nevek, csomópontok hierarchiája) megjelenik az adatábrázolásban is.
{
"Termékek": {
"Termék": [
{
"Név": "Kanapé",
"Raktárkészlet": "3",
"Ár": "150.000",
"Kategória": "Nappali",
"GyártóNév": "Nagy Bútor Kft"
},
{
"Név": "Dohányzóasztal",
"Raktárkészlet": "3",
"Ár": "12.000",
"Kategória": "Nappali",
"GyártóNév": "Nagy Bútor Kft"
},
{
"Név": "Szék",
"Raktárkészlet": "15",
"Ár": "22.000",
"Kategória": "Iroda",
33
"GyártóNév": "Fabric Rt"
},
{
"Név": "Hintaágy",
"Raktárkészlet": "1",
"Ár": "32.000",
"Kategória": "Kert",
"GyártóNév": "Bútorgyár"
}
]
}
}
A fa struktúrával leírt adatmodellekben általában olyan lekérdező nyelvet használnak, ami megkönnyíti
a csomópontok közötti navigációt és keresést. XML esetén ez leggyakrabban az XPath3 lekérdező nyelv
szokott lenni. Például azon termék kiválasztása, amelynek a neve „Hintaágy”, a következő XPath
lekérdezéssel lehetséges:
//Termékek/Termék[@Név=’Hintaágy’]
Napjaink népszerű félig strukturált adatbázis-kezelő rendszerei közé tartozik például a CouchDB5, a
MongoDB6 vagy az Elasticsearch7.
3
XML Path Language (XPath) Version 1.0 - http://www.w3.org/TR/xpath/
4
XML Schema Definition Language (XSD) 1.1 - http://www.w3.org/TR/xmlschema11-1
5
Apache CouchDB - http://couchdb.apache.org/
6
MongoDB - https://www.mongodb.org/
7
Elasticsearch - https://www.elastic.co/
34
3 Relációs adatbázisok tervezésének elmélete
𝐴1 … 𝐴𝑚 → 𝐵1 … 𝐵𝑛
Egy funkcionális függőség nem csak egy konkrét reláció példányra kell, hogy teljesüljön, hanem minden
olyan n-esre, ami előfordulhat az adott relációban, tehát az adott relációra általánosan igaz.
A funkcionális jelző onnan ered, hogy az ilyen függőségek gyakorlatilag függvények, amelyek
paraméterként egy konkrét 𝐴1 , … , 𝐴𝑚 attribútum érték n-est kapnak meg, és ehhez az n-eshez
rendelnek hozzá egy 𝐵1 , … , 𝐵𝑛 érték n-est.
Tekintsük a 𝐺𝑦á𝑟𝑡ó(𝐺𝑦á𝑟𝑡ó𝑁é𝑣, 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚, 𝑉á𝑟𝑜𝑠, 𝐶í𝑚, 𝐸𝑚𝑎𝑖𝑙) relációt (3.1. táblázat). A reláció
az eddig alkalmazottal (2.38. táblázat) ellentétben néhány új attribútumot is tartalmaz: az eredeti 𝐶í𝑚
attribútumot három részre bontottuk: 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚, 𝑉á𝑟𝑜𝑠 és 𝐶í𝑚 – ez utóbbi már csak az
utca/házszám értéket tartalmazza.
35
GyártóNév Irányítószám Város Cím Email
Nagy Bútor Kft 5000 Szolnok Fa u 2 nb@example.org
Fabric Rt 9012 Győr Ág u 15 fabric@example.org
Bútorgyár 1111 Budapest Hős krt 3 xyb@example.org
Kiss Bútor Kft 1111 Budapest Virágos tér 4 kb@example.org
A 𝐺𝑦á𝑟𝑡ó relációban kijelenthetjük, hogy ha két cégnek ugyanaz az irányítószáma, akkor ugyanaz lesz
a 𝑉á𝑟𝑜𝑠 mező értéke is. Ez általánosan is igaz, hiszen egy irányítószám pontosan egy települést határoz
meg. Formálisan:
𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠
𝑓(1111) = "𝐵𝑢𝑑𝑎𝑝𝑒𝑠𝑡"
𝑓(5000) = "𝑆𝑧𝑜𝑙𝑛𝑜𝑘"
𝑓(9012) = "𝐺𝑦ő𝑟"
𝑉á𝑟𝑜𝑠 → 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚
függőséget is, hiszen a példában Budapesthez mindig a 1111-es irányítószám tartozik, de ne felejtsük,
hogy a függőségek valamennyi reláció példányra vonatkoznak, és nem kizárt (mint például Budapest
esetében), hogy egy város több irányítószámmal is rendelkezik. Tehát a városnévből nem következik
egyértelműen egy konkrét irányítószám.
A fenti példában még számos egyéb függést is felfedezhetünk: egy gyártó neve (mivel a cégnevek
egyediek kell, hogy legyenek), egyértelműen meghatározza a gyártó székhelyének címét, illetve e-mail
címét, ahol kapcsolatba léphetünk velük, vagyis
függőség is teljesül. Sokszor meg is követeljük, hogy az egyes felhasználók különböző e-mail címekkel
regisztráljanak egy rendszerbe. Az
függőséget viszont nem feltételezhetjük, hiszen gyakori, hogy ugyanarra a címre több cég is be van
jegyezve, például egy irodaház esetén. A fenti példák alapján jól látható, hogy a függőségek
megállapításánál nagyon körültekintően kell eljárnunk, mivel ezáltal felesleges, esetenként
teljesíthetetlen kötöttségeket vihetünk be az adatbázisunkba.
36
Az olyan függőségeket, amelyek jobb oldalán egynél több attribútum áll (mint például 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 →
𝐼𝑟á𝑛𝑦í𝑡ó𝑆𝑧á𝑚 𝑉á𝑟𝑜𝑠 𝐶í𝑚 𝐸𝑚𝑎𝑖𝑙), több egyszerű függőségre is bonthatjuk, ahol a jobb oldalon
csupán egy attribútum áll:
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝐼𝑟á𝑛𝑦í𝑡ó𝑆𝑧á𝑚
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝑉á𝑟𝑜𝑠
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝐶í𝑚
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝐸𝑚𝑎𝑖𝑙
𝐴1 … 𝐴𝑛 → 𝐵1 … 𝐵𝑚
𝐴1 … 𝐴𝑛 → 𝐵1 , . .. , 𝐴1 … 𝐴𝑛 → 𝐵𝑚
függőségek halmazára. A felbontás művelet visszafelé is alkalmazható: azonos bal oldallal rendelkező
függőségek összekombinálhatók egy függőségbe, amely jobb oldalán az egyedi jobb oldalak uniója áll.
Triviálisnak nevezzük azokat a funkcionális függőségeket, amelyek egy reláció valamennyi példányára
teljesülnek, függetlenül az esetleges egyéb függőségektől. Ezen függőségek általános alakja:
Vagyis a függőség jobb oldala a bal oldal részhalmaza. Az ilyen jellegű függőségek annyit állítanak, hogy
ha az 𝐴1 … 𝐴𝑛 attribútumokhoz rendelt értékek két n-esben megegyeznek, akkor ezen attribútumok
részhalmazára is meg fognak egyezni. Például
𝐸𝑚𝑎𝑖𝑙 → 𝐸𝑚𝑎𝑖𝑙
A nem triviális funkcionális függőségek ezzel szemben olyan függőségek, amelyek jobb oldalán
szerepel olyan attribútum, ami a bal oldalon nem:
Például:
𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠
Ez utóbbi példánál, bár az 𝐸𝑚𝑎𝑖𝑙 attribútum a bal és jobb oldalon is szerepel, a 𝑉á𝑟𝑜𝑠 attribútum csak
a jobb oldalon fordul elő, így a függőség nem triviális.
37
A teljesen nem triviális függőségek esetén a jobb oldalon csak a bal oldalon nem szereplő
attribútumok fordulhatnak elő:
Például:
𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠
A nem triviális függőségek mindig átalakíthatók velük ekvivalens jelentéssel bíró, teljesen nem triviális
függőségekké, ha a jobb oldalról eltávolítjuk a bal oldalon is szereplő attribútumokat: ha
akkor
3.2 Kulcsok
Egy 𝑅(𝐴1 , … , 𝐴𝑛 ) reláció kulcsának azt a {𝐾1 , … , 𝐾𝑘 } attribútum halmazt nevezzük, i) amelytől
funkcionálisan függ a reláció összes többi attribútuma, és ii) ez semelyik valódi részhalmazára nem igaz.
A fenti példában a 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 attribútum kulcs, hiszen a cégnevek egyediek kell, hogy legyenek,
következésképp egy cégnév egyértelműen meghatározza annak postacímét és elsődleges e-mail címét:
Mivel a bal oldalon egyetlen attribútum szerepel, ezért az biztosan minimális. Ha a bal oldal tartalmazza
a kulcs attribútumait, de esetleg még egyéb attribútumokat is, akkor beszélünk szuperkulcsról. A
szuperkulcs teljesíti a kulcs definíciójának egyik pontját (funkcionálisan függ tőle a reláció összes
attribútuma), de a másikat nem feltétlenül: nem feltétlenül minimális. Minden kulcs egyben
szuperkulcs is. A szuperkulcs ha minimális, akkor egyben kulcs is.
A kulcs nem szükségszerűen egyedi, ugyanazon relációnak több kulcsa is lehet. Példánkban mind a
𝐺𝑦á𝑟𝑡ó𝑁é𝑣, mind az 𝐸𝑚𝑎𝑖𝑙 attribútum egyértelműen meghatároz egy konkrét gyártót, tehát
mindkettő kulcs. Ha több kulcs is létezik, akkor ezek közül az egyiket ki szoktuk nevezni elsődleges
kulcsnak. Az elsődleges kulcs megválasztása adatbázis-kezelő rendszerekben általában teljesítmény
megfontolások alapján történik: ez sokszor egy egyszerű attribútum szokott lenni, amely egy
sorszámot vagy egy mesterséges azonosítót tartalmaz, nem pedig a modellezett entitás természetes
attribútumai közül áll elő (pl. cím, név stb). Mivel az elsődleges kulcs attribútumaihoz rendelt értékeket
használjuk fel más relációkban külső kulcs attribútum értékekként, illetve az ún. index állományokban
is (lsd. 7. fejezet), célszerű rövid, de garantáltan egyedi értékeket választani. Azokat a kulcsokat,
amelyeket nem választottunk ki elsődleges kulcsnak, kulcsjelöltnek is szoktuk nevezni. Ha tehát a
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 attribútumot elsődleges kulcsnak nevezzük ki, akkor az 𝐸𝑚𝑎𝑖𝑙 attribútum egy kulcsjelölt.
A kulcs lehet összetett is, vagyis állhat több attribútumból. A már jól ismert
38
𝑇𝑒𝑟𝑚é𝑘(𝑁é𝑣, 𝐺𝑦á𝑟𝑡ó𝑁é𝑣, Á𝑟, 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎)
relációban például nem feltételezzük, hogy két különböző gyártónak nem lehet azonos nevű terméke,
csak azt, hogy egy gyártó nem gyárt két különböző, de azonos nevű terméket. Vagyis egy terméknév
nem azonosít egyértelműen egy konkrét terméket, csak a gyártó nevével együtt. Tehát a reláció kulcsa:
{𝑁é𝑣, 𝐺𝑦á𝑟𝑡ó𝑁é𝑣}, ami minimális, mivel egy termék név nem azonosít egyértelműen egy gyártót,
illetve egy gyártó gyárthat több terméket is, tehát egy gyártó név nem azonosít egy termék nevet.
3. Tranzitivitás: Ha 𝐴1 … 𝐴𝑛 → 𝐵1 … 𝐵𝑚 és 𝐵1 … 𝐵𝑚 → 𝐶1 … 𝐶𝑘 , akkor 𝐴1 … 𝐴𝑛 → 𝐶1 … 𝐶𝑘 .
Például:
𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 ∧ 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠 ⇒ 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝑉á𝑟𝑜𝑠
39
𝐴 → 𝐴𝐵 ∧ 𝐴𝐵 → 𝐵𝐶 ⇒ 𝑨 → 𝑩𝑪, a tranzitivitás alapján.
Például: 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝑉á𝑟𝑜𝑠 ∧ 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝐶í𝑚 ⇒ 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝑉á𝑟𝑜𝑠 𝐶í𝑚
6. Pszeudotranzitivitás: Ha 𝐴 → 𝐵 é𝑠 𝐵𝐶 → 𝐷, akkor 𝐴𝐶 → 𝐷
𝐴 → 𝐵 mindkét oldalát 𝐶-vel bővítve kapjuk, hogy 𝑨𝑪 → 𝑩𝑪
A tranzitivitás miatt 𝐴𝐶 → 𝐵𝐶 ∧ 𝐵𝐶 → 𝐷 ⇒ 𝑨𝑪 → 𝑫
Példaként tekintsük a következő relációt:
𝑇𝑒𝑟𝑚é𝑘3(𝑁é𝑣, 𝐺𝑦á𝑟𝑡ó𝑁é𝑣, 𝐺𝑦á𝑟𝑡ó𝐴𝑑ó𝑠𝑧á𝑚, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎, Á𝑟, 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡)
Tehát a szokásos termék relációt (2.1. fejezet) kiegészítettük a gyártó adószámával. A reláció
kulcsa továbbra is {𝑁é𝑣, 𝐺𝑦á𝑟𝑡ó𝑁é𝑣}, következésképpen igaz, hogy 𝑁é𝑣 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → Á𝑟.
Egy cég adószáma viszont egyértelműen azonosítja a céget, vagyis teljesül a
𝐺𝑦á𝑟𝑡ó𝐴𝑑ó𝑠𝑧á𝑚 → 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 függőség, vagyis egy gyártó adószáma és egy terméknév
egyértelműen meghatározza az árat:
𝐺𝑦á𝑟𝑡ó𝐴𝑑ó𝑠𝑧á𝑚 𝑁é𝑣 → Á𝑟
Illetve nem csak az árat, hanem valamennyi attribútum értékét, hiszen a
{𝐺𝑦á𝑟𝑡ó𝐴𝑑ó𝑠𝑧á𝑚, 𝑁é𝑣} páros egy kulcsjelölt a 𝑇𝑒𝑟𝑚é𝑘3 relációban.
A lezárt előállítása egy iteratív algoritmussal történik: a kezdeti attribútum halmazhoz hozzávesszük az
összes olyan függőség jobb oldalát, amelyeknek a bal oldalán található összes attribútum már szerepel
a kezdeti halmazban. Ezután a kibővített halmazt tekintjük a kezdetinek, és megismételjük a bővítést
egészen addig, amíg már egyetlen funkcionális függőséget figyelembe véve sem tudunk új elemeket a
halmazhoz venni. Az így előálló halmaz lesz a kiindulási attribútumok lezártja. Formálisan:
Módszer:
40
1. Legyen 𝒀𝟎 = 𝑿, 𝒊 = 𝟎
Vagyis a halmazhoz hozzávesszük azon függőségek jobb oldalát, amelyek bal oldala már a
halmazban van.
Ha azt a kérdést szeretnénk megválaszolni, hogy 𝐶𝐷 → 𝐹 teljesül-e a fenti reláción, akkor tehát {𝐶, 𝐷}
lezártját kell kiszámolnunk. A 𝐷 → 𝐸 függőség miatt a halmazhoz hozzávehetjük az E attribútumot:
{𝐶, 𝐷, 𝐸}, majd ezután a 𝐷𝐸 → 𝐹 függőség miatt az 𝐹 attribútumot is: {𝐶, 𝐷, 𝐸, 𝐹}. Vagyis a keresett
függőség következik az 𝐹 függőséghalmazból.
A bázis tehát nem egyértelmű, és számos redundáns (a többi függőség segítségével kifejezhető)
függőséget is tartalmazhat. A gyakorlatban legtöbbször egy olyan bázisra van szükségünk, amelyből a
redundanciákat eltávolítottuk, és a függőségek a lehető legegyszerűbb formában szerepelnek. Ezt
hívjuk minimális bázisnak. A minimális bázisnak három feltételnek kell megfelelnie:
41
1. minden egyes szabály jobb oldalán pontosan egy attribútum szerepel,
2. bármely függőséget eltávolítva az eredmény már nem bázis (nem tartalmaz olyan
függőséget, amely következik a többi függőségből),
3. ha bármely szabály bal oldaláról eltávolítunk egy attribútumot, akkor a kapott
függőséghalmaz már nem bázis.
1. Ha egy szabály jobb oldalán több attribútum szerepel, akkor a felbonthatóság miatt
szétszedjük azt olyan szabályokra, amelyek jobb oldalán már csak egy-egy attribútum szerepel.
Vagyis az 𝑋 → 𝐴1 … 𝐴𝑛 alakú szabályokat felbontjuk 𝑋 → 𝐴1 , … , 𝑋 → 𝐴𝑛 szabályokra, ahol
𝐴𝑖 ∉ 𝑋 (a triviális függőségeket elhagyjuk).
2. Minden függőséget megvizsgálunk, hogy levezethető-e a többi segítségével. Ha igen, akkor
elhagyjuk. A vizsgálatot legegyszerűbben úgy tudjuk elvégezni, hogy minden egyes függőség
bal oldalának attribútum halmazára kiszámítjuk annak a lezártját az adott szabály nélkül, és ha
a lezártak között szerepel a szabály jobb oldalán álló összes attribútum, akkor a függőség
levezethető a többi függőségből is, tehát eltávolítható.
3. Azon függőségek esetén, ahol a bal oldal több attribútumból áll, az egyes bal oldali
attribútumokra egyenként megvizsgáljuk, hogy szerepelnek-e a függőség rajtuk kívüli bal oldali
attribútumainak lezártjában: vagyis 𝐴 → 𝐵 esetén valamely 𝑋 ∈ 𝐴-ra teljesül-e 𝑋 ∈
(𝐴 − {𝑋})+ vagy sem. Ha egy 𝑋 attribútumra ez teljesül, akkor eltávolíthatjuk a bal oldalról,
vagyis az 𝐴 → 𝐵 szabályt kicserélhetjük egy (𝐴 − {𝑋}) → 𝐵 szabályra.
1. Egyetlen szabály jobb oldalán szerepel két attribútum (𝐴 → 𝐵𝐶), ezért azt felbontjuk két
függőségre: 𝐹1 = {𝐴 → 𝐵, 𝐴 → 𝐶, 𝐶 → 𝐷, 𝐴 → 𝐷, 𝐴𝐷 → 𝐸}
2. A redundáns függőségek kiszűréséhez kiszámítjuk minden függőség bal oldalának lezártját a
függőség nélkül:
{𝐴}+ 𝐹−{𝐴→𝐵} = {A, C, D, E}, mivel 𝐵 nincs a lezártban, ezért az 𝐴 → 𝐵 függőség nem
távolítható el.
{𝐴}+
𝐹−{𝐴→𝐶} = {𝐴, 𝐵}, mivel 𝐶 nincs a lezártban, 𝐴 → 𝐶 sem távolítható el
{𝐴}+
𝐹−{𝐴→𝐷} = {𝐴, 𝐵, 𝐶, 𝐷, 𝐸}, mivel D szerepel a lezártban, ezért az 𝑨 → 𝑫 függőség
elhagyható, hiszen a többi szabály is kifejezi ezt a függőséget, mégpedig az 𝐴 → 𝐶, 𝐶 →
𝐷 szabályokból következik a tranzitív tulajdonság miatt.
{𝐶}+
𝐹−{𝐶→𝐷} = {𝐶}, a 𝐶 → 𝐷 függőség nem hagyható el.
{𝐴, 𝐷}+
𝐹−{𝐴𝐷→𝐸} = {𝐴, 𝐵, 𝐶, 𝐷}, az 𝐴𝐷 → 𝐸 függőség sem hagyható el.
𝐹2 = {𝐴 → 𝐵, 𝐴 → 𝐶, 𝐶 → 𝐷, 𝐴𝐷 → 𝐸}
3. Egyetlen függőség bal oldala áll egynél több attribútumból (𝐴𝐷 → 𝐸), ezért elég ezt az egyet
megvizsgálnunk. Mivel 𝐷 ∈ {𝐴}+, ezért 𝑫 elhagyható a függőség bal oldaláról.
𝐹3 = {𝐴 → 𝐵, 𝐴 → 𝐶, 𝐴 → 𝐸, 𝐶 → 𝐷}
42
A minimális bázis nem feltétlen egyértelmű. Tekintsük a 𝐺 = {𝐴 → 𝐵, 𝐵 → 𝐶, 𝐶 → 𝐴}
függőséghalmazt. Az egyes függőségek bal és jobb oldalán csupán 1-1 attribútum szerepel, viszont
bármelyik függőség kifejezhető a másik kettő segítségével a tranzitív tulajdonság alkalmazásával.
Ennek megfelelően 3 minimális bázisa is létezik a 𝐺 halmaznak:
{𝐴 → 𝐵, 𝐵 → 𝐶}
{𝐵 → 𝐶, 𝐶 → 𝐴}
{𝐶 → 𝐴, 𝐴 → 𝐵}
Módszer:
1. 𝑭’ = {}
Tehát 𝑭’-höz azokat a függőségeket adjuk, amelyek levezethetők 𝑭-ből, de csak 𝑹’-beli
attribútumokat használnak.
43
1. {𝐴}+ = {𝐴, 𝐵, 𝐶, 𝐷}, vagyis 𝐴 → 𝐵, 𝐴 → 𝐶, 𝐴 → 𝐷, 𝐴 → 𝐸. Mivel 𝐵 nem szerepel a vetített
attribútumok között, ezért 𝐹1′ = {𝐴 → 𝐶, 𝐴 → 𝐷, 𝐴 → 𝐸}.
2. {𝐶}+ = {𝐶}, vagyis nem találtunk nemtriviális függőséget.
3. {𝐷}+ = {𝐷, 𝐸}, tehát 𝐷 → 𝐸. Így 𝐹2′ = {𝐴 → 𝐶, 𝐴 → 𝐷, 𝐴 → 𝐸, 𝐷 → 𝐸}
4. {𝐸}+ = {𝐸}, nem eredményez új függőséget.
Mivel {𝐴} lezártja az összes attribútumot tartalmazza, ezért csak az 𝐴-t nem tartalmazó részhalmazokat
érdemes tovább vizsgálni:
5. {𝐶, 𝐷}+ = {𝐶, 𝐷, 𝐸}, nem eredményez új függőséget, hiszen 𝐷 → 𝐸 már ismert
6. Hasonlóképpen {𝐶, 𝐸}, {𝐷, 𝐸} és {𝐶, 𝐷, 𝐸} sem eredményez új függőséget.
1. Redundancia: ugyanazt az adatot többször is eltároljuk. A példában a Nagy Bútor Kft adatai
kétszer is szerepelnek, mivel két termékét is forgalmazzuk a cégnek. Természetesen, ha több
terméket is forgalmazunk egy cégtől, akkor annyiszor tároljuk el (feleslegesen) a cég adatait,
ahány terméket forgalmazunk tőlük. Ez nyilvánvalóan pazarolja a tárolási kapacitást.
2. Módosítási anomália: a redundáns tárolásból kifolyólag bizonyos adatok megváltoztatását
több helyen is el kell végeznünk. Ha a Nagy Bútor Kft e-mail címe megváltozik, és ezt
szeretnénk az adatbázisunkban is eltárolni, akkor nem elég egy helyen átírnunk az e-mail
címet, hanem annyi helyen kell ezt elvégeznünk, ahány termékét forgalmazzuk.
3. Törlési anomália: mivel egy n-es egyszerre több különálló dologról is hordoz információt,
ezért egy n-es eltávolításával egyszerre több dologról törlünk információt. Tegyük fel, hogy
befejezzük a Hintaágy nevű termék forgalmazását, és töröljük a kapcsolódó bejegyzést. Ezzel
egyúttal a Bútorgyár nevű gyártó összes adatát is töröljük, még ha ez nem is állt
szándékunkban.
44
Az anomáliák megszűntetésére a megoldás a reláció dekompozíciója, szétszedése kisebb darabokra.
Az 𝑅(𝐴1 , … , 𝐴𝑛 ) reláció dekompozíciójakor úgy bontjuk azt szét 𝑆(𝐵1 , … , 𝐵𝑚 ) és 𝑇(𝐶1 , … , 𝐶𝑘 )
relációkra, hogy
A vetítés elvégzése után a már ismert Termék és Gyártó relációkat kapjuk (3.3. táblázat, 3.4. táblázat).
A dekompozícióval megszűntettük a gyártók adatainak redundáns tárolását: most már minden egyes
gyártóról csupán egy n-esben tárolunk adatokat. Megoldottuk a módosítási anomáliát, hiszen, ha egy
gyártónak változik valamelyik tulajdonsága, akkor elég csupán egy n-esben módosítani. (Kivétel ez alól
a gyártó neve, mivel az kulcsként és külső kulcsként is funkcionál, így annak megváltozása esetén a
Termék relációban is frissíteni kell a hivatkozásokat. Erre szolgál megoldásként egy mesterséges kulcs
– pl. egy futó sorszám – használata, amely soha nem változik meg). A dekompozíció szintén
megszűntette a törlési anomáliát, hiszen egy gyártó utolsó termékének törlése után a gyártó alap
adatai még mindig rendelkezésre fognak állni a Gyártó relációban. A dekompozíció szükség esetén
rekurzívan tovább alkalmazható, tehát a már eredményül kapott kisebb relációkat is tovább
bonthatjuk. De természetesen nem mindegy, hogy a dekompozíciót hogy hajtjuk végre, mely
attribútumokat tesszük az egyik, melyeket a másik relációba, és melyek jelennek meg mindkét
relációban.
A dekompozíció módja döntően befolyásolja, hogy az eredeti információ tartalmat helyre tudjuk-e
állítani a dekomponált táblázatokból: nem veszítünk adatot, és nem jelennek meg érvénytelen adatok
45
a helyreállítás során. A redundáns relációk felbontására szisztematikus módszereket az ún. normál
formák, és a normál formára alakító algoritmusok biztosítanak, amelyeket a továbbiakban részletesen
is ismertetünk.
Ezt a lehetőséget tiltja meg a Boyce-Codd normál forma (BCNF) [4]: Egy 𝑅 reláció akkor és csak akkor
van Boyce-Codd normál formában, ha a relációban minden nem triviális 𝐴1 … 𝐴𝑛 → 𝐵1 … 𝐵𝑚
funkcionális függőségre {𝐴1 , … , 𝐴𝑛 } szuperkulcs 𝑅-ben. Másképp fogalmazva: minden nem triviális
funkcionális függőség bal oldalában szerepel egy kulcs.
Vegyük észre, hogy a definíció nem szabja meg, hogy ugyanannak a kulcsnak kell szerepelnie minden
bal oldalban, a kulcs pedig nem feltétlen egyedi egy relációban.
A 𝑇𝑒𝑟𝑚é𝑘𝐺𝑦á𝑟𝑡ó reláció egyértelműen sérti a BCNF kritériumát: a reláció kulcsa a {𝑁é𝑣, 𝐺𝑦á𝑟𝑡ó𝑁é𝑣}
attribútum páros, hiszen ahogyan már korábban is megállapítottuk, a termék neve nem feltétlen
egyedi, de a gyártó nevével együtt már bizonyosan az. Egy konkrét termék pedig egyértelműen
meghatározza a saját és gyártója tulajdonságait. Más attribútum kombinációt, amely egyértelműen
meghatározna egy terméket, nem találunk. Ebből következően a nem triviális funkcionális függőségek
bal oldala csak ez az attribútum páros, vagy ennek egy kibővített változata lehet. A 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝐶í𝑚
ill. 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 → 𝐸𝑚𝑎𝑖𝑙 függőségek pedig ennek nem felelnek meg.
A 𝐺𝑦á𝑟𝑡ó reláció (3.4. táblázat) ezzel szemben BCNF-ban van, hiszen benne csak az előbbi két függőség
áll fenn, mivel ugyanarra a címre több gyártó is be lehet jegyezve, illetve egy e-mail cím (ha az például
egy kisvállalkozás ügyvezetőjének a címe) több gyártót is azonosíthat (pl különböző profilú
vállalkozások közös tulajdonossal). Tehát az 𝐸𝑚𝑎𝑖𝑙 és 𝐶í𝑚 attribútumoktól nem függ funkcionálisan
semelyik másik attribútum. Mivel a 𝐺𝑦á𝑟𝑡ó𝑁é𝑣 attribútumtól viszont funkcionálisan függ a többi, ezért
az lesz a reláció kulcsa, így teljesül a BCNF kritériuma, amely szerint minden függőség bal oldalán
szuperkulcs kell, hogy álljon.
Azt, hogy az adatbázis sémánk BCNF-ben legyen, úgy tudjuk elérni, hogy a nem BCNF-ben lévő
relációkat kisebb részekre dekomponáljuk, amelyekben már biztosított a BCNF. Természetesen nem
mindegy, hogy hogy darabolunk fel egy relációt, mert lehet, hogy az eredményre teljesül a BCNF, de
közben információt vesztünk. A dekomponálás során keresünk egy olyan funkcionális függőséget,
amely sérti a normál formát, tehát a bal oldala nem szuperkulcs. Ezután képezzük a bal oldali
attribútumok lezártját, az így kapott attribútumokat tesszük az egyik relációba (amelynek a kulcsa –
vagy szuperkulcsa – a függőség bal oldala lesz, hiszen tőle a reláció összes attribútuma funkcionálisan
függ), a másik relációba pedig a lezárton kívüli attribútumokat és a vizsgált függőség bal oldalának
attribútumait (amelyek így külső kulcsként fognak funkcionálni). A műveletsorozatot addig ismételjük
a keletkezett relációkra, amíg találunk bennük BCNF-t sértő függőséget. Amikor egy relációt két részre
46
bontunk, akkor a relációhoz tartozó függőségeket is vetítenünk kell a két rész attribútumaira, mivel a
kisebb relációkon az eredeti függőséghalmaznak már csak egy része fog teljesülni.
Módszer:
1. 𝑅0 = 𝑅, 𝐹0 = 𝐹 kezdeti értékek
2. Ha 𝑅𝑖 BCNF-ben van, akkor a válasz {𝑅𝑖 }
3. Legyen 𝑋 → 𝑌 ∈ 𝐹𝑖 egy BCNF-et sértő függőség
4. Számítsuk ki 𝑋 + -t
5. Legyen 𝑅𝑖+1 = 𝑋 + és 𝑅𝑖+2 pedig 𝑅 𝑋 + nélküli attribútumai és 𝑋 uniója; 𝐹𝑖+1 és 𝐹𝑖+2 legyenek
𝐹𝑖 projekciói 𝑅𝑖+1 -re és 𝑅𝑖+2 -re.
6. Végezzük el a felbontást 2.-től rekurzívan 𝑅𝑖+1 -re és 𝑅𝑖+2 -re. A válasz a két felbontás uniója.
Példaként tekintsük az
Ö𝑠𝑠𝑧𝑉á𝑠á𝑟𝑙á𝑠(𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑, 𝑇𝑒𝑟𝑚é𝑘𝑁é𝑣, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎, 𝑆𝑧í𝑛, 𝑉𝑒𝑣ő𝐾ó𝑑, 𝑉𝑒𝑣ő𝑁é𝑣, 𝐼𝑟𝑠𝑧á𝑚, 𝑉á𝑟𝑜𝑠, 𝐶í𝑚, 𝑇𝑒𝑙𝑒𝑓𝑜𝑛, 𝐷𝑎𝑟𝑎𝑏)
301232 Kerti szék Kert Fehér 87438 Nagy János 1111 Bp Ág u 1 123-456 6
412340 Műanyag Kert Fehér 87438 Nagy János 1111 Bp Ág u 1 123-456 1
asztal
301232 Kerti szék Kert Fehér 98142 Kovács Péter 7622 Pécs Tél u 3 234-567 2
674573 Fotel Nappali Barna 01845 Kiss Norbert 9028 Győr Ősz u 6 345-678 1
𝐼𝑟𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠
Ahhoz, hogy meg tudjuk állapítani, hogy mely függőségek sértik a BCNF-t, először meg kell keresnünk
a reláció kulcsát. Érdemes azokkal az attribútumokkal kezdeni a vizsgálatot, amelyek semelyik másiktól
sem függnek funkcionálisan (ha vannak ilyenek), hiszen azok az attribútumok az összes kulcs részének
kell, hogy legyenek. Ezek az attribútumok: 𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑, 𝑉𝑒𝑣ő𝐾ó𝑑. Ezeknek az attribútumoknak a
lezártja a reláció valamennyi attribútumát tartalmazza, ezért ez a páros biztosan szuperkulcs, és mivel
47
minimális is (hiszen egyik sem függ funkcionálisan a másiktól), ezért kulcs is. Mellesleg, mivel ezek az
attribútumok valamennyi kulcs részének kell, hogy legyenek, de már önmagukban is kulcsot alkotnak,
ezért biztosan az egyetlen kulcs is egyben.
Ezután válasszunk ki egy olyan függőséget, amelynek a bal oldala nem szuperkulcs. Mivel több ilyen is
létezik, mi válasszuk mondjuk a 𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑 → 𝑇𝑒𝑟𝑚é𝑘𝑁é𝑣 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎 𝑆𝑧í𝑛 függőséget.
(Választhatnánk másik függőséget is, ekkor nem feltétlen azonos, de szintén helyes eredményre
jutnánk végül). Képezzük a bal oldal lezártját:
Illetve
Az egyes relációkhoz tartozó függőségeket az 𝐹 függőséghalmaz 𝑅1 -re, illetve 𝑅2 -re történő vetítésével
kapjuk meg:
Az 𝑅2 reláció kulcsa R-hez hasonlóan szintén 𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑, 𝑉𝑒𝑣ő𝐾ó𝑑. A BCNF definícióját sérti a
𝑉𝑒𝑣ő𝐾ó𝑑 → 𝑉𝑒𝑣ő𝑁é𝑣 𝐼𝑟𝑠𝑧á𝑚 𝐶í𝑚 𝑇𝑒𝑙𝑒𝑓𝑜𝑛 függőség, tehát a 𝑉𝑒𝑣ő𝐾ó𝑑 lezártjában szereplő
attribútumokat egy új relációba helyezzük (𝑅3 ), a maradék attribútumot és a 𝑉𝑒𝑣ő𝐾ó𝑑 attribútumot
szintén (𝑅4 ):
Az 𝑅4 relációban a kulcs a 𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑, 𝑉𝑒𝑣ő𝐾ó𝑑 páros, hiszen a relációhoz tartozó egyetlen nem
triviális függőség bal oldalán ők szerepelnek, és lezártjuk a reláció összes attribútumát tartalmazza.
Következésképpen az 𝑅4 reláció BCNF-ban van.
48
Az 𝑅3 reláció kulcsa a 𝑉𝑒𝑣ő𝐾ó𝑑 attribútum (hiszen lezártja a reláció összes attribútumát tartalmazza).
Következésképpen az 𝐼𝑟𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠 függőség sérti a normál formát. Az 𝐼𝑟𝑠𝑧á𝑚 attribútum
lezártjában található attribútumokat egy új relációba helyezzük (𝑅5 ), a többi attribútumot valamint az
𝐼𝑟𝑠𝑧á𝑚 attribútumot szintén (𝑅6 ):
𝑹𝟓 (𝑰𝒓𝒔𝒛á𝒎, 𝑽á𝒓𝒐𝒔)
𝐹5 (𝐼𝑟𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠)
Az 𝑅5 és 𝑅6 relációk kulcsai rendre 𝐼𝑟𝑠𝑧á𝑚, illetve 𝑉𝑒𝑣ő𝐾ó𝑑, a két reláció nem triviális függőségeinek
bal oldalán (szuper)kulcs áll, tehát a relációk BCNF-ben vannak, az algoritmus itt leáll. Az eredmény
tehát az 𝑅1 reláció (amely a termék adatokat tartalmazza), az 𝑅6 (amely a vevők adatait tartalmazza),
az 𝑅5 (amely az irányítószám – város összerendelést tartalmazza), valamint az 𝑅4 (amely egy vevő –
termék pároshoz megmondja, hogy az adott vevő mennyit rendelt az adott termékből eddig összesen).
A mintaadatoknak megfelelő reláció példányokat a 3.6. táblázat - 3.9. táblázat ismerteti.
3.6. táblázat: R1: termék adatok 3.7. táblázat: R6: vevő adatok
3.8. táblázat: R5: város adatok 3.9. táblázat: R4: vásárlás adatok
Jól látható, hogy megszűnt mind a termékek, mind a vevők és a város adatok redundáns tárolása is.
Bizonyítható, hogy minden két-attribútumos reláció BCNF-ben van. Egy 𝑅(𝐴, 𝐵) reláció esetén nem
triviális függőségeknek 4-féle variációja fordulhat elő:
1. A relációra egyetlen nem triviális funkcionális függőség sem teljesül. Ekkor a reláció kulcsa az
𝐴, 𝐵 páros. Mivel nem létezik nem triviális függőség, ezért teljesül a BCNF feltétele, amely
szerint valamennyi ilyen függőség bal oldalán szuperkulcs áll.
2. 𝐴 → 𝐵 teljesül, de 𝐵 → 𝐴 nem. Ekkor 𝐴 a kulcs, és teljesül, hogy valamennyi nem triviális
függőség (𝐴 → 𝐵) bal oldala szuperkulcs.
3. 𝐵 → 𝐴 teljesül, de 𝐴 → 𝐵 nem. Ez az előző esettel ekvivalens (𝐴 és 𝐵 szerepet cserélt).
49
4. 𝐴 → 𝐵 és 𝐵 → 𝐴 is teljesül. Ekkor 𝐴 és 𝐵 is kulcs, így szintén igaz, hogy minden nem triviális
függőség bal oldala tartalmazza valamelyiket.
3.8.2 Helyreállítás
A 3.8.1 fejezetben ismertetett példa alapján minden két-attribútumos reláció BCNF-ben van.
Felmerülhet a kérdés, hogy akkor miért van szükség a BCNF-ra alakító dekompozíciós algoritmusra,
miért nem daraboljuk a relációkat egyszerűen két-attribútumos részekre? A válasz pedig az, hogy a
dekompozíciós algoritmust alkalmazva az eredeti reláció (az eredeti információ) helyreállítható a már
BCNF-ben lévő relációk természetes illesztésével. A relációk tetszőleges darabolása esetén pedig ez
nem feltétlen lenne így, vagyis információt veszíthetnénk. Egy R relációs séma 𝑅1 , 𝑅2 , … , 𝑅𝑛
felbontását veszteségmentesnek nevezünk, ha 𝑅 = 𝑅1 ⋈ 𝑅2 ⋈ ⋯ ⋈ 𝑅𝑛 .
Tekintsük a 3.10. táblázatban ismertetett 𝑅 relációt, amelyre egyetlen nem triviális függőség sem
teljesül. A relációt vetítsük le az 𝐴, 𝐵, illetve 𝐵, 𝐶 attribútumokra: 𝑅1 = 𝜋𝐴,𝐵 (𝑅) (3.11. táblázat), illetve
𝑅2 = 𝜋𝐵,𝐶 (𝑅) (3.12. táblázat).
A B C
a 1 2 A C B C
b 1 2 a 2 1 2
b 2 2 b 2 2 2
A B C
a 1 2
b 2 2
a 2 2
b 1 2
3.13. táblázat: R’
Ellenben ha egy relációt a dekompozíciós algoritmus szerint mindig a BCNF-t sértő függőségek mentén
darabolunk fel, akkor az eredeti reláció helyreállítható természetes illesztéssel. Ennek igazolására
tekintsük az 𝑅(𝐴, 𝐵, 𝐶) relációt az 𝐹 = {𝐵 → 𝐶} függőséghalmazzal. A reláció kulcsa ekkor A,B lesz,
tehát a 𝐵 → 𝐶 függőség sérti a BCNF-t. Bontsuk fel a függőség mentén a relációt:
Legyen 𝑡 = (𝑎, 𝑏, 𝑐) egy 𝑅-beli n-es, ennek 𝑅1 -beli képe (𝑎, 𝑏), 𝑅2 -beli képe (𝑏, 𝑐). 𝑅1 és 𝑅2
természetes illesztésével egyértelmű, hogy az (𝑎, 𝑏, 𝑐) n-est visszakapjuk.
A kérdés csupán az, hogy kaphatunk-e vissza más n-est is, amely eredetileg nem volt 𝑅 része? Legyen
𝑣 = (𝑑, 𝑏, 𝑒) szintén az 𝑅 reláció egy n-ese. Ekkor 𝑡 𝑅1 -beli képe 𝑢 = (𝑎, 𝑏), 𝑣 𝑅2 -beli képe 𝑤 = (𝑏, 𝑒).
Ez a két n-es szintén illeszthető egymással, mégpedig egy 𝑥 = (𝑎, 𝑏, 𝑒) n-est eredményeznek.
50
Lehetséges, hogy 𝑥 egy hibás n-es, amely eredetileg nem volt 𝑅 része? A válasz pedig nem, mivel a
𝐵 → 𝐶 függőség miatt 𝑐 = 𝑒, hiszen mindkét értékhez 𝐵 = 𝑏 attribútum érték tartozik, eredményképp
𝑡 = 𝑥.
𝑈𝑡𝑐𝑎𝐻á𝑧𝑠𝑧á𝑚 𝑉á𝑟𝑜𝑠 → 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚: bár egy utca több kerületet is érinthet, egy konkrét
városban egy konkrét címhez pontosan egy irányítószám tartozik.
𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠: Az irányítószám egy országon belül mindig pontosan azonosít egy
konkrét települést.
Az első lépés, hogy megkeressük a reláció kulcsait. Egyetlen attribútum lezártja sem tartalmazza az
összes attribútumot, ezért a kételemű attribútum halmazokkal próbálkozunk. Ezek közül kettő is
kulcsnak bizonyul: az {𝑈𝑡𝑐𝑎𝐻á𝑧𝑠𝑧á𝑚, 𝑉á𝑟𝑜𝑠}, illetve az {𝑈𝑡𝑐𝑎𝐻á𝑧𝑠𝑧á𝑚, 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚} attribútum
párosok.
𝑅1 (𝑈𝑡𝑐𝑎𝐻á𝑧𝑠𝑧á𝑚, 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚), 𝐹1 = {}
𝑅2 (𝑉á𝑟𝑜𝑠, 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚), 𝐹2 = {𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠}
51
UtcaHázszám Irányítószám Város Irányítószám UtcaHázszám Irányítószám Város
Ág u 3 1111 Budapest 1111 Ág u 3 1111 Budapest
Ág u 3 1085 Budapest 1085 Ág u 3 1085 Budapest
Ág u 3 5000 Szolnok 5000 Ág u 3 5000 Szolnok
𝑅1 𝑅2 𝑅1 ⋈ 𝑅2
Tehát minden BCNF-ben lévő reláció egyben 3NF-ben is van. A BCNF definíciójához képest a különbség
az, hogy ha a függőség jobb oldala prím attribútum, akkor nem szükséges, hogy a bal oldal szuperkulcs
legyen. A 3.9 fejezetben ismertetett példában ennek megfelelően sem az 𝑈𝑡𝑐𝑎𝐻á𝑧𝑠𝑧á𝑚 𝑉á𝑟𝑜𝑠 →
𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚, sem az 𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚 → 𝑉á𝑟𝑜𝑠 függőség nem sérti a 3NF-t, hiszen mind az
𝐼𝑟á𝑛𝑦í𝑡ó𝑠𝑧á𝑚, mind a 𝑉á𝑟𝑜𝑠 attribútum része egy kulcsnak. A következőkben ismertetjük a 3NF
dekompozíciós algoritmusát.
Módszer:
Ahhoz, hogy lássuk, hogy az algoritmus miért helyes, három dolgot kell megvizsgálnunk:
52
𝐾 + lezártat előállítanánk. Illesszük az egyes 𝑋 → 𝑌 függőségeknek megfelelő relációkat a már
illesztett relációhoz egymás után. Ekkor az 𝑅𝑖 (𝐴, 𝑋) alakú relációhoz illesztjük az 𝑅𝑋→𝑌 (𝑋, 𝑌)
relációt. Az illesztés során az 𝑋 → 𝑌 függőség miatt a 3.8.2 fejezetben ismertetett indoklás
alapján csak olyan n-es jöhet létre, amely az eredeti relációban is szerepelt (az eddig
felhasznált attribútumokra vetítve), és azok mind létre is jönnek.
2. A dekompozíció megőrzi a függőségeket: Mivel a minimális bázis összes függősége alapján
készítettünk egy relációt, ezért a függőségeket egyenként biztosítani tudjuk valamelyik
relációban.
3. Az eredményül kapott relációk mind 3NF-ben vannak: Ha a prím attribútumokat egy külön
relációként fel kellett venni, akkor az bizonyosan 3NF-ben van, hiszen minden a reláción
érvényes függőségre teljesül, hogy a jobb oldala prím attribútum. A minimális bázis függőségei
alapján felvett relációkra szintén bizonyítható, hogy azok 3NF-ben vannak, de annak indoklása
túlmutat ezen könyv keretein.
1. A függőségek jobb oldalán csak egy attribútum állhat, ehhez az 𝐸𝐹 → 𝐺𝐻, illetve 𝐴𝐶𝐷𝐹 →
𝐸𝐺 függőségeket ketté bontjuk:
𝐹1 = {𝐴 → 𝐵, 𝐴𝐵𝐶𝐷 → 𝐸, 𝐸𝐹 → 𝐺, 𝐸𝐹 → 𝐻, 𝐴𝐶𝐷𝐹 → 𝐸, 𝐴𝐶𝐷𝐹 → 𝐺}
2. A függőségek bal oldaláról eltávolítjuk azokat az attribútumokat, amelyek a bal oldal többi
attribútumától funkcionálisan függnek. A 𝐵 attribútum funkcionálisan függ az 𝐴-tól, ezért az
𝐴𝐵𝐶𝐷 → 𝐸 szabály bal oldaláról eltávolítható. A többi, jobb oldalon szereplő attribútum közül
egyedül az 𝐸 szerepel a bal oldalon is, ekkor az 𝐹 attribútum mellett, viszont 𝐸 nem szerepel
𝐹 lezártjában, ezért más egyszerűsítést nem végezhetünk el.
𝐹2 = {𝐴 → 𝐵, 𝐴𝐶𝐷 → 𝐸, 𝐸𝐹 → 𝐺, 𝐸𝐹 → 𝐻, 𝐴𝐶𝐷𝐹 → 𝐸, 𝐴𝐶𝐷𝐹 → 𝐺}
3. Utolsó lépésként eltávolítjuk a redundáns szabályokat. Az 𝐴𝐶𝐷 → 𝐸 függőség miatt az
𝐴𝐶𝐷𝐹 → 𝐸 függőség nem hordoz többlet információt, hiszen ha 𝐴𝐶𝐷-től már funkcionálisan
függ 𝐸, akkor 𝐴𝐶𝐷𝐹-től is biztosan. Az 𝐴𝐶𝐷 → 𝐸 és 𝐸𝐹 → 𝐺 szabályokból a
pszeudotranzitivitás miatt következik az 𝐴𝐶𝐷𝐹 → 𝐺 szabály, tehát az is eltávolítható.
𝐹3 = {𝐴 → 𝐵, 𝐴𝐶𝐷 → 𝐸, 𝐸𝐹 → 𝐺, 𝐸𝐹 → 𝐻 }
𝑅1 (𝐴, 𝐵)
𝑅2 (𝐴, 𝐶, 𝐷, 𝐸)
𝑅3 (𝐸, 𝐹, 𝐺)
𝑅4 (𝐸, 𝐹, 𝐻)
53
Mivel egyik reláció sem tartalmazza az összes kulcs attribútumot, ezért fel kell vennünk még egy
további relációt:
𝑅5 (𝐴, 𝐶, 𝐷, 𝐹)
Természetesen, ha létezik harmadik normál forma, akkor logikusan következik, hogy léteznie kell
elsőnek és másodiknak is. Az első normál forma (1NF) csupán annyit követel meg, hogy minden
entitás minden attribútuma atomi (egy sor és oszlop kereszteződésében egy egyszerű érték áll).
A második normál forma (2NF) az 1NF-hez képes még azt is megköveteli, hogy a relációnak ne
legyen olyan nem prím attribútuma, amely a reláció egy kulcsának valódi részhalmazától függ. Vagyis
a nem prím attribútumok nem függhetnek egy kulcs valódi részhalmazától (nem kulcs attribútumoktól
függhetnek).
A harmadik normál forma (3NF) a 2NF-t azzal egészíti ki, hogy a nem prím attribútumok csakis a
(szuper)kulcstól függhetnek (mástól nem, a prím attribútumok pedig bármitől).
A BCNF (egyesek 3.5NF-nek is nevezik) a 3NF feltételeit is tovább szűkíti: minden attribútum csak a
szuperkulcstól függhet (nem triviális függőségektől eltekintve természetesen).
Bár a BCNF dekompozíció megszűnteti az összes funkcionális függőségből eredő redundanciát, egy
másik típusú, ún. többértékű függőségből következően még előfordulhatnak redundanciák a
relációban. A negyedik normál forma (4NF) ezen redundanciák megszűntetését célozza. Tekintsük
a 3.15. táblázatban ábrázolt relációt.
Ha egy vásárlóhoz több telefonszám és több cím is tartozhat, akkor (az 1NF-ből következően) azt csak
külön n-esekben tárolhatjuk el. De mivel a telefonszám és a cím egymástól függetlenek (hiszen egy
címen több vásárló is lakhat különböző telefonszámokkal, és egy telefonszámon akár több vásárló is
lehet regisztrálva különböző címekkel), ezért az összes cím-telefonszám párosnak meg kell jelennie a
relációban. A 𝑉𝑒𝑣ő(𝑉𝑒𝑣ő𝐼𝑑, 𝑁é𝑣, 𝐶í𝑚, 𝑇𝑒𝑙𝑒𝑓𝑜𝑛) reláció kulcsa a 𝑉𝑒𝑣𝑜𝐼𝑑 attribútum, mivel tőle
funkcionálisan függ az összes többi attribútum, más funkcionális függőséget viszont nem
fogalmazhatunk meg, a reláció BCNF-ben van, mégis nyilvánvalóan hemzseg a redundanciától. A
54
logikus reprezentáció a 𝑉𝑒𝑣ő(𝑉𝑒𝑣ő𝐼𝑑, 𝑁é𝑣), 𝐶í𝑚(𝑉𝑒𝑣ő𝐼𝑑, 𝐶í𝑚), 𝑇𝑒𝑙𝑒𝑓𝑜𝑛(𝑉𝑒𝑣ő𝐼𝑑, 𝑇𝑒𝑙𝑒𝑓𝑜𝑛)
relációkkal történne, de csupán a funkcionális függőségek alapján, a BCNF dekompozíciót alkalmazva
ide nem juthatunk el.
Az 𝑅 relációban érvényes 𝑋 ↠ 𝑌 többértékű függőség azt fejezi ki, hogy ha 𝑅 két n-ese 𝑋 összes
attribútumán páronként azonos értékkel rendelkezik, akkor a két n-es 𝑌-hoz tartozó részei
felcserélhetők, és az így kapott n-esek szintén a relációban lesznek. Másképp fogalmazva: 𝑋 minden
lehetséges értékére az 𝑌-hoz rendelt értékek függetlenek az 𝑅 − 𝑋 − 𝑌 értékektől. A példában két
többértékű függőség is található: 𝑉𝑒𝑣ő𝐼𝑑 ↠ 𝐶í𝑚, illetve 𝑉𝑒𝑣ő𝐼𝑑 ↠ 𝑇𝑒𝑙𝑒𝑓𝑜𝑛. Tehát definíció szerint,
ha a relációban megtalálható a következő két n-es (3.16. táblázat):
Akkor a felcserélt cím és telefonszámmal rendelkező n-esnek és szerepelnie kell benne (3.17. táblázat):
A funkcionális függőségek egyben többértékű függőségek is, hiszen egy 𝑋 → 𝑌 funkcionális függőség
esetén ha két n-esben, amely az X-hez rendelt értékben megegyezik, akkor az Y-hoz rendelt értékben
is meg fog egyezni. Tehát az Y értékeket felcserélve ugyanazt a két n-est kapjuk vissza.
Egy reláció akkor van 4NF-ben, ha minden nem triviális 𝑋 ↠ 𝑌 többértékű függőségre 𝑋 szuperkulcs.
Egy többértékű függőség nem triviális, ha
Mivel minden funkcionális függőség egyben többértékű függőség is, ezért minden 4NF-ben lévő reláció
egyben BCNF-ben is van.
Léteznek magasabb szintű normál formák is (5NF, 6NF, 7NF), de a gyakorlatban kisebb jelentőséggel
bírnak, mint az eddig ismertetettek. Az egyes normál formák egymásra épülnek, az alacsonyabb
szintűeket a magasabbak újabb és újabb kritériumokkal egészítik ki (3.1. ábra).
55
1NF
2NF
3NF
BCNF
4NF
Magasabb normál formák
56
4 Magas szintű adatbázis modellek
Egy adatbázis létrehozása mindig a tervezéssel kezdődik. A tervezés során azonosítjuk azokat a
dolgokat, amelyekről információt szeretnénk tárolni, azok számunkra fontos tulajdonságait.
Feltérképezzük a különböző kapcsolatokat, amelyek az egyes dolgokat egymáshoz kötik, megkeressük
azokat a kényszereket, amelyeknek a tárolt adatra teljesülniük kell. Az első kérdés, amellyel a tervezés
során találkozunk, hogy pontosan hogy néz ki egy adatbázis terv? Hogy érdemes egy ilyen tervet
strukturált formában megfogalmazni úgy, hogy az később számunkra és mások számára is érthető és
egyértelmű legyen?
Magas szintű
Ötlet Relációs séma Implementáció
terv
8
Unified Modeling Language - http://www.omg.org/spec/UML/
57
4.1 Alapfogalmak
Az Entitás-relációs modellezésben használt három legalapvetőbb fogalom az entitás, az attribútum
és a kapcsolat (reláció – az angol relationship kifejezésből, nem keverendő az eddig használt relációval,
ami egy kétdimenziós táblázatot jelentett). Entitásoknak nevezzük azokat a megkülönböztethető
objektumokat, amelyekről adatot szeretnénk tárolni. Valamelyest hasonlítanak az objektum-orientált
programozásban használt objektumokhoz: az entitások is rendelkezhetnek tulajdonságokkal, illetve
kapcsolódhatnak más entitásokhoz, viszont – az objektumokkal ellentétben – nem definiálhatunk
rajtuk műveleteket. Az, hogy miből lesz entitás, és miből nem, azt mindig a modellezett probléma dönti
el. Például egy hallgatói nyilvántartásban egy a hallgatóhoz rendelt telefonszám csak a hallgató egy
jellemzője, míg egy online telefonkönyvben egy különálló entitás, ami egyéb jellemzőkkel is
rendelkezhet (pl., hogy titkos-e, emelt díjas-e stb). De a hallgató sem feltétlen lesz mindig entitás: bár
a hallgatói nyilvántartásban az egyes hallgatókat természetesen megkülönböztetjük egymástól, egy
egyetemi rangsorban a hallgatói összlétszám csupán egy numerikus jellemzője az egyetemnek.
58
kapcsolatokat és létrehozhatunk. Ha egy tanuló nyilvántartásban önálló entitásként tároljuk el a
szemesztereket (pl. mert egyéb tulajdonságokat is szeretnénk egy félévhez rendelni), akkor azt, hogy
egy hallgató egy adott félévben felvett egy adott kurzust, egy hármas (ternális) kapcsolattal tudjuk
kifejezni, hiszen három különböző entitáshalmaz elemei között kell kapcsolatot teremteni.
Szín Raktárkészlet
Email Cím
A 4.2. ábrán egy olyan adatbázis entitás-relációs diagramját láthatjuk, amelyben termékekről, és az
őket előállító gyártókról tárolhatunk el információt. A 𝐺𝑦á𝑟𝑡ó entitás halmaz négy attribútummal
rendelkezik: 𝐺𝑦á𝑟𝑡ó𝐾ó𝑑, 𝑁é𝑣, 𝐸𝑚𝑎𝑖𝑙, 𝐶í𝑚. Az aláhúzás a 𝐺𝑦á𝑟𝑡ó𝐾ó𝑑 attribútum alatt jelzi azt, hogy
ez a tulajdonság az entitáshalmaz kulcsa. A 𝑇𝑒𝑟𝑚é𝑘 relációt hat attribútum jellemzi: a 𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑, a
𝑁é𝑣, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎, 𝑆𝑧í𝑛, Á𝑟 és a 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡. A két entitáshalmazt a 𝐺𝑦á𝑟𝑡 kapcsolat köti össze,
amely egy termékhez legfeljebb egy gyártót rendel (amennyiben az ismert), egy gyártóhoz pedig
tetszőleges számú gyártott terméket. (A számosság korlátozást az él végpontján elhelyezett nyíl jelöli,
ld. részletesen a 4.2.1. fejezetben).
4.2 Attribútumok
Attribútumot nem csak entitás halmazokhoz, hanem akár kapcsolatokhoz is rendelhetünk. Például ha
nyilván szeretnénk tartani, hogy egy gyártó mióta gyárt egy bizonyos terméket, akkor ezt egy a 𝐺𝑦á𝑟𝑡
kapcsolathoz rendelt 𝑀𝑖ó𝑡𝑎 nevű attribútummal írhatjuk le (4.3. ábra). A kapcsolathoz rendelt
attribútum nem az egyes entitásokat jellemzi, hanem a köztük fennálló viszonyhoz rendel
tulajdonságot.
59
GyártóKód Név TermékKód Név
Mióta
Ár
Szín Raktárkészlet
Email Cím
Entitás halmazon és kapcsolaton túl rendelhetünk attribútumokat akár más attribútumokhoz is, ezáltal
modellezhetünk összetett attribútumokat. Például a cím tulajdonságot gyakran nem egy egyszerű
szöveges mezőben szokták leírni, hanem külön tárolják el az irányítószámot, a város nevet, az utca
nevet, valamint a házszámot (4.4. ábra), ezáltal könnyebben rendszerezhetővé és feldolgozhatóvá téve
azokat.
GyártóKód Név
Irányítószám
Gyártó Város
Utca
Email Cím
Házszám
Adott esetben egy gyártó rendelkezhet több címmel is (pl. székhely és telephelyek címei). A
modellünkben ezt az ún. többértékű attribútumokkal jelölhetjük, amelyet dupla ellipszis vonallal
ábrázolunk (4.5. ábra).
60
GyártóKód Név
Irányítószám
Gyártó Város
Utca
Email Cím
Házszám
Többértékű attribútumokkal tipikusan halmaz vagy lista jellegű tulajdonságokat szoktunk modellezni,
a modellezett tulajdonság lehet egyszerű, vagy akár összetett is, mint ahogy a példában a 𝐶í𝑚
attribútumnál is láttuk.
Vegyük észre, hogy az egyed-kapcsolat diagramok eszközkészlete mennyivel gazdagabb, mint a relációs
adatmodellé. Míg a relációs adatmodellben az attribútumok egy-egy primitív érték tárolására voltak
alkalmasak, addig az egyed-kapcsolat diagramon attribútumok segítségével összetett, többszörös
tulajdonságokat is leírhatunk. A relációs adatmodellben két entitás (n-es) kapcsolatát egy külső kulccsal
fejezhettük ki, míg az entitás-relációs modellben a kapcsolathoz is rendelhetünk tulajdonságokat.
4.2.1 Kapcsolatok
A bináris kapcsolatok tovább osztályozhatók aszerint, hogy hány entitás között létesítenek viszonyt.
Ezt nevezzük a kapcsolat multiplicitásának:
Az egy-egy kapcsolat olyan viszonyt fejez ki, amelyben az egyik entitás halmaz egy elemét
egy másik entitás halmaz pontosan egy elemével hozzuk összefüggésbe és fordítva. Például
egy egyetemnek egy rektora van, és egy rektor egyszerre pontosan egy egyetemnek lehet az
első számú embere. Tehát egy egyetemi nyilvántartásban, ahol eltároljuk az egyes
intézményeket (𝐸𝑔𝑦𝑒𝑡𝑒𝑚), illetve az alkalmazottakat (𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧𝑜𝑡𝑡), akkor a
𝑉𝑒𝑧𝑒𝑡(𝐸𝑔𝑦𝑒𝑡𝑒𝑚, 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧𝑜𝑡𝑡) kapcsolat egy-egy típusú lesz, mivel egy egyetem entitáshoz
legfeljebb egy alkalmazottat köthetünk, mint vezetőt (az átmeneti időszakban esetleg nincs
rektor), és egy alkalmazott egyszerre csak egy intézményt vezethet.
Több-egy kapcsolatról akkor beszélünk, ha az egyik entitás halmaz egy konkrét eleméhez a
másik entitás halmaz több elemét is hozzárendelhetjük. Például az 𝐺𝑦á𝑟𝑡(𝐺𝑦á𝑟𝑡ó, 𝑇𝑒𝑟𝑚é𝑘)
kapcsolat egy gyártóhoz több terméket is rendelhet (hiszen egy vállalkozás egyszerre több
terméket is előállíthat, de akár nullát is), egy konkrét terméket viszont mindig pontosan egy
gyártó állít elő (eltekintve a részegységek előállításától)
Több-több kapcsolat esetén az egyik entitás halmaz több elemét is kapcsolatba hozhatjuk a
másik entitás halmaz egy konkrét elemével és fordítva. Tehát egy 𝑉á𝑠á𝑟𝑜𝑙(𝑉𝑒𝑣ő, 𝑇𝑒𝑟𝑚é𝑘)
kapcsolat – amely azt fejezi ki, hogy egy vevő megvásárol egy terméket – több-több típusú,
hiszen egy vevő több terméket is vásárolhat, illetve egy termékből több vevő is vásárolhat.
Hasonlóképpen az 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧(𝐸𝑔𝑦𝑒𝑡𝑒𝑚, 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧𝑜𝑡𝑡) kapcsolat szintén több-több típusú,
61
hiszen egy egyetemen több alkalmazott is dolgozik, és egy alkalmazottat egyszerre akár több
egyetem is foglalkoztathat.
Ha a kapcsolat egy oldalán legfeljebb egy entitás állhat, akkor azt egy tömör nyíllal jelöljük (4.7. ábra).
Például egy terméket mindig egy gyártó állít elő, egy gyártó viszont tetszőleges számú terméket
gyárthat. Ez a típusú kapcsolat megengedi a nulla darab entitást is, tehát jelen esetben egy terméktől
nem várjuk el, hogy feltétlen kapcsolódjon egy gyártóhoz (ismert legyen annak gyártója).
A 4.8. ábrán látható a 𝑉𝑒𝑧𝑒𝑡(𝐸𝑔𝑦𝑒𝑡𝑒𝑚, 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧𝑜𝑡𝑡) kapcsolat, amelyben a kapcsolat legfeljebb egy
egyetemet kapcsol legfeljebb egy alkalmazotthoz.
Ha viszont egy kapcsolat egyik vagy másik oldalán pontosan egy entitás kell, hogy szerepeljen (vagyis
az egyik entitás halmaz egy eleméhez a másik entitás halmaznak mindig pontosan egy elemét rendeli),
akkor azt félkörrel jelöljük. A 4.9. ábrán látható adatbázis modell például egy nemzetközi település
nyilvántartó része lehet, amelyben minden egyes városhoz kötelezően meg kell adni a tartalmazó
országot is. Természetesen egy országon belül tetszőléges számú város helyet foglalhat.
Elképzelhető olyan eset is, amikor az eddig ismertetetteknél pontosabb kényszerekre van szükségünk,
tehát a számosságokat pontos számok közé akarjuk szorítani (pl. 0 és 10 között, legalább 1, kevesebb,
mint 30 stb). Ilyenkor a számosság kényszereket a vonal megfelelő végénél egyenlőtlenségekkel
jelezzük. Például egy hallgatói nyilvántartásban el szeretnénk tárolni, hogy melyik hallgató melyik
tankörbe tartozik. Minden hallgató pontosan egy tankörhöz tartozik, egy tankör viszont legalább 1,
maximum 30 hallgatóból állhat (4.10. ábra).
62
>0, <31
Tankör Tagja Hallgató
4.2.2 Szerepek
Egy kapcsolat nem feltétlenül különböző entitás halmazokat köt össze, gyakran előfordul, hogy például
egy bináris kapcsolat mindkét oldalán ugyanaz az entitás halmaz szerepel. Egy családnyilvántartásban,
ahol eltároljuk a személyi adatokat, és a két személy közt fennálló házastársi viszonyt, egy
𝐻á𝑧𝑎𝑠𝑡á𝑟𝑠𝑎(𝑆𝑧𝑒𝑚é𝑙𝑦, 𝑆𝑧𝑒𝑚é𝑙𝑦) kapcsolat két 𝑆𝑧𝑒𝑚é𝑙𝑦 entitást kapcsol össze ugyanabból a
𝑆𝑧𝑒𝑚é𝑙𝑦 entitás halmazból. Mivel ebben az esetben nem lenne egyértelmű, hogy a kapcsolat melyik
oldalán milyen szerepben áll az entitás halmaz (férj vagy feleség), ezért a kapcsolat végpontjain
elhelyezett szerepnevekkel határozhatjuk meg azokat (4.11. ábra).
férj
Személy Házastársa
feleség
szülő
Személy Szülője
gyermek
Szerepneveket nem csak akkor használhatunk, ha a kapcsolat több végén is ugyanaz az entitás halmaz
szerepel, általános esetben is alkalmazhatjuk jelentés pontosítása érdekében, így téve egyértelművé
az entitás halmazok szerepét az egyes kapcsolatokban.
Ahogyan már utaltunk rá, a gyakorlatban legtöbbször bináris kapcsolatokkal találkozunk, de bizonyos
esetekben egy kapcsolat kettőnél több entitás halmaz között is teremthet kapcsolatot. Ha egy
üzlethálózat több telephellyel is rendelkezik, és azon túl, hogy egy vásárló milyen termékeket vásárolt
eddig, azt is nyilván szeretnénk tartani, hogy pontosan hol, melyik telephelyeken tette ezt meg, akkor
ezt egy hármas kapcsolattal modellezhetjük, amely a vevő, a termék és a telephely között teremt
összefüggést. Ezt a sémát illusztrálja a 4.13. ábra.
63
Telephely
Mivel i) egy vevő egy telephelyen tetszőleges számú terméket vásárolhat, ii) egy vevő egy terméket
több telephelyen és megvásárolhat, illetve iii) egy telephelyen egy bizonyos termékből több vevő is
vásárolhat, ezért az élek végpontjain nem jelöltük a multiplicitást. Úgy is fogalmazhatunk, hogy
semelyik két különböző entitás halmazból származó entitás sem határozza meg egyértelműen a
harmadik entitást.
Mivel a gyakorlati megvalósítások legtöbbször csak a bináris kapcsolatokat támogatják (4.5. fejezet),
ezért a többes kapcsolatokat előbb vagy utóbb le kell képeznünk binárisakra. Ez úgy lehetséges, hogy
a többes kapcsolatot egy entitás halmazzal helyettesítjük, amelyet bináris kapcsolatokkal kötjük össze
az eredeti entitás halmazokkal. Ezt az új entitás halmazt szokták kapcsoló entitás halmaznak
nevezni.
Telephely
Helyszíne
A 4.14. ábrán azt láthatjuk, hogy a 𝑉á𝑠á𝑟𝑜𝑙 hármas kapcsolatot kicseréltük egy 𝑉á𝑠á𝑟𝑙á𝑠 entitás
halmazra. Minden egyes entitás ebben a halmazban egy vevőt, egy terméket és egy telephelyet kapcsol
össze az 𝐴𝑙𝑎𝑛𝑦𝑎, 𝑇á𝑟𝑔𝑦𝑎 és 𝐻𝑒𝑙𝑦𝑠𝑧í𝑛𝑒 kapcsolatokon keresztül, amelyek már mind bináris
kapcsolatok. Mivel minden egyes 𝑉á𝑠á𝑟𝑙á𝑠 entitás egyértelműen meghatároz pontosan egy vevőt, egy
terméket és egy telephelyet, ezért a kapcsolatok 𝑉𝑒𝑣ő, 𝑇𝑒𝑙𝑒𝑝ℎ𝑒𝑙𝑦 és 𝑇𝑒𝑟𝑚é𝑘 entitáshalmazok felé
mutató élein félkörökkel jelöljük a pontosan 1-es multiplicitást. Viszont egy vevőhöz tetszőleges számú
(beleértve a nullát is) vásárlás köthető (hasonlóképpen egy telephelyhez és egy termékhez is), ezért a
kapcsolatok másik oldalára nem tehetünk multiplicitásbeli megkötést.
64
Tegyük fel, hogy nem csak a vevők összesített vásárlásait szeretnénk eltárolni, hanem az egyes
megrendeléseit külön-külön. Egy megrendelésen belül egyszerre tipikusan több típusú termékből
rendelünk valamennyi darabot, a megrendelésen belül ezeket az elemeket megrendeléstételeknek
szoktuk nevezni. Minden megrendelés rendelkezik egy egyedi sorszámmal, amely egyértelműen
azonosítja azt. Minden termék és vevő szintén rendelkezik egy-egy egyedi sorszámmal. Az egyes
megrendelések több tételből állnak össze.
Név
Vevő
VevőKód
Darabszám Név TermékKód
Elküld MegrendelésKód
Dátum
A 4.15. ábrán ábrázoltuk a fent vázolt adatbázis sémát. A példában a 𝑀𝑒𝑔𝑟𝑒𝑛𝑑𝑒𝑙é𝑠𝑇é𝑡𝑒𝑙 entitásokat
nem azonosítják egyértelműen a saját attribútumai (𝐷𝑎𝑟𝑎𝑏𝑠𝑧á𝑚), ezért az egy gyenge entitás halmaz,
amelyet dupla körvonallal jelölünk. Ahhoz, hogy az egyes tételeket mégis azonosítani tudjuk, hozzá kell
vennünk a 𝑀𝑒𝑔𝑟𝑒𝑛𝑑𝑒𝑙é𝑠 és a 𝑇𝑒𝑟𝑚é𝑘 halmazok elsődleges kulcsait (𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑,
𝑀𝑒𝑔𝑟𝑒𝑛𝑑𝑒𝑙é𝑠𝐾ó𝑑). Tehát egy megrendeléstételt a tartalmazó megrendelés kulcsa, és a megrendelt
termék kulcsa fogja azonosítani. Ha megmondjuk, hogy melyik megrendelésben melyik termékre
vagyunk kíváncsiak, akkor ahhoz egyértelműen rendelhető egy darabszám, hiszen egy megrendelésen
belül egy termék csak egyszer szerepelhet (ha többet rendelünk, akkor a darabszámot növeljük). A
𝑀𝑒𝑔𝑟𝑒𝑛𝑑𝑒𝑙é𝑠 és 𝑇𝑒𝑟𝑚é𝑘 halmazokat meghatározó entitás halmazoknak nevezzük, a 𝑅é𝑠𝑧𝑒 és
𝑇á𝑟𝑔𝑦𝑎 kapcsolatokat pedig meghatározó kapcsolatoknak, ez utóbbiakat dupla körvonallal jelöljük.
Annak, hogy egy 𝑅 kapcsolat meghatározó legyen 𝐺 gyenge és 𝑀 meghatározó entitás halmaz között,
a következő feltételei vannak:
Előfordul, hogy a meghatározó entitás halmaz is gyenge entitás halmaz, tehát az ő attribútumai sem
azonosítják egyértelműen az egyes egyedeit. Ilyenkor rekurzívan tovább kell haladnunk az ő
meghatározó entitás halmazai felé. Ha 𝐺 gyenge entitás halmaz meghatározó entitás halmaza 𝑀,
amelynek meghatározó entitás halmaza 𝑁, akkor 𝑀 kulcsának része lesz 𝑁 kulcsa, 𝐺 kulcsának pedig
𝑀 kulcsa (amely magában foglalja 𝑁 kulcsát is). A 4.16. ábrán egy egyetemi adatbázis modellt
illusztrálunk, amelyben nyilvántartjuk az egyetemeket, az oktatott szakokat és a tanköröket. Egy
tankört az évfolyam és a sorszám azonosít egyértelműen, de csak egy adott szakon belül, mivel két
65
különböző szakon lehet azonos sorszámú tankör egy évben. Ezért a 𝑇𝑎𝑛𝑘ö𝑟 gyenge entitás halmaz
lesz, a 𝑆𝑧𝑎𝑘 pedig meghatározó entitás halmaz, köztük a 𝑅é𝑠𝑧𝑒 meghatározó kapcsolattal. Egy
egyetemen belül a szak neve egyedi, de több egyetemen is nevezhetnek ugyanúgy szakokat, tehát a
𝑆𝑧𝑎𝑘𝑁é𝑣 önmagában nem lehet kulcs, a 𝑆𝑧𝑎𝑘 is gyenge entitás halmaz lesz, amelyet az
𝐸𝑔𝑦𝑒𝑡𝑒𝑚 entitás halmaz fog meghatározni az 𝐸𝑔𝑦𝑒𝑡𝑒𝑚𝑁é𝑣 kulccsal az 𝑂𝑘𝑡𝑎𝑡 kapcsolaton keresztül.
A 𝑆𝑧𝑎𝑘 kulcsa tehát {𝐸𝑔𝑦𝑒𝑡𝑒𝑚𝑁é𝑣, 𝑆𝑧𝑎𝑘𝑁é𝑣}, a 𝑇𝑎𝑛𝑘ö𝑟 kulcsa pedig
{𝐸𝑔𝑦𝑒𝑡𝑒𝑚𝑁é𝑣, 𝑆𝑧𝑎𝑘𝑁é𝑣, É𝑣𝑓𝑜𝑙𝑦𝑎𝑚, 𝑆𝑜𝑟𝑠𝑧á𝑚}.
4.4 Öröklés
Az objektum-orientált modellezésből már jól ismert öröklésnek is van alternatívája és létjogosultsága
a magas szintű adatbázis modellezésben. Öröklést használhatunk például, ha egy entitás halmaz
bizonyos attribútumai az entitás halmaz összes eleménél, míg bizonyos attribútumai csak egyes
elemeinél használatosak. Ilyenkor a közös attribútumokat kiemelhetjük egy szülő entitás halmazba, és
a speciális attribútumokat egy vagy több leszármazott entitás halmazba mozgathatjuk.
Példaképp tegyük fel, hogy az áruházunkban különböző fajta termékeket forgalmazunk: játékokat,
ruhaneműket, lábbeliket és könyveket. Valamennyi termék rendelkezik egy árral, egy raktárkészlettel,
egy névvel, valamint egy termék kóddal. A játékoknak ezen kívül van egy minimum korhatára (szám),
amikortól ajánlott a használatuk, a ruhaneműknek van egy szín, egy méret (XS, S, M, L ,XL…), valamint
egy anyag tulajdonsága. A lábbeliknek van típusa (papucs, csizma, félcipő…), valamint egy mérete
(szám), a könyveknek ISBN száma, szerzője, hossza. Objektum-orientált programozásban kézenfekvő
lenne, hogy a 𝑇𝑒𝑟𝑚é𝑘 ősosztályból származzanak le a 𝐽á𝑡é𝑘, 𝑅𝑢ℎ𝑎, 𝐶𝑖𝑝ő és 𝐾ö𝑛𝑦𝑣 osztályok a
megfelelő attribútumokkal. Entitás-relációs diagramon ezt szintén kifejezhetjük az öröklés típusú él
segítségével, amelyet egy üres háromszög fejez ki az entitás halmazokat összekötő élen. Az ismertetett
termék típusok modelljét a 4.17. ábrán láthatjuk.
66
Név
Korhatár
Játék Ruha Cipő Könyv
Szerző
Szín
Hossz
RuhaMéret Anyag CipőMéret Típus
ISBN
Fontos, hogy az öröklés él nem rendelkezik multiplicitással. Mindig 1-1 típusú, vagyis a leszármazott
entitás halmazban található entitáshoz mindig egy, a szülő relációban található entitás kapcsolódik, és
a szülő halmazban található entitáshoz is legfeljebb egy entitás kapcsolódik a leszármazott halmazban.
Szülő entitás tehát mindig létezik, de leszármazott egy szülőhöz nem feltétlenül. Az öröklés kapcsolat
segítségével összetett öröklési fákat építhetünk fel, de lényeges, hogy csak fákat. Irányított körök nem
lehetnek az öröklési láncban, egy entitás halmaz nem lehet a saját szülője / saját maga leszármazottja.
67
Név
Kód Szín
A 4.18. ábrán azt láthatjuk, hogy az általános ruha terméket tovább specializálhatjuk: például
bevezethetünk egy 𝐼𝑛𝑔 entitás halmazt, amely a 𝑅𝑢ℎá𝑡ó𝑙 és a 𝑇𝑒𝑟𝑚é𝑘𝑡ő𝑙 örökölt attribútumokat
kiegészíti egy 𝐹𝑎𝑧𝑜𝑛 attribútummal is (Slim fit, Regular fit, …).
Bár a fenti megközelítés az adatbázis modell nagy részét képes relációs sémára képezni, számos
hiányossággal és hátránnyal is rendelkezik:
sokszor felesleges egy kapcsolathoz külön relációt létrehozni, különösen akkor, ha a kapcsolat
1-1 vagy 1-több típusú, és nem rendelkezik attribútumokkal,
a gyenge entitáshalmazokat a bemutatott módszer nem kezeli,
illetve nem megoldott az öröklések leképezése sem.
A továbbiakban azt fogjuk megvizsgálni, hogyan készíthetjük el egy egyed-kapcsolat modell teljes és
lehetőség szerint hatékony leképezését relációs sémára.
Az erős entitás halmazok definíció szerint önállóan értelmezhetők, egyedei saját attribútumai alapján
megkülönböztethetők. Ezért ezekhez a halmazokhoz egyszerűen létrehozhatunk egy-egy relációt az
entitás halmazok neveivel és azok attribútumaival. A kapcsolatokat és kapcsolódó entitáshalmazokat
ezen a ponton nem kell figyelembe vennünk. Tekintsük a 4.19. ábrán bemutatott adatbázis modellt.
68
VevőKód Név Mennyit TermékKód Név
Ár
Szín Raktárkészlet
Email Cím
A bemutatott 𝑉𝑒𝑣ő és 𝑇𝑒𝑟𝑚é𝑘 entitás halmazok egyaránt erős entitás halmazok, így hozzájuk
elkészítjük a következő két relációt:
4.5.2 Kapcsolatok
A kapcsolatok leképezésére az alap ötletünk az volt, hogy minden kapcsolathoz készítsünk egy relációt,
és a kapcsolat saját attribútumain kívül adjuk hozzá a kapcsolódó entitáshalmazok kulcs attribútumait
is. Amennyiben egy kapcsolat két vagy több végén is ugyanaz az entitás halmaz áll (pl. a 𝑆𝑧ü𝑙ő𝑗𝑒
kapcsolat a 4.12. ábrán), akkor az érintett entitás halmaz kulcs attribútumait többször is hozzá kell
adnunk a létrehozott reláció attribútumaihoz (természetesen különböző nevekkel). Az ebből a célból
létrehozott relációkat szoktuk kapcsoló relációknak vagy kapcsoló tábláknak nevezni. A 4.19. ábrán
bemutatott példának megfelelően tehát fel kell vennünk egy
Ez egy több-többes kapcsolat (mint amilyen a 𝑉á𝑠á𝑟𝑜𝑙 is) esetén megfelelő megoldásnak tűnik. De
vizsgáljuk meg, mi történik az egy-többes típusú kapcsolatok esetén.
69
GyártóKód Név TermékKód Név
Ár
Szín Raktárkészlet
Email Cím
A 𝐺𝑦á𝑟𝑡 táblázat minden egyes sora két külső kulcs értéket tárol el, az egyik a megfelelő termékre, a
másik a termékhez tartozó gyártóra mutat. Mivel a termékek túlnyomó többségéhez tartozik gyártó,
ezért pazarlás egy külön relációt fenntartani csak arra a célra, hogy a termékhez tartozó gyártó
azonosítót eltároljuk. Az, hogy ezt az információt egy külön táblában tartjuk nyilván, bonyolítja és
lassítja a lekérdezéseket, mivel a 𝑇𝑒𝑟𝑚é𝑘-𝐺𝑦á𝑟𝑡ó kapcsolatot csak két illesztéssel tudjuk feloldani.
Jobb megoldásnak tűnik, hogy ha külön reláció létrehozása helyett a 𝐺𝑦á𝑟𝑡ó𝐾ó𝑑 attribútumot mint
külső kulcsot egyszerűen elhelyezzük a 𝑇𝑒𝑟𝑚é𝑘 táblában (4.2. táblázat).
TermékKód Név GyártóKód … GyártóKód Név …
1 Dohányzóasztal 1 1 Nagy Bútor Kft
2 Kanapé 1 2 Fabric Rt
3 Szék 2 3 Bútorgyár
4 Hintaágy 3
𝑇𝑒𝑟𝑚é𝑘 𝐺𝑦á𝑟𝑡ó
4.2. táblázat: Termék és Gyártó relációk kapcsolata külső kulcson keresztül
Ha a kapcsolat rendelkezik attribútumokkal is (pl. egy 𝑀𝑖ó𝑡𝑎 mező a 𝐺𝑦á𝑟𝑡 kapcsolaton), akkor az 1-
többes kapcsolat attribútumait is a kapcsolat többes oldalán található entitás halmaznak megfelelő
relációba (𝑇𝑒𝑟𝑚é𝑘) helyezzük. Ezeket az attribútumokat (mint a külső kulcsot is) természetesen csak
akkor töltjük ki az egyes entitásoknál, amennyiben létezik az entitáshoz az adott kapcsolat.
Teljesítmény okokból bizonyos esetekben megfontolás tárgya lehet, hogy a kapcsolatot mégis külön
relációra képezzük le, például ha a kapcsolat sok és/vagy nagy helyigényű attribútummal rendelkezik.
Azáltal, hogy a kapcsolat attribútumaihoz tartozó értékeket az összekapcsolt relációktól elkülönítve
70
tároljuk, csökkenthetjük az összekapcsolt relációk helyigényét, ezáltal gyorsítva rajtuk a lekérdezések
végrehajtását, amennyiben a kapcsolat nem szerepel a lekérdezésben.
4.5.3 Attribútumok
71
GyártóKód Név
Irányítószám
Gyártó Város
UtcaHsz
Email Cím
Ugyanígy járunk el, amennyiben a többszörös attribútum nem összetett: ilyenkor a létrehozott
relációban csupán a kapcsolódó reláció külső kulcsa és a többszörös, de egyszerű attribútum szerepel.
Időnként szükség lehet rá, hogy a többszörös attribútum értékeit sorrendezzük (pl. a címeket,
telefonszámot sorrendezzünk fontosság szerint). Ilyenkor a létrehozott relációban egy sorszám
attribútummal jelezhetjük, hogy az adott n-es a külső kulccsal hivatkozott entitáson belül a többszörös
attribútum hányadik pozícióján szerepel (4.5. táblázat). A sorszám attribútum értéke a kapcsolódó
entitást hivatkozó külső kulcs értékenként (𝐺𝑦á𝑟𝑡ó𝐾ó𝑑) egyedi.
72
GyártóKód Sorszám Irányítószám Város UtcaHsz
1 1 5000 Szolnok Fa u 2
2 1 9012 Győr Ág u 15
2 2 1222 Budapest Ló u 3
3 1 1111 Budapest Hős krt 3
3 2 7691 Pécs Tél u 1
4 1 1111 Budapest Virágos tér 4
4.5. táblázat: Sorrendezett többszörös attribútum leképezése
Dátum
Mivel a meghatározó kapcsolatok mindig 1-1 vagy 1-több típusúak, ezért ezeket a kapcsolatokat nem
kell külön relációként leképeznünk.
4.5.5 Öröklés
A leszármazott entitások relációs sémára történő leképezésére több lehetőség is kínálkozik, amelyek
eltérő hatékonyságúak és bonyolultságúak. A megfelelő módszer kiválasztása mindig az aktuális
73
feladattól függ. A következőkben a korábban már ismertetett 4.23. ábrán illusztrált egyed-kapcsolat
diagramon keresztül fogjuk bemutatni a különböző módszerek előnyeit és hátrányait.
Név
Ár
Korhatár
Játék Ruha Cipő Könyv
Szerző
Szín
Hossz
RuhaMéret Anyag CipőMéret Típus
ISBN
𝐽á𝑡é𝑘(𝐾ó𝑑, 𝐾𝑜𝑟ℎ𝑎𝑡á𝑟)
𝑅𝑢ℎ𝑎(𝐾ó𝑑, 𝑆𝑧í𝑛, 𝑅𝑢ℎ𝑎𝑚é𝑟𝑒𝑡, 𝐴𝑛𝑦𝑎𝑔)
𝐶𝑖𝑝ő(𝐾ó𝑑, 𝐶𝑖𝑝ő𝑚é𝑟𝑒𝑡, 𝑇í𝑝𝑢𝑠)
𝐾ö𝑛𝑦𝑣(𝐾ó𝑑, 𝐼𝑆𝐵𝑁, 𝐻𝑜𝑠𝑠𝑧, 𝑆𝑧𝑒𝑟𝑧ő)
A fenti relációs sémában a sárga pamut póló terméket a 4.7. táblázatban látható két n-es fog kifejezni.
74
Kód Név Ár Darabszám Kód Szín Ruhaméret Anyag
1 Póló 4500 15 1 sárga M pamut
… … … … … … …
Termék Ruha
4.7. táblázat: Leszármazott entitás reprezentációja két relációban
Figyeljük meg, hogy a két relációban a két n-es ugyanazzal a kóddal rendelkezik: mindkét reláció
elsődleges kulcsa a 𝐾ó𝑑 attribútum, és a 𝑅𝑢ℎ𝑎 reláció 𝐾ó𝑑 attribútuma egy a 𝑇𝑒𝑟𝑚é𝑘 reláció 𝐾ó𝑑
attribútumát hivatkozó külső kulcs is. Ha egy ruhanemű valamennyi adatára szükségünk van, akkor a
𝐾ó𝑑 attribútum mentén össze kell illesztenünk a 𝑇𝑒𝑟𝑚é𝑘 és 𝑅𝑢ℎ𝑎 relációkat. Ez ennek a
megvalósításnak a hátránya is, mivel minél mélyebb az öröklési lánc, annál több illesztésre van szükség
egy entitás adatainak kinyeréséhez. Ezzel a struktúrával viszont könnyen tudunk reagálni az
entitásmodell változásaira: egy új leszármazott entitás felvételéhez csupán egy új relációt kell
létrehoznunk, illetve egy új attribútum felvétele / törlése esetén is csupán mindig az attribútumot
tartalmazó relációt kell módosítanunk.
reláció, valamint valamennyi leszármazott entitáshoz egy-egy újabb reláció, amelyek megöröklik a
termék reláció attribútumait is:
A fenti sémában egy sárga pamut pólót a 4.8. táblázatban látható egyetlen n-esként tudjuk eltárolni.
Amint látható, a megoldás nagy előnye, hogy az egy entitáshoz tartozó adatokat egy n-esben tárolja,
és csak az entitásra jellemző attribútumok jelennek meg benne. Eredményképpen hatékonyan tudjuk
a jellemző adatokat kinyerni, nincs szükség több tábla illesztésére. További előny, hogy ebben a
leképezésben elegendő csupán a nem absztrakt entitás halmazokat relációkra leképeznünk. Az
absztrakt ősöknek megfelelő relációkban úgysem tárolnánk egyetlen n-est sem.
75
A megoldás hátránya, hogy mivel a leszármazott entitáshalmazoknak megfelelő relációk az ős
halmazok valamennyi attribútumát is tartalmazzák, ezért egy ős entitás halmazban elvégzett
változtatás (pl. egy attribútum felvétele vagy törlése) a teljes öröklési fán végiggyűrűzik, így valamennyi
leszármazott halmazhoz tartozó relációban el kell végezni a változtatást. További problémát jelenthet
az olyan entitások kezelése, amelyek egyszerre több típussal is rendelkeznek: például egyszerre 𝐽á𝑡é𝑘
és 𝐾ö𝑛𝑦𝑣 egy termék. Ilyenkor mind a 𝐽á𝑡é𝑘, mind a 𝐾ö𝑛𝑦𝑣 relációban meg fog jelenni egy-egy n-es,
de mivel az ősöktől örökölt attribútumok (Név, Ár, Darabszám) mindkét relációban megjelennek, ezért
az ezekhez az attribútumokhoz rendelt értékek redundánsan, mindkét n-esben eltárolódnak.
relációt hozunk létre, amelyben a TermékTípus mező szolgál diszkriminátorként, vagyis hogy az egyes
n-eseknek megfelelő entitások típusát jelölje. A diszkriminátor típusa általában numerikus, amely az
egyes típusokat kódolja. Példánkban legyen az egyszerű termék a 0, a játék az 1-es, a ruha a 2-es, a
cipő a 3-as és a könyv a 4-es típussal kódolt. Ennek megfelelően egy építőkocka játék és egy sárga
pamut póló adatait a 4.9. táblázatban látható módon tárolhatjuk el.
Ennek a megoldásnak szintén az a legnagyobb előnye (azon túl, hogy könnyen megvalósítható), hogy
az összes adat egy helyen van, ezért egy adott entitáshoz tartozó adatok könnyen, egyszerű
lekérdezések segítségével illesztés nélkül kinyerhetők. Hátránya pedig természetesen az, hogy minden
egyes entitáshoz (típustól függetlenül) valamennyi attribútum számára biztosítanunk kell a tárolási
lehetőséget.
Mivel bizonyos esetekben előfordulhat, hogy egy entitás egyszerre több típussal is rendelkezik, ezért
az egyszerű típus sorszámozás nem mindig elegendő, hiszen egy mezőben egyszerre nem tudjuk az 1-
es és a 4-es értéket is eltárolni. Ilyenkor több lehetőségünk is van:
76
pedig a 16-os (10000), és egy termék egyszerre könyv és játék is, akkor hozzá a 2 OR 16=18-at
rendeljük típusként (10010). (Vegyük észre, hogy különböző kettő hatványoknál a bináris vagy
művelet az összeadással azonos eredményt produkál). Egy konkrét n-es bizonyos típusba
tartozását az n-es típus azonosítójának és a konkrét típus azonosítójának összeéselésével
dönthetjük el (10010 AND 10000 = 10000).
További lehetőség minden egyes típushoz felvenni 1-1 saját diszkriminátor oszlopot, amely
logikai igazat (1) tartalmaz, ha egy n-es az adott típusba tartozik, és logikai hamist (0), ha nem
az adott típus példánya.
Szín Raktárkészlet
Email Cím
Ugyanezen adatbázis Crow’s feet jelölésmóddal történő ábrázolása a 4.25. ábrán látható.
77
Termék
Gyártó TermékKód
Név
GyártóKód Ár
Név Kategória
Cím Szín
Email Raktárkészlet
GyártóKód
GyártMióta
Vegyük észre, hogy az egy-többes kapcsolatot egy egyszerű éllel jelöljük (az él multiplicitását az él
végein elhelyezett jelölésekkel fejezzük ki, lásd később), a kapcsolathoz rendelt 𝑀𝑖ó𝑡𝑎 attribútumot
pedig a 𝑇𝑒𝑟𝑚é𝑘 entitás halmazba helyeztük 𝐺𝑦á𝑟𝑡𝑀𝑖ó𝑡𝑎 néven (ez egy önkényes döntés volt,
készíthettünk volna kapcsoló entitást is). A Chen-féle jelölésmóddal ellentétben ezen a diagramon már
megjelenik az egy-többes kapcsolatot kifejező 𝐺𝑦á𝑟𝑡ó𝐾ó𝑑 külső kulcs is a 𝑇𝑒𝑟𝑚é𝑘 relációban. Az
entitás halmazok elsődleges kulcsát jelen esetben az attribútumok alatti aláhúzás fejezi ki, de ez
tervező eszközönként eltérő lehet, gyakori még a kulcs piktogram vagy a PK (Primary Key) felirat is az
attribútum mellett.
Ahogyan említettük, a kapcsolatok számosságát az él végein elhelyezett jelölésekkel fejezzük ki. A kör
a 0-t, a függőleges vonal az 1-t, a szétágazó vonal (köznyelvben „tyúkláb”), pedig a tetszőleges számot
jelöli. Az él mindkét végén 2-2 jelölés található: az él középpontjához közelebbi jelzi a minimum, az
entitás halmazhoz közelebbi pedig a maximum számosságot. A 4.25. ábrán látható él 𝐺𝑦á𝑟𝑡ó felöli
oldalán látható jel tehát azt jelenti, hogy egy termékhez minimum 0, maximum 1 𝐺𝑦á𝑟𝑡ó entitás
kapcsolódhat. Az él jobb oldalán látható jel pedig azt fejezi ki, hogy egy 𝐺𝑦á𝑟𝑡óhoz tetszőleges
számú (beleértve 0) 𝑇𝑒𝑟𝑚é𝑘 tartozhat. 4.10. táblázatban összefoglaljuk a Chen és a Crow’s foot
jelölésmód tipikus multiplicitás jelöléseit.
1..1
0..*
1..*
Vegyük észre, hogy mivel a relációs sémában nem tudunk tetszőleges számosságot kifejezni (pl. 3..5
vagy 0..30), ezért a Crow’s foot jelölésmód sem biztosít erre lehetőséget.
78
Egyetem Rektor
Egyetem Vezet Rektor Vezet
A 4.27. ábra egy egy-többes kapcsolatot mutat be: egy egyetem tetszőleges számú (de legalább egy)
karból áll (ezért az 1..* multiplicitás), egy kar viszont pontosan egy egyetemhez tartozik (1..1).
A 4.28. ábrán látható több-többes kapcsolat azt fejezi ki, hogy egy egyetemen tetszőleges számú (0..*)
oktató oktathat (akár 0 is, közvetlenül az alapítás után), illetve egy oktató tetszőleges számú (0..*)
egyetemen dolgozhat.
Egyetem Oktató
Egyetem Alkalmaz Oktató Alkalmaz
Az ábrán látható több-többes kapcsolat nem rendelkezik attribútumokkal, ezért ebben az esetben
egyetlen éllel ki tudjuk fejezni. Gyakorlatban a több-többes kapcsolatot relációs sémára csak kapcsoló
táblák segítségével tudjuk leképezni, mint ahogy a 4.29. ábrán is látható.
Az 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧 kapcsolatot egy relációvá alakítottuk, a több-többes multiplicitást pedig két 1-többessé:
egy 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧 kapcsolat példány (tehát egy 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧á𝑠 entitás) pontosan egy 𝐸𝑔𝑦𝑒𝑡𝑒𝑚 és egy
𝑂𝑘𝑡𝑎𝑡ó között teremt kapcsolatot, ezért használjuk az 1..1 multiplicitásokat az 𝐸𝑔𝑦𝑒𝑡𝑒𝑚 és 𝑂𝑘𝑡𝑎𝑡ó
entitás halmazok felé. Egy 𝑂𝑘𝑡𝑎𝑡óhoz, illetve egy 𝐸𝑔𝑦𝑒𝑡𝑒𝑚hez viszont tetszőleges számú (akár 0)
𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧á𝑠 entitás is kapcsolódhat (az 𝐸𝑔𝑦𝑒𝑡𝑒𝑚 felől nézve egy 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧á𝑠 entitás egy 𝑂𝑘𝑡𝑎𝑡ó𝑡
fejez ki, az 𝑂𝑘𝑡𝑎𝑡ó felől nézve egy 𝐴𝑙𝑘𝑎𝑙𝑚𝑎𝑧á𝑠 entitás pedig pontosan egy 𝐸𝑔𝑦𝑒𝑡𝑒𝑚𝑒𝑡), ezért az élek
másik végén a 0..* multiplicitást alkalmazzuk.
79
5 Structured Query Language (SQL)
Miután Edgar F. Codd 1970-es cikke lefektette a relációs adatbázis-kezelés elméleti alapjait, kollégái –
Donald D. Champerlin és Raymond F. Boyce – kifejlesztettek egy SQUARE (Specifying Queries As
Relational Expressions) [7] nevű lekérdező nyelvet, amely halmazelméleti és predikátum kifejezések
segítségével tette lehetővé relációs adatok lekérdezését. Ez a nyelv még bonyolult matematikai
szintaxist használt, amelyet felismerve elkészítették a nyelv továbbfejlesztését, amelyet 1974-ben
SEQUEL (Structured English QUEry Language) néven publikáltak. Ez a változat megtartotta elődje
kifejezőerejét, de – ahogyan a neve is mutatja – egyszerű angol nyelvű kifejezésekkel operált, ezáltal
sokkal olvashatóbb és kezelhetőbb kódot eredményezett, valamint a nem szakértők is könnyebben el
tudták sajátítani.
Például az 𝐸𝑀𝑃(𝑁𝐴𝑀𝐸, 𝐷𝐸𝑃𝑇, 𝑆𝐴𝐿𝐴𝑅𝑌, 𝑀𝐺𝑅) alkalmazotti relációban azon alkalmazottak nevének
és fizetésének listáját, akik a ’TOY’ részlegen (DEPT) dolgoznak és ’ANDERSON’-nak hívják a
menedzserét (MGR), az alábbi SQUARE kifejezéssel lehet előállítani:
A SEQUEL megtartotta a SQUARE deklaratív jellegét, vagyis azt fogalmazzuk meg, hogy milyen adatokra
vagyunk kíváncsiak, nem pedig azt, hogy hogyan kell ezeket az adatokat előállítani. A SEQUEL
elnevezést később SQL-re egyszerűsítették (mivel a SEQUEL már bejegyzett védjegy volt egy teljesen
más iparágban), bár kiejtésben még mindig gyakori (és elfogadott) a régi névre utaló „szíkvel” alak is.
1979-ben jelent meg az első kereskedelmi relációs adatbázis-kezelő rendszer, amely már az SQL-t
használta. A termék neve Oracle V2 volt, a szoftvert pedig a Relational Software nevű vállalat
készítette, amelyet 1983 óta Oracle-nek hívunk. Hetekkel az Oracle V2 megjelenése után az IBM is
bemutatta saját SQL-alapú termékét, a System R-t, amelyet 1981-ben az SQL/DS, majd 1983-ban végül
a DB2 követett. A DB2 a mai napig az az IBM első számú adatbázis-kezelő rendszere.
Az SQL nyelvet először az ANSI szabványosította 1986-ban, majd az ISO 1987-ben. A bejegyzett
szabványokat 1989-ben átdolgozták, és SQL89 néven publikálták. Azóta számos újabb SQL szabvány
jelent meg: SQL92, SQL99, SQL:2003, SQL:2006, SQL:2008, SQL:2011, valamennyi a megelőző
változatot egészíti ki új nyelvi és szemantikai elemekkel. Az implementáció megkönnyítése érdekében
minden szabvány három szintet definiál: belépő, közepes és teljes implementáció. Általában egy újabb
szabvány belépő szintje mindig a megelőző szabvány teljes szintjéből indul ki. Mivel a szabványok
általában csak lassan reagálnak az iparági igényekre, ezért a legtöbb adatbázis-kezelő rendszer a
szabványos elemeken túl saját, szabványon kívüli nyelvi elemeket és funkcionalitást is tartalmaz.
1. Data Definition Language (DDL): ebbe az utasítás csoportba tartoznak azok a műveletek,
amelyekkel új adatbázisokat, relációkat tudunk létrehozni, illetve meglévők sémáját
módosítani (pl. CREATE, ALTER, DROP…).
80
2. Data Manipulation Language (DML): a DML-be tartoznak a leggyakrabban használt
utasítások, amelyek segítségével a relációkban tárolt adatokon tudunk műveleteket végezni
(beszúrás, módosítás, lekérdezés stb) (pl. INSERT, SELECT, UPDATE, DELETE…)
3. Data Control Language (DCL): a DCL utasítások segítségével az adatbázis bizonyos részeihez
történő felhasználói hozzáférést tudjuk szabályozni (pl. GRANT, REVOKE)
4. Transaction Control Language (TCL): a tranzakciós műveletek segítségével tudjuk az
adatbázis integritását biztosítani úgy, hogy összetartozó SQL utasítások egy csoportját
oszthatatlan egységbe fogjuk velük össze (pl. BEGIN TRAN, COMMIT, ROLLBACK)
Ebben a fejezetben csak a legalapvetőbb SQL utasításokat fogjuk áttekinteni, amelyeket már az
SQL:2003 szabvány is támogatott (természetesen ebből sem az összeset), és a legtöbb ma használatos
rendszeren ugyanúgy működnek. Továbbá néhány szabványon kívüli utasítást és típust is
megismerünk, amelyek a mindennapi munkához nélkülözhetetlenek (pl. dátum és sztring kezelő
függvényeket). Ismertetőnk elsősorban a MySQL9 5 nyílt forrású adatbázis-kezelő rendszerre épül, de
ahol lehet, igyekszünk bemutatni az Oracle10 12c és Microsoft SQL Server11 2014-beli (TSQL)
implementációt is.
Az első lépés új táblák létrehozása előtt, hogy készítünk egy adatbázist, amelyben a táblákat tárolni
szeretnénk. Új adatbázis létrehozása tipikusan a
9
MySQL website: http://www.mysql.com/
10
Oracle database website: https://www.oracle.com/database/
11
Microsoft SQL Server Website: http://www.microsoft.com/en-us/server-cloud/products/sql-server/
81
név a lemezen, fájl méret, alapértelmezett kódolás stb.). Számos implementációban (MySQL, MSSQL)
lehetőségünk van az aktuálisan használt adatbázis kiválasztására, illetve cserélésére egy felhasználói
folyamaton belül, ezt általában a
USE adatbázisnév
utasítással tehetjük meg. Más rendszerekben (pl. Oracle) minden felhasználóhoz egy saját adatbázis
rendelődik, amelynek a neve a benne létrehozott táblák nevének is a része lesz, így nem szükséges
aktuális adatbázist választani, valamennyi táblanév így is egyedi lesz.
Itt jegyeznénk meg, hogy az SQL nyelv (sztringeket leszámítva) nem különbözteti meg a kis és
nagybetűket. Mégis, az utasításokat és beépített kulcsszavakat általában nagybetűvel szokták írni, a
felhasználói objektumokat (tábla, oszlopnevek stb.) pedig kicsivel, esetleg nagy kezdőbetűvel, hogy így
javítsák a kód olvashatóságát.
5.1.1 Típusok
A táblák attribútumai (mezői) minden esetben típusosak, tehát csak bizonyos, előre meghatározott
típusú adatot lehet bennük tárolni. A következőkben bemutatjuk az SQL-alapú rendszerek által
leggyakrabban támogatott típusokat.
82
FLOAT[(n)] (REAL[(n)]): az n paraméterrel azt adhatjuk meg, hogy a lebegőpontos szám
mantisszáját hány biten tároljuk el. n maximális és egyben alapértelmezett értéke 53. (MySQL,
MSSQL).
MySQL esetén valamennyi típusnál használható azok UNSIGNED változata is (UNSIGNED INT,
UNSIGNED BIGINT …), ilyenkor a típusok nem rendelkeznek negatív tartománnyal, viszont a pozitív
tartomány a korábbi duplájára nő.
5.1.1.2 Sztringek
SQL-ben a sztring literálokat egyszeres aposztrófok között ábrázoljuk: ’szöveg’. Az egyes
implementációkban támogatottak mind fix, mind változó hosszúságú sztringek, amelyek eltérő
ábrázolási pontossággal (ASCII, Unicode) és megengedett hosszúsággal rendelkeznek:
DATE: a szabvány által is rögzített egyszerű dátum típus, amely év, hónap, nap értékek
reprezentálására alkalmas. Értékadás során ’2015-01-01’ formátumú sztringről automatikusan
konvertálódik. A legtöbb SQL implementáció támogatja, de nagyon eltérő értelmezési
tartományokkal: MySQL esetén 1000-01-01…9999-12-31, MSSQL-nél 0001-01-01…9999-12-
31, Oracle esetén -4712-01-01…9999-12-31, viszont Oracle esetén a típus óra, perc,
másodperc értékeket is tárol a dátumon kívül.
83
TIME: a szabvány szerint óra, perc, másodperc adatok tárolására alkalmas, a legtöbb
implementáció ezen kívül még törtmásodpercek leírását is támogatja. MySQL esetén az idő
formátuma óra:perc:másodperc és a következő intervallumból veheti fel az értékét:
–838:59:59...838:59:59. A TIME típust nem csak egy napon belüli időpont leírására, hanem
időtartam leírására is használjuk, ezért lehet az óra értéke nagyobb, mint 23. Az MSSQL-nél
00:00:00.0000000... 23:59:59.9999999 tartományon belüli értéket vehet fel egy TIME típusú
oszlop. Bár az SQL 92 szabvány rögzíti, az Oracle mégsem támogatja ezt a típust.
TIMESTAMP: a szabvány által deklarált típus dátum és idő együttes tárolására. MySQL esetén
az értelmezési tartomány 1970-01-01 00:00:01…2038-01-19 03:14:07 (másodperces
pontossággal), Oracle esetén TIMESTAMP(n) típust használva n-nel (<=9) a törtmásodpercek
tárolási pontosságát (tizedes jegy) adhatjuk meg (ha nem adjuk meg, alapértelmezetten n=6).
MSSQL-ben nem támogatott.
DATETIME: az MSSQL és MySQL által támogatott típus, amely a DATE és TIME
kombinációjának felel meg. Oracle esetén a DATE típus már önmagában is képes napon belüli
idő értékek eltárolására. MySQL esetén az értelmezési tartomány 1000-01-01 00:00:00…9999-
12-31 23:59:59, MSSQL esetén pedig 1753-01-01...9999-12-31, 0.00333 másodperces
pontossággal.
DATETIME2: az MSSQL által támogatott típus, amely a DATETIME egy precízebb változata, az
idő reprezentáció pontossága 100ns.
Új SQL tábla létrehozása a CREATE TABLE utasítással lehetséges, amelyet a tábla neve, majd
zárójelben vesszővel elválasztva az egyes mezői követnek. Például a 4. fejezetben is bemutatott
𝑇𝑒𝑟𝑚é𝑘(𝑇𝑒𝑟𝑚é𝑘𝐾ó𝑑, 𝑁é𝑣, Á𝑟, 𝐾𝑎𝑡𝑒𝑔ó𝑟𝑖𝑎, 𝑆𝑧í𝑛, 𝑅𝑎𝑘𝑡á𝑟𝑘é𝑠𝑧𝑙𝑒𝑡) relációnak megfelelő táblát az
alábbi SQL utasítással lehet létrehozni:
84
TermekKod INT,
Nev VARCHAR(100),
Ar INT,
Kategoria VARCHAR(50),
Szin VARCHAR(20),
Raktarkeszlet INT
)
Vegyük észre, hogy az SQL utasításban – az elméleti szinten létező relációs sémával szemben – nem
használtunk ékezetes betűket a tábla vagy az oszlopok elnevezésére. Ennek az az oka, hogy sok SQL
implementáció csak az angol betűket, számokat és néhány speciális karaktert (például _) támogatja a
tábla és oszlopnevekben, ezért célszerű a speciális és ékezetes karaktereket az implementációban
elkerülni.
Termékkódként egy egész számot (INT) használunk, a termék nevét egy sztringben (VARCHAR(n))
írjuk le, amelynek maximális hosszát önkényesen 100 karakterben szabtuk meg. A termék kategóriáját
50, színét pedig 20 karakteren írhatjuk le. Az ár és a raktárkészlet reprezentálására természetesen
szintén INT típust használunk (váltópénz használata esetén az árnál megfontolandó valamilyen
lebegőpontos típus alkalmazása). Egy sztring hosszát érdemes úgy megválasztani, hogy ne legyen túl
rövid, mert akkor bevitel során a szöveg csonkolódna, vagy hibaüzenetet kapnánk. De nem érdemes
feleslegesen nagy méretet sem választani, mivel implementációtól függően ennek teljesítménybeli ára
lehet.
Az egyes mezőkre a típuson kívül további kényszereket fogalmazhatunk meg (például kötelező
kitölteni, a tárolt értéknek egyedinek kell lennie stb.), illetve az egész táblára is adhatunk meg
kényszereket, tárolási paramétereket. A CREATE TABLE utasítás általános alakja a következőképp fest:
85
Szin VARCHAR(20),
Raktarkeszlet INT
)
A virtuális oszlop típusa a számított értéket előállító kifejezésből automatikusan következik. MSSQL
esetén a PERSISTED kulcsszó használatával (BruttoAr AS Ar*1,27 PERSISTED) a számított érték
el is tárolható a lemezen. Lekérdezések kiértékelésekor nem kell a számítást futási időben elvégezni
minden egyes alkalommal, hanem az érték azonnal rendelkezésre áll, ami hatékonyságnövekedést
eredményez. (Természetesen az Ar érték módosításakor a BruttoAr-at is újra ki kell értékelni, és el
kell tárolni, ami viszont többletműveletet jelent. Ez nem okoz problémát, ha – mint általában – a
lekérdezés művelet jóval gyakoribb, mint a módosítás).
5.1.3 Adatbevitel
Egy már létező táblába új rekordot az INSERT utasítással vehetünk fel. Általános alakja:
Az új rekord létrehozása során nem kötelező az összes attribútumot felsorolni és annak értéket adni.
A fel nem sorolt oszlopokhoz rendelt érték vagy üres (NULL) vagy az oszlophoz rendelt alapértelmezett
érték lesz (lsd. 5.1.4 fejezet). A táblanév után szereplő attribútum lista el is hagyható, ekkor az oszlopok
adatbázis sémában tárolt sorrendjében kell az új értékeket megadnunk a VALUES kulcsszó után (az
5.1.2 fejezetben létrehozott sémát feltételezve):
Mivel egy tábla különböző rendszereken tárolt példányaiban az oszlopok sorrendje nem feltétlenül
egyezik meg (például egyes módosító szkripteket különböző sorrendben futtattunk le a két
adatbázison, ezért az újonnan felvett oszlopok más sorrendben jöttek létre), ezért mindig célszerű az
oszlop neveket is pontosan megadni. Ezáltal a kódunk is olvashatóbb lesz, és csökkentjük a hibák
lehetőségét.
MySQL és MSSQL rendszereken a VALUES kulcsszó után akár több rekord adatait is felsorolhatjuk
vesszővel elválasztva, így egy utasítással több entitás adatait is el tudjuk tárolni:
86
5.1.4 NULL, alapértelmezett érték
Az SQL-ben is létezik egy a C-beli NULL-hoz hasonló speciális érték, amit szintén NULL-lal jelölünk. SQL-
ben bármely típusú attribútumhoz rendelhetünk NULL értéket, tehát akár egész számokhoz is. NULL-t
használunk abban az esetben, ha egy adott attribútum értéke az entitás rögzítésének pillanatában nem
ismert, vagy nem használatos.
Például ha egy online boltban nem csak fizikai, hanem virtuális termékeket is árulunk (például újság
előfizetés), amelynek nem értelmezett a színe vagy a darabszáma, akkor a nem értelmezett
attribútumokat vagy nem tűntetjük fel, vagy expliciten NULL értéket adunk nekik:
illetve
Bizonyos attribútumoknál nem értelmezett a NULL érték. Például egy termék azonosítója tipikusan
nem lehet ismeretlen, hasonlóképp egy termék neve sem, illetve minden termék kategorizálható. Az
ilyen attribútumoknál a NOT NULL kényszerrel már a tábla létrehozásakor megtilthatjuk, hogy
kitöltetlenek maradjanak:
Az adatbevitel egyszerűsítése céljából az egyes mezőkhöz rendelhetünk alapértelmezett értéket is, így
ha nem rendelünk hozzájuk konkrét értéket a beszúrás során, akkor azok nem a NULL, hanem az
alapértelmezett értéket fogják felvenni. Ezt az értéket a DEFAULT kulcsszót követően adhatjuk meg
minden egyes oszlop esetén. Például egy újonnan felvett termék típusnál a raktárkészlet
alapértelmezetten 0 darab, az ára szintén 0:
Így az
87
INSERT INTO Termek (TermekKod, Nev, Kategoria) VALUES(3, ’Labda’, ’Játék’)
Mivel a Szin-hez nem rendeltünk konkrét értéket, ezért az a NULL értéket fogja felvenni, az Ar és a
Rakterkeszlet attribútumhoz szintén nem rendeltünk konkrét értéket a beszúrás során, de ők
rendelkeznek alapértelmezett értékkel (0), ezért hozzájuk az fog rendelődni.
A megoldás előnye, hogy név alapján a kényszer könnyen hivatkozható, így egyszerűen törölhető vagy
módosítható.
Bár a TermekKod attribútumra megköveteljük, hogy minden termék esetén ki legyen töltve,
valahogyan biztosítani kellene, hogy az minden egyes termékre egyedi is legyen. Erre SQL-ben két
lehetőség is kínálkozik: a UNIQUE és a PRIMARY KEY kényszerek. Mindkét kényszer megköveteli, hogy
az attribútum, amelyen alkalmaztuk, nem vehet fel azonos értéket a reláció két különböző
rekordjában. A fő különbségek a két kényszer között a következők:
Amíg a UNIQUE kényszer megengedi, hogy NULL értéket is felvehessen az attribútum egy vagy
több rekordban (és csak a nem NULL értékek egyediségét követeli meg), addig a PRIMARY KEY
kényszer nem engedi meg NULL értékek tárolását az adott attribútumban.
Egy táblában egyszerre csak egy elsődleges kulcs lehet, viszont akár több attribútumra is
helyezhetünk UNIQUE kényszert.
A PRIMARY KEY kényszer alapértelmezetten a rekordok lemezen történő tárolási sorrendjét is
meghatározza (lsd. 7.1 fejezet).
88
Nev VARCHAR(100) NOT NULL, Nev VARCHAR(100) NOT NULL,
Ar INT DEFAULT 0, Ar INT DEFAULT 0,
Kategoria VARCHAR(50) NOT NULL, Kategoria VARCHAR(50) NOT NULL,
Szin VARCHAR(20), Szin VARCHAR(20),
Raktarkeszlet INT DEFAULT 0 Raktarkeszlet INT DEFAULT 0
) )
Gyakran találkozhatunk SQL kódban PRIMARY KEY NOT NULL kényszerrel is, amely – mivel a
specifikáció megköveteli, hogy a PRIMARY KEY attribútum nem vehet fel NULL értéket – redundánsnak
tűnik. Az, hogy gyakran mégis használják a NOT NULL kényszert is, azért van, mert így egyrészt
olvashatóbb az SQL kód, másrészt így nem hagyatkozunk arra, hogy az aktuális SQL szerver
implementáció valóban teljes mértékben megvalósítja a szabványt, és nem engedi meg a NULL
értékeket.
Előfordulhat, hogy nem egy attribútum egyediségét szeretnénk előírni, hanem több attribútumét
együtt. Például nem létezik két olyan termék, amely azonos kategóriában szerepel és azonos a nevük
is (tehát csak különböző termékkategóriában engedélyezünk névegyezést). Ilyen esetekben a UNIQUE
kényszert a tábla szintű kényszerek között kell felvennünk, az oszlopneveket pedig utána zárójelek
között kell felsorolnunk:
A példában mind az elsődleges kulcsot, mind az egyedi név-kategória párost leíró kényszert tábla szintű
kényszerként hoztuk létre pk_Termek, illetve uc_NevKat néven. A fenti megoldásnak az a legfőbb
89
előnye, hogy a kényszerekre ezután név szerint tudunk hivatkozni, a név alapján egyszerűen tudjuk
őket módosítani, törölni.
Természetes elsődleges kulcs esetén (például személyi szám, taj szám stb…) két különböző
személyre (rekordra) ezen attribútumok mindig különböző értékeket vesznek fel, így nem
szükséges további művelet.
Előállíthatunk elsődleges kulcsot például egy időbélyeg alapján vagy az időbélyeget valamilyen
természetes tulajdonsággal kombinálva. Például a rekord beszúrásának időpontja
milliszekundumos pontossággal (természetesen ehhez biztosítani kellene, hogy egy
milliszekundumon belül ne lehessen két rekordot beilleszteni).
Használjunk globálisan egyedi azonosítókat (GUID, UUID …), amelyek általában 128 bites
számok, és egyediségükről az operációs rendszer gondoskodik. Új azonosítót MySQL-ben a
UUID(), MSSQL-ben a NEWID(), Oracle alatt pedig a sys_guid() függvényekkel állíthatunk
elő. Bár használatuk kényelmes, a generált számok nem folytonos, véletlenszerű eloszlása
nehézségeket okozhat: ahogyan említettük, az elsődleges kulcs gyakran a rekordok tárolási
sorrendjét is meghatározza, ezért ha a kulcs kiosztása nem folytonos, akkor egy új rekord
felvételekor azt a megfelelő helyre kell beszúrni, és az utána következő rekordokat „arrébb
tolni”, ami többlet terhelést jelent az adatbázis kezelőnek. Továbbá egy véletlennek tűnő nagy
128 bites szám, például 48cd04e8-74b0-478d-b1dc-3ca5ba904f8c (a globálisan egyedi
azonosítók tipikus ábrázolási módja) humán használatra nem kifejezetten alkalmas.
A leggyakrabban használt módszer, hogy az adatbázis-kezelőre bízzuk egy egyedi sorszám
előállítását. MySQL környezetben ez az AUTO_INCREMENT kényszer segítségével lehetséges,
MSSQL-ben pedig az IDENTITY(seed, increment) kényszerrel:
90
utasítás paraméterei között, és nem adhatunk neki explicit értéket, azt az adatbázis-kezelő
rendszertől fogja megkapni (5.2. táblázat):
Oracle rendszereken az egyedi értékeket nem attribútum szintű kényszerekkel adhatjuk meg,
hanem úgynevezett szekvenciát kell létrehoznunk, amely egy önálló adatbázis objektum, és
az a feladata, hogy egyedi értékeket állítson elő:
A fenti utasítás seq_termek néven hoz létre egy új szekvenciát, amely 1-től indul, egyesével
nő, és a gyorsabb hozzáférés érdekében egyszerre 10 új értéket generál, amelyek közül a még
fel nem használtakat a memóriában tárolja. Új rekord beszúrásakor a szekvencia által generált
következő értéket a NEXTVAL tulajdonsággal érhetjük el:
Külső kulcsokkal SQL-ben csak PRIMARY KEY vagy UNIQUE oszlopokra hivatkozhatunk. Külső kulcs
kényszert legcélszerűbb tábla szinten deklarálni, névvel ellátott kényszerként:
CONSTRAINT <kényszer név> FOREIGN KEY <külső kulcs oszlop1, külső kulcs oszlop2…>
REFERENCES <hivatkozott tábla>(<kulcs oszlop1, kulcs oszlop2…> )
Ahogy a szintaktikából látszik, a hivatkozott kulcs lehet egyszerű és összetett is, ennek megfelelően a
külső kulcs is állhat akár több attribútumból is. Illusztrációképp készítsük el a gyártók tárolására
szolgáló Gyarto relációt, amelynek rekordjaira a termékekből hivatkozni tudunk, mint az egyes
termékek gyártóira:
91
Cím VARCHAR(200) NOT NULL,
Email VARCHAR(100)
)
A tábla elsődleges kulcsa a GyartoKod egész típusú attribútum, minden egyes gyártóhoz eltároljuk
annak nevét és (amennyiben ismert), címét és az e-mail címét. A Termek relációból ezek után a
következőképp hivatkozhatjuk a megfelelő gyártót:
A Termek tábla GyartoKod oszlopát NOT NULL-ként deklaráltuk, tehát kötelező kitölteni, és az
FK_Termek_Gyarto kényszerrel írjuk elő, hogy annak értékei csak a Gyarto reláció GyartoKod
attribútumai közül kerülhetnek ki. Természetesen a külső kulcs típusának egyeznie kell a hivatkozott
kulcs oszlop típusával.
A külső kulcs oszlopoknak nem feltétlen kell kizárniuk a NULL értékeket. Ha megengedik azokat, akkor
az integritás ellenőrzés csak akkor történik meg (vagyis hogy a hivatkozott táblában létezik kulcs azonos
értékkel), ha a külső kulcs attribútum egy konkrét rekordnál ki van töltve. Az integritást az adatbázis-
kezelő rendszer minden körülmények között fenntartja:
ellenőrzi az érték helyességét, amikor egy külső kulcsnak új értéket adunk (vagyis hogy a
hivatkozott táblában szerepel-e rekord azonos kulcs értékkel), illetve
ellenőrzi, hogy egy kulcs megváltozásakor (a teljes rekord törlésekor, vagy amennyiben a kulcs
attribútum új értéket kap) vannak-e más olyan rekordok, amelyek külső kulcsokon keresztül
ezt a rekordot hivatkozzák.
92
ON DELETE SET NULL
ON UPDATE CASCADE
)
A fenti példában a hivatkozott gyártó rekord törlésekor a kapcsolódó Termek rekordok GyartoKod
külső kulcs egyszerűen NULL-ra állítódik, míg ha a hivatkozott gyártó elsődleges kulcsa megváltozik,
akkor a hivatkozó termékek GyartoKod külső kulcsa automatikusa az új elsődleges kulcs értéket
venné fel.
A különböző implementációkban használatosak a típus után fűzött külső kulcs deklarációk is, mint
például (MSSQL, Oracle):
Mi mégis a tábla szinten, névvel ellátott konstrukciót javasoljuk, hiszen – a többi kényszerhez
hasonlóan – ha névvel tudunk hivatkozni a kényszerre, az megkönnyíti annak módosítását, esetleges
törlését.
Miután felépítettünk egy adatbázis sémát, a legtöbb esetben nem tekinthetjük azt véglegesnek, előbb-
utóbb szükségünk lesz rá, hogy módosítsunk rajta. Ez lehet bizonyos elemek törlése, attribútumok
típusának megváltoztatása vagy akár új kényszerek létrehozása stb. Ha az adatbázisban már tárolunk
felhasználói adatot, akkor az adatbázis törlése és újbóli létrehozása az új sémával nem járható út. Az
SQL nyelv és az adatbázis-kezelő rendszerek biztosítják a megfelelő műveleteket az adatbázis séma
módosítására.
Például, ha a Termek relációhoz szeretnénk felvenni egy új oszlopot, amelyben szöveges megjegyzést
tárolhatunk a termékről, akkor azt a következőképp tehetjük meg:
93
táblában már voltak rekordok, és mi egy NOT NULL oszlopot hozunk létre alapértelmezett érték nélkül,
akkor – mivel nem tudja, hogy mivel kellene feltölteni az új oszlopot a már létező rekordoknál –
hibaüzenetet kapunk.
Egy létező oszlop típusát szintén az ALTER TABLE utasítással változtathatjuk meg, a művelet-
specifikus kulcsszavak viszont implementációnként eltérőek. MySQL esetén a MODIFY COLUMN
kulcsszavakat használhatjuk az oszlopdefiníció megváltoztatására.:
Például ha használat közben kiderül, hogy a termékek nevének kicsi a 100 karakteres méret, inkább
150-re lenne szükség, és meg akarjuk engedni a NULL értéket is, akkor azt a következőképp tehetjük
meg:
ALTER TABLE <tábla név> RENAME COLUMN <oszlop> TO <új oszlop név>
MSSQL rendszeren az ALTER COLUMN utasítás használatos egy létező oszlop tulajdonságainak
megváltoztatására:
Oszlopot átnevezni pedig az eddigiektől eltérően az ún. sp_rename nevű tárolt eljárással (5.4. fejezet)
lehet:
Például
94
Az adatbázis-kezelő rendszerek az integritás biztosítása miatt csak azokat az oszlopokat engedik
törölni, amelyekre nem hivatkozik külső kulcs kényszer, tehát az oszlop törlése előtt a kapcsolódó külső
kulcsokat a többi táblából törölni kell.
utasítással lehet törölni. Ez vonatkozik a külső kulcsokra (FOREIGN KEY), az elsődleges kulcs kényszerre
(PRIMARY KEY), illetve MSSQL esetén az alapértelmezett értékre (Oracle-ben az alapértelmezett érték
kényszer nem törölhető, csak NULL-ra állítható az értéke). A Gyarto és Termek tábla közti kapcsolatot
tehát a következő utasítással szűntethetjük meg:
MySQL esetén enyhén eltérő szintaktikát kell használni. Külső kulcsok törlésére a következő utasítás
szolgál:
(Tehát CONSTRAINT helyett a FOREIGN KEY kulcsszavakat kell használni). Elsődleges kulcs törlésekor
nincs szükségünk a kulcs megnevezésére (mivel egy táblában úgyis csak egy kulcs lehet), a PRIMARY
KEY kulcsszavak használatosak:
Alapértelmezett kényszer törlésekor pedig egy oszlop módosítást kell végrehajtanunk, és az érintett
oszlop nevével azonosíthatjuk a kényszert (ami egyértelmű, hiszen egy oszlopon úgyis csak egy
alapértelmezett kényszer lehet):
Természetesen nem csak törölhetjük a kényszereket, hanem akár újakat és létrehozhatunk. Itt
implementációtól függően szintén eltérő módszereket használhatunk. Külső kulcs felvételére
egységesen alkalmazható a következő alak:
95
Ha a Gyarto és Termek táblák közt korábban nem létesítettünk külső kulccsal kapcsolatot, akkor azt
a következőképp tehetjük meg:
Hasonlóképp hozhatunk létre új elsődleges kulcsot is (amennyiben a tábla még nem rendelkezett
ilyennel, vagy töröltük azt):
ALTER TABLE <tábla név> ALTER COLUMN <oszlop> SET DEFAULT (érték)
Oracle:
MSSQL:
ALTER TABLE <tábla név> ADD CONSTRAINT <kényszer név> DEFAULT (érték)
Szintén elmondható, hogy más szintaktikák is használatosak, de terjedelmi okokból mi csak egy-egy
gyakran használt módot mutattunk be a lehetségesek közül.
utasítással tudjuk törölni. Egyes rendszerek nem engedik az éppen használt adatbázis törlését, tehát
mielőtt az utasítást kiadjuk, célszerű az aktív kapcsolatokat a törlendő adatbázishoz lezárni.
Hasonlóképp, ha egy bizonyos táblára nincs már szükségünk, akkor azt a
Az adatbázis-kezelő rendszerek az integritás biztosítása miatt csak azokat a táblákat engedik meg
törölni, amelynek oszlopaira nem hivatkozik másik táblából külső kulcs kényszer, tehát a tábla törlése
előtt a kapcsolódó külső kulcsokat a többi táblából törölni kell.
5.2 Lekérdezések
A lekérdezések célja, hogy az adatbázis tábláiban tárolt, bizonyos feltételnek megfelelő adatokat
megkeresse, és a felhasználó számára valamilyen szűrt formátumban visszaadja. Az SQL nyelv
96
lekérdezéseket megfogalmazó elemei gyakorlatilag az algebrai lekérdezések (2.2-2.3 fejezetek)
implementációjának tekinthetők, amelyekhez az adatbázis-kezelő rendszer természetesen végrehajtó
motort is biztosít. A továbbiakban tekintsük a következő táblákat adottnak:
Továbbá feltételezzük a vásárló és megrendelés adatok tárolására alkalmas táblákat szintén létezőnek:
Egy tipikus SQL lekérdezés tartalmaz projekciót és szelekciót is, tehát mind a rekordokat, mind a
megjelenített attribútumokat szűri. Egy projekciót és szelekciót tartalmazó egyszerű lekérdezés a
következőképp fest:
A SELECT után adhatjuk meg az eredményben várt attribútumokat (oszlopokat), a FROM kulcsszó után
pedig a táblát vagy táblákat, amelyekben a keresést végezzük. A WHERE kulcsszó utáni szelekciós
feltétel határozza meg, hogy a vizsgált rekordok közül melyik fog belekerülni a végeredménybe. Reláció
algebrában ez a következő kifejezésnek felel meg:
Az összes olyan termék, amelyből 5-nél kevesebb darab van, a következő lekérdezéssel kereshető meg:
97
utasítás az összes termék összes attribútumát megjeleníti, ahogy az például az 5.3. táblázatban látható.
A projekció közben nem csupán bizonyos oszlopok tartalmát jeleníthetjük meg, hanem műveleteket is
végezhetünk az egyes attribútumok értékein. Ha a 100.000 Ft-nál drágább termékek nevét és bruttó
árát szeretnénk megkapni (feltételezve, hogy az Ár oszlop nettó árakat tartalmaz, és az ÁFA kulcs 27%),
a következő lekérdezést alkalmazhatjuk:
A számított értéket tartalmazó oszlopok neve implementációtól függően vagy üres, vagy például maga
az értéket előállító képlet (Ar*1.27). Ha pontosan meg akarjuk határozni az oszlop nevét, akkor
alkalmazhatjuk az AS átnevező kulcsszót:
Megjegyezzük, hogy az AS kulcsszó tetszőleges, nem számított oszlop mellett is használható, valamint
akár el is hagyható:
Az 5.3. táblázatban látható értékek alapján a végeredmény egyetlen rekordot, a kanapé termék adatait
fogja tartalmazni (5.4. táblázat).
TermekNev BruttoAr
Kanapé 190.500
5.4. táblázat: Oszlop átnevezés projekcióban
Az eddigi lekérdezésekben már használtunk egyszerű relációs operátorokat (> és <) ezeken kívül
használható az =, <=, >= és az egyenlőtlenséget jelölő <> is, valamint a segítségükkel előállított primitív
műveletek logikai kombinációi: a logikai „és”, „vagy”, illetve „nem” műveleteket az SQL AND, OR, illetve
NOT logikai operátoraival fejezhetjük ki. Például azon termékek listája, amelyekre nem igaz, hogy
100.000 Ft-nál olcsóbbak és 5-nél több darab van belőlük, a következő lekérdezéssel állítható elő:
98
SELECT * FROM Termek WHERE NOT(Ar < 100.000 AND Raktarkeszlet > 5)
amivel ekvivalens a
lekérdezés (feltételezve, hogy az Ar és Raktarkeszlet oszlopok mindig ki vannak töltve, lsd. 5.2.4.
fejezet).
Sztringek összehasonlítására használhatók a szokásos relációs műveletek (=, >, <, >=, <=), amelyek két
szöveges érték alfabetikus összehasonlítását végzik el. Például a kanapé nevű termék adatai a
utasítással listázhatók ki, de ha az összes olyan termék adataira vagyunk kíváncsiak, amelyek az ABC
szerinti összehasonlításban a ’Kanapé’ szó után következnek, azt is könnyedén megkaphatjuk:
Az SQL nyelv az egyszerű összehasonlításnál jóval összetettebb sztring vizsgálatot is lehetővé tesz a
LIKE operátor segítségével. A LIKE kulcsszó után egy szöveg mintát adhatunk meg, amelyben az
ismeretlen részeket %-kal (tetszőleges hosszúságú rész), illetve _ karakterrel (pontosan egy tetszőleges
karakter) jelölhetjük. Például a ’K’ betűvel kezdődő termékeket a következő utasítással kérdezhetjük
le:
Ha minden olyan termékre vagyunk kíváncsiak, ami valahol tartalmaz egy ’k’ betűt, akkor azt a
következőképp tehetjük meg:
Ahogy említettük, a _ karakter pontosan egy tetszőleges karaktert jelöl. Ennek megfelelően a LIKE
’K_ _ _ _’ kifejezés például az 5 karakter hosszú, ’K’-val kezdődő termékekre szűr.
A ’%!%%’ mintában az első és utolsó % jel tetszőleges karaktersorozatot jelöl, míg a !% minta pedig
magát a % karaktert.
A különböző SQL implementációk a LIKE utáni sztringben definiált mintában beállítástól függően
megkülönböztethetik a kis és nagy betűket. Ha biztosak akarunk benne lenni, hogy az összehasonlítás
nem tesz különbséget köztük, akkor a vizsgált sztringet a kis vagy nagybetűs megfelelőjére
konvertálhatjuk az UPPER(), illetve LOWER() függvényekkel:
99
SELECT * FROM TERMEK WHERE LOWER(TermekNev) LIKE ’k%’
A fenti lekérdezés a ’k’ betűvel (kicsi vagy nagy) kezdődő nevű termékeket jeleníti meg.
Szintén gyakran használt sztring művelet egy szöveges érték hosszának lekérdezése. A művelet
elvégzésére MySQL-ben a CHAR_LENGTH(szöveg), MSSQL-ben a LEN(szöveg), Oracle-ben pedig a
LENGTH(szöveg) függvények szolgálnak. MySQL szintaktikát követve a
TermekNev Hossz
Kanapé 6
Dohányzóasztal 14
Szék 4
Hintaágy 8
Táblázat 5-6 – Termékek neve és nevük hossza
5.2.3 Dátumok
A legtöbb SQL implementáció biztosítja az alapvető műveleteket a dátum és idő típusú értékek
manipulációjához. Az aktuális dátum és idő lekérdezésére MySQL környezetben a NOW() függvény
(illetve csak a dátum lekérdezésére a DATE()), MSSQL rendszereken a CURRENT_TIMESTAMP, Oracle-
ben pedig a SYSDATE kulcsszó szolgál, amelyek DATETIME, illetve Oracle alatt DATE (amely idő értéket
is képes tárolni) típusú értéket ad vissza.
Tegyük fel, hogy a gyártókra vonatkozó információk között eltároljuk az első kapcsolatfelvétel idejét is:
INSERT INTO Gyarto VALUES(5, ’Kiss Bútor Kft’, 1111, ’Budapest’, ’Virágos tér 4’,
’kb@example.org’, NOW())
100
INSERT INTO Gyarto VALUES(5, ’…’, …, ’…’, ’…’,’…’, ’2015-01-01’)
Ha a dátumot más formátumban szeretnénk megadni, akkor konverziós függvényt kell alkalmaznunk.
MySQL esetén az STR_TO_DATE(string, format) függvény használható erre a célra, ahol a
string paraméter tárolja a dátum(és idő) szöveges reprezentációját, a format paraméter pedig a
szöveges reprezentáció felépítését adja meg. Például a ’31.01.2015’ formában megadott dátumot
a ’%d.%m.%Y’ formátum sztringgel lehet feldolgozni:
lekérdezés 86400000-t fog eredményezni, ami egy nap hossza milliszekundumban kifejezve.
Ahogyan már korábban is említettük, a NULL egy speciális érték, amely ismeretlen, vagy adott rekord
esetén nem értelmezhető attribútum értéket takar. A NULL-lal végzett különböző műveletek kiemelt
figyelmet igényelnek, mivel művelettől függően NULL vagy UNKNOWN (ismeretlen) értéket
eredményezhetnek:
101
Ha NULL értéket relációs operátorok (>, >=, =, <=, <) segítségével hasonlítunk össze
bármilyen másik értékkel (akár másik NULL értékkel is), akkor a művelet eredménye
UNKNOWN (ismeretlen), tehát se nem logikai igaz (TRUE), se nem logikai hamis (FALSE). Az
x=NULL esetén az x<3 kifejezés eredménye UNKNOWN. Ha az x<3 egy WHERE feltételben
szerepel, akkor azok a rekordok, amelyeknél a kifejezés UNKNOWN-ra értékelődik ki (ahol az
x attribútum értéke NULL), nem fognak az eredményhalmazba kerülni.
Mivel a NULL nem hasonlítható össze semmilyen értékkel, ezért annak eldöntésére, hogy egy kifejezés
vagy attribútum értéke NULL, nem használhatjuk a szokásos =, illetve <> operátorokat, hiszen az
x=NULL kifejezés eredménye mindig UNKNOWN lesz. A NULL érték vizsgálata az IS NULL, illetve IS
NOT NULL operátorokkal lehetséges: az x IS NULL kifejezés logikai igazat ad vissza, ha x értéke
NULL, különben logikai hamisat. Az x IS NOT NULL kifejezés pedig akkor ad vissza logikai igazat, ha
x egy NULL-tól különböző értéket reprezentál. Ha azokat a gyártókat szeretnénk kilistázni tehát,
amelyeknek az e-mail címe NULL, akkor azt a következő kifejezéssel tehetjük meg:
Megjegyezzük, hogy ha azt a kérdést szeretnénk megválaszolni, hogy melyek azok a gyártók,
amelyeknél az e-mail cím nincs kitöltve (tehát az Email mező értéke NULL vagy üres sztring), akkor azt
például a következő, kibővített lekérdezéssel tehetjük meg (MySQL):
A legtöbb SQL implementáció arra is biztosít lehetőséget, hogy lekérdezésekben a NULL értékeket egy
alapértelmezett értékkel helyettesítsük: például ha a NULL e-mail címek helyett üres sztringet vagy
akár azt szeretnénk látni, hogy „<ismeretlen>”. A művelet szintaktikája nem egységes a különböző
implementációkban, MySQL alatt az IFNULL(<kifejezés1>, <kifejezés2>), MSSQL-ben az
ISNULL, Oracle-ben pedig az NVL (azonos paraméterezéssel) utasítások valósítják meg ezt a funkciót.
A kifejezés értéke <kifejezés1>, ha annak az értéke nem NULL, egyébként <kifejezés2>. A
következő utasítás a gyártók nevét és e-mail címét kérdezi le, és a NULL értékű e-mail címeket üres
sztringgel helyettesíti:
Ahogy láttuk, bizonyos logikai kifejezések a szokásos TRUE és FALSE értéken kívül az „ismeretlen”
UNKNOWN értéket is eredményezhetik. Mivel a logikai kifejezéseket az AND, OR, illetve NOT
operátorokkal összefűzhetjük, ezért ezek viselkedése UNKNOWN operandusok esetén további
vizsgálatot igényel:
Két logikai érték „és” kapcsolata (AND) akkor és csak akkor logikai igaz (TRUE), ha mindkét
operandusa igaz, és pontosan akkor hamis (FALSE), ha legalább az egyik operandusa hamis.
Minden egyéb esetben (egyik operandusa sem FALSE, és legalább az egyik operandusa
UNKNOWN) az eredmény UNKNOWN. Ezt úgy is értelmezhetjük, hogy ha az ismeretlen operandus
értékét ismernénk, akkor attól függően a végeredmény még igaz, illetve hamis is lehetne, ezért
102
eldöntetlen a végeredmény. Míg ha az egyik operandus hamis, akkor az ismeretlen operandus
értékétől függetlenül (akár igaz, akár hamis), a végeredmény mindenképpen hamis.
Két logikai érték „vagy” kapcsolata (OR) akkor és csak akkor igaz, ha legalább az egyik
operandusa igaz, és pontosan akkor hamis, ha mindkét operandusa hamis. Minden egyéb
esetben (tehát legalább az egyik operandus UNKNOWN, és egyik operandus sem igaz) az
eredmény UNKNOWN.
Egy logikai érték negáltja (NOT) pontosan akkor igaz, ha az eredeti érték hamis volt, és
pontosan akkor hamis, ha az eredeti érték igaz volt. UNKNOWN érték negáltja szintén UNKNOWN.
Az SQL nyelv támogat egy, a C-ből már ismert ? : és switch-case operátorokhoz hasonló feltételes
operátort, amely tetszőleges számú elágazással rendelkezhet (csak úgy, mint a switch-case), de
kiértékelhető kifejezés (mint a ? : ), nem pedig vezérlési szerkezet. Általános alakja:
CASE [<kifejezés0>]
WHEN <feltétel1> THEN <kifejezés1>
WHEN <feltétel2> THEN <kifejezés2>
…
WHEN <feltételn> THEN <kifejezésn>
ELSE <alapértelmezett kifejezés>
END
Tehát az SQL-beli CASE szerkezet egyes ágai nem procedurális szemantikát fogalmaznak meg, mint a
C-beli switch-case elágazás ágai. A CASE operátor rendelkezik eredménnyel, mégpedig azon
kifejezés értékével, amelyhez tartozó feltétel elsőként teljesül. Ha egyik feltétel sem teljesül, akkor az
ELSE ágban megadott kifejezés értéke lesz a CASE operátor értéke is.
A CASE szerkezet állhat bárhol, ahol SQL kifejezés állhat (SELECT utáni oszlop listában, WHERE szűrő
feltételben, akár másik CASE szerkezetbe ágyazva is. Első példánkban a termékeket szeretnénk
osztályozni ár szerint: a legfeljebb 15000Ft-ba kerülő termékek ’olcsó’-k, a 15001-50000Ft-ba kerülő
termékek ’normál’ árúak, az 50000Ft-nál drágább termékek pedig ’drágá’-k:
103
SELECT TermekKod, TermekNev, Ar,
CASE WHEN Ar <=15000 THEN ’olcsó’ WHEN Ar <= 50000 THEN ’normál’
ELSE ’drága’ END Osztaly FROM Termek
A CASE kifejezéssel számított oszlopnak az ’Osztaly’ nevet adtuk. Ebben az esetben tehát a
szerkezet első változatát alkalmaztuk, amelynek megfelelően a WHEN kulcsszó után logikai kifejezések
szerepelnek. A lekérdezés egy lehetséges kimenete az 5.8. táblázatban látható.
Ebben az esetben a CASE szerkezet második formáját alkalmaztuk: a WHEN ágakban a Kategoria
attribútum bizonyos értékekkel való egyezését vizsgáljuk. (A megoldásban feltételeztük, hogy csak ez
a három kategória létezik, ezért szerepel a Kert kódja (2) az ELSE ágban.) A lekérdezés egy lehetséges
kimenete 5.9. táblázatban látható.
Ahogyan már az 5.1. fejezet elején is említettük, a kereskedelmi forgalomban elterjedt relációs
adatbázis-kezelők zsák-alapú relációkat alkalmaznak. Tehát amikor az adatbázis-kezelő rendszer az SQL
lekérdezést kiértékeli, akkor a végeredményben előforduló ismétlődéseket nem távolítja el. Ha az 5.3.
táblázatban ismertetett Termek táblából lekérdezzük a kategóriákat a következő lekérdezéssel, akkor
az eredményben a ’Nappali’ kategória kétszer fog szerepelni (5.10. táblázat).
104
Kategoria
Nappali
Nappali
Iroda
Kert
5.10. táblázat: A Termek relációban szereplő kategória értékek
A fenti lekérdezés megválaszolásához csupán végig kell iterálni a Termek táblában szereplő
rekordokon, és minden egyes rekord Kategoria attribútum értékét az eredmény relációhoz (zsák!)
kell adni. Bizonyos esetekben viszont nem kívánatos az ismétlődések megjelenítése. Tegyük fel, hogy
a relációban nem 4, hanem 400 termékről tárolunk adatokat, amelyeket 10 kategóriába sorolunk be.
Ha a fenti módszerrel kérdezzük le az alkalmazott kategóriákat, akkor minden egyes kategória
átlagosan 40-szer fog szerepelni a végeredményben, ami az adatok emberi vagy további automatikus
feldolgozását igencsak megnehezíti. Az SQL szerencsére biztosít eszközt a duplikációk kiszűrésére:
ehhez a DISTINCT kulcsszót kell a SELECT utasítás után illeszteni:
Következésképp minden egyes rekord (amelyek jelen esetben egyetlen értékből állnak, de ugyanúgy
alkalmazható több attribútumos rekordokra is) egyszer fog szerepelni az eredmény relációban. A
DISTINCT operátor a relációs algebrából már megismert 𝛿 operátornak (2.3.5 fejezet) felel meg.
5.2.7 Halmazműveletek
Az SQL nyelv támogatja a relációs algebrában már megismert alapvető halmazműveleteket (2.2.3): az
uniót (UNION operátor), a metszetet (INTERSECT) és a különbséget (EXCEPT). Kivételes módon ezen
műveletek implementációja nem zsák-, hanem halmaz-alapú megközelítést alkalmaz: az operandus
relációkat halmazokká alakítja, majd ezek után halmaz alapon végzi el a műveletet. A szabvány szerint
mindhárom operátornak létezik zsák-alapú megfelelője is: UNION ALL, INTERSECT ALL, EXCEPT
ALL, de a gyakorlatban a legtöbb adatbázis kezelő (ahogyan a MySQL, MSSQL és Oracle is) csupán az
UNION ALL operátort szokta implementálni. Az operátorok felhasználásának általános alakja:
R [UNION|UNION ALL|INTERSECT|EXCEPT] S
ahol R és S két azonos sémájú reláció. A két reláció attribútumainak nem kell azonos nevekkel
rendelkezniük, de az attribútumoknak azonos típussal és azonos sorrendben kell szerepelniük a két
relációban.
Ha elő szeretnénk állítani egy listát, amely az összes vásárló és gyártó címét tartalmazza, akkor azt a
következő lekérdezéssel tehetjük meg:
105
SELECT Irszam, Varos, Cim FROM Gyarto
Ha biztosan tudjuk, hogy nincs olyan gyártó, amely vevőként is megjelenik – vagy ha előfordul ilyen
eset, akkor nem probléma, hogy ezen gyártók címe kétszer is szerepel a végeredményben (mivel a vevő
relációban is szerepel) –, akkor az UNION operátor helyett használhatjuk az UNION ALL operátort is:
Megjegyezzük, hogy az Oracle a MINUS kulcsszót használja a különbség képzésre, a MySQL pedig
egyáltalán nem valósítja meg ezt az operátort, de máshogy (például külső illesztéssel 5.2.8.3. fejezet)
lehetőségünk van két reláció különbségét képezni.
Az eddigiekben áttekintettük, hogy hogyan lehet egy táblára vonatkozó egyszerű lekérdezéseket
megfogalmazni, a kapott eredményeket halmazműveletekkel kombinálni. A relációs adatbázis-kezelő
rendszerek igazi ereje viszont abban rejlik, hogy lehetőséget nyújtanak olyan lekérdezések
megfogalmazására és végrehajtására, amelyek több relációból gyűjtik össze a kívánt adatot. A relációs
algebrában erre szolgált a szorzat operátor és a különböző illesztések. A továbbiakban áttekintjük ezen
operátorok SQL-beli megfelelőit, valamint a lekérdezések egymásba ágyazásának lehetőségét.
Például a
Az eredmény reláció sémája a két séma egymás után illesztéséből áll elő:
106
Vegyük észre, hogy mivel a GyartoKod attribútum mindkét reláció sémájában szerepel, ezért az
eredmény relációban is kétszer fog szerepelni. Amennyiben meg szeretnénk különböztetni az egyes
attribútumokat akár a projekciós listában, akár a szelekciós feltételek között, akkor a forrás reláció
nevével egyértelműen azonosíthatjuk a tábla.oszlop formátumú hivatkozással:
A fenti lekérdezés a Termek tábla összes oszlopából álló n-eseit párosítja össze a Gyarto tábla
GyartoKod és GyartoNev oszlopaiból álló n-eseivel. Ha csak az összeillő rekordokat szeretnénk az
eredményben tartani - tehát ahol a Termek és a Gyarto táblák rekordjainak GyartoKod attribútumai
megegyeznek – akkor a két reláció természetes illesztését (2.2.5.fejezet) kell végrehajtanunk. A
természetes illesztést SQL-ben a NATURAL JOIN operátorral lehet végrehajtani:
Az eredmény reláció sémájában az azonos nevű attribútumok csak egyszer fognak szerepelni:
SELECT TermekNev, GyartoNev FROM Termek NATURAL JOIN Gyarto WHERE Ar > 20.000
5.2.8.2 Theta-illesztés
Relációk theta-illesztésekor (2.2.6. fejezet) az illesztési feltétel segítségével pontosan megadhatjuk,
hogy a különböző relációk mely rekordjait illesszük össze. SQL-ben az INNER JOIN vagy egyszerűen
csak JOIN kulcsszót használhatjuk a theta-illesztés elvégzésére, az illesztési feltételt pedig az ON
kulcsszó után adhatjuk meg. Egyszerre tetszőleges számú táblát kapcsolhatunk össze az alábbi
formában:
107
INNER JOIN C ON <feltétel2>
…
Ha a termékek és a hozzájuk tartozó gyártók adatait szeretnénk lekérdezni, akkor azt a következő
lekérdezéssel tehetjük meg:
Mivel a GyartoKod oszlopnév nem egyértelmű, ezért a szűrési feltételben ezen attribútumokat a
táblanévvel különböztetjük meg. Az eredmény reláció sémája – a keresztszorzathoz hasonlóan – az
összekapcsolt relációk sémájának egymás után illesztésével áll elő.
Megjegyezzük, hogy az illesztési feltételben – csakúgy, mint a reláció algebrai illesztésben – nem csak
egyenlőséget, hanem tetszőleges logikai igaz-hamisra kiértékelhető kifejezést is vizsgálhatunk.
Megjegyezzük, hogy mivel a külső kulcsok ugyanazon tábla attribútumait hivatkozzák, ezért a
kényszereket csak a tábla létrehozása után készíthetjük el séma módosító utasításokkal, hiszen a tábla
létrehozása pillanatában még nem létezne a hivatkozott Szemely tábla.
Ha a fenti relációból ki szeretnénk listázni az összes személyt és azok anyjának a nevét, akkor a
Szemely táblát önmagával kell illeszteni, mégpedig az AnyaId=Id feltétel mentén. Ahhoz, hogy a
projekció során mind a gyerek, mind az anya szerepben használt Szemely tábla Nev attribútumát
hivatkozni tudjuk, az egyik reláció hivatkozást (például az anya szerepben lévőt) átnevezésen keresztül
valósítjuk meg:
108
INNER JOIN Szemely AS Anya ON Szemely.AnyaId=Anya.Id
(Mivel a theta-illesztés csak a párosított rekordokat tartja meg, ezért azon személyek, akiknek az anyja
nem ismert, nem kerülnek bele az eredmény halmazba.)
Hasonlóképp járhatunk el, ha például a testvérpárokat szeretnénk felsorolni. Testvérpár minden olyan
személy páros, amelyeknek azonos vagy az anyja vagy az apja. Első megközelítésben használhatjuk a
következő lekérdezést:
Mivel a NULL értékek összehasonlítása UNKNOWN logikai értéket eredményez, ezért egy páros nem fog
az eredménybe kerülni csak azért, mert például mindkettőjük ApaId-je NULL. Tételezzük fel az 5.11.
táblázatban látható Szemely táblát mint forrás adatot.
Mivel Kovács József és Kiss Anna szülei ismeretlenek, ezért ők nem fognak az eredménybe kerülni,
viszont mivel az illesztés a keresztszorzatból indul ki, ezért az eredményben a valós testvérpárok
kétszer is szerepelnek, és minden személy saját magával is testvérpárt alkot (5.12. táblázat).
Id Nev Id Nev
3 Kovács Béla 3 Kovács Béla
3 Kovács Béla 4 Kovács Anna
4 Kovács Anna 3 Kovács Béla
4 Kovács Anna 4 Kovács Anna
5.12. táblázat: Testvérpárok
Triviális kiegészítése a szelekciós feltételnek a Szemely.Id <> Masik.Id feltétel, de ez csak az egy
személy alkotta párokat szűri ki. Ellenben ha Szemely.Id < Masik.Id-t használunk, vagyis a Masik
relációból illesztett személynek mindig a Szemely relációból illesztett személy utáni azonosítóval kell
rendelkeznie, akkor azzal kizárhatjuk a testvérpárok fordított felsorolását is:
109
FULL OUTER JOIN: teljes külső theta-illesztés
LEFT OUTER JOIN: bal oldali külső theta-illesztés
RIGHT OUTER JOIN: jobb oldali külső theta-illesztés
NATURAL FULL OUTER JOIN: teljes külső természetes illesztés
NATURAL LEFT OUTER JOIN: bal oldali külső természetes illesztés
NATURAL RIGHT OUTER JOIN: jobb oldali külső természetes illesztés
Példaként soroljuk fel az összes gyártót és gyártónként az általuk előállított termékeket (ha egy gyártó
több terméket is készít, akkor annyiszor fog szerepelni, ahány terméket készít). Ha egy gyártó egyetlen
terméket sem állít elő, akkor is kerüljön bele az eredménybe, a kapcsolódó termék tulajdonságokat
üresen hagyva:
Ha csak azokat a gyártókat szeretnénk visszakapni, akiktől egyetlen terméket sem forgalmazunk (vagyis
egyetlen előfordulásuknál a külső illesztésben a kapcsolódó termék adatai NULL-lal vannak feltöltve),
akkor a szelekciós feltételben azt kell vizsgálnunk, hogy a kapcsolódó termék kulcsa NULL-e (hiszen az
is üres lesz, minden egyéb esetben a kulcs ki lenne töltve):
Megjegyezzük, hogy mivel az MSSQL implementációk nem támogatják a természetes illesztést, ezért
ezeken a rendszereken a természetes külső illesztések sem elérhetők. MySQL rendszereken pedig a
FULL OUTER JOIN művelet nem támogatott, de két reláció teljes külső illesztése egyszerűen
előállítható a bal oldali és a jobb oldali külső illesztések uniójaként (hiszen az egyik a bal oldali, míg a
másik a jobb oldali párosítatlan rekordokkal egészíti ki a párosított rekordokat):
110
akkor nekünk az A párosítatlan rekordjaira van szükségünk. Feltételezve, hogy A és B kulcsa az Id
attribútum, A – B a következő lekérdezéssel állítható elő:
SELECT A.* FROM A LEFT OUTER JOIN B ON A.Id=B.Id WHERE B.Id IS NULL
5.2.9 Aggregálás
1. COUNT: a COUNT(A) művelet megszámolja, hogy hány ürestől különböző, de nem feltétlen
egyedi érték található az A oszlopban. Egyszerűsítésként használható a COUNT(*) kifejezés is,
amely a reláció összes sorának számát adja vissza.
2. SUM: a SUM(A) művelet összegzi az A oszlopban tárolt numerikus értékeket.
3. MIN, MAX: a MIN(A), illetve MAX(A) operátorok az A oszlop legkisebb, illetve legnagyobb
értékeit adják vissza. Numerikus értékeket tároló oszlopoknál a működésük egyértelmű,
sztringeket tároló oszlopok esetén az alfabetikusan legkisebb, illetve legnagyobb értéket
eredményezik.
4. AVG: az AVG(A) művelet az A oszlopban tárolt numerikus értékek átlagát állítja elő.
Amennyiben aggregálást végzünk, akkor ugyanezen lekérdezésen belül nem kérdezhetünk le egyszerű
attribútumokat, csak más aggregált vagy csoportosított értékeket (lsd. 5.2.10. fejezet). A lekérdezés
eredménye egyetlen rekordból fog állni. A maximális raktárkészlet értékét (a legnagyobb termék
mennyiség) a következőképp kérdezhetjük le:
Egy lekérdezésen belül egyszerre több aggregált értéket is számíthatunk, például a maximális, a
minimális és az átlagos raktárkészletet:
Aggregációt nem csak oszlopokon végezhetünk, hanem akár számított értékeken is. A következő
lekérdezés a raktáron lévő termékek összegzett értékét állítja elő:
Vagyis kiszámoljuk termékenként a raktárkészlet értékét, majd ezt összegezzük az összes termékre.
5.2.10 Csoportosítás
Az algebrai lekérdezéseknél már megismert csoportosítás műveletet (2.3.7. fejezet) SQL-ben a GROUP
BY utasítással valósíthatjuk meg, amelyet a csoportosító attribútumok követnek. A csoportosítás a
szelekció (WHERE feltétel) alkalmazása után történik meg, és szintaktikailag is a szelekciós feltétel után
következik. Ha a lekérdezés csoportosítást alkalmaz, akkor a SELECT kulcsszó után csak csoportosító
attribútumok vagy aggregált értékek állhatnak: ekkor az aggregálás csoportonként történik meg.
Tekintsünk egy egyszerű esetet, amikor csak csoportosító attribútumokat kérdezünk le:
111
SELECT Kategoria FROM Termek GROUP BY Kategoria
Ha azt is meg szeretnénk kapni, hogy hány fajta termék szerepel az egyes kategóriákban, akkor a
lekérdezést kiegészíthetjük a COUNT(*) kifejezéssel:
Mivel a csoportosítás a WHERE feltétel alkalmazása után történik meg, ezért a következő lekérdezés a
10.000 Ft-nál többe kerülő termék típusokat számolja meg kategóriánként:
SELECT Kategoria, COUNT(*) FROM Termek WHERE Ar > 10000 GROUP BY Kategoria
Ha a csoportosítás után előálló rekordokra szeretnénk szelekciós feltételt megadni, akkor erre a célra
használhatjuk a GROUP BY utáni HAVING kulcsszót, amely mögött egy további szelekciós feltételt
adhatunk meg, ami már a csoportosítás után alkalmazódik. Ha azt szeretnénk megtudni, hogy melyek
azok a kategóriák, amelyekben legalább 5 különböző, 10.000 Ft-nál többe kerülő termék fajta található,
akkor az a következőképp válaszolhatjuk meg:
A következő példában az egyes termékekből eddig megrendelt összes darabszámot jelenítjük meg, a
termék nevével és azonosítójával együtt:
Mivel csoportosítást alkalmazó lekérdezésben csak aggregált értéket vagy csoportosító attribútumot
kérdezhetünk le (hiszen csak ezek az értékek azok, amelyek egyértelműek a csoporton belül), ezért ha
a termék nevét is meg szeretnénk jeleníteni, akkor azt is a csoportosító attribútumokhoz kell adnunk.
5.2.11 Rendezés
Gyakori igény, hogy a lekérdezés során előállított rekordokat valamely attribútum szerint sorba
rendezzük (𝜏 operátor - 2.3.8. fejezet). Az SQL nyelvben ennek elvégzésére az
kifejezés szolgál, amely a lekérdezésekben a GROUP BY / HAVING utasítások után foglal helyet, és
szemantikailag közvetlenül a SELECT által meghatározott projekció végrehajtása előtt fut le. Az ORDER
BY utasítás az eredmény rekordokat az utána megadott kifejezések szerint rendezi alapértelmezetten
növekvő sorrendbe, a DESC módosító szó alkalmazásával pedig csökkenőbe (a növekvő sorrendet
112
expliciten is kifejezhetjük az ASC kulcsszóval). A rendező kifejezésnek skalár értékre kell kiértékelődnie:
legegyszerűbb esetben egy attribútum hivatkozásból áll, de tetszőleges számítást is takarhat. Egyszerre
több kifejezés szerint is rendezhetünk: a második, harmadik stb. kifejezés szerinti rendezés akkor
történik meg, amennyiben az előző kifejezéshez rendelt értékek megegyeznek két rekord esetén. A
következő utasítás a ’Nappali’ kategóriájú termékeket rendezi ár szerint csökkenő sorrendbe,
amennyiben két terméknek egyezik az egységára, akkor őket pedig név szerint növekvő sorrendbe:
Ha az adatbázisban tárolt termékek közül csak azokat akarjuk megjeleníteni, amelyekből eddig
összesen már legalább 100.000 Ft értékben rendeltek, és a termékeket az összes megrendelés érték
szerint csökkenő sorrendbe akarjuk rendezni, akkor az a következő lekérdezéssel tehetjük meg:
Ha pedig 6..15. helyeken állókat szeretnénk visszakapni, akkor a következő lekérdezést használhatjuk:
Az Oracle adatbázis kezelő rendszerek a lekérdezés által előállított sémát kiegészítik egy ROWNUM nevű
pszeudo-oszloppal, amely minden kiválasztott rekordhoz egy számot rendel, amely azt adja meg, hogy
az adott rekord sorrendben hányadikként került kiválasztásra a tartalmazó táblából vagy illesztett
relációkból (a számozás 1-től indul). Amennyiben a ROWNUM hivatkozás sorrendben az ORDER BY után
következik, akkor a sorszámok is újrarendezésre kerülnek. Az abc sorrendben a 6..15 helyen álló
termékeket tehát a következőképp kaphatjuk meg Oracle-ben:
SELECT * FROM TERMEK ORDER BY Nev WHERE ROWNUM >5 AND ROWNUM < 16
MSSQL-ben a visszakapott rekordok számát közvetlenül a SELECT után szereplő TOP n utasítással
korlátozhatjuk legfeljebb n elemre:
113
SELECT TOP 5 * FROM Termek ORDER BY Nev
A lapozás MSSQL rendszerekben a ROW_NUMBER( ) OVER (…) [8] utasítással lehetséges, amely
részletes ismertetésére terjedelmi okokból nem térünk ki.
Ahogyan az algebrai lekérdezésekben, úgy az SQL is biztosít lehetőséget arra, hogy egy lekérdezés
eredményét egy másik lekérdezés részeként használjuk fel. Egy SQL lekérdezés eredményét
háromféleképp ágyazhatjuk be egy másikba:
1. Felhasználhatjuk egy másik lekérdezés FROM listájában, akárcsak egy fizikailag létező táblát, vagy
illeszthetünk is hozzá. Ebben az esetben az al-lekérdezést mindig nevesíteni kell. Például:
SELECT TermekId FROM (SELECT * FROM Termek WHERE Raktarkeszlet > 3) AS temp
SELECT * FROM
(SELECT Kategoria, COUNT(*) CNT FROM Termek WHERE Ar > 10000
GROUP BY Kategoria) Csoportok
WHERE CNT >=5
2. Amennyiben egy lekérdezés skalár értéket eredményez (egy sor, egy oszlop), akkor azt
felhasználhatjuk olyan helyeken, ahol egy egyszerű skalár érték vagy attribútum az elvárt. Például
a termékek és a rájuk leadott összes megrendelés (kapcsolódó megrendeléstételek
darabszámának az összege) listája a következőképp is előállítható:
A külső lekérdezés minden egyes Termek rekordra lefuttatja a beágyazott lekérdezést, és előállítja
az egyes termékekhez tartozó összegzett megrendelés számot, amelyet „Osszes”-nek nevezünk
el. Ahogy a példában is látjuk, a beágyazott lekérdezésben hivatkozhatjuk a beágyazó
lekérdezésben használt táblák oszlopait is. Megjegyezzük, hogy az SQL végrehajtó motor a fenti
114
lekérdezést általában illesztésre fordítja át, és nem végzi el ténylegesen minden egyes termékre a
beágyazott lekérdezést újra és újra.
3. Egy lekérdezés szerepelhet egy másik WHERE feltételében. A visszaadott rekordokra logikai
feltételeket fogalmazhatunk meg:
Az EXISTS kulcsszóval azt vizsgálhatjuk meg, hogy egy reláció üres-e. Ha azokat a
termékeket szeretnénk kilistázni, amelyekhez tartozik legalább egy megrendeléstétel,
akkor azt egy beágyazott lekérdezéssel is előállíthatjuk:
A lekérdezés minden termékre megvizsgálja, hogy van-e akár egy, a kódjához tartozó
megrendeléstétel.
Az IN kulcsszóval azt ellenőrizhetjük, hogy egy bizonyos érték szerepel-e egy egyoszlopos
relációban. A megrendeléssel rendelkező termékek listáját úgy is előállíthatjuk, hogy a
MegrendelesTetel táblából először leválogatjuk a termék kódokat (minden termék kód,
amely szerepel a MegrendelesTetel táblában, értelemszerűen rendelkezik
megrendeléssel), majd a Termek táblából azokat a rekordokat választjuk ki, amelyek kódja
szerepel a leválogatott kódok között:
Az ALL, illetve ANY kulcsszavakkal azt írhatjuk elő, hogy az eredmény reláció összes (ALL)
vagy bármely (ANY) elemére teljesül egy logikai feltétel. Ha azokat a Termékeket
szeretnénk megkeresni, amelyekből legalább egy megrendeléstételt ki tudunk elégíteni
(tehát több van raktárkészleten, mint a megrendelésben szereplő darabszám az adott
termékből), akkor azt az alábbi beágyazott lekérdezéssel valósíthatjuk meg (többek
között):
SELECT * FROM Termek WHERE Raktarkeszlet >= ANY (SELECT Darabszam FROM
MegrendelesTetel WHERE MegrendelesTetel.TermekKod = Termek.TermekKod)
5.2.13 Nézetek
A táblák az általuk reprezentált adatot fizikailag is eltárolják az adathordozón, és megőrzik azt, amíg
utasítást nem adunk annak megváltoztatására. Az SQL-beli relációk egy másik fajtája a (virtuális) nézet
(view), amely nem tárol adatot, tartalmát egy SQL lekérdezéssel határozhatjuk meg, amely más
nézeteket és táblákat használ adatforrásként. A nézeteket éppúgy fel tudjuk használni
lekérdezésekben, mint a fizikailag is eltárolt táblákat, viszont a lekérdezés kiértékelésekor a felhasznált
115
nézetek mögött álló lekérdezést is ki kell értékelni (akárcsak egy beágyazott lekérdezést). Egy nézet
definiálására a következő SQL utasítást használhatjuk:
Például ha egy olyan nézetre van szükségünk, amely a legnépszerűbb öt terméket listázza ki (a
legnagyobb megrendelés-állománnyal rendelkező termékek), akkor azt a következő utasítással
hozhatjuk létre (MySQL):
A TopTermekek nézetnek, mint relációnak a sémája 3 oszlopból fog állni: TermekKod, Nev, Darab
(mivel a SUM(Darab) összeget egyszerűen Darab-nak neveztük el). Ezek után, ha szükségünk van a
nézet által reprezentált adatokra, akkor egyszerűen megkaphatjuk azt a következő lekérdezéssel:
A nézeteket alkalmazó lekérdezésekben ugyanúgy használhatunk szelekciós feltételt is, mint a csak
táblákat használó társaikban, de akár illeszthetünk is nézetekhez. A következő lekérdezés a
legnépszerűbb termékekről megjelenített adatokat még kiegészíti a termékek aktuális
raktárkészletével is:
Amennyiben egy nézetre már nincs szükség, akkor azt a következő utasítással törölhetjük:
Ha pedig meg szeretnénk változtatni egy nézet definícióját, akkor azt az ALTER VIEW utasítással
tehetjük meg:
116
automatikusan: a hivatkozott táblák tartalmának megváltozásakor a hivatkozó
materializált nézetek újraszámítódnak (optimalizáltan, a változásokat inkrementálisan
alkalmazva).
A materializált nézeteket csak a nagyobb adatbázis-kezelő rendszerek támogatják (pl. MSSQL, Oracle,
DB2 – a MySQL nem), részletes ismertetésükre terjedelmi okokból nem kerül sor.
Az 5.1.3 fejezetben már találkoztunk az INSERT INTO utasítással, amely segítségével új rekordokat
tudunk egy adatbázis táblába beszúrni. A példákban feltételeztük, hogy az új adatok az utasítás
megírásának pillanatában ismertek, hiszen csak konstans vagy egyszerű számított (aktuális dátum)
értékeket használtunk. Az INSERT INTO utasítást használhatjuk azonban úgy is, hogy a beszúrandó
rekordokat futási időben, egy lekérdezés segítségével állítjuk elő:
Amennyiben korábban eltárolt adatokon módosítást szeretnénk végrehajtani, akkor azt az UPDATE
utasítás segítségével végezhetjük el, amelynek általános alakja:
Az UPDATE utasítás utáni első paraméter a tábla neve, amelyben tárolt adatot módosítani szeretnénk,
a WHERE utáni szűrőfeltétellel pedig kiválaszthatjuk a módosítandó rekordokat: itt a lekérdezésekhez
hasonló feltételeket alkalmazhatunk, de a feltételt (a WHERE-rel együtt) akár el is hagyhatjuk, ekkor a
117
módosítás a tábla minden egyes rekordjára lefut. A SET kulcsszó után vesszővel elválasztva
felsorolhatjuk az attribútum érték módosításokat, amelyek minden egyes a feltételnek megfelelő
rekordra alkalmazódnak. Az értékadások jobb oldalán nem csak konstans, hanem számított értékek is
szerepelhetnek, amelyek – akárcsak egy lekérdezésnél – állhatnak a módosított rekord
attribútumaiból, beépített függvényekből, különböző aritmetikai kifejezésekből, de akár beágyazott
lekérdezésekből is, amelyek skalár értéket szolgáltatnak. A következő utasítás a 100.000 Ft-nál többe
kerülő termékek árát csökkenti 10%-kal:
Előfordulhat, hogy a módosítandó rekordok kiválasztásához más táblák adataira is szükség van, de akár
az új értékek előállításához is szükség lehet más relációkban tárolt rekordokra. Tegyük fel, hogy
azoknak a termékeknek az árát akarjuk csak 10%-kal csökkenteni, amelyekből még soha nem
rendeltek. A szelekciós feltételben – csakúgy, mint egy lekérdezésnél – alkalmazhatunk egy beágyazott
lekérdezést a megfelelő termékek kiválasztásához:
Vagyis a beágyazott lekérdezésben összesítjük azokat a termék kódokat, amelyekre történt már
megrendelés, és a módosítást ezen a halmazon kívüli termékekre végezzük el. Egyes SQL
implementációk – bár nem egységes módon – támogatják illesztések alkalmazását is az UPDATE
utasítások használata közben. MySQL-ben a táblákat és az illesztési feltételeket egyszerűen az UPDATE
utasítás után kell felsorolni:
Oracle-ben az illesztett módosítás nem támogatott, beágyazott lekérdezésekkel tudjuk a több táblán
átívelő módosításokat megoldani.
5.3.2 Törlés
Az utolsó, még nem részletezett adatkezelési primitív a törlés, amelyre a DELETE utasítás szolgál.
Szintaktikája:
118
A szűrő feltételben a szelekciós feltételeknél már megismert konstrukciók használhatók, a feltétel
elhagyása a tábla valamennyi rekordjának törlését eredményezi. Például az összes, ’Nappali’
kategóriájú terméket a következő utasítással törölhetjük:
A termékek törlése csak akkor lehetséges, ha már nem hivatkozik rájuk másik rekord külső kulcson
keresztül, ennek megfelelően a termékek törlése előtt el kell távolítanunk a kapcsolódó
megrendeléstételeket is:
119
DELETE FROM Megrendeles WHERE MegrendelesKod=kod
END
CALL DeleteMegrendeles(5)
(Az eredmény egy három oszlopos reláció, a harmadik oszlopban ’drága’, illetve ’olcsó’
szerepel az aktuális termék árától függően.)
Trigger: A triggerek esemény-vezérelt tárolt eljárások, amelyek nem felhasználói utasításra
futnak le, hanem automatikusan, az adatbázisban tárolt adatokon történő módosítások
hatására. Implementációtól függően triggereket rendelhetünk a beszúrás, törlés és módosítás
eseményekhez, azok bekövetkeztekor a triggerek által reprezentált műveletek is
automatikusan végrehajtódnak. A következő MySQL trigger új megrendeléstételek beszúrása
után fut le, összesíti a kapcsolódó termékre eddig leadott összes megrendelést, és eltárolja azt
a Termek rekord Osszrendeles mezőjében:
Jelen fejezetben csupán egy rövid bepillantást nyújtottunk az adatbázis-kezelő rendszerek procedurális
képességeibe. Terjedelmi okokból sajnos nem áll módunkban valamennyi rendszer összes képességét
ismertetni, a téma iránt érdeklődőknek javasoljuk az alkalmazott környezet dokumentációjának
áttanulmányozását.
120
6 Adattárolási módszerek
Cache
Rendszermemória
Virtuális Fájl
memória Lemez rends zer
Másodlagos tár
Harmadlagos
tár
A leggyorsabb memória típus a cache, amelyet manapság már általában a CPU-ba ágyazva valósítanak
meg. Mérete tipikusan csupán néhány megabájt (3-12), de a tartalmát nagyon kicsi, néhány
nanoszekundumos (1-10) késleltetetéssel éri el a feldolgozó egység. Amikor a CPU-nak szüksége van
egy adatra a rendszermemóriából (RAM), akkor először a cache memóriát ellenőrzi, hogy oda korábban
betöltötték-e már esetleg az adatot. Amennyiben igen, akkor meg lehet spórolni a sokkal lassabb RAM-
hoz történő hozzáférési időt. Amennyiben az adat nem található meg a cache-ben, akkor azt először
oda kell másolni a rendszermemóriából. Általában külön cache-t tartanak fent az adatoknak és az
utasításoknak. Az adat cachet legtöbbször szintén több szinten valósítják meg: Level 1, Level 2, Level 3
(L1, L2, L3), amelyek közül az L1 cache található a legközelebb a feldolgozó egységhez, ez a leggyorsabb
és a legkisebb, az L3 pedig a legtávolabb helyezkedik el a feldolgozó egységtől, a leglassabb a három
közül, de egyben a legnagyobb is.
121
CPU-ba ágyazott cache-é (10-100ns). A rendszermemória a számítógép kikapcsolásakor elveszti
tartalmát, tehát nem alkalmas az adatok tartós megőrzésére.
A másodlagos tárolón valósítható meg az ún. virtuális memória is: az operációs rendszer a futtatott
alkalmazásoknak virtuális címeken keresztül teszi elérhetővé a (fizikai) rendszermemóriát, és az egyes
memória eléréseknél leképezést végez a két (virtuális és fizikai) címzés között. Azáltal, hogy az
alkalmazások a RAM-ot nem közvetlenül címzik, lehetővé válik a virtuális címzésen keresztül a fizikailag
létezőnél nagyobb memória terület elérése. Az operációs rendszer rendszermemóriát tud szabaddá
tenni az aktuális művelethez azáltal, hogy az éppen nem használt adatokat a másodlagos tárra másolja,
és csak akkor tölti onnan vissza a RAM-ba, amikor azokra újra szükség lesz.
Bár a másodlagos tárolók akár több TB adat tárolására is alkalmasak, előfordulnak esetek, amikor még
ennél is sokkal több adat tárolására van szükség, például tudományos mérési eredmények tárolására
vagy sok milliós felhasználói bázisú alkalmazások biztonsági mentésére. Ilyen esetekben alkalmaznak
ún. harmadlagos tárakat, amelyeket legtöbbször mágnesszalagos meghajtókkal valósítanak meg:
napjainkban egy tipikus mágnesszalag több tíz (de akár 185!) TB adatot is képes tárolni a legkisebb
fajlagos költség, hosszú élettartam és minimális helyigény mellett. Mivel a mágnesszalagot csak
szekvenciálisan lehet olvasni (úgy viszont akár több 100MB/s sebességgel), ezért nem megfelelő az
adatok véletlenszerű, gyors elérésére, viszont kiválóan alkalmas biztonsági mentések készítésére, és
azok igény szerinti másodlagos tárba történő visszatöltésére. Komplex szalagos rendszerek lehetővé
teszik a mágnesszalagok automatizált kezelését is, amely során egy robotkar mozgatja a keresett
mágnesszalagot az író/olvasó egységhez. A harmadlagos tárak adatelérési ideje a másodperc-perc
tartományban mozog.
Az operációs rendszer az adatot mindig két szomszédos hierarchia szint között mozgatja. A másodlagos
és harmadlagos tárban az adat elérése már sok nagyságrenddel lassabb, mint a fentebbi hierarchia
szinteken, ezért ezeken a szinteken az adatkezelés már nagyobb, másodlagos tárak esetén 4-64kB-os
blokkokban történik. Mivel egy lépésben úgyis egy teljes blokk írásra/olvasásra kerül, ezért alapvető
fontosságú az összetartozó adatokat egymás közelében tartani, hiszen a blokk olvasás során így a
kívánttal együtt a kapcsolódó adatok is (amelyekre nagy valószínűséggel szintén szükség lesz)
betöltődnek a rendszer memóriába. Hasonló megállapítás tehető a harmadlagos tárakra is: az
összetartozó adatokat célszerű egy tároló egységen elhelyezni, ezáltal minimalizálva az azok
eléréséhez szükséges időt.
122
6.1.1 Merevlemezes háttértárak
Sáv
Tányér
Szektor
6.2. ábrán láthatjuk egy merevlemezes meghajtó egység sematikus és valós belső felépítését. Az
adatok tárolása kör alakú, közös tengely mentén forgó, mindkét oldalán mágneses réteggel bevont
tányérokból épül fel, amelyeken az adatok elérését szintén együtt mozgó író-olvasó fejekkel érhetjük
el. Az egyes tányérok felszíne koncentrikus sávokból (track) épül fel, az egymás alatt (azonos tányér
különböző oldalain, illetve különböző tányérokon) található sávok együttesen alkotnak egy cilindert
(henger, cylinder). Az egyes tányérokon található sávok sugár irányú képzeletbeli egyenesekkel
határoltan több szektorból állnak össze, amelyeket egy keskeny, nem mágneses terület választ el
egymástól. Napjainkban egy szektor tipikus mérete 4kB (régebben 512 bájt), egy vagy több (1-16)
szektor együttesen alkot egy blokkot, amely a lemez és a rendszermemória közti adatmozgatás logikai
egysége. A lemezeken tárolt adatok elérése az egyes felületekhez nagyon közel mozgó író-olvasó fejek
segítségével történik, amelyek egy közös tengelyhez kapcsolódnak, és mindig együttesen mozdulnak a
kívánt pozícióba. A fejek pozícionálását, az adatok átvitelét és pufferelését a lemez vezérlő
elektronika végzi. Ahhoz, hogy egy blokkhoz hozzáférjünk, a következő lépésekre van szükség:
1. A lemez vezérlő a fejet (együtt az összes fejet) a kívánt sáv fölé mozgatja. Ehhez természetesen
időre van szükség, az így kapott késleltetést nevezzük keresési időnek (seek time).
2. Ezután meg kell várni, hogy a lemez folyamatos forgása következtében az író-olvasó fej a keresett
blokk első szektora fölé forduljon. Az ehhez szükséges időt nevezzük forgási időnek (rotation
time).
3. Végül meg kell várni, hogy a keresett blokk valamennyi szektora áthaladjon a fej alatt, és az el tudja
végezni az írást / olvasást. A kapcsolódó késleltetést átviteli időnek (transfer time) nevezzük.
A szilárdtest meghajtók (Solid State Drive – SSD) meghajtók legfőbb jellemzője – ahogyan a nevük is
mutatja –, hogy nem tartalmaznak mozgó alkatrészt. Bár az SSD eszközök több évtizedes múlttal
123
rendelkeznek, megfizethetetlen áruk miatt nem tudtak olyan népszerűek lenni, mint lemezes társaik.
A gyártási költségek csökkenésével és a technológiai problémák leküzdésével azonban az utóbbi
években piacuk robbanásszerű növekedésnek indult. A napjainkban kapható legtöbb SSD meghajtó
NAND kapu alapú flash memóriára épül, amely elektromos úton írható és törölhető, valamint tartalmát
tápellátás nélkül is megőrzi.
Az SSD eszközök két fő részből épülnek fel: a vezérlő egységből és a flash memória chipekből. A vezérlő
egység feladata a számítógép többi komponensével történő kapcsolattartás – tipikusan Serial ATA
(SATA) interfészen keresztül –, ez végzi el az adatok pufferelését, hibakezelést, esetleg tömörítést,
titkosítást, és természetesen vezérli a flash memória írását, olvasását. A flash média általában NAND
kapukból épül fel, amelyek napjainkban már több síkban, egymás felett helyezkednek el, így lehetővé
téve a párhuzamos hozzáférést.
A szilárdtest meghajtóknál jóval kisebb késleltetési időkkel kell számolnunk, mint lemezes társaiknál.
Természetesen nem értelmezhető a keresési és a forgási idő, csupán egyetlen átviteli késleltetés
létezik, ami nagyságrendekkel kisebb (5-10 µs), mint a HDD-k átlagosan 10ms-os késleltetése.
Szintén az olvasó fejek mozgatásához szükséges idő csökkenthető azáltal, ha az egységhez érkező I/O
kéréseket a lemezvezérlő megfelelően sorrendezi: ahelyett, hogy a kéréseket a beérkezés
sorrendjében szolgálná ki, a fej egységgel oda-vissza pásztázásokat végez a lemez felületén a közepétől
a széle felé és vissza, és csak akkor vált irányt, ha az adott irányban már nincs több kérés a várakozási
sorban. Egy adott irányba mozogva pedig valamennyi, a sorban álló I/O kérést kiszolgálja, mivel ez a
fajta viselkedés sokban hasonlít a liftek mozgására, ezt az algoritmust lift vagy elevátor (az angol
elevator szó után) algoritmusnak szokták nevezni.
HDD és SSD meghajtók esetén egyaránt alkalmazható az a megoldás, hogy ugyanazt az adatot
egyszerre több meghajtón, tükrözve tároljuk el. Így azon túl, hogy hardware hiba esetén nem történik
124
adatvesztés, hiszen az adatot többszörösen is eltároltuk, jelentősen gyorsítható az adatok olvasási
sebessége, hiszen az olvasáskérést – mivel minden meghajtón ugyanaz a tartalom szerepel – bármelyik,
éppen szabad átviteli kapacitással rendelkező meghajtóhoz rendelhetjük. Lemezes meghajtók esetén
a tükrözés a keresési időt is jelentősen csökkenti, hiszen egy együttmozgó fejegység helyett több,
egymástól függetlenül mozgó fejegységhez jutunk. A megoldás hátránya természetesen a
megnövekedett tárterület igény, továbbá vegyük észre, hogy az írási sebesség ilyen módon nem
növelhető, hiszen ugyanazt az adatot párhuzamosan minden tárolóra ki kell írni.
Szintén késleltetés csökkenést, illetve átviteli sebesség növekedést érhetünk el azáltal, ha az adatokat
nem tükrözve, hanem csíkokra bontva (striping) tároljuk el több meghajtó egységen. Ez azt jelenti,
hogy az eltárolandó adatot egyforma részekre osztjuk, és az egyes darabokat különböző meghajtókon
helyezzük el. Például ha egy reláció rekordjait két meghajtón osztjuk el, akkor a két tárolóról
párhuzamosan tudjuk felolvasni az adatokat (közel megkétszerezve az olvasási sebességet).
Hasonlóképpen, az írási sebesség is közel kétszereződik, hiszen az egyes meghajtókra csak az eredeti
adat felét kell kiírni. A megoldás hátránya, hogy – mivel nem alkalmaz redundanciát – bármelyik
meghajtó meghibásodása esetén jelentős adatvesztéssel számolhatunk, hiszen minden egyes
meghajtó részt vesz a teljes adathalmaz egyes részeinek eltárolásában, a hiba nem korlátozódik
bizonyos relációkra, bizonyos állományokra.
A rekordok tipikusan rendelkeznek egy fejléccel, ami a rekordról szolgáltat különböző információkat:
Hivatkozást tartalmaz a rekordot tartalmazó reláció sémájára, így egyértelmű, hogy az adott
rekordban tárolt adatok pontosan melyik relációhoz tartoznak, a rekord milyen mezőkkel
rendelkezik, és – állandó hosszúságú rekordok esetén – azok pontosan hányadik bájtnál
kezdődnek.
A fejléc általában tartalmazza a rekord teljes hosszát is, aminek ismeretében egyszerűen át
tudunk ugrani egy rekordot a kapcsolódó séma elérése nélkül is.
A fejlécben el szokták tárolni a rekord legfontosabb időbélyegeit is: az utolsó módosítás és
esetleg az olvasás időpontjait. Ezek ismerete különösen a tranzakciókezelés megvalósításánál
lesz fontos (8. fejezet).
Az olyan rekordok esetén, amelyek hossza változhat annak függvényében, hogy pontosan
milyen értékeket tárolunk az egyes mezőikben, a fejléc mutatókat is tartalmaz az egyes mezők
kezdőpontjára.
125
6.3.1 Állandó hosszúságú rekordok
A legegyszerűbb dolgunk akkor van, ha a reláció rekordjainak hossza állandó, vagyis valamennyi mezője
állandó hosszúsággal rendelkezik. Tekintsük a szokásos Termek relációt azzal a módosítással, hogy a
szöveges mezőit állandó hosszúságú sztringekben tároljuk el:
A tábla egy rekordjának egy lehetséges (egyszerűsített) memóriatérképe a 6.3. ábrán látható,
feltételezve, hogy a rekord egyes mezői 4-es bájthatárra vannak igazítva.
séma
hossz
időbélyeg
A rekord fejlécében három tulajdonság található: egy mutató a sémára, a rekord hossz, valamint egy
időbélyeg (utolsó módosítás). Mindhárom mezőt 32biten ábrázoljuk, vagyis a teljes fejléc összesen 12
bájtot foglal el. Ezt követi a 4 bájtos INT típusú TermekKod (TK) mező, a 100 bájtos Nev, a szintén 4
bájtos Ar, majd az 50 bájtos Kategoria, amelyet 4-es bájthatárra igazítva kapjuk az 52 bájtos
helyfoglalást. A rekord végén a Szin (20 bájt) valamit a Raktarkeszlet (RK) mezők foglalnak helyet.
Az egyes rekordok a másodlagos tároló blokkjaiban kerülnek eltárolásra. A legegyszerűbb esetben, egy
blokkban csupán egy tábla n-eseit helyezzük el, amelyek mind azonos méretűek (6.4. ábra).
A blokk elején szintén egy fejléc foglal helyet, amely tipikusan öt dolgot tárol:
126
4. mutatókat más blokkokra (a blokkokból hálózatok építhetők fel, mint például index fák,
amelyek az adatbázisban történő hatékony keresést segítik, lsd. 7. fejezet),
5. a blokk szerepe a hálózatban, amelynek építőeleme.
Általános esetben az adatbázis rekordok sajnos nem állandó méretűek, hanem tartalmazhatnak olyan
mezőket is, amelyek helyfoglalása az aktuális tartalmuktól függ. Például a Termek táblában pazarlás
minden esetben 100 karaktert fenntartani a név számára, tekintve, hogy a legtöbb termék név
tipikusan 10-20 karakteres csupán. Szerencsésebb lenne a tárolt szöveg hosszával arányos
helyfoglalást alkalmazni, következésképpen a teljes rekord mérete is a tárolt szöveg hosszától függne.
Tételezzük fel a következő módosított Termek relációt, amelyben a szöveges mezők már változó
hosszúságúak (VARCHAR a CHAR helyett):
Ha az egyes mezők pontos hossza nem következik a sémából, akkor a mezők eltolása sem ugyanaz az
egyes rekordok memória képében, tehát ezt az információt rekordonként el kell tárolni. Hogy az ehhez
szükséges területet és többlet adminisztrációs terhet minimalizálják, a tipikus megoldás az, hogy
1. az állandó méretű mezőket a rekord elején helyezik el, hogy pozíciójuk megállapításához ne
legyen szükség mutatókra,
2. a változó hosszúságú mezők az állandó méretűek után következnek, és a második ilyen
mezőtől kezdve – hiszen az első pozíciója még ismert – minden egyes mezőre egy mutatót
helyeznek el a rekord fejlécében. Mivel a rekord hosszát már egyébként is eltároljuk a
fejlécben, ezért az utolsó mező hossza is könnyedén megállapítható.
A fenti elvet illusztráltuk a 6.5. ábrán: A Nev mező pozíciója még ismert (hiszen rögtön az állandó
méretű mezők után következik), a Kategoria és Szin mezőkre viszont már egy mutató mutat a
rekord fejlécéből.
127
séma Kategoria eltolás
hossz
időbélyeg Szin eltolás
fejléc
A megoldás segítségével könnyedén megvalósítható a NULL értékek kezelése is: ebben az esetben a
NULL-t tároló mezőre mutató eltolás értéke 0.
Valós alkalmazásokban gyakran előfordul, hogy egy rekord vagy annak már akár csak egy mezője is
több helyet igényel, mint a másodlagos tár egy blokkja. Tipikus példa erre, ha egy adatbázis mezőben
képfájlt, videót, hanganyagot vagy akár csak hosszabb szöveges tartalmat tárolunk. Ilyenkor egy olyan
megoldásra van szükség, ami lehetővé teszi egy rekord több blokkon átnyúló tárolását. Hasonló
problémával szembesülünk, ha az átlagos rekord méret kevéssel több, mint a blokk méretének a fele:
ha a blokk másik felét feltöltetlenül hagynánk – mivel egy újabb rekord számára már nincs elég hely–,
a tábla által igényelt hely közel felén nem tárolnánk semmit. Ebben az esetben szintén arra van szükség,
hogy a rekordot több blokkon átnyúló módon tudjuk eltárolni.
1. minden egyes rekord fejlécben egy biten jelezni kell, hogy az adott rekord töredék-e vagy sem,
2. további biteken el kell tárolni, hogy az aktuális darab első vagy az utolsó töredék-e,
3. a fejlécben mutatókat kell elhelyezni a megelőző és a következő töredékre (amennyiben
létezik).
A fenti elvet illusztráltuk a 6.6. ábrán: a 2-es rekord két töredékének fejlécében jelezni kell, hogy azok
töredékek, továbbá hogy a 2/1-es darab ezek közül az első, a 2/2-es pedig az utolsó. Ezen kívül mindkét
töredék a fejlécében mutatóval rendelkezik a másikra.
128
Blokk Blokk
rekord 1 rekord 2/1 rekord 2/2 rekord 3
fejléc fejléc
blokk1 blokk2
6.3.4 Blobok
A 6.3.3. fejezetben ismertetetthez hasonló megoldásra van szükség, amikor egy adatbázis mezőben
nagyon nagy méretű adatot, ún. BLOB-ot (Binary, Large OBject) szeretnénk tárolni. BLOB-nak
minősülnek például a nagyméretű képfájlok vagy a sok mega-, akár gigabájtos hang- és video
állományok. Mivel például egy video állomány folyamatos lejátszásához az egyes rekord töredékeket
megfelelő sebességgel kell visszaadni, ezért a BLOB-okhoz tartozó blokkokat célszerű folytonosan, egy
cilinderen vagy szomszédos cilindereken lefoglalni a gyorsabb elérés érdekében. További lehetőség a
BLOB tartalom csíkokra bontása (lsd. 6.2. fejezet), és több háttértárolón történő elhelyezése.
Például hang és video tartalmak olvasásakor további probléma, hogy az eltárolt adatra általában nem
egyszerre, teljes egészében van szükség, hanem a rekordnak mindig csak egy bizonyos töredékére, ami
éppen lejátszás alatt áll. Hasonlóképpen, természetes elvárás, hogy például egy video lejátszása
közben lehetőségünk legyen a tartalomban tetszőleges pontról folytatni a megtekintést. Ehhez arra
van szükség, hogy a kliensnek lehetősége legyen a rekord egyes blokkjait az adatbázis-kezelő
rendszertől igény szerint elkérni. A megoldás során gyakran használnak indexeket (lsd. 7.fejezet),
amelyek megkönnyítik a tartalomban történő navigálást (például egy másodperc felbontású indexet
készítenek a video tartalom blokkjaira).
129
1. Megnézzük, hogy a szomszédos blokkokban esetleg van-e elegendő hely, és amennyiben igen,
az egyik rekordot (a megelőző blokkba az elsőt, a követő blokkba az utolsót) átmozgatjuk a
másik blokkba, így teremtve helyet az új rekordnak.
2. Ha egyik szomszédos blokkban sincs elegendő hely, akkor ún. túlcsordulás blokkokat adunk a
cél blokkhoz: a blokkok fejlécében fenntartunk helyet egy mutatónak, amely segítségével egy
túlcsordulás blokkra mutathatunk, amelyben a beszúrandó rekordot elhelyezhetjük. A
mutatók segítségével szükség esetén akár több blokk is egymás után fűzhető. A túlcsordulás
blokkokat az ütemezett adatbázis karbantartó folyamatok szüntetik meg és alakítják egyszerű
blokkokká.
Rekordok törlésekor valamivel egyszerűbb dolgunk van, mivel ilyenkor új blokk lefoglalására soha
nincs szükség. Törléskor a blokk fejlécben lévő rekord mutatót fel kell szabadítani. A rekord által
lefoglalt helyre blokkon belül más blokkokat is mozgathatunk, hogy az egybefüggő szabad helyet
maximalizáljuk. Problémába akkor ütközünk, ha a rekordra az adatbázis más részeiből esetleg
közvetlenül hivatkozhatnak objektumok mutatók segítségével. Ekkor a mutatók érvénytelen területre
mutatnának vagy esetleg egy másik rekordra, ha időközben a törölt helyére létező rekordot
mozgattunk. Ennek kiküszöbölésére a tényleges törlés helyett a rekord helyére egy ún. sírkövet
(tombstone) helyezünk, ami meggátolja, hogy az adatbázis újraszervezéséig az adott helyre más
rekordot helyezzünk, és a hivatkozó objektum számára egyértelművé teszi, hogy a mutató már
érvénytelen területre mutat. Ha a hivatkozások a blokk kezdőcímén és az abban szereplő rekord
eltoláson keresztül történnek, akkor a sírkövet e fejlécbe helyezzük (6.7. ábra: Sírkő a blokk fejlécben).
Állandó hosszúságú rekord módosításakor további adminisztrációra nincs szükség, hiszen pontosan
ugyanannyi helyet foglal el, mint a korábbi változat. Ha viszont az új változat mérete eltér a korábbitól,
akkor a beszúrás és a törlés során alkalmazott technikák alkalmazhatók:
130
Ha viszont a rekord mérete megnőtt, akkor a beszúrás műveletnél látott módon a többi,
azonos blokkon belüli rekordot odébb mozgathatjuk, szükség esetén akár más blokkba is, vagy
túlcsordulás blokkokat hozhatunk létre.
131
7 Indexek
Ha csak azt tudjuk, hogy a Termek tábla rekordjai mely blokkokban szerepelnek, akkor a kérdés
megválaszolásához végig kell olvasnunk a táblához tartozó összes blokkot, és minden Termek rekord
esetén meg kell vizsgálnunk a GyartoKod attribútum értékét. Természetesen a 6. fejezetben
megismert trükköket alkalmazhatjuk (például, hogy egy tábla blokkjait egy vagy egymáshoz közeli
cilinderekre helyezünk), de nagyságrendbeli javulást így nem érhetünk el. A probléma olyan speciális
attribútumok szerinti keresésnél is jelentkezik, mint például az elsődleges kulcs:
Mivel tudjuk, hogy az elsődleges kulcs értéke egyedi, ezért a keresés az első találat után leállítható, de
átlagosan még így is a tábla blokkjainak felét be kell olvasni a rendszermemóriába.
A problémára a megoldást az index jelenti, amely nem más, mint egy hatékonyan kereshető,
kisméretű tartalomjegyzék a tábla egyes rekordjaira. A tartalomjegyzék a tábla tetszőleges oszlopa
szerint készülhet, de akár több indexet is készíthetünk különböző oszlopok szerint. Az indexben tárolt
értékek az index kulcs értékei (nem összetévesztendő az eddigi kulcs fogalmakkal). 7.1. ábrán egy
nagyon egyszerű index megvalósítást láthatunk: az index a Termek tábla GyartoKod oszlopa szerinti
keresést gyorsítja meg. Az index felépítésénél a termék tábla aktuális tartalma alapján megvizsgáljuk,
hogy milyen gyártókódok szerepelnek. Ezek után ezeket az értékeket rendezve (annak érdekében, hogy
hatékonyan tudjunk keresni benne) eltároljuk. Ezután az egyes értékekhez mutatókat tartunk karban,
minden érték referálja a Termek tábla összes olyan sorát, amelyben az adott érték található.
Természetesen, amikor a Termek tábla minden módosításakor gondoskodni kell az index frissítéséről.
GyartoKod index 1 2 3 4 5 6 7 8 9
GyartoKod 3 5 1 3 8 7 5 4 2 2 6 9 6 5 1 9
Termek
Az index mérete sokkal, tipikusan több nagyságrenddel kisebb, mint a teljes tábla mérete, hiszen
bejegyzésenként csupán egy attribútum értéket és néhány mutatót kell eltárolni. Következésképpen,
ha az indexált attribútum szerint keresünk, akkor először az indexben végezzük el a keresést – amely
sokkal kevesebb blokkművelettel bejárható – majd egyszerűen követjük a releváns bejegyzések
mutatóit a tényleges rekordokhoz. A gyakorlatban az indexeket általában fa adatszerkezetekkel
reprezentálják, így lineáris keresés helyett a rekordok számának logaritmusával közelíthetjük a
132
szükséges I/O műveletek számát. Ideális esetben az index néhány megabájt helyfoglalás mellett elfér
a rendszermemóriában is, így I/O műveletek nélkül tudjuk megkeresni a releváns rekordokat, és a
másodlagos tárból már csak a valóban szükséges blokkokat kell beolvasni.
Sűrű indexről akkor beszélünk, ha az adat fájl minden egyes rekordjára mutat bejegyzés az index
állományból. A 7.2. ábra bal oldalán láthatóak az index blokkok, amelyekben található bejegyzések a
jobb oldalon látható adatblokkokban tárolt rekordokra mutatnak. Egy blokkban tipikusan sokkal (akár
nagyságrendekkel) több index bejegyzés elfér, mint adatblokk, ezért jóval hatékonyabban kereshető,
mint maga az adat állomány. Az index állomány bejegyzései mindig rendezetten helyezkednek el, de
ez a sorrend nem feltétlen egyezik meg az adat rekordok sorrendjével.
1 3
2 7
3 1
4 4
5 5
6 6
7
2
8
8
Amennyiben egy konkrét értékhez tartozó rekordot keresünk, akkor megkeressük azt az index blokkot,
amelyben ehhez az értékhez tartozó mutató(k) vannak, majd a mutató(k) segítségével kiválaszthatjuk
a betöltendő adat blokkokat. Az index bejegyzésekben történő keresést több tényező is gyorsítja:
Mivel az index bejegyzések lényegesen kisebbek, mint az adat rekordok, sokszor a teljes index
struktúra betölthető a rendszermemóriába, és a keresés ott sokkal hatékonyabban
elvégezhető.
Az index bejegyzéseket rendezetten tároljuk, ezért a lineáris keresésnél hatékonyabb
módszerek is alkalmazhatók.
Mivel a sűrű indexek minden egyes rekordra tartalmaznak bejegyzést, ezért megengedik az ismétlődést
is az index kulcs értékek között (7.4. ábra).
133
A ritka indexek bejegyzései általában adatblokkonként egy rekordra mutatnak (7.3. ábra), ezáltal
kevesebb helyet foglalnak, mint a sűrű indexek, viszont csak akkor alkalmazhatók, ha az adat rekordok
az indexált oszlop értékei szerinti sorrendben tárolódnak el. Ezzel szemben a sűrű indexek tetszőleges
oszlop esetén alkalmazhatók.
1
2
1 3
3 4
5
5
7
6
7
8
Egy indexet akkor nevezünk elsődlegesnek, ha az index kulcs értékeinek sorrendje megegyezik a
kapcsolódó adat rekordok tárolási sorrendjével – mint ahogy ez a 7.3. ábrán látható esetben is fennáll.
A ritka indexek mindig csak elsődleges indexek lehetnek, ellenkező esetben az index állományban nem
szereplő kulcs értékhez nem találhatnánk meg a kapcsolódó rekordot tartalmazó blokkot.
1 2
2 5
2 4
3 3
4 1
4 2
5
6
6
4
Mivel a másodlagos indexek nem követelik meg, hogy az index kulcsokkal azonos sorrenden
tárolódjanak el az adat rekordok is, ezért tetszőleges attribútum esetén alkalmazhatók. Amennyiben
az adat rekordokat egy ún. heap-állományban tároljuk el (mindenfajta rendezés nélkül), akkor az
elsődleges kulcsra is csak másodlagos indexet készíthetünk.
134
Az adatbázis rekordokat klaszterezetten is tárolhatjuk, ami azt jelenti, hogy az egyik tábla rekordjait
egy másik tábla hozzá tartozó rekordjaival egy csoportban tároljuk el. Például ha gyakran van együtt
szükség a Vevo és a hozzájuk tartozó Megrendeles rekordokra, akkor az egyes vevőkhöz tartozó
megrendelés rekordokat közvetlenül a kapcsolódó rekord mellett is eltárolhatjuk a lemezen (7.5. ábra).
Egy vevő után azok a megrendelések helyezkednek el, amelyeknek a VevoKod attribútum értéke
megegyezik a megelőző Vevo rekord VevoKod attribútumával.
Ezzel az adattárolási módszerrel hatékonyan tudjuk megválaszolni például a következő típusú illesztett
lekérdezéseket:
Mivel a Megrendeles rekordok nem folytonosan, és nem egy egyedi értékekkel rendelkező
attribútum szerint rendezetten helyezkednek el, ezért értelemszerűen minden indexe csak másodlagos
lehet.
7.2 B-fák
Bár az index állomány sokkal kisebb helyen elfér, mint az adat fájl, sokszor még ez sem tölthető be
teljes egészében a rendszer memóriába, ezért hatékony módszerekre van szükség az index
állományokban történő keresésre is.
Mivel az index kulcsok rendezettek, ezért ha a teljes index struktúra elfér a memóriában,
alkalmazhatunk bináris keresést, így a kereséshez szükséges összehasonlítások száma legrosszabb
esetben log 2 𝑛, ahol 𝑛 az index struktúra elemeinek a száma. Mivel a teljes index sok esetben nem fér
be a rendszer memóriába, ezért a kulcs értékeket felfűzhetjük például egy bináris keresőfába (7.6.
ábra), ahol a csomópontokat a harmadlagos tárban tároljuk el. A bináris keresőfa csomópontjai egy-
egy index kulcs értéket tárolnak, illetve mutatót a kulcs értékhez tartozó rekord(ok)ra. Továbbá minden
egyes csomópontra teljesül, hogy legfeljebb két gyermeke lehet (bal és jobb): a bal oldali gyermekével
induló részfában csak a csomópontban tárolt kulcs értéknél kisebb, a jobb oldali részfában pedig csak
nagyobb értékek szerepelhetnek.
135
4
2 6
1 3 5 7
1 2 3 4 5 6 7
Egy K értékhez tartozó rekord keresése a fa gyökerében indul: ha a gyökérben tárolt érték egyezik a K
értékkel, akkor követjük a csomópont mutatóját a rekordhoz, ha K kisebb, mint a gyökérben tárolt
érték, akkor a keresést a bal oldali részfában folytatjuk rekurzívan, egyébként pedig a jobban.
Nyilvánvaló, hogy minden egyes összehasonlítással felezzük a keresési teret, ezért ha az index
n bejegyzést tárol, akkor legrosszabb esetben log 2 𝑛 összehasonlítással megtaláljuk a keresett
csomópontot. Ez egyben log 2 𝑛 csomópont bejárását is jelenti, ami – mivel a csomópontok lemezen
való elhelyezkedéséről nem feltételeztünk semmit – akár ugyanennyi blokk betöltését is jelentheti.
Mivel az I/O műveletek sok nagyságrenddel lassabbak, mint a rendszermemóriában végzett
összehasonlítások, joggal vetődik fel a kérdés, hogy az adatstruktúra átalakításával csökkenthetjük-e
valahogy a betöltött blokkok számát?
7.2.1 B-fa
A B-fák úgy csökkentik az I/O műveletek számát, hogy a fa csomópontjaiban növelik a kulcsok és a
mutatók számát, ezáltal csökkentik a fa mélységét. Mivel a beolvasás alapegysége a blokk, ezért ideális
esetben, egy csomópontban annyi kulcs és mutató foglal helyet, hogy a blokkban rendelkezésre álló
helyet maximálisan kihasználják. Egy B-fa általában három rétegből épül fel: i) egy gyökércsomópont,
ii) köztes csomópontok, iii) levél csomópontok (7.7. ábra). Minden csomópontot egy önálló blokkban
tárolunk.
15
18 25 30
5 9
1 2 4 5 7 8 9 11 13
136
A gyökér és a köztes csomópontok azonos felépítésűek (7.8. ábra): 𝑛 kulcs érték, és 𝑛 + 1 mutató
számára tartanak fent helyet. Minden 𝑝𝑖 mutatóra teljesül, hogy az általa mutatott részfában a
legkisebb kulcs érték 𝐾𝑖−1, és az összes érték kisebb, mint 𝐾𝑖 .
K1 K2 K3
p1 p2 p3 p4
1. A gyökér csomópontban mindig legalább 2 mutató szerepel (feltéve, hogy létezik legalább két
adatrekord a táblában).
2. Minden index blokk legalább félig tele van (legalább ⌈(𝑘 + 1)/2⌉ mutató található benne), így
akadályozva meg, hogy a fa mélysége feleslegesen nőjön.
3. A levél csomópontok kulcs értékei rendezetten helyezkednek el.
1) A gyökér és köztes csomópontokban megkeressük azt a maximális 𝐾𝑖 -t, amely még kisebb,
mint a keresett 𝑘 kulcs.
a. Ha létezik ilyen 𝐾𝑖 , akkor a keresést a 𝑝𝑖+1 által mutatott részfában folytatjuk.
b. Ha nem létezik ilyen 𝐾𝑖 , akkor a keresést a 𝑝0 által mutatott részfában folytatjuk.
2) Ha eljutottunk egy levél csomópontig, akkor megkeressük a 𝑘-val egyező 𝐾𝑖 kulcsot.
a. Ha megtaláltuk, akkor követjük a 𝑝𝑖 mutatót az adat rekordig.
b. Egyébként nem létezik a keresett értékkel adat.
A fa mélysége log 𝑛+1 𝑚-mel arányos, ahol 𝑛 a csomóponton belüli kulcsok maximális száma (tehát 𝑛 +
1 a fa elágazás száma), 𝑚 pedig az összes kulcs érték száma.
7.2.2 B+ fa
A B-fákban hatékonyan tudunk egy konkrét értékhez tartozó rekordot keresni, viszont tartomány-
alapú keresésnél (pl. WHERE X > a AND X < b) a különböző levél csomópontok bejárásához vissza
kell lépni a köztes vagy akár a gyökér csomópontig is. A B+ fa ezt a hiányosságot úgy küszöböli ki, hogy
a levél csomópontokban egy további mutató segítségével a következő levél csomópontra mutat (7.9.
ábra). Következésképpen a B+ fában egységesen, minden csomópontban 𝑛 érték és 𝑛 + 1 mutató
számára van hely.
137
15
18 25 30
5 9
1 2 4 5 7 8 9 11 13 15 ... ...
7.9. ábra: B+ fa
Egy konkrét érték utáni keresés ugyanúgy történik, mint a B-fa esetén, viszont például a
jellegű szelekciós feltételek esetén (ha az X oszlopra korábban készítettünk egy indexet) a
következőképp hajtható végre a keresés:
Amennyiben az intervallum valamelyik oldalon nyitott (X>a vagy X<b hiányzik), akkor a bejárást az első
levél csomóponttól vagy az utolsó levélcsomópontig folytatjuk.
Az eddigiekben feltételeztük, hogy az indexált attribútum értékek egyediek, vagyis hogy egy értékhez
csak egy rekord tartozik. A gyakorlatban ez természetesen nem feltétlenül lesz így, ezért a gyökér és
köztes csomópontok szerkezetét nem, de szemantikáját némileg módosítani kell: az eredeti definíció
szerint a 𝐾𝑖 kulcs a 𝑝𝑖+1 mutató által hivatkozott részfa legkisebb kulcsa volt. Ezt a feltételt annyiban
módosítjuk, hogy mostantól a 𝐾𝑖 kulcs a 𝑝𝑖+1 által mutatott ág legkisebb új kulcsa lesz:
138
15
1)
5 - 7 3) 15 25 30
2)
1 2 4 4 5 6 6 6 6 7 7 15 ...
4) 5) 6) 7) 8)
A 7.10. ábrán egy olyan B+ fát láthatunk, amelyben a leveleken ismétlődő kulcs értékek találhatók.
Például a 2) csomópont 𝐾1 értéke azért 5, mert a 𝑝2 mutatója által mutatott 5) csomópont legkisebb
új értéke 5, hiszen bár a legkisebb kulcsa a 4-es, de a 4-es érték már a 4) csomóponton is szerepelt.
Hasonló okokból a 2) csomópont 𝐾3 kulcs értéke 7, mivel a 𝑝4 által mutatott 7) csomópont legkisebb
új kulcsa 7 (hiszen a 6-os érték már megelőző leveleken is szerepelt). A 2) csomópont 𝐾2 kulcsa pedig
azért üres, mert a 𝑝3 mutatója által hivatkozott 6) csomóponton egyetlen új érték sem szerepel: az
összes kulcs értéke 6, de az már az 5) levélen is szerepelt.
Az ábrán szereplő fában, ha a 7-es értéket keressük, akkor a gyökérben (mivel 7 < 15) 2) felé indulunk,
amelynek a kulcsai között szerepel 7, tehát 2) 𝑝4 -es mutatóján a 7) levélhez jutunk. Itt a 𝑝2 mutatótól
kezdve bejárjuk az összes adatmutatót egészen a 8) csomópont első mutatójáig.
Ellenben, ha a 6) értékhez tartozó rekordokat keressük, akkor 1)-nél szintén 2) felé indulunk, majd –
mivel 5 < 6 < 7 – a keresést a 2) 𝑝2 mutatója felé követjük: 𝑝3 -at soha nem választjuk, mivel azon az
ágon nincs új kulcs érték, 𝑝4 -ben pedig a legkisebb új érték a 7-es. A 𝑝2 -t követve eljutunk a 5) levélhez,
ahonnan elindulva már betölthetők a keresett rekordok.
1. Ha a B-fa index kulcsa az adat fájl elsődleges kulcsa, és az index sűrű: ekkor az adatfájl minden
egyes rekordjára hivatkozik az index fa levél szintjén egy-egy mutató. Mivel az indexált
attribútum az elsődleges kulcs, ezért az index kulcs értékek mind különbözőek.
2. Ha a B-fa index kulcsa az adat fájl elsődleges kulcsa, és az adat fájl az elsődleges kulcs szerint
rendezett, akkor az index lehet ritka: ekkor a levél szinten elhelyezkedő mutatók nem adat
rekordokra, hanem adat blokkokra hivatkoznak, a levél szinten elhelyezkedő kulcs értékek
adatblokkonként a legkisebb értéket tartalmazzák.
3. B-fát felépíthetjük a reláció tetszőleges attribútuma alapján is: amennyiben az adatfájl ezen
attribútum szerint rendezett, akkor az index lehet ritka, egyébként csak sűrű.
Bár az eddig ismertetett példákban az index kulcs egész szám volt, indexet természetesen nem csak
egész típusú oszlopokra készíthetünk, hanem bármely összehasonlítható típusú oszlopra (valós
139
számok, dátum értékek, sztringek stb), hiszen az index fa felépítéséhez az szükséges, hogy tetszőleges
két értéket sorba tudjunk rendezni.
Sztringek esetén az összehasonlítás a két sztring elejétől kezdődik, és karakterenként történik. Ennek
következtében indexek segítségével hatékonyan tudunk prefixum keresést is végrehajtani:
A fenti lekérdezés végrehajtása során (amennyiben a Nev oszlopra készítettünk indexet) az index fában
az ’Asztal’ kulcs értéket tartalmazó levél blokkig kell elnavigálni – vagy ha ilyen nincs, akkor az első,
’Asztal’-nál nagyobb, de még ’Asztal’-lal kezdődő értékig, majd innentől kezdve kell bejárni a
szomszédos levél csomópontokat, amíg mással kezdődő kulcs értéket nem találunk.
Érthető okokból a szuffixum keresést (’%asztal’) nem gyorsítják az ilyen indexek, hiszen míg az
azonos prefixummal rendelkező értékek közvetlenül egymás mellett helyezkednek el a levél szinten,
az azonos végződésű értékek eleje tetszőleges lehet, ezért tetszőleges helyen helyezkedhetnek el a
levél szinten. Hasonlóképp nem alkalmazhatók az ilyen típusú indexek akkor sem, ha a keresett
kifejezés a kulcs érték közepén szerepel (’%asztal%’).
Nem alkalmazhatók indexek a lekérdezés kiértékelése során akkor sem, ha az indexált attribútumot
valamilyen módon transzformáljuk a szelekciós feltételben, például:
Ekkor a végrehajtás során az összes adat rekord betöltődik a memóriába, és a végrehajtó egyenként
kiértékeli rajtuk a szelekciós feltételt. Ilyen egyszerű feltételeknél még akár el is várhatnánk, hogy a
feltétel átrendezésével az Ar=500 feltételt értékelje ki a végrehajtó (amelynél már hatékonyan tudná
az indexet is használni), de a legtöbb esetben ez nem kivitelezhető, ezért ezzel meg sem próbálkozik.
Például a következő lekérdezésben a LEN(Nev)<10 feltételt lehetetlen úgy átalakítani, hogy a Nev
attribútum értékét önmagában hasonlítsuk össze egy vagy több konstans értékkel:
4 ∗ 𝑛 + 8 ∗ (𝑛 + 1) ≤ 4096
140
𝑛 ≤ 340, 6̇
Vagyis csomópontonként 340 kulcs értéket és 341 mutatót tudunk eltárolni: az index fa minden egyes
csomópontja legfeljebb 341 felé ágazik el. Egy ilyen konfiguráció esetén – mivel a mutatók legalább
fele mindig ki kell, hogy töltve legyen az egyes csomópontokban – átlagosan (341 + 341/2)/2 ≈ 255
az egyes csomópontok elágazásszáma. Ez azt jelenti, hogy a fa második (gyökér alatti) szintjén
átlagosan 255, a harmadik szinten 255*255=65536 stb. csomópont található. Ha a levél szint a
harmadik, akkor azok átlagosan 65536*255=16M adatrekordra tudnak mutatni, tehát a legtöbb index
fa valószínű nem lesz mélyebb, mint 3 szint.
Mivel a 4kB-os gyökér csomópontot, és a 255*4kB méretű második szintet (~1MB) általában a
memóriában tartja az adatbázis-kezelő rendszer, ezért egy 3 szintű index fa esetén egy konkrét érték
keresésekor elég egyetlen I/O műveletet elvégezni az érintett levél csomópont betöltéséhez, majd
további egy blokkművelettel beolvasni a keresett adat rekordot. Ha az index fa alacsonyabb – például
csak 2 szint – és az egész betölthető a rendszermemóriába, akkor a keresés elvégezhető csak a
memóriában is, és a keresett adatblokk egyetlen I/O művelettel betölthető.
1. Egyik indexet sem használjuk: Ekkor az egyik tábla összes rekordját (blokkját) fel kell olvasni,
és minden egyes rekordhoz végig kell olvasni a másik tábla összes blokkját is. Ez összesen
1.000*100.000=100.000.000 blokkolvasást jelentene.
2. A t1 tábla col1 oszlopára készített indexet használjuk: a t2 reláció minden egyes blokkját
beolvassuk (100.000 blokkművelet), és index alapú keresést végzünk az illeszkedő t1-beli
rekord után: ez rekordonként 3 I/O műveletet jelent, tehát összesen
100.000+3*100.000=400.000 blokkműveletet eredményez összesen.
3. Ha a t2 tábla col2 oszlopára készített indexet használjuk, és a t1 reláció rekordjait olvassuk
be szekvenciálisan, akkor ez összesen 1.000+4*1.000 = 5.000 olvasást jelent, mivel t2 egy
konkrét rekordjának index alapú megkeresése 4 blokkműveletet igényelt.
Ha az index fák első két szintjét a memóriában tartjuk, akkor t1-ben blokkművelet nélkül tudunk
keresni, és egy olvasással elérhetjük a keresett rekordot, t2-ben pedig 1 index blokk és 1 adatblokk
141
olvasással tudunk megkeresni egy rekordot. Ebben az esetben a fenti illesztés elvégzéséhez
100.000+1*100.000 = 400.000, illetve 1.000+2*1.000 = 3.000 I/O művelet szükséges, ha col1 ,illetve
col2 indexét használjuk. A megfelelő végrehajtási terv kiválasztása szerencsére nem az adatbázis
programozó feladata, hanem az adatbázis aktuális állapota alapján az adatbázis kezelő rendszer állítja
azt elő.
142
17 30 50
3)
2) 5 9 14
1) 1 2 4 5 7 8 9 11 13 14 15 16
Példaképp a 7.11. ábrán látható B+ fába próbáljuk meg beszúrni a 3-as értéket. A fa egyes
csomópontjaiban 3 kulcs és 4 mutató számára van hely. Egy, a gyökérből induló kereséssel kiderítjük,
hogy a 3-at az 1-es csomópontba kellene beszúrni, de mivel abban már nincs elég hely, ezért
kettévágjuk: a régi csomópontban marad az 1 és 2 érték, az új csomópontba pedig bekerül a 3 és a 4.
Az új csomópont a régi levél jobb oldalán foglal helyet, ezért a 2-es szülő csomópontba be kell szúrni a
3-as értéket és egy mutatót az új levélre. Mivel 2-ben nincs már szabad hely, ezért azt is kettévágjuk:
az eredetiben maradnak a 3-as és 5-ös értékek a kapcsolódó 3 mutatóval, az új csomópontba kerül a
14-es érték a kapcsolódó 2 mutatóval, és a 9-es kulcsot felmozgatjuk a gyökérbe (3-as csomópont).
Mivel a gyökérben így már a megengedettnél több kulcs és mutató lenne, ezért azt is kettévágjuk: az
eredetiben marad a 9-es és 17-es kulcs a kapcsolódó 3 mutatóval, az újba kerül az 50-es érték két
mutatóval, valamint egy új gyökér csomópontot hozunk létre a 30-as értékkel és két mutatóval a
kettévágott eredeti gyökér két darabjára. Az eredmény a 7.12. ábrán látható.
30
9 17 50
3 5 14
1 2 3 4 5 7 8 9 11 13 14 15 16
Egy adott érték eltávolítása a fából szintén az érintett levél megkeresésével kezdődik. Ha az érték és a
mutató eltávolítása során a telítettség a minimum alá csökken, akkor összevonásokkal próbáljuk
fenntartani a telítettséget, miközben a szülő csomópontokban adminisztráljuk a gyerekekben
bekövetkező változásokat. A pontos lépések a következők:
143
1. Megkeressük azt a levél csomópontot, amelyben az eltávolítandó érték szerepel. Ha az érték
nem található a fában, akkor nincs egyéb teendő.
2. Eltávolítjuk a kulcsot és a mutatót a levél csomópontból.
a. Ha a legkisebb kulcsot távolítottuk el a levélből, akkor a szülő csomópont kapcsolódó
kulcsát frissíthetjük, de nem kötelező, a fa enélkül is helyes marad (hiszen az adott
ágon továbbra is a szülőben lévő kulcsnál csak nagyobb értékek szerepelnek).
b. Ha a levél csomópont még mindig legalább félig tele van, akkor nincs egyéb teendő.
3. Ha a levél csomópontban a lehetséges mutatóknak már kevesebb, mint fele van kitöltve, akkor
megpróbáljuk valamelyik szomszédos testvér (azonos szülő) csomópontból pótolni a szükséges
számú mutatót és kulcsot.
a. Ha az egyik szomszédos testvérben a minimumnál nagyobb számú mutató volt, akkor
átmozgatjuk a jobb testvér legkisebb (vagy a bal testvér legnagyobb) kulcs-mutató
párosát, és frissítjük a szülőben lévő kulcsot (pl. a jobb szomszéd módosítása esetén a
jobb szomszéd legkisebb kulcsát tükröző szülőbeli értéket frissítjük az új legkisebb
értékre).
b. Ha egyik szomszédos testvérben sincs a minimálisnál több mutató, akkor a módosított
csomópontot összevonhatjuk valamelyik testvérrel, majd a szülőben kitöröljük a
hivatkozást a megszűntetett csomópontra.
Ha a szülőben a minimum alá csökken a mutatók száma, akkor 3-tól rekurzívan
folytatjuk az algoritmust a szülő csomópontra.
30
5) 9 17 50
3 5 14
4) 3)
2) 1)
1 2 3 4 5 7 8 9 11 14 15 16
A keresett érték a 2-es levél csomópontban található, amelyben az érték és a kapcsolódó mutató
eltávolítása után a telítettség a minimum 2 alá csökken. A csomópont testvérét (1-es csomópont)
megvizsgálva látjuk, hogy abban a minimumnál több mutató van, így a legkisebb kulcsot (14) és a
kapcsolódó mutatót átmozgatjuk. Egyúttal a 3-as szülő csomópontban a 14-et 15-re cseréljük ki, hiszen
az 1-es csomópont legkisebb kulcsa most már a 15. További módosítást nem kell elvégezni, az
eredmény 7.14. ábrán látható.
144
30
5) 9 17 50
3 5 15
4) 3)
2) 1)
1 2 3 4 5 7 8 9 14 15 16
Ezek után próbáljuk meg eltávolítani a 14-es értéket, amely szintén a 2-es csomópontban található.
Mivel a levélben eleve csak a minimális mutató szerepel, és egyetlen szomszédos testvérében (1-es
csomópont) szintén a minimális számú mutató van, ezért 1-et és 2-t összevonhatjuk. A csomópontok
összevonása után a 3-as szülő csomópontból az egyik mutató eltávolítható, ezáltal az is a minimálisnál
kisebb telítettséggel fog rendelkezni. Mivel a 4-es számú szomszédos testvérben még van hely, ezért
összevonhatjuk őket: az összevont 2-es csomópont legkisebb kulcsa 9, ezért a 4-es csomópontba is a
9-es értéket helyezzük és egy mutatót az összevont 2-es levélre. Miután az 5-ös csomópont egyik
gyermekét (3-as) megszűntettük, ezért a kapcsolódó kulcs (9-es) ott is eltávolítható. Az eredmény a
7.15. ábrán látható.
30
17 50
3 5 9
1 2 3 4 5 7 8 9 15 16
Amennyiben egy, már a fában lévő értéket egy újra akarunk módosítani, akkor azt a korábbi érték
törlésével, majd az új érték beszúrásával tudjuk elvégezni.
Amint látható, mind a beszúrás, mind a törlés műveletre igaz, hogy az érintett levél csomóponton kívül
csak 1 szomszéd, illetve a szülő csomópontok, és azok szomszédjai lehetnek érintettek, vagyis
legrosszabb esetben is a fa mélységével arányos számú I/O műveletre van szükség.
145
7.2.7 B* fa
Minden index rendelkezik egy névvel, és pontosan egy tábla egy vagy több attribútumát foglalja
magában. Például a következő utasítás a GyartoKod attribútumra épülő indexet építi fel, amely így az
ezen attribútum szerinti keresést gyorsítja:
Amennyiben egy indexre már nincs többé szükségünk, akkor a DROP INDEX utasítással tudjuk azt
törölni. MySQL és MSSQL rendszeren a tartalmazó táblát is definiálni kell az ON kulcsszó után:
146
Oracle esetén elég csupán az index nevét megadni:
Tehát nem célszerű automatikusan minden egyes oszlopra indexet helyezni, hanem csak azokra,
amelyek szerint valóban keresni is akarunk. Amire viszont minden esetben érdemes, az az elsődleges
kulcs. A legtöbb adatbázis-kezelő rendszer ezt alapértelmezetten meg is teszi, hiszen sok lekérdezés
hivatkozik az elsődleges kulcsra a szelekciós feltételében vagy több táblát magában foglaló lekérdezés
esetén az illesztési feltételben. Mivel az elsődleges kulcs szerinti lekérdezések tipikusan egyenlőséget
vizsgálnak, ezért azok mindig legfeljebb egy eredmény rekordot eredményeznek, hiszen a kulcs egyedi.
Az ilyen lekérdezéseknél elég csupán az index fa 1 vagy 2, a rendszermemóriában nem szereplő
blokkját betölteni a lemezről, majd további egy blokkolvasással már elérhető a keresett rekord.
Egy index annál hatékonyabban alkalmazható, minél egyedibbek a kapcsolódó oszlop(ok) értékei,
minél kevesebb ismétlődés található köztük. Ezt a fajta egyediséget nevezzük az index
szelektivitásának. A szelektivitás az indexált oszlop(ok)ban található különböző értékek és az összes
rekord arányát jelenti. Ideális esetben ez 1, vagyis az adott oszlopban minden egyes rekordhoz
különböző érték található. Tételezzünk fel egy vásárló nyilvántartást, amelyben 10.000 vevő adatait
tároljuk, a 10.000 vevő nevei közül összesen 9.000 a különböző. Ekkor, ha az indexet a neveket tároló
oszlopra helyezzük, a szelektivitás 9.000/10.000=0.9. Ha név alapján keresünk egy vásárlót, akkor
átlagosan 10.000/9.000=1.1 találatunk lesz egy konkrét névhez, tehát átlagosan 1-2 adat blokkot kell
beolvasnunk, illetve a memóriában nem bent lévő 1-2 index blokkot. Ezzel szemben, ha például a
vásárlók nemére építenénk fel egy indexet, akkor abban értelemszerűen csak két lehetséges érték
lenne: férfi és nő (pl. 0 és 1), amelyek közül az egyik átlagosan a rekordok egyik felére, a másik pedig a
másik felére mutatna (7.16. ábrán).
147
Nem index 0 1
Vevo
Az index szelektivitása ekkor 2/10.000=0.0002. Egy konkrét nem szerinti kereséshez átlagosan
10.000/2=5.000 találatunk lenne. Mivel a rekordokat valószínűleg nem a nemek szerint rendezetten
tároljuk, ezért jó eséllyel a tábla összes blokkját fel kellene olvasni például a női vásárlók
megjelenítéséhez, hiszen nagy valószínűséggel minden blokkra jutna egy női vásárló rekord. Ekkor az
index blokkok betöltése és a köztük történő navigáció, illetve az index fa leveleiből történő véletlen
adatblokk elérés miatt a lekérdezés jóval lassabb lenne, mintha egyszerűen szekvenciálisan
felolvasnánk a tábla összes blokkját a lemezről. Ezeket az eseteket a lekérdezés végrehajtó logika
szerencsére általában ki is szűri nekünk, tehát egy indexet csak akkor fog a lekérdezés kiértékelése
során alkalmazni, ha úgy ítéli meg, hogy azzal csökkenteni tudja az I/O műveletek számát.
Sajnos nem létezik egy általános szelektivitás határérték, amely megmondaná nekünk, hogy egy adott
oszlopra érdemes-e indexet helyezni vagy sem, a végrehajtó alkalmazná-e azt, vagy pedig nem. A
konkrét határérték sok tényezőtől függ (például a blokkmérettől, a rekordok számától, a rekordok
méretétől, illetve a futtatott lekérdezéstől is), de az esetek többségében valahol 0.8 és 1 között szokott
lenni. Továbbá hiába nagy egy index szelektivitása, ha a táblán futtatott lekérdezések az index előnyeit
nem tudják kihasználni, tehát a futtatott lekérdezések jellegét is figyelembe kell venni (lsd. 7.4.2.
fejezet).
A feltételek szelektivitása szintén befolyásolja, hogy a végrehajtó motor alkalmaz-e egy indexet, vagy
sem. Tegyük fel, hogy a termék nyilvántartásban indexet készítettünk a Nev oszlopra, majd értékeljük
ki a következő lekérdezést:
Nyilvánvaló, hogy a Nev oszlopban bármilyen különbözőek is az értékek (bármilyen nagy is a rajta lévő
index szelektivitása), mindenképp a teljes relációt fel kell olvasni a lemezről. Míg a
148
lekérdezés csupán az index használatával megválaszolható, hiszen már az indexben történő keresés
során kiderül, hogy egyetlen rekord sem felel meg a feltételnek.
Természetesen a fenti lekérdezések szélsőséges esetek, de jól érzékeltetik, hogy a szelekciós feltétel
döntően befolyásolhatja, hogy a kiértékelés során érdemes-e használni egy adott indexet vagy sem.
Említettük továbbá, hogy az indexek karbantartása erőforrást igényel, amely mindhárom módosítás
műveletnél jelentkezik. De azt is vegyük figyelembe, hogy az indexek használata általában ezen
műveletek végrehajtását is gyorsítani tudja:
Index használata nélkül ehhez a Gyarto táblát szekvenciálisan fel kellene olvasni a lemezről,
míg index használatával elég csupán az index fában navigálni. Törlés műveletnél pedig azt kell
ellenőrizni, hogy van-e akár egyetlen rekord is, amely külső kulccsal hivatkozik a törlendő
rekordra:
Amennyiben a Termek reláció GyartoKod oszlopán nincs index, úgy a teljes Termek táblát
végig kell olvasni, hogy az adatbázis-kezelő ellenőrizni tudja, hogy egyetlen termék rekord sem
hivatkozza a törlendő gyártót. Index használata mellett a Termek tábla egyetlen
adatblokkjának beolvasása nélkül, csak az index alapján ellenőrizhető ez a feltétel.
Tehát az indexek megválasztásakor érdemes figyelembe venni az adott táblán futtatott lekérdezések
és egyéb műveletek jellegét is. Az adatbázison a kliens alkalmazásból futtatott lekérdezéseket mindig
célszerű az adatbázis fejlesztő környezetével verifikálni: megtekinteni a lekérdezések végrehajtási
tervét, és ellenőrizni, hogy valóban azokat az indexeket használja-e fel, amelyekre számítottunk. Az
egyes adatbázis-kezelő rendszerek gyakran támogatást is nyújtanak az indexek helyes
megválasztásához statisztikai adatok alapján, illetve az adatbázis napló elemzésével javaslatot tudnak
adni, hogy milyen indexekkel tudnánk gyorsítani a gyakran használt és / vagy erőforrás-igényes
lekérdezések végrehajtását.
149
8 Tranzakciókezelés
Az eddigi példákban azt feltételeztük, hogy az adatbázis-kezelő rendszert egy felhasználó használja, az
egymás után kiadott utasítások az adatbázist olyan állapotban találják, amilyenben az előző utasítás
hagyja. Továbbá feltételeztük, hogy a kiadott módosító utasítások minden esetben végrehajtódnak.
A való életben sajnos nem ennyire idealizált környezetben működnek a rendszerek, az általános
működéshez hozzátartozik, hogy egyszerre több felhasználó vagy több felhasználói folyamat
párhuzamosan írja és olvassa akár ugyanazokat az adatokat is. Például banki vagy tőzsdei
rendszerekben másodpercenként akár több száz vagy ezer művelet is végrehajtódhat, nem zárható ki,
hogy egyes műveletek időben átlapolódnak, és ez az átlapolódás érdekes dolgokat eredményezhet.
Képzeljük el, hogy egy bankszámlára egy időben két helyről érkezik terhelés: egy bankkártyás fizetés
50.000 Ft értékben és egy csoportos beszedés 10.000 Ft értékben. A kiindulási számlaegyenleg 55.000
Ft. Ha a két műveletet közel egy időben hajtjuk végre, akkor előfordulhat olyan ütemezés, amely során
párhuzamosan ellenőrizzük a két művelet végrehajthatóságát (az egyenleg nagyobb, mint a terhelés),
majd mivel mindkettő előfeltétele teljesül, ezért mindkettőt végrehajtjuk, így a záró egyenleg -5.000
Ft lesz még akkor is, ha az adott bankszámla típuson nem engedélyezett a hitelezés. Még nagyobb
problémát jelent, ha a két terhelést az azonos kiindulási értéken hajtjuk végre, így a végeredmény vagy
55.000-50.000=5.000 vagy 55.000-10.000=45.000 Ft lesz (attól függően, hogy végül melyik érték íródik
ki később a lemezre). Bármelyik eredmény jut is érvényre, mindenképp hibás lesz.
Nem kizárható továbbá annak az esélye sem, hogy egy adatmódosítás végrehajtása valamilyen
hardveres vagy szoftveres hiba folytán megszakad. Például két számla közti banki átutalás közben
áramkimaradás történik, vagy hardver hiba miatt leáll az adatbázis-kezelő szerver. Az átutalás közben
az egyik számla egyenlegét csökkenteni, a másikét pedig ugyanennyivel növelni kell. Ha a rendszerhiba
szerencsétlen módon épp a két művelet végrehajtása közben következik be, akkor előfordulhat, hogy
az egyik számláról levonjuk az összeget, de a másikon már nem írjuk jóvá, vagy fordítva.
Szerencsére a mai modern adatbázis-kezelő rendszerek kész megoldással szolgálnak a fenti problémák
elkerülésére: ez pedig a tranzakciókezelés. A fejezetben megismerkedünk az adatbázis tranzakció
fogalmával, tulajdonságaival, valamint azzal, hogy az adatbázis-kezelő rendszerek milyen eszközök
segítségével képesek ezeket a tulajdonságokat biztosítani.
Atomitás (Atomicity)
Konzisztencia (Consistency)
Izoláció (Isolation)
Tartósság (Durability)
Az atomitás azt fejezi ki, hogy a tranzakciók atomi műveletek, amelyek vagy teljes egészében
végrehajtódnak, vagy egyáltalán nem. Tehát nem fordulhat elő, hogy az egy tranzakcióba tartozó
műveletek egy része még lefut, de valamilyen hiba folytán a másik része már nem. Erre szolgált
150
példaként az átutalás végrehajtása két számla között: nem történhet meg, hogy az egyik egyenleget
csökkentettük, de a másikat már nem növeltük.
A tranzakciók mindig konzisztens állapotból konzisztens állapotba visznek át: ha hiba történik a
tranzakció végrehajtása közben, akkor az adatbázis-kezelő tranzakciókezelő alrendszerének
gondoskodnia kell a már végrehajtott utasítások hatásainak visszavonásáról, és az utolsó konzisztens
állapot visszaállításáról. Az adatbázis konzisztens állapotához hozzá tartozik az adatbázisban
lemodellezett különböző kényszerek, referenciális integritás kényszerek, valamint az
alkalmazásfejlesztő által elvárt, de az adatbázisban expliciten nem modellezett kényszerek teljesülése:
például a termékek darabszáma soha nem negatív (még ha az oszlop típusa ezt meg is engedi), vagy
egy megrendeléshez mindig kapcsolódik legalább egy megrendeléstétel.
A tranzakciók izolációja azt fejezi ki, hogy a párhuzamosan futó tranzakcióknak nem lehetnek
egymásra nem kívánatos mellékhatásai, a végeredménynek azonosnak kell lennie azzal, mintha a
párhuzamosan futó tranzakciókat egymás után hajtanánk végre. Például az egyik tranzakció a vele
párhuzamosan futó másik tranzakció közben előállított ideiglenes állapotból kiindulva nem viheti az
adatbázist inkonzisztens állapotba, nem történhet adatvesztés stb. Ezt az adatbázis-kezelők általában
zárakkal vagy időbélyegekkel valósítják meg. Az egyes rendszerek azonban általában biztosítanak
számunkra kevésbé szigorú – és kevésbé erőforrás igényes – megoldásokat is, így az adatbázis
programozó maga határozhatja meg, hogy biztonságos-e, ha más tranzakciók láthatják a tranzakció
futása közben előálló ideiglenes állapotokat, vagy sem (8.2. fejezet).
A tranzakciók hatása a végrehajtás után tartósan meg is marad: miután az adatbázis-kezelő rendszer
egy tranzakcióra azt mondta, hogy végrehajtódott, akkor az általa végrehajtott változtatások már a
lemezre is kiírásra kerültek. Ha ezután történik valamilyen rendszerhiba, akkor az adat már helyes
állapotban elérhető marad. Itt elsősorban olyan soft-crash jellegű hibákra gondolunk, mint például egy
váratlan áramszünet, rendszer újraindulás stb., de semmiképp sem a háttértárolók fizikai
károsodására. Hard-crash esetén – amikor a tároló egységek is károsodnak, vagy a rajtuk tárolt adatok
visszaállíthatatlanul sérülnek – általában manuálisan kezdeményezett rendszer visszaállításra van
szükség biztonsági mentések segítségével.
Amikor SQL utasításokat adunk ki, akkor tipikusan minden egyes utasítás egy-egy külön tranzakcióban
fut le. Ha több utasítást szeretnénk egy tranzakcióban végrehajtani, akkor ezt expliciten jeleznünk kell:
meg kell jelölnünk a tranzakció elejét és végét, vagyis a tranzakciós határokat. A tranzakciós határok
definiálására a következő három SQL utasítás szolgál:
BEGIN TRANSACTION: a tranzakció kezdetét jelöli meg. Ettől a ponttól kezdve a kiadott
utasítások egy tranzakció részét képzik.
COMMIT: a tranzakció sikeres lezárását jelezzük vele: a COMMIT jelzés után az utolsó BEGIN
TRANSACTION óta elvégzett műveletek hatásai tartósan az adatbázis részévé válnak, az újabb
tranzakciók már ebből az állapotból kiindulva végzik műveleteiket.
ROLLBACK: az utasítással megszakíthatjuk egy tranzakció futását, a tranzakcióban eddig
elvégzett módosítások visszavonásra kerülnek, és nem válnak az adatbázis részévé. Rollbackre
általában akkor szoktuk utasítani az adatbázis-kezelőt, ha a tranzakció végrehajtása során
151
valamilyen, a művelet folytatásához szükséges feltétel nem teljesül. Rollbacket nem csak mi
kérhetünk, hanem az atomitás biztosítására automatikusan is megtörténhet, ha például a
tranzakció egy rendszerhiba miatt megszakad. Ekkor a következő rendszerinduláskor a
tranzakciós napló (8.3. fejezet) alapján a megszakadt tranzakció már elvégzett műveleteinek
hatása visszavonásra kerül.
A következő példában egy új megrendelést és egy kapcsolódó megrendeléstételt hozunk létre egy
tranzakcióban. Ha a megrendeléstétel létrehozása alatt például egy rendszerhiba miatt megszakadna
a tranzakció, akkor a megrendelés létrehozása is visszavonódik:
BEGIN TRANSACTION
INSERT INTO Megrendeles (MegdelesKod, VevoKod, Datum)
VALUES (1001, 100, ’2015-01-01’)
INSERT INTO MegrendelesTetel (MegrendelesKod, TermekKod, Darab)
VALUES (1001, 210, 5)
COMMIT
Az egyik lényeges különbség, hogy a tranzakciók elejét kell-e jelölni, vagy sem. MySQL és
MSSQL rendszereken alapértelmezetten ún. auto commit mód van érvényben, vagyis minden
egyes utasítás önálló tranzakciót alkot. Ha több utasítást akarunk egy tranzakcióba összefogni,
akkor a tranzakció elejét és végét jelölnünk kell. Oracle-ben az implicit tranzakciók a
szokásosak, vagyis az egyik tranzakció vége után (COMMIT ill. ROLLBACK) automatikusan egy
új tranzakció indul, ami a következő COMMIT vagy ROLLBACK utasítással ér véget. Tehát nincs
szükségünk / lehetőségünk a tranzakció elejének jelzésére.
o Megjegyezzük, hogy Oracle esetén ki és be is kapcsolható az implicit tranzakciók
használata a SET IMPLICIT_TRANSACTIONS [ON|OFF] utasítás segítségével, de még
ha át is térünk az auto commitált üzemmódra, a tranzakciók elejét expliciten akkor
sem jelölhetjük meg. MySQL esetén szintén van lehetőség az implicit tranzakciókra
történő áttérésre, a SET autocommit=0 utasítás kiadásával, ekkor az Oracle-höz
hasonló működést érhetünk el. MSSQL esetén ezt a SET IMPLICIT TRANSACTIONS
OFF utasítással tehetjük meg.
Szintaxis tekintetében is lehetnek eltérések: bár a COMMIT és a ROLLBACK utasítások általában
minden rendszerben szerepelnek, új tranzakciót azonban MySQL esetén a START
TRANSACTION utasítással, MSSQL-ben pedig a BEGIN TRAN, illetve BEGIN TRANSACTION
utasításokkal indíthatunk.
Explicit tranzakciók használata esetén eltérések lehetnek abban, hogy mi történik, ha egy már
futó tranzakció alatt egy új tranzakciót indítunk. Az MSSQL rendszerek támogatják az
egymásba ágyazott tranzakciókat, így ha egy új tranzakciót indítunk egy másik lezárása előtt,
akkor az egy beágyazott tranzakcióként fog működni, amelyet akár önállóan is vissza tudunk
görgetni szükség esetén. A beágyazott tranzakciók csak akkor fognak az adatbázison is
visszavonhatatlanul alkalmazódni, ha valamennyi szülő tranzakció is kommitálódott. A MySQL
csak egyszintű tranzakciókat támogat, így egy új tranzakció indításakor a még esetleg futó
tranzakció automatikusan kommitálódik. Mivel Oracle-ben nem tudunk expliciten új
tranzakciót indítani, ott értelemszerűen nem merül fel ez a kérdés.
152
Az egyes implementációk eltérhetnek a tekintetben is, hogy milyen jellegű utasításokat lehet
egy tranzakcióba foglalni. Míg az MSSQL rendszerek DML és DDL utasításokat is megengednek
(néhány globális hatású utasítástól eltekintve, mint például a CREATE DATABASE, BACKUP,
RESTORE…), addig a MySQL és ORACLE rendszerek csak DML utasításokat engednek
tranzakcióba foglalni, a DDL utasítások automatikusan commitot eredményeznek.
BEGIN TRANSACTION
INSERT INTO Megrendeles (MegdelesKod, VevoKod, Datum)
VALUES (1001, 100, ’2015-01-01’);
SAVEPOINT megrendeles_created;
...
INSERT INTO MegrendelesTetel (MegrendelesKod, TermekKod, Darab)
VALUES (1001, 210, 5);
...
ROLLBACK TO SAVEPOINT megrendeles_created;
...
COMMIT
Adatbázis tranzakciók párhuzamos végrehajtása során négy fajta nemkívánatos mellékhatás fordulhat
elő:
Elveszett módosítás
153
Piszkos olvasás
Nem megismételhető olvasás
Fantom rekordok
Az elveszett módosítás alatt azt a problémát értjük, amikor két (vagy több) párhuzamosan futó
tranzakció ugyanazon adat azonos kommitált képéből kiindulva módosításokat hajt végre, majd a
módosításokat kommitálják, és a különböző tranzakciók által elvégzett műveletek közül csak az egyik
hatása fog érvényre jutni az adatbázisban, tipikusan az, amelyik időben utoljára kommitálódott. A
következő példában feltételezzük, hogy a Termek(TermekKod, Nev, Ar, Raktarkeszlet)
táblában szerepel az „Asztal” nevű termék 1-es kulccsal és 10 db-os raktárkészlettel. Közel azonos
időben történik egy eladás és érkezik egy új példány ebből a termékből, a két műveletet két
munkaállomásról közel egy időben hajtják végre két különböző tranzakcióban. Mindkét művelet során
először lekérdezzük az aktuális raktárkészletet, majd az egyik tranzakcióban eggyel nagyobb, a
másikban pedig eggyel kisebb értékkel frissítjük azt. Az egyes utasítások végrehajtási sorrendjét a
következő ábra szemlélteti.
Tranzakció 1 Tranzakció 2
SELECT Raktarkeszlet FROM Termek
WHERE TermekKod=1
SELECT Raktarkeszlet FROM Termek
WHERE TermekKod=1
UPDATE Termek SET Raktarkeszlet = 11
WHERE Id=1
UPDATE Termek SET Raktarkeszlet = 9
WHERE Id=1
COMMIT
COMMIT
8.1. ábra: Elveszett módosítás lehetősége
Ha a 10 darabos raktárkészletből 1-et kiadunk, 1-et pedig hozzá veszünk, akkor azt várnánk el, hogy
végül ismét 10 darab legyen raktáron az adott termékből, de a fenti végrehajtás után – mivel mindkét
tranzakció a 10 darabos kezdeti készletből indult ki – végül 9-et kapunk eredményül. Tehát az 1-es
számú tranzakció módosításai elvesztek.
Piszkos olvasás akkor történik, amikor két, egymással párhuzamosan futó tranzakció közül az egyik
módosít egy adatot, és még mielőtt kommitálná azt, egy másik tranzakció ezt az ideiglenes értéket
kiolvassa és felhasználja. A 8.2. ábrán bemutatott példában azt láthatjuk, hogy két tranzakció 1-1
darabbal csökkenti az 1-es számú termék raktárkészletét, de az egyik tranzakció időközben egy
ROLLBACK művelettel visszavonja a módosításait. Ha a 2-es tranzakció az 1-es tranzakció által
módosított értékből indul ki, akkor 10 darabos kezdő raktárkészlet esetén a végeredmény 9 helyett 8
lesz. A 2-es tranzakciónak nem lett volna szabad felhasználnia az 1-es tranzakció által ideiglenesen
előállított értékeket, mivel azok végül nem váltak az adatbázis részévé.
154
Tranzakció 1 Tranzakció 2
UPDATE Termek SET Raktarkeszlet =
Raktarkeszlet – 1 WHERE TermekKod=1
UPDATE Termek SET Raktarkeszlet =
Raktarkeszlet – 1 WHERE TermekKod=1
ROLLBACK
COMMIT
8.2. ábra: Piszkos olvasás lehetősége
Egy olvasást akkor nevezünk nem megismételhetőnek, ha ugyanazon tranzakción belül ugyanazt az
adatmezőt kétszer lekérdezve különböző eredményt kapunk. Ez akkor fordulhat elő, ha az egyik
tranzakció által lekérdezett adatot egy másik tranzakció időközben módosítja (és kommitálja is, tehát
nem piszkos adatot olvas), majd az első tranzakcióban ugyanezt az adatot újból lekérdezve már a
módosított értéket kapjuk. Ezt illusztrálja a 8.3. ábrán látható példa: az 1-es tranzakcióban először
lekérdezzük az 1-es sorszámú termék raktárkészletét, majd amikor konstatáljuk, hogy elegendő áll
rendelkezésre, tízzel csökkentjük azt. Időközben a 2-es tranzakció nullára csökkenti a termék
raktárkészletét, és kommitálja is ezt a módosítást. Ezek után, amikor az 1-es tranzakció csökkenti a
raktárkészletet, akkor már a 0-ás értékből fog kiindulni, így a végeredmény -10 lesz.
Tranzakció 1 Tranzakció 2
SELECT Raktarkeszlet FROM Termek WHERE
Id=1
UPDATE Termek SET Raktarkeszlet=0 WHERE
Id=1
COMMIT
UPDATE Termek SET Raktarkeszlet =
Raktarkeszlet – 10 WHERE Id=1
COMMIT
8.3. ábra: Nem megismételhető olvasás lehetősége
Az elvárt helyes működés ebben az esetben vagy az lenne, hogy a kettes tranzakció módosító utasítása
addig ne tudjon lefutni, amíg az egyes tranzakció be nem fejeződik, vagy az 1-es tranzakció lekérdezése
addig várakozzon, amíg a 2-es tranzakció kommitálódik.
A fantom rekordok problémája a nem megismételhető olvasás egy változata azzal a különbséggel,
hogy nem csak egyszerű attribútumokra vonatkozik, hanem teljes rekordokra: ugyanazt a lekérdezést
ismételten lefuttatva egy tranzakcióban új rekordok jelennek meg az eredményben, vagy korábbiak
tűnnek el. A nem megismételhető olvasáshoz hasonlóan ez akkor fordulhat elő, ha időközben egy
másik tranzakció új rekordokat szúr be vagy töröl a táblából, és kommitálja is a módosítást (tehát nem
piszkos adat olvasása történik). A 8.4. ábrán látható példában az 1-es tranzakcióban a Termek tábla
teljes tartalmát kérdezzük le kétszer, de a második esetben már egy új rekord is megjelenik az
eredményben, mivel a párhuzamosan futó 2-es tranzakcióban beszúrtunk egy új rekordot (és
kommitáltuk is az eredményt).
155
Tranzakció 1 Tranzakció 2
SELECT * FROM Termek
INSERT INTO Termek VALUES(101, ’Szék’,
10)
COMMIT
SELECT * FROM Termek
COMMIT
8.4. ábra: Fantom rekordok előfordulásának lehetősége
A fenti problémák természetesen csak akkor fordulhatnak elő, ha az egyes tranzakciókat egyáltalán
nem izoláljuk egymástól, de a tranzakciókezelő alrendszer az adatbázis műveletek megfelelő
ütemezésével és zárak alkalmazásával biztosítani tudja az elvárt működést. A továbbiakban részletesen
megvizsgáljuk, hogy ez pontosan hogyan történik.
Felmerül a kérdés, hogy honnan tudjuk, hogy a párhuzamosan futó tranzakciók működését nem
megengedett módon befolyásolná egy művelet végrehajtása? Amit biztosan tudunk, hogy ha a
párhuzamosan elindított tranzakciókat úgy hajtjuk végre, hogy egyszerre csak egy tranzakció
műveleteit végezzük el, és azokat közvetlenül egymás után, megszakítás nélkül, akkor biztosan helyes
végeredményt kapunk. Ezt nevezzük soros ütemezésnek: a különböző tranzakciókat átfedés nélkül,
egymás után sorban futtatjuk, párhuzamosítás nélkül.
A 8.5. ábrán két tranzakció soros ütemezését szemléltetjük. A tranzakciók futása során a
rendszermemóriából adatot olvasunk a tranzakció változóiba (READ(A,x): az A adat beolvasása az x
memória rekeszbe), illetve írunk vissza oda (WRITE(A,x): az A adat x helyen álló értékkel történő
frissítése).
156
Tranzakció 1 Tranzakció 2 A B
100 100
READ(A,x)
x = x+10
WRITE(A,x) 110
READ(B,x)
x = x+10
WRITE(B,x) 110
READ(A,y)
y = y*2
WRITE(A,y) 220
READ(B,y)
y = y*2
WRITE(B,y) 220
8.5. ábra: Tranzakciók soros ütemezése
Tranzakció 1 Tranzakció 2 A B
100 100
READ(A,x)
x = x+10
WRITE(A,x) 110
READ(A,y)
y = y*2
WRITE(A,y) 220
READ(B,x)
x = x+10
WRITE(B,x) 110
READ(B,y)
y = y*2
WRITE(B,y) 220
8.6. ábra: Egy sorosítható ütemezés
A 8.6. ábra egy sorosítható ütemezést láthatunk: a 2-es tranzakció A-t az után módosítja, hogy az 1-es
tranzakció már megtette ezt, de még az előtt, hogy az 1-es tranzakció B-t megváltoztatta volna.
Könnyen belátható, hogy ez az ütemezés is mindig konzisztens állapotba visz: Tetszőleges A=B=c
kezdőállapot esetén a tranzakciók lefutása után A és B értéke is (c+10)*2 lesz. Másrészt viszont a 8.7.
157
ábrán látható ütemezés nem sorosítható, mivel c közös kezdeti érték esetén a tranzakciók lefutása
után A értéke (c+10)*2, míg B értéke c*2+10 lesz.
Tranzakció 1 Tranzakció 2 A B
100 100
READ(A,x)
x = x+10
WRITE(A,x) 110
READ(A,y)
y = y*2
WRITE(A,y) 220
READ(B,y)
y = y*2
WRITE(B,y) 200
READ(B,x)
x = x+10
WRITE(B,x) 210
8.7. ábra: Egy nem sorosítható ütemezés
Tranzakció 1 Tranzakció 2 A B
100 100
READ(A,x)
x = x+10
WRITE(A,x) 110
READ(A,y)
y = y+2
WRITE(A,y) 112
READ(B,y)
y = y+2
WRITE(B,y) 102
READ(B,x)
x = x+10
WRITE(B,x) 112
8.8. ábra: Művelettől függően sorosítható ütemezés
Ekkor tetszőleges c kezdeti érték esetén a fenti ütemezést alkalmazva A végső értéke c+10+2=c+12,
B értéke pedig szintén c+2+10=c+12 lesz. Tehát az elvégzett művelettől függően, azonos ütemezés
lehet sorosítható és nem sorosítható is. Sajnos az ütemezőtől nem várhatjuk el, hogy értelmezze a
tranzakciók által elvégzett műveleteket, mivel azokat sokszor általános célú programozási nyelveken
írják, és csak bináris formában érhetők el, illetve általános esetben a szemantika elemzése valós időben
nem kivitelezhető. Ezért az ütemező csak az írás és olvasás műveleteket veszi figyelembe, és ha írás
történik, akkor feltételezi, hogy a kiírt értéket a tranzakció úgy változtatta meg, hogy a párhuzamosan
futó tranzakciókban ugyanezen adatokkal végzett műveletekkel együtt inkonzisztens állapot jön létre.
Ennek megfelelően a sorosíthatóság vizsgálatakor azzal az egyszerűsítéssel élünk, hogy az ütemező
csak az alábbi két műveletet ismeri:
158
A fenti két művelettel a 8.6. ábrán látható két tranzakció az alábbi módon írható le:
Egy ilyen művelet szekvenciában két műveletre azt mondjuk, hogy konfliktusban állnak, ha
sorrendjüket felcserélve legalább az egyik tranzakció viselkedése megváltozhat. Vizsgálatunkat kezdjük
azokkal az esetekkel, amelyek biztosan nem állnak konfliktusban (𝑖 ≠ 𝑗):
𝑟𝑖 (𝑋); 𝑟𝑗 (𝑌): két olvasás művelet mindig felcserélhető, függetlenül az olvasott adattól. Ezt
azért tehetjük meg, mert az olvasás művelet nem változtatja meg az adatot.
𝑟𝑖 (𝑋); 𝑤𝑗 (𝑌), ha 𝑋 ≠ 𝑌: különböző adatok írása és olvasása két különböző tranzakcióban
mindig felcserélhető. Ez azért lehetséges, mert 𝑋 értékét nem befolyásolja, ha előtte Y-t egy
másik tranzakcióban módosítottuk, illetve 𝑌 módosításához nincs szükségünk a másik
tranzakcióban kiolvasott 𝑋 értékre. Hasonló okból 𝑤𝑖 (𝑋); 𝑟𝑗 (𝑌) sem áll konfliktusban.
𝑤𝑖 (𝑋); 𝑤𝑗 (𝑌), ha 𝑋 ≠ 𝑌: különböző adatok írása mindig felcserélhető, hiszen bármely
sorrendben is hajtjuk végre őket, az adatbázis állapota azonos lesz.
Ugyanazon tranzakció két művelete (pl. 𝑟𝑖 (𝑋); 𝑤𝑖 (𝑌)). A tranzakción belüli műveletek
sorrendje kötött, nem rendezhetők át.
𝑤𝑖 (𝑋); 𝑤𝑗 (𝑋): ha két különböző tranzakció ugyanazt az adatelemet írja, akkor az írás
műveletek nem felcserélhetők. Feltételezzük, hogy a két tranzakció más-más adatot írna
vissza, így 𝑋 új értéke attól függne, hogy melyik tranzakció írás művelete hajtódott később
végre.
𝑟𝑖 (𝑋); 𝑤𝑗 (𝑋): ugyanazon adatelem írása és olvasása két különböző tranzakcióból szintén
konfliktusban áll, hiszen ha 𝑤𝑗 (𝑋)-et 𝑟𝑖 (𝑋) előtt hajtjuk végre, akkor 𝑟𝑖 (𝑋) már az új értéket
olvasná ki.
A fenti feltételeket úgy foglalhatjuk össze, hogy két különböző tranzakció két művelete akkor nem
cserélhető fel, ha mindkettő ugyanarra az adat elemre hivatkozik, és legalább az egyik művelet írás.
159
𝑟1 (𝐴); 𝑤1 (𝐴); 𝑟2 (𝐴); 𝑤2 (𝐴); 𝑟1 (𝐵); 𝑤1 (𝐵); 𝑟2 (𝐵); 𝑤2 (𝐵)
Ehhez olyan szomszédos művelet párokat kell keresni, amelyek nem állnak konfliktusban, és a
cseréjükkel meg kell próbálni egy soros ütemezést előállítani. Az alábbiakban ismertetünk egy
lehetséges csere-sorozatot, amellyel a soros ütemezés elérhető, így bizonyítjuk, hogy az ütemezés
sorosítható. Az aktuálisan vizsgált műveletpárt aláhúzással jelöljük:
8.2.2.3 Zárak
Az ütemező feladata tehát, hogy a különböző tranzakciókból érkező írás és olvasás műveleteket
fogadja, és vagy közvetlenül végrehajtsa őket az adatbázison, vagy várakoztassa őket, amíg
biztonságosan végre nem hajthatók. Biztonságos végrehajtás alatt ebben az esetben azt értjük, hogy
az adatbázis konzisztens állapotba kerül, amikor valamennyi aktív tranzakció kommitálódik. A legtöbb
tranzakció ütemező a megismert konfliktus-sorosíthatóságot biztosítja, és ezt kétfázisú zárolás
segítségével teszi. Kétfázisú zárolás során az egyes tranzakcióknak zárat kell elhelyezniük az egyes
adatelemeken, mielőtt írni vagy olvasni tudnák őket, majd a zárat fel kell szabadítaniuk, amint már
nincs rá szükség. Az adott tranzakcióból az elérni kívánt adathoz csak akkor lehet hozzáférni, ha a zárat
a tranzakció sikeresen el tudta helyezni, zárat pedig csak akkor lehet elhelyezni, ha egy másik
tranzakció már nem zárolta a hivatkozott adatot. Ellenkező esetben a tranzakció műveleteit az
ütemező addig várakoztatja, amíg a kapcsolódó zár fel nem szabadul, és a várakozó tranzakció zár-
kérése nem teljesíthető. A zárak elhelyezése történhet automatikusan (az olvasó és író műveletek
előtt) vagy manuálisan (lsd. 8.2.3.1. fejezet), felszabadításuk pedig automatikusan, a tranzakció
befejeztével (commit vagy rollback) történik meg.
A gyakorlatban általában két fajta zárat szoktak használni: osztott (shared) és kizáró (exclusive)
zárat. Az osztott zárakat olvasási műveletek előtt szokták elhelyezni a hivatkozott adaton, a kizáró
zárat pedig írási műveletek előtt. Egy adatra egyszerre több osztott zár is hivatkozhat, ami azt fejezi ki,
hogy ugyanaz az adat párhuzamosan több tranzakcióból is olvasható, kizáró zár viszont csak akkor, ha
egy másik tranzakció nem helyezett még el semmilyen zárat ugyanerre az adatra. (Ugyanazon
tranzakcióból igényelhetünk kizáró zárat egy adatra, ha csak ebből a tranzakcióból helyeztünk rá
olvasási zárat korábban). A fenti zárolási módszer két dolgot biztosít:
Ha egy 𝑇1 tranzakció már kiolvasott egy 𝐴 adatot, akkor egy 𝑇2 tranzakció ugyanezt az 𝐴 adatot
csak akkor módosíthatja, ha a 𝑇1 tranzakció kommitálódott (vagy rollbackelt). Vagyis mivel az
𝑟1 (𝐴); 𝑤2 (𝐴) műveletek konfliktusban állnak, az aktuális ütemezést csak olyan soros
ütemezéssé alakíthatjuk, amelyben a 𝑇2 tranzakció műveletei a 𝑇1 tranzakció után
következnek.
160
Ha egy 𝑇1 tranzakció egy A adatot kiírt, akkor egy 𝑇2 tranzakció ugyanezt az A adatot csak akkor
olvashatja vagy írhatja, ha a 𝑇1 tranzakció már kommitálódott (vagy rollbackelt). Vagyis mivel
a 𝑤1 (𝐴); 𝑟2 (𝐴) ill. 𝑤1 (𝐴); 𝑤2 (𝐴) műveletek konfliktusban állnak; az aktuális ütemezést csak
olyan soros ütemezéssé alakíthatjuk, amelyben a 𝑇2 tranzakció műveletei a 𝑇1 tranzakció után
következnek.
A fenti két feltétel éppen a konfliktus-sorosíthatóságot biztosítja (két különböző tranzakció két
művelete akkor nem cserélhető fel, ha mindkettő ugyanarra az adat elemre hivatkozik, és legalább az
egyik írás művelet).
8.2.2.4 Holtpont
Zárak használata esetén felmerül a holtpont létrejöttének lehetősége. Például ha 𝑇1 és 𝑇2
párhuzamosan futó tranzakciók ugyanarra az 𝐴 adatra kérnek először osztott zárat, majd 𝑇1 megpróbál
az 𝐴 adatra kizáró zárat is igényelni, akkor várakozni kényszerül, mivel a másik tranzakció már osztott
zárral rendelkezik erre az adatra. Ha ezek után 𝑇2 is kizáró zárat igényel 𝐴-ra, akkor ő is kénytelen várni
𝑇1 -re, így a két tranzakció egymásra vár, holtpont alakul ki. Hasonló helyzetet eredményez, ha a 𝑇1
tranzakció először az 𝐴 adatra igényel kizáró zárat, a 𝑇2 tranzakció pedig 𝐵-re. Ha ezek után a 𝑇1
tranzakció a 𝐵 adatra igényel zárat, akkor várakozni kényszerül. Ha a 𝑇2 tranzakció pedig az 𝐴-ra kér
zárat, akkor ő is várakozni kényszerül a 𝑇1 tranzakcióra, tehát ismét holtpont alakul ki (8.9. ábra).
Tranzakció 1 Tranzakció 2
LOCK(A)
LOCK(B)
LOCK(B)
LOCK(A)
8.9. ábra: Holtpont
Számos esetben azonban nem élhetünk a fenti lehetőségekkel, mivel a később elhelyezett zárak a
tranzakció elején még nem megjósolhatók, vagy a zár elhelyezési sorrend nem módosítható. Ekkor a
holtpontot detektálni és kezelni kell. A holtpont kezelése általában azt jelenti, hogy az egyik tranzakció
megszakításra kerül, és a változtatásait visszagörgeti a tranzakciós rendszer. Holtpontok észlelésére
több módszert is alkalmazhatnak:
161
irányított kör keresésre. Az ilyen modelleket szoktuk erőforrás foglalási gráfnak nevezni,
amelyben az erőforrások az adatok. 8.10. ábrán egy egyszerű erőforrás foglalási gráfot
láthatunk: 𝑇1 és 𝑇2 tranzakciók, 𝐴 és 𝐵 pedig adatok. A 𝑇1 tranzakció zárolja 𝐴-t, és arra vár,
hogy zárat helyezhessen el 𝐵-n is, a 𝑇2 tranzakció pedig 𝐵-re rendelkezik zárral, és 𝐴-ra
szeretne zárat felhelyezni. A tranzakciókezelő az irányított kör detektálásával a holtpontot
érzékeli, és az egyik tranzakciót abortálhatja.
A T2
T1 B
Ahogyan láttuk, az ideális ütemezés a soros, illetve sorosítható ütemezések, de számos esetben ez
feleslegesen sok várakozásra kényszerítené a párhuzamos tranzakciókat. Gyakran az üzleti logika, ami
az adatbázis műveleteket vezérli, több párhuzamosságot is megenged, mint amit adatbázis szinten az
ütemező az írás és olvasás műveletekből ki tud következtetni. Az SQL szabvány az ütemezés
szigorúságát ún. izolációs szintekkel engedi szabályozni, és az adatbázis programozóra bízza a
megfelelő izolációs szint kiválasztását. A szabvány által meghatározott izolációs szintek a következők:
UNCOMMITTED READ: semmilyen izolációt nem biztosít, egyik izolációs alapproblémát sem
zárja ki (MySQL, MSSQL).
COMMITTED READ: a piszkos olvasás nem lehetséges, az egyes tranzakciók mindig csak
kommitált adatot olvashatnak (MySQL, MSSQL, Oracle).
REPEATABLE READ: a piszkos olvasást és a nem megismételhető olvasást is kizárja (MySQL,
MSSQL).
SERIALIZABLE (sorosítható): egyik alapprobléma sem fordulhat elő (MySQL, MSSQL, Oracle).
SNAPSHOT: az MSSQL rendszerek által támogatott izolációs szint, amely azt biztosítja, hogy
a tranzakció futása során minden olvasott adat a tranzakció kezdetekor érvényes állapotot
tükrözi, mintha csak egy, a tranzakció kezdetekor készített pillanatfelvételen dolgozna a
tranzakció. Az izolációs szint megvalósításához sor verziózást alkalmaz az adatbázis-kezelő
162
rendszer: a tranzakciók által módosított rekordokat a tranzakció azonosítójával felcímkézve
egy ideiglenes táblában (tempdb) tárolja el, így minden tranzakció saját másolattal rendelkezik
a módosított adatokról. Ha egy tranzakció lezárult, és már nincs olyan, még futó tranzakció,
ami a lezárás előtt jött volna létre (tehát még szüksége lehetne az eredeti adatokra), akkor a
lezárt tranzakció módosításai alkalmazhatók az adatbázison.
READONLY: az Oracle rendszerek által támogatott izolációs szint, amely konzisztensen, a
tranzakció kezdete elején érvényes állapotnak megfelelő értékeket adja vissza a lekérdezések
során, de nem ad lehetőséget a módosításra. Szintén sorverziózással valósítják meg, amely
némileg eltér az MSSQL-beli implementációtól: az Oracle a korábbi változatokat a tranzakciós
napló (8.3. fejezet) bejegyzései alapján tudja előállítani.
A SERIALIZABLE izolációs szint implementációja általában zárak segítségével (8.2.2.3. fejezet) történik,
és a sorosíthatóságot sértő művelet várakozni kényszerül, de például Oracle alatt (csak úgy, mint a
READ COMMITTED izolációs szinten) sorverziózás segítségével – a tranzakciós napló visszagörgetésével
– biztosítják a tranzakció futása alatt a konzisztens képet. Az adatok párhuzamos elérése ekkor nem
okoz blokkolást vagy azonnali hibát, viszont a tranzakció végén, amennyiben a sorosíthatóság nem
biztosítható, hibaüzenetet kapunk. Az Oracle e tekintetben optimista konkurencia kezelést valósít meg.
Az MSSQL ezen két izolációs szint biztosításához zárakat használ: rekord módosítás előtt írási zárat
helyez fel az érintett rekordokra, így megakadályozza, hogy más tranzakciók ezután olvasni tudják őket
a módosító tranzakció végéig (meggátolva a piszkos olvasást). Olvasás előtt pedig megosztott zárat
helyez el a rekordokon. A megosztott zárakat REPEATABLE READ izolációs szint esetén a tranzakció
végéig fent is hagyja, ezáltal megakadályozza, hogy más tranzakciók módosítsák ezeket a rekordokat,
és az olvasás ne legyen megismételhető. READ COMMITTED izolációs szint esetén az osztott zárat
rögtön fel is oldja, amivel csak azt teszteli, hogy van-e az érintett rekordokon kizáró zár, ami
párhuzamos, még nem kommitált módosítást jelentene. Természetesen, ha osztott zár elhelyezésekor
már van az érintett rekordokon kizáró zár, illetve kizáró zár felhelyezésekor osztott zár, akkor a
tranzakció várakozni kényszerül a másik zár feloldásáig. Felmerülhet a kérdés, hogy MSSQL-ben mi a
különbség a SERIALIZABLE és a REPEATABLE READ izolációs szint implementációja között, hiszen
mindkettő osztott zárat helyez el az érintett rekordokon olvasás előtt, illetve kizáró zárat írás előtt,
amelyeket a tranzakció végéig fent is tart. De ha végiggondoljuk, ez önmagában a fantom rekordok
megjelenését nem gátolja meg, hiszen zárak csak a korábban is létező rekordokon vannak.
SERIALIZABLE izolációs szint esetén az osztott zárakat nem egyes rekordokra, hanem intervallumokra
helyezi el a tranzakciókezelő, így várakoztatva azokat a műveleteket, amelyek olyan új rekordokat
eredményeznének, amelyek megjelenhetnének más, még futó tranzakciók lekérdezéseiben.
163
8.2.3.1 Zárak manuális elhelyezése
Ahogyan láttuk, bizonyos izolációs szinteknél az adatbázis-kezelő rendszer automatikusan osztott vagy
kizáró zárakat helyez az érintett rekordokra. Egyes esetekben azonban előnyösebb, ha az adatbázis
programozó saját maga dönthet arról, hogy a zárak pontosan mikor jöjjenek létre. Így például
biztosíthatjuk, hogy az érintett adatokat garantáltan azonos sorrendben zároljuk az egyes
tranzakciókban, és ne fordulhasson elő holtpont, de akár egy alacsonyabb szintű izolációs szinten is
megvalósíthatjuk a tranzakciók teljes izolációját. A zárak elhelyezése rendszerenként eltérő
szintaktikával, de hasonló formában történik: az érintett rekordokat egy SELECT utasítással
kiválasztjuk, és a SELECT utasításhoz fűzött megfelelő kulcsszavakkal elhelyezzük rajtuk a zárakat. (Az
utasítás természetesen várakozik, ha a zárakat nem lehet elhelyezni). A zárakat általában változatos
granularitással hozhatjuk létre: csak bizonyos rekordokon, a teljes táblán, de MSSQL-ben akár
blokkonként is helyezhetünk el zárakat. A 8.11. ábrán, 8.12. ábrán és 8.13. ábrán röviden összefoglaljuk
a MySQL, MSSQL és Oracle rendszerek azon alapvető utasításait, amelyek segítségével sor és tábla
szintű zárakat tudunk elhelyezni. Az egyes funkciók mélyebb megértése és további lehetőségek
megismerése végett ajánljuk az alkalmazott adatbázis-kezelő rendszer dokumentációjának
tanulmányozását.
MySQL
Megosztott zár létrehozása SELECT FROM … WHERE … LOCK IN SHARE MODE
MSSQL
Oracle
A következő MySQL-t alkalmazó példával azt szemléltetjük, hogy hogyan kerülhető el az elveszett
módosítás zárak explicit használatával és a tranzakciók abortálása nélkül. A példában azt az esetet
valósítjuk meg, amikor a 3-as azonosítóval rendelkező termékből párhuzamosan 20-20 darabot
164
szeretnénk kiadni. A termék raktárkészletét (kezdetben 100) mindkét tranzakcióban leellenőrizzük egy-
egy lekérdezés segítségével (SELECT), majd egy 20-szal kisebb értékkel frissítjük (UPDATE). Ha a
párhuzamosan futó tranzakciókban SERIALIZABLE izolációs szintet használunk, akkor a 8.14. ábrán
látható ütemezés mellett holtpont alakulna ki, hiszen először mindkét tranzakció megosztott zárat
helyez fel, majd pedig mindkettő kizáró zárat szeretne kérni, de várni kényszerül. Ugyanez az ütemezés
REPEATABLE READ izolációs szint esetén elveszett módosítást eredményezne, hiszen mindkét
tranzakció ugyanabból a kommitált képből indul (100) ki, majd ezt az értéket 20-szal csökkentve
mindkét tranzakció 80-at írna vissza.
Tranzakció 1 Tranzakció 2
SELECT * FROM TERMEK WHERE ID=3;
SELECT * FROM TERMEK WHERE ID=3;
UPDATE Termek SET Raktarkeszlet = 80
WHERE ID=3;
COMMIT;
UPDATE Termek SET Raktarkeszlet = 80
WHERE ID=3;
COMMIT;
8.14. ábra: Adat párhuzamos írása és olvasása
A probléma egyszerűen megoldható, akár READ COMMITTED izolációs szint használata mellett is, ha
az érintett rekordra már a lekérdezéssel egy időben kizáró zárat helyezünk. Ezt a megoldást szemlélteti
a 8.15. ábra: A SELECT utasítás után a FOR UPDATE kulcsszavakkal jelöljük, hogy a lekérdezéssel együtt
kizáró zárat kell igényelni a visszaadott rekordra. Így egy párhuzamos tranzakció lekérdezése – amely
szintén kizáró zárat igényel – várakozni fog, ameddig a kizáró zárral rendelkező tranzakció nem ér
véget. Ekkor a lekérdezés lefut, de már a módosított raktárkészlet értéket (80) adja vissza.
165
Tranzakció 1 Tranzakció 2
SET SESSION TRANSACTION ISOLATION LEVEL
READ COMMITTED;
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL
READ COMMITTED;
START TRANSACTION;
SELECT * FROM TERMEK WHERE ID=3 FOR
UPDATE;
SELECT * FROM TERMEK WHERE ID=3 FOR
UPDATE;
UPDATE Termek SET Raktarkeszlet = 80 VÁR...
WHERE ID=3;
COMMIT;
UPDATE Termek SET Raktarkeszlet = 60
WHERE ID=3;
COMMIT;
8.15. ábra: Zárak manuális használata
166
Tranzakciókezelő
T1 Tn
Tranzakció Tranzakció
WRITE READ
READ WRITE
Memória puffer
Naplókezelő
(Puffer kezelő)
FLUSH-LOG
OUTPUT
INPUT
Adatbázis
Tranzakciós napló
A tranzakciók futásuk során elsősorban a memória pufferben dolgoznak, ahonnan adatot olvasnak ki a
saját memória terükbe, illetve írnak oda vissza. A pufferbe – amennyiben még nem szerepel benne – a
felhasznált adatot természetesen be kell tölteni a háttértárról. A tranzakciók által elvégzett
műveletekről a naplókezelő bejegyzéseket készít, amelyet szintén a memória pufferbe ír. Mivel a
másodlagos tároló használata időigényes, ezért a pufferben elvégzett változtatásokat a perzisztens
tárra nem azonnal, hanem ütemezetten írják ki. A tranzakciós napló perzisztálása szintén nem minden
egyes bejegyzés után, hanem kötegelve történik meg.
Azért, hogy a különböző naplózási stratégiákat ismertetni tudjuk, szükségünk van egy
jelölésrendszerre, amellyel le tudjuk írni a tranzakciók működése során elvégzett adatmozgató
műveleteket:
A fenti műveletek feltételezik, hogy minden egyes adatelem elfér legfeljebb egy blokkon. Amennyiben
egy adat több blokknyi helyet foglal el, akkor minden egyes blokkot egy önálló adatelemnek kell
elképzelni, amelyeknek az atomi írásáról / olvasásáról a bemutatott naplózási stratégiák fognak
gondoskodni.
A továbbiakban három naplózási stratégiát ismertetünk részletesen (undo, redo, undo/redo), valamint
bemutatjuk, hogy a napló segítségével hogyan tudjuk az utolsó konzisztens állapotot helyreállítani.
167
8.3.1 Semmisségi (undo) naplózás
Az első ismertetett stratégia az semmisségi (undo) naplózás, amely a bejegyzések segítségével azt
teszi lehetővé, hogy a konzisztens állapotot az utolsó adatbázis műveletek visszagörgetésével állítsuk
helyre. A tranzakciós naplóba a következő bejegyzések kerülnek:
1. Egy <T,X,v> naplóbejegyzéseknek még azelőtt a lemezre kell kerülnie, hogy X új értékét a
lemezre írnánk.
2. A COMMIT jelet csak akkor szabad kiírni a naplóba (akkor viszont minél előbb), ha a tranzakció
által elvégzett összes korábbi változtatást kiírtuk a lemezre.
Összefoglalva tehát, a napló bejegyzéseket mindig a módosítás előtt kell a lemezre írni, a COMMIT jelet
pedig minden más után.
168
Adatbázis Puffer
Művelet Napló
A B A B
1 BEGIN T1 10 20 - - <BEGIN T1>
2 INPUT(A) 10 20 10 -
3 READ(A,u) 10 20 10 -
4 WRITE(A,u) 10 20 8 - <T1,A,10>
5 INPUT(B) 10 20 8 20
6 READ(B,u) 10 20 8 20
7 WRITE(B,u) 10 20 8 22 <T1,B,20>
8 FLUSH-LOG 10 20 8 22
9 OUTPUT(A) 8 20 8 22
10 OUTPUT(B) 8 22 8 22
11 <COMMIT T1>
12 FLUSH-LOG
8.17. ábra: Semmisségi naplózás
A táblázat első sorában a tranzakció kezdetét jelöljük, aminek hatására a <BEGIN T1> bejegyzés kerül
a naplóba. A 2. lépésben az A adatot beolvassuk a másodlagos tárból a pufferbe, a 3. lépésben a
pufferből átmásoljuk azt a tranzakció saját u változójába. Ezután a tranzakció elvégzi az adaton a
változtatást, és visszaírja az eredményt a pufferbe (4. lépés). Ezzel egy időben a naplóban egy
<T1,A,10> bejegyzés keletkezik. Az 5-7 lépések a B adaton végzik el ugyanezeket a műveleteket. A 7.
lépéssel a tranzakció sikeresen befejeződött, a felhasználó kiadta a COMMIT utasítást, aminek hatására
– a semmisségi naplózási szabályoknak megfelelően:
1. egy FLUSH-LOG művelet biztosítja, hogy az eddigi naplóbejegyzések a lemezre kerüljenek (8.
lépés),
2. a változtatott adatok kiírásra kerülnek a lemezre (9-10. lépés),
3. végül a COMMIT jel bekerül a naplóba (11. lépés), amit szintén kiírunk a lemezre (12. lépés).
Ha a COMMIT jelet nem írnánk ki azonnal a lemezre, akkor – bár a felhasználó úgy látná, hogy a
tranzakció sikeresen lezárult – egy rendszerleállás esetén a tranzakció hatása nem feltétlen lenne
megőrizhető. Ha a 11. és 12. lépés között történik a hiba, akkor a tranzakciót kénytelenek lennénk
visszagörgetni, mivel a napló alapján nem lenne megállapítható, hogy a tranzakció sikeresen
befejeződött-e vagy sem.
A semmisségi naplózás során számos kötöttséget kell figyelembe venni: az adatbázist csak a
kapcsolódó napló bejegyzések perzisztálása után módosíthatjuk, a COMMIT jel csak az adatbázis
módosítása után kerülhet ki a naplóba, illetve miután a felhasználó kiadta a COMMIT utasítást, a
tranzakció nem fejeződik be addig, ameddig valamennyi változtatás és a napló is a lemezre nem került.
169
Például, ha a 8.17. ábrán látható tranzakció közvetlenül a 7-es művelet után egy rendszerhiba miatt
megszakad, akkor a tranzakcióhoz tartozó bejegyzések nem kerülnek perzisztálásra, de ez a
helyreállítás során nem okoz problémát, hiszen az adat változtatásokat a lemezen még úgysem
végeztük el. Ha a tranzakció a 9-es művelet után szakad meg, akkor az A és B adatok eredeti értékét
tükröző bejegyzések már a lemezre kerültek, így helyreállítás során először B majd A értékét állítjuk
helyre a tranzakció előtti állapotra. Vegyük észre, hogy mivel B új értéke még nem perzisztálódott,
ezért a helyreállítása nem okoz tényleges változást.
8.3.1.2 Ellenőrzőpontok
Amint láttuk, a semmisségi napló visszafelé történő bejárásával a megszakadt tranzakciók változtatásai
visszagörgethetők. Problémát jelent ugyanakkor, hogy a napló olvasása közben nem tudhatjuk
biztosan, hogy van-e még olyan művelet a napló korábbi részeiben, amelyek félbeszakadt
tranzakciókhoz tartoznak. Ezért kénytelenek lennénk a teljes naplót egészen az elejéig ellenőrizni,
amely nagy naplók esetén rendkívül időigényes lenne.
A problémára megoldást az ellenőrző pontok (checkpoint) bevezetése jelent. Ennek egy egyszerű
megvalósítása a következő lépésekből áll:
Mivel a <CKPT> bejegyzések előtt az összes futó tranzakció befejeződött, ezért helyreállítás során
elegendő csupán a (hátulról) legelső ellenőrzőpontig feldolgozni a naplóbejegyzéseket, mivel előtte
már garantáltan nem található félbeszakadt tranzakcióhoz tartozó bejegyzés.
Ha a naplót visszafelé bejárva <END CKPT> bejegyzést találunk, akkor biztosan tudjuk, hogy a
hozzá tartozó <START CKPT(Tm,…,Tn)> bejegyzésben azonosított tranzakciók már mind
befejeződtek, ezért elég a Tn utáni tranzakciókat ellenőrizni.
Ha a naplót visszafelé bejárva <START CKPT(Tm,…,Tn)> bejegyzést találunk, még az <END
CKPT> előtt, akkor a rendszerhiba valamikor az ellenőrzőpont készítése közben történt. A
170
<START CKPT(Tm,…,Tn)> bejegyzésből tudjuk, hogy ebben a pillanatban a Tm előtti
tranzakciók már mind lezárultak, ezért ekkor elég csupán a Tm és azt követő tranzakciókat
ellenőrizni.
A 8.18. ábrán látható napló szerint a rendszerhiba épp egy ellenőrzőpont készítése közben történt.
Helyreállítás során a naplóban visszafelé haladva először (9.sor) konstatáljuk, hogy a T6 tranzakció
kommitált, ezért az ő műveleteit nem kell visszagörgetni. A 8-as és 7-es sorban nem befejezett
tranzakcióhoz tartozó műveleteket találunk, ezért E-t visszaállítjuk 15-re, C-t pedig 10-re. Az 5. sorban
megtaláljuk a <START CKPT(T5,T6)> bejegyzést, tehát tudjuk, hogy már csak a T5 tranzakció
kezdetéig kell visszatekintenünk a naplóban. Mivel T6 sikeresen befejeződött, ezért a 4. sorban
található bejegyzéshez kapcsolódóan nincs teendőnk, viszont a 2. sornak megfelelően A értékét vissza
kell állítanunk 10-re. Az 1-es sorban megtaláljuk a T5 tranzakció kezdetét, amivel a helyreállítást
leállíthatjuk.
... ...
1 <BEGIN T5>
2 <T5,A,10>
3 <BEGIN T6>
4 <T6,B,20>
5 <START CKPT(T5,T6)>
6 <BEGIN T7>
7 <T7,C,10>
8 <T5,E,15>
9 <COMMIT T6>
8.18. ábra: Helyreállítás ellenőrzőpont készítése közben
A semmisségi naplózás egyik nagy hátránya, hogy a tranzakció addig nem kommitálható, amíg az összes
változtatást ki nem írtuk a lemezre. Előnyösebb lenne, ha a változtatásokat egy ideig még a
memóriában tarthatnánk, ameddig a lemezvezérlő fel nem szabadul, és az adatok perzisztálását
elvégezhetjük. Ezt csak akkor tehetjük meg, ha a naplóbejegyzések alapján a változtatásokat egy
rendszerhiba esetén is újra el tudjuk végezni. A helyrehozó naplózás (redo) a következő pontokon
különbözik a semmisségitől:
Az továbbra is igaz, hogy az adatok perzisztálása csak a tranzakciós napló lemezre mentése után
kezdődhet el.
8.19. ábrán egy lehetséges helyrehozó napló részletet láthatunk. A 4-es és 5-ös sorban tehát A és B új
értékét naplózzuk. A felhasználó a COMMIT utasítást a 7-es lépés után adta ki, aminek hatására a
naplóba kerül a <COMMIT T1> bejegyzés, és a naplót kiírjuk a lemezre (8-9 lépés). Mivel a T1
171
tranzakció által elvégzett műveletek hatását naplóztuk, ezért elkezdődhet a változtatások lemezre
mentése (10-11. lépések).
Adatbázis Puffer
Művelet Napló
A B A B
1 BEGIN T1 10 20 - - <BEGIN T1>
2 INPUT(A) 10 20 10 -
3 READ(A,u) 10 20 10 -
4 WRITE(A,u) 10 20 8 - <T1,A,8>
5 INPUT(B) 10 20 8 20
6 READ(B,u) 10 20 8 20
7 WRITE(B,u) 10 20 8 22 <T1,B,22>
8 <COMMIT T1>
9 FLUSH-LOG 10 20 8 22
10 OUTPUT(A) 8 20 8 22
11 OUTPUT(B) 8 22 8 22
8.19. ábra: Helyrehozó naplózás
Amint látható, a kommitálási folyamat a felhasználó szempontjából jóval rövidebb: csupán egy
<COMMIT> bejegyzést kell készíteni a naplóban, és a naplót ki kell írni a lemezre. Az adatok tényleges
megváltoztatása később, egy megfelelő időpontban történik meg.
8.3.2.2 Ellenőrzőpontok
Mivel a helyreállító naplózásnál egy adott pillanatban már sikeresen befejeződött tranzakciók hatása
még nem feltétlenül jelent meg a lemezen tárolt adatbázisban is, ezért az ellenőrzőpontok
létrehozásánál nem csak a még aktív tranzakciókat kell figyelembe vennünk, hanem még a nem
perzisztált módosításokkal rendelkező tranzakciókat is. Azt, hogy a pufferben tárolt mely adatok nem
kerültek még a lemezre, és azokat melyik tranzakció módosította, a pufferkezelőnek kell
nyilvántartania. Helyreállító naplózás esetén az ellenőrzőpontokat a következő logika szerint kell
létrehozni:
Az <END CKPT> bejegyzésnél tehát azt tudjuk biztosan, hogy a Tm előtti tranzakciók valamennyi
változtatása már a lemezre kerül. Helyreállítás során – amikor a naplót az elejétől kezdve olvassuk – ha
172
az utolsó <END CKPT> bejegyzés után már nem szerepel újabb <START CKPT…> bejegyzés, akkor elég
a Tm és az azt követő tranzakciók változtatásait újra a lemezre írni. Ezzel szemben, ha az utolsó <START
CKPT(Tm,…,Tn)> bejegyzés után már nem szerepel <END CKPT>, akkor nem lehetünk benne
biztosak, hogy a Tm előtti kommitált tranzakciók módosításait már a lemezre írtuk. Ezért meg kell
keresni a megelőző <END CKPT> és a kapcsolódó <START CKPT(Ti,…,Tj)> bejegyzéseket, és a Ti
tranzakciótól kezdődően kell a változtatásokat újra perzisztálni.
Egy X módosított adat perzisztálása előtt a lemezen meg kell jelennie a <T,X,u,v>
naplóbejegyzésnek, amely azt jelöli, hogy a T tranzakció az X adatot u-ról v-re megváltoztatta.
Mivel az adatelemek megelőző és következő értéke is rendelkezésre áll az adat perzisztálása előtt, a
COMMIT jel az adatok lemezre írása előtt, közben és után is megjelenhet a naplóban. Az előző két
ismertetett naplózási stratégiához képest ez a módszer igényli a folyamatok közti legkevesebb
szinkronizációt: az adatelemet még a tranzakció vége előtt átírhatjuk az adatbázisban. Félbeszakadt
vagy visszagörgetett tranzakció esetén természetesen ez többletmunkát igényelhet, de általánosan
elmondható, hogy több a sikeres, mint a sikertelen tranzakció, ezért az előnyösebb pufferkezelés
(előbb felszabadítható az adatelemek számára lefoglalt memória) és kisebb szinkronizációs igény ezt
bőven kárpótolja.
8.20. ábrán egy olyan forgatókönyvet láthatunk, amely során a COMMIT jel a két adatbázis írás között
kerül a naplóba. Figyeljük meg, hogy a változtatások perzisztálása előtt a kapcsolódó napló
bejegyzéseket a lemezre írtuk (8-as lépés).
173
Adatbázis Puffer
Művelet Napló
A B A B
1 BEGIN T1 10 20 - - <BEGIN T1>
2 INPUT(A) 10 20 10 -
3 READ(A,u) 10 20 10 -
4 WRITE(A,u) 10 20 8 - <T1,A,10,8>
5 INPUT(B) 10 20 8 20
6 READ(B,u) 10 20 8 20
7 WRITE(B,u) 10 20 8 22 <T1,B,20,22>
8 FLUSH-LOG 10 20 8 22
9 OUTPUT(A) 8 20 8 22
10 <COMMIT T1>
11 FLUSH-LOG 8 20 8 22
12 OUTPUT(B) 8 22 8 22
8.20. ábra: Semmisségi / helyrehozó naplózás
Az <END CKPT> bejegyzés a naplóban tehát azt fejezi ki, hogy a kapcsolódó <START
CKPT(Tm,…,Tn)> bejegyzés Tm tranzakciója előtti tranzakciók változtatásai a lemezre kerültek, a Tm
és azt követő tranzakciók változtatásai pedig részben. Tehát a Tm és az utáni kommitált tranzakciókat
újra végre kell hajtani a lemezen, a félbeszakadt tranzakciókat változtatásait pedig vissza kell állítani az
eredeti értékre.
174