2023.02.14
NoSQL – relációs adatbázisok megközelítése
A NIX Tech Kft. a nemzetközi IT-piac meghatározó szereplője. A brand több világhírű projektben is részt vett már. Szakértői sokéves szakmai tapasztalattal rendelkeznek az IT outsourcing világában. A NIX Tech Kft. csapatának eltökélt szándéka, hogy valós tudáson alapuló szakmai fejlődési lehetőségeket és karrierutakat kínáljanak az informatikai piac résztvevőinek. Ezért is indítanak szakmai cikksorozatot az IT világ iránt érdeklődők számára, hogy megosszák gyakorlati tapasztalataikat és támogassák őket céljaik elérésében.
“A problémák fontosabbak, mint a megoldások. Az utóbbiak elavulhatnak, azonban a problémák megmaradnak” – mondta Niels Bohr, Nobel-díjas fizikus. Ezzel mi is teljes mértékben egyetértünk a NIX Tech-nél, különösen, ha az alkalmazásokban lévő adatok modellezéséről és rendszerezéséről van szó, hiszen ez a probléma még mindig fennáll.
A modern világban a relációs adatbázisok nem ideálisak, és az új NoSQL-modelleknek is vannak hátrányai, főleg ha az adatok duplikálásáról van szó. Így a legtöbb fejlesztő keresi a legjobb módját az adatok rendszerezésének. Néha ez a feladat a relációs és nem relációs adatbázisok kombinálásával oldható meg.
Cikkünkben megmutatjuk, hogyan és mikor lehet relációs adatbázisokban NoSQL megközelítéseket alkalmazni, de az elméleti kérdések helyett inkább gyakorlati példákra összpontosítunk.
Relációs adatbázisok jellemzői
A legtöbb fejlesztő jól ismeri a relációs adatbázisok fogalmát, és szinte minden projektben használja azokat. A modell alapjait, Edgar Codd fektette le az 1970-ben publikált “A Relational Model of Data for Large Shared Data Banks” című cikkében, amelyben először írta le a relációs algebrán alapuló koncepciót. Akkoriban elképzeléseit nem igazán értették meg, vagy fogadták el, emiatt később közzétette a “Codd’s 12 rules” névre hallgató szabályrendszerét, amelyben részletesebben magyarázta el elméletét.
Mitől lesz egy adatbázis relációs?
- Az adatbázis sémája: tartalmazza a táblázatokat, azok attribútumait és a közöttük lévő kapcsolatokat, illetve az elsődleges kulcsot, idegen kulcsot stb. Fontos, hogy ezt a sémát előre ismerjük, mivel enélkül nem lehet adatokat helyezni az adatbázisba.
- Normál formák. Ha az adatbázist egyetlen hatalmas táblaként alakítjuk ki, akkor az rosszul fog működni. Az adatokat kisebb táblázatokra kell osztani, és kapcsolatokat kell létrehozni köztük. Ezek az úgynevezett normál formák, amikre a relációs adatbázisok épülnek
- ACID tranzakciók. Azt merjük állítani, hogy ez a legfontosabb dolog a relációs adatbázisokban. Az ACID az Atomicity, Consistency, Isolation és Durability (atomiság, konzisztencia, izoláció és tartósság) rövidítése. Egy relációs adatbázis-rendszerben van egy tranzakciós mechanizmus, amelynek eredményeképpen az összes adat mentve lesz a lekérdezések vagy a műveletek véglegesítése után. Ugyanakkor ezek az adatok csak ezután kerülnek egyeztetésre egymással, és akkor is elérhetőek lesznek, ha a teljes rendszer összeomlik.
Ezek segítenek könnyebbé és egyértelműbbé tenni az adatbázisokkal való munkát, ezért is olyan népszerűek a relációs adatbázisok.
A relációs adatbázisok hátrányai
Idővel felmerültek különböző kérdések és problémák a relációs adatbázisokkal kapcsolatban, mint például az objektum-relációs impedancia eltérés. Emiatt a fejlesztők próbáltak alternatívát találni, erre nézzünk is most egy példát.
Képzeljük el, hogy van egy User classunk, benne a lájkolt posztok gyűjteményével. Azonban ha a leírt szabályok szerint építjük fel az adatbázis sémát, és elhelyezzük ezeket az adatokat a leírt szabály alapján, akkor azok kissé máshogy vagy eltérő formátumban lesznek tárolva, mint a memóriában. kor azok kissé máshogy vagy eltérő formátumban lesznek tárolva, mint a memóriában. Emiatt a fejlesztőknek ismerniük kell a táblák közötti kapcsolatot, tudniuk kell query-t írni join-nal, illetve olyan mechanizmusokat kell kitalálniuk, amelyekkel snapshotokat kaphatnak az adatokról, a lekérdezés után pedig vissza kell küldeniük a megfeleltetett adatokat. Mindez rengeteg gondot okoz.
Ezt a problémát objektum-orientált adatbázisokkal próbálták megoldani. De az ötlet nem vált népszerűvé, hisz ezek ugyanazok a relációs adatbázisok voltak, beépített mapping mechanizmussal. Az igazi nehézségek azonban akkor kezdődtek, amikor a háztartásokban megjelent a szélessávú internet, és a nagyvállalatok megkezdték operatív működésüket világszerte. A fejlesztők ekkor a horizontális skálázást kezdték el alkalmazni, ahol további elemeket vehettek fel, csomópontokra osztva. Az ötlet egyszerű: egy lekérdezést egy csomópontnak kell feldolgoznia. Minél több csomópont van, annál több lekérdezést lehet végrehajtani. A relációs adatbázisok esetében azonban ez nem így van.
Tegyük fel, hogy az adatbázisban vannak userek és posztok. Úgy döntünk, hogy skálázzuk az adatbázist, azonban ilyenkor felmerülhetnek bizonyos problémák. Először is, a felhasználók különböző csomópontokon lehetnek. Másodszor, egy felhasználóhoz tartozó bejegyzések különböző csomópontokon jelenhetnek meg. Emiatt egy lekérdezés végrehajtásához, például select from users joins post két csomópontot is meg kell vizsgálnia. Emiatt fennáll az adatok nem egyenletes eloszlásának a lehetősége. Ebből kifolyólag csökken az adatfeldolgozás sebessége, és a rendszer túlságosan összetetté válik, így szükség volt egy másik alternatívára.
Mi az a NoSQL?
Ahogy az általában lenni szokott, az új típusú adatbázisok létrehozásában is az első sikereket a nagyvállalatok – a Google és az Amazon – érték el. Ők voltak az elsők, akik úgy döntöttek, hogy hátat fordítanak a relációnak, és új paradigmát alkalmaznak. 2006-2007 folyamán jelentek meg a Google Big Table és az Amazon Dynamo cikkei a felhőalapú adatbázisokról. A publikációkban elhangzott gondolatok nem kapcsolódtak a relációs elmélethez. Nem voltak táblázatok, nem voltak kapcsolatok, nem voltak joinok, és ami a legfontosabb, ezeknek a rendszereknek a fejlesztői valódi skálázást értek el.
Ez sok programozót inspirált hasonló ötletek kidolgozására. Ezért 2009-ben néhány fejlesztő szervezett egy meet up-ot a témához kapcsolódóan, amihez neves márkák csatlakoztak, köztük a MongoDB, a CouchDB és mások. A szerevezők ahhoz, hogy figyelemfelketővé tegyék az eseményt, egy szlogen kitalálása helyett egy hashtag jellegű dolgot kerestek, amely által épszerűsíteni tudták az eseményt a Twitteren. Így született meg a NoSQL elnevezés.
A NoSQL-adatbázisoknak többféle típusa ismert, és mindegyiknek megvannak az előnyei és hátrányai. Lássuk ezeket:
Kulcs érték (key-value)
Ez a legegyszerűbb NoSQL paradigma: a kulcs-érték egy kivonattáblában tárolódik. Nincsenek bonyolult fieldek, nincsenek kapcsolatok a kulcs-érték között. Legnagyobb előnye, hogy egy ilyen struktúra a végtelenségig skálázható. Ennek legjobb példája a Redis, amelyet sokan gyorsítótárként használnak.
Oszloporientált adatbázis
Ez a modell továbbfejlesztette a kulcs-értéket. Itt is van egy kulcs, de az érték már nem egy egyszerű sor, hanem saját kulcs-érték párok halmaza. Vagyis ez már egy értékekkel rendelkező oszlopok rendszere, ahol minden sornak lehet saját halmaza. Ilyen adatbázis például a Cassandra.
Gráf adatbázisok
Ez az adatbázis a gráfok koncepcióján alapul. Az elképzelés szerint az adatok közötti kapcsolat is valamilyen adat. Az ilyen adatkapcsolatok felépítése egy gráfhoz hasonlít, így lehetőségünk van a hierarchikus viszonyok vagy a bonyolult kapcsolatok feltárására. Tulajdonképpen a közösségi platformok így működnek. Vegyünk egy példát: az egyik felhasználó egy másik felhasználó ismerőse, az utóbbi fel van iratkozva valamilyen csoportra, az előbbinek pedig tetszik valamilyen poszt – és ezek mind olyan kapcsolatok, amelyek adatok. A Neo4j ezt a modellt alapul véve jött létre, méghozzá egy nagyon érdekes lekérdezés szintaxissal is rendelkezik.
Dokumentumtár adatbázisok
Sok fejlesztő a NoSQL kapcsán elsősorban a dokumentumt-orientált adatbázisokra gondol Elvük a következő: egy teljes dokumentumot kulcs-érték alapján tárolnak, végtelenül egymásba ágyazható és különböző struktúrájú kulcs-érték halmazok formájában. Népszerű példája a MongoDB.
MongoDB a gyakorlatban
A MongoDB-ről érdemes bővebben is ejteni néhány szót. Képzeljünk el egy olyan projektet, amelyben egyszerű Google Forms-ot kell létrehozni. A felhasználó létrehozhat egy űrlapot, amely tetszőleges számú mezőt tartalmazhat.
Egy sima “NoSQL vs SQL” űrlap gombokkal, skálával és szabad szöveggel van ellátva. A felhasználó azonban összetett űrlapot is létrehozhat. Például a Személyes adatok esetében a következő mezőket találjuk: Név, születési dátum, munkatapasztalat, technológia stb.
Ha ezt a feladatot a relációs paradigmában oldanánk meg, nagyon nehéz lenne létrehozni egy adatbázis-sémát, hisz nem tudjuk előre, hogy végül hány mező lesz, milyen mezők jönnek létre, hogyan bővül a funkcionalitásuk stb. Itt jön jól a MongoDB.
A probléma megoldásához létrehozhatunk egy form schema-t, amely leírja a struktúrát és a lehetséges adatokat. Például a form schema a következő mezőkkel rendelkezhet: createdDate, description, formName. A schema tartalmazza a séma által leírt mezők halmazát.
Tegyük fel, hogy szükségünk van egy nem túl bonyolult Employee Details űrlapra. Létrehozhatunk egy field type-ot field group néven, amely saját sorokkal rendelkezik – ez egyfajta rekurzív struktúra lesz. Hozzáadhatunk szabad szöveget, numerikus adatokat, dátumot stb.
Ideális mindezt JSON-ban tárolni, a formátum által biztosított letisztultság, hatékonyság és rugalmasság miatt. A eredményeket pedig egy másik formátumban. Egy példán keresztül nézve: van fillDate (dátumozás), formName és formVersion (űrlap megnevezése és verziója), és data mező, ahol minden szükséges adatot kitöltünk a leírt séma szerint.
Hogyan lehet ezt megvalósítani MongoDB-ben? Az adott adatbázisban léteznek ún. gyűjtemények. Ha összehasonlítjuk a relációs világgal, ezek is táblázatok, de egy eltéréssel – bármi bekerülhet egy gyűjteménybe.
A Mongo API és Query szintaxisát is használhatjuk az összes elérhető form_schema keresésére és a dokumentumok listájának lekérdezésére.
Ugyanígy el tudjuk érni az összes form_data -t és dokumentumot, melyek az adott gyűjteményhez tartoznak.
Ezenkívül lekérdezéseket is hozhatunk létre. Például, ha ki kell választanunk egy adott form_data adatot, akkor a MongoDB speciális szintaxisával és eszközeivel csak azokra keresünk rá, ahol a workExperience 10 év.
Ha ezekre a mezőkre nincs szükség, kibővíthetjük a lekérdezést, és kiválaszthatjuk azokat a mezőket, amelyek valóban szükségesek. Például name és workExperience.
Fontos megjegyezni, hogy a gyűjtemények sémától függetlenek. Tehát, nem szükségszerű olyan adatokat beszúrni, amelyek egy séma alá tartoznak. Ez véletlenszerű is lehet, pl., mint az alábbi ábrán látható objektum. Probléma nélkül végrehajtható egy ilyen lekérdezés:
NoSQL előnyei és hátrányai
A nem-relációs modellnek is vannak előnyei és hátrányai. Ha általánosítjuk a különböző NoSQL modelleket, akkor következők az előnyei:
- Skálázás. Általában a partíciókulcsnak köszönhetően történik. Ez egy meghatározott elem, amely a horizontális skálázás alapja. Ha előre betesszük az adatokat, mert ismerjük a partíciókulcsot, a skálázás szinte korlátlan lesz.
- Az objektum-relációs impedancia eltérés megszüntetése. A MongoDB példáin keresztül megmutattuk, hogy az adatok kívánt sémával és struktúrával rendelkezhetnek. Nem kell összetett joinokat írni, vagy az adatokat másképp modellezni, mint ahogyan használjuk.
- Nem szükséges séma (schemeless). Tetszés szerint bármilyen adatot illeszthetünk be egy gyűjteménybe. Bár ez természetesen a NoSQL hátránya is lehet.
Legfőbb hátrányai:
- A tranzakciók részleges támogatása. A különböző NoSQL adatbázisok alapvetően nem támogatják az ACID-t. Ezt feláldozták a rendszer skálázása érdekében.
- A reláció elutasítása. Ha nincsenek join-ok, akkor jön képbe az adatduplikáció. Fontos megérteni, hogy ez normális egy ilyen rendszer esetében – nem bug, hanem egyik jellemzője. Ezért az adatokat úgy kell modellezni, hogy egy ilyen rendszer a lehető leghatékonyabb működjön.
- Korlátozott keresési lehetőségek. A NoSQL-ben kereshetünk, ahogy a példában is említettük, azonban a globális skálázási koncepcióval problémák vannak. Ha az adatok több csomóponton helyezkednek el, akkor egy bizonyos kritérium kereséséhez partíció nélkül szó szerint az összes csomóponton végig kell mennie. Ez tulajdonképpen kiküszöböli a skálázást mint olyat.
Ami a nem relációs adatbázisokat illeti, érdemes átgondolni, hogy hol fogjuk azt használni. Végül is ami jó az egyik fejlesztőnek, az nem mindig jó a másiknak.
A különböző vállalatok eltérő NoSQL modelleket használnak. Például a Redis szabványa a kulcs-értéken alapul. Az oszlopalapú megtalálható a Facebook, az Instagram és a Netflix termékeiben – előnézetekhez, Machine Learning-hez és a tartalomszűrés javításához. Az eBay katalógusa gráf adatbázisokra épül. A dokumentum-adatbázisok a NoSQL-adatbázisok legelterjedtebb típusa. Bármilyen adatszerkezet és CMS modellezhető vele.
Relációs adatok formázása JSON formátumban
Skálázhatóságuk miatt a nem-relációs adatbázisok igen népszerűek, de nem jelentenek ideális megoldást. Azonban lehetséges-e felhasználni a NoSQL-t relációs adatok formázása esetén?
Íme egy példa, ahol láthatjuk az űrlap sémáját, a felhasználó által kitöltött valós adatokat, valamint a duplikált adatokat: _id, fillDate, formVersion, fromName. Úgy tűnik, hogy az adatok létezhetnek SQL-ben táblázat formájában is, mivel mennyiségük és minőségük ugyanaz. De akkor mit kezdjünk a data mezővel?
Először írjunk egy data oszlopot SQL, NVARCHAR(MAX)-ban. Ez lényegében egy sztring, és azt írhatunk bele, amit csak akarunk. Azonban az Application Layer-ben validálni kell, hogy ez JSON. Ha keresésre van szükség, ez némi problémát okoz. Ezért mára a legtöbb adatbázis egy speciális adattípust vagy eszközt biztosít, amely segítségével a JSON-t magában az adatbázisban lehet kezelni.
Vegyük például a PostgreSQL-t, ahol ezt az adattípust jsonb-nek hívják. A PostgreSQL bináris, és ugyanazokat a műveleteket támogatja, mint a JSON: formátum validálása, fieldek közötti keresés, és index létrehozására. De hogyan működik ez a gyakorlatban?
A PostgreSQL-ben három táblánk van: users, form_schemas és form_data. Ez ugyanaz a struktúra, mint a MongoDB-ben. A form_schemas az űrlap definíciója, a form_data pedig a felhasználó által kitöltött adatok.
Ezen kívül a form_schemas-ban a közös mezők saját oszlopokra vannak osztva, és a schema – JSON.
Ugyanaz a helyzet a form_data-val.
Megjegyzés: itt külön mezők és kapcsolatok vannak – érvényes foreign key-ek. Például a tábla definíciójának megnyitása után megtalálhatjuk a data-t (ugyanazt a jsonb-t), és a foreign keys-ben láthatjuk a schema_id-t ( jelzi, hogy a jsonb melyik sémához tartozik) és a user_id-t (jelzi a felhasználót, aki kitöltötte a táblát). Mindez biztosítja az adatok konzisztenciáját.
A program az adatmanipulációhoz is biztosít eszközöket. Először is érdemes megérteni, hogy mit lehet kinyerni a JSON-ból. A “->” operátorral bármilyen oszlopban található értéket lekérdezhetünk. Az egyszerű és a kettős nyíl között az a különbség, hogy a kettős nyíl terminális műveletként működik. Vagyis a select-et szövegként írjuk, ez egy sztring.
Használhatunk rövidebb szintaxist is. Például a personalInformation firstName eléréséhez, és az adatok lekérdezéséhez. Ráadásul ez nagyon gyors, mégha 100 ezer kérés van:
Ezt nem csak select, hanem where esetén is használhatjuk. Például, ki kell keresni a 10 évnél nagyobb munkatapasztalattal rendelkező felhasználókat. Mivel szöveges adatokat kapunk, ezeket át kell alakítani számokká, de végül minden olyan felhasználót megkapunk, akik több mint 10 éves tapasztalattal rendelkeznek.
Az alábbi képen látható “@>” operátor képes meghatározni a “contains” definíciót JSON-ban. Például van egy workExperience és az databaseSkills field-ünk, amely konkrét adatbázisok ismeretét tartalmazza. Tehát egy olyan lekérdezést hajthatunk végre, amely megmutatja az összes olyan felhasználót, aki ismeri a PostgreSQL-t. Ez nagyon hasznos lehet, amikor egy tömbben keressük az információt.
Fontos megemlíteni a JSONPath-t. Ez egy lekérdezési nyelv a JSON számára, amely hasonlít az XMlPath-re XML-ben. Van egy sztringünk, ahol megadjuk a gyökérobjektumot- “$”. Ezután lekérdezzük a mezőket JSON-ban. Tegyük fel, hogy ki akarjuk választani a firstName, lastName, databaseSkills értékeket azoknál a felhasználóknál, akiknek kettőnél több készségük van a databaseSkills értékek között. Ha nem akarjuk nyilakkal kijelölni az értékeket, akkor hivatkozhatunk a szintaxisra, és beírhatjuk a “@@” operátort a where használatához. És mindezt azért, mert a jsonb_path_query a mi függvényünk:
Ahogyan észrevehetjük, a sztringben van egy size függvény, amely a JSONPath-be van beépítve, és képes megadni a tömb elemeinek számát. Ez a “>” operátorral egészül ki, amely nagyobb, mint valamilyen szám. A mi esetünkben nagyobb mint 2. Ennek köszönhetően a PostgreSQL lefutatta ezt a sztringet, és kivonta a szükséges adatokat.
És végül – indexek. A JSON belüli adatokra használható, hogy gyorsabban tudjuk feldolgozni az ilyen fájlhoz intézett lekérdezéseket. Például indexet adunk meg workExperience-hez, majd meg akarjuk találni az összes felhasználót, akiknek ez az érték 10 év. Ez nagyon egyszerű, azonban az indexet egy karakterláncra és nem egy számra hozzuk létre. Ellenkező esetben az index egyáltalán nem kerül feldolgozásra. Ez jól látható az execution plan-ben. Az alábbi ábrán látható, hogy a felhasználók keresésére a PostgreSQL Seq Scan-t használtuk. Ez nem több mint 129 milliszekundumot vett igénybe:
Ha hozzáadjuk az indexet és futtatjuk, akkor Bitmap Heap Scan-ként fut le, és az adatokat 9 milliszekundum alatt kapjuk meg. Ebben az esetben tízszeres a sebességnövekedés.
Sajnos ez nem működik a JSONPath szintaxissal. Ez a kérés 78 milliszekundum alatt jut át a Seq Scan-en. Észrevehető némi javulás, azonban nem jelentős.
Ahhoz, hogy az index egy ilyen expression-el működjön, egy másik típust kell létrehozni a JSON mezőben. PostgreSQL-ben ez a gin-index. A megadott expression-ök Full-Text Search-ként működnek. Amikor futtatjuk, akkor elindul a Bitmap Heap Scan, és a ráfordított idő mindössze 21 milliszekundum lesz:
Bár mindezt a PostgreSQL példáján mutattuk be, az összes nagyobb adatbázis-gyártó támogatja ezt a megközelítést, hisz látják a fejlesztők igényét a JSON adaptálására. Az alábbi kód például az MsSQL szintaxist mutatja. Vannak OPENJSON és JSON_QUERY funkciók is, amelyek a JSONPath-t használják. Hasonló megoldások vannak a MicrosoftSQL-ben és az Oracle-ben is.
JSON előnyei és hátrányai a relációs adatbázisokban
A NoSQL megközelítésnek sok előnye van, többek között:
- SQL – nem kell eltérnünk azoktól az elvektől és módszerektől, amelyekkel dolgozni szoktunk, és amelyeket szeretünk.
- “Kompatibilitás” a relációs modellel. Tudunk joinolni és select-álni, használhatunk where és belső JSON mezőket a Query-ben.
- További eszközök az adatmodellezéshez. Az új megközelítés lehetővé teszi, hogy az SQL és a NoSQL legelőnyösebb tulajdonságait használjuk.
Ami a JSON használatának hátrányait illeti, a következőkkel kell számolnunk:
- Bizonyos esetekben csökken a hatékonyság. A jsonb-szerű adattípus használatához és az index hozzáadásához több erőforrásra van szükség. Egy olyan NoSQL adatbázis, mint például a MongoDB, sokkal produktívabb lesz ilyen feladatok esetén.
- Nem teszi lehetővé a skálázást. A NoSQL fő előnye abban rejlik, hogy nem egy új modellel dolgozunk, hanem maradunk a relációs paradigmában, azonban egy kicsit több feladatot tudunk benne elvégezni.
Összegezve tehát kijelenthetjük, hogy a JSON használata a relációs modellben több esetben is indokolt lesz:
- Meglévő SQL projektek esetén. Ha a relációs adatbázissal rendelkező alkalmazásodnak van olyan funkciója, amely dinamikus adatokat igényel, a NoSQL módszerek biztosan jól jönnek.
- Dinamikus konstruktorok esetén, amelyek figyelembe veszik a skálázást. Ha nem tudjuk, hogy a felhasználó milyen objektumokat fog működtetni, és azok között milyen kapcsolatok lesznek, érdemes kipróbálni a fent leírt elveket.
- Optimalizálás, ha a relációs modell problémás. Néha a fejlesztők annyira belefeledkeznek az adatok SQL-ben történő optimalizálásba, hogy a táblák és kapcsolatok száma túl nagy lesz. Ez egyes írási vagy olvasási műveletek leállásához vezethet. Ebben az esetben – a sok egyesítés miatt – az indexek sem fognak segíteni, ezért érdemes a NoSQL megközelítéshez fordulni. Milliónyi kapcsolat helyett egy JSON-formát kapunk egy oszlopban. Ugyanakkor, szűrés esetén az új adatbázis-funkcióknak és szintaxisoknak köszönhetően a kapcsolatok megmaradnak.
Rengeteg más feladat is megoldható ezzel a megközelítéssel, elsősorban Metadata Formok (összetett dinamikus űrlapok) kialakítása során. Másodszor, BPMN Workflows esetén, ahol minden lépés JSON-ban van kódolva, és CMS esetén, amelyben biztosíthatod az alkalmazás felhasználói számára a dinamikus tartalom létrehozását. Mindenesetre ne feledd, ami másnak bevált, az nem biztos, hogy működni fog a te esetedben. Fontold meg a leírt eseteket, kísérletezz, és minden sikerülni fog!
A cikket angolul is elolvashatod itt.