Mérnöki kézikönyv · 03 / 09 · Adatmérnökség

Adat, adatmodellezés és adatbázisok

Fejlesztőknek, CTO-knak és IT vezetőknek szóló kézikönyv arról, hogyan lesz egy adatbázis helyes, gyors és biztonságosan üzemeltethető. Codd relációs modelljétől a Postgres 18 gyakorlatáig minden módszernél megmutatja a lépéssort, a tipikus hibát és azt, hol csúszik el leggyakrabban az AI által generált séma.

Kalmár Dániel · kb. 76 perc olvasás · A sorozat része, Mérnöki kézikönyvek

Tartalom

Minden módszernél leírjuk a lépéssort, az alkalmazás idejét, a tipikus hibát és a mai, AI asszisztált fejlesztésben szerzett tapasztalatot. Példáink Postgres dialektusban készültek, mert a Supabase és más menedzselt Postgres szolgáltatások ma a kis és közepes projektek gyakori alapjai. Ahol egy funkció verzióhoz kötött, a verzió ott áll mellette. A Postgres 18 2025 szeptemberében jelent meg, sok éles projekt viszont még 15 vagy 17 verzión fut, ezt a select version(); lekérdezés mutatja meg.

A kódszerkezetről és a modulhatárokról a szoftverarchitektúra, a személyes adatok kezeléséről az adatbiztonság és adatvédelem kézikönyv szól részletesen.

Gyorstérkép

FeladatReleváns szakasz
Új séma, új tábla, új entitás2. Normalizálás, 6. Gyakorlati Postgres, 12. Idő, pénz, azonosítók, 13.2 checklist
Lassú lekérdezés5. Indexek és EXPLAIN
Versenyhelyzet, duplikált sor, elveszett frissítés3.7 Tranzakciók és izoláció
Séma módosítása élő rendszeren7. Migrációk
Webhook, fizetés, email küldés, háttérfeladat8. Idempotencia és outbox
Riport, dashboard, analitika9. Adatmérnökség, Kimball
Mennyi adatvesztés fér bele11. Mentés és helyreállítás
AI keresés, embedding, RAG3.11 és 6.4 pgvector

1. Klasszikus gyökerek

Minden mai vita (dokumentum vagy relációs, ORM vagy SQL, NoSQL vagy Postgres) egyszer már lezajlott 1960 és 1980 között. Aki ismeri az eredeti érveket, gyorsabban helyre teszi a mai divatokat.

1.1 Hierarchikus modell (IBM IMS, 1966)

Az IMS az Apollo program alkatrészjegyzékéhez készült, az IBM, a North American Rockwell és a Caterpillar közös munkájaként, 1966 és 1968 között. Fa szerkezetben tárolja az adatot. Minden rekordnak egy szülője van, a lekérdezés a fán lefelé navigál.

Ami jól működött. Az egy a többhöz kapcsolat, ahol a gyerek mindig ugyanazzal a szülővel együtt kell (rendelés és tételei). Gyors, mert a gyerekek fizikailag a szülő mellett vannak.

Ami eltört. A több a többhöz kapcsolat. Ha egy alkatrész több szerelvényben is szerepel, vagy duplikálni kell, vagy mutatót tartani rá, és a következetességért a programozó felel. A lekérdezés útvonala a programba van égetve, így a fizikai szerkezet változása minden programot érint.

Mai megfelelője. A dokumentum adatbázis (MongoDB, Firestore) és a JSONB oszlop beágyazott tömbbel. Ugyanaz az erény és ugyanaz a csapda. Ha egy JSON dokumentumba ágyazott elemre később más entitásból is hivatkozni kell, a csapat visszaérkezett az 1968-as problémához.

1.2 Hálós modell (CODASYL, Bachman, 1969)

Charles Bachman az Integrated Data Store (IDS, 1963 körül, General Electric) tervezője. A CODASYL Data Base Task Group 1969 és 1971 közötti jelentései szabványosították a hálós modellt. Itt egy rekordnak több szülője is lehet (set típusok), így a több a többhöz kapcsolat ábrázolható.

Bachman 1973-ban Turing díjat kapott, díjelőadásának címe The Programmer as Navigator. Az előadás képe szerint a programozó egy n dimenziós adattérben navigál, mutatókat követ, és tudja, hol áll éppen (current of run unit). Erős modell, de minden lekérdezés egy kézzel írt bejárási algoritmus.

Tanulság. A navigációs modellben a hozzáférési útvonal a kód része. Új kérdéshez új program kell. Az ORM lusta betöltése (user.orders[0].items) ugyanez a navigáció, és ugyanazért vezet N+1 lekérdezéshez.

1.3 Codd relációs modellje (1970)

Edgar F. Codd 1970-ben a Communications of the ACM hasábjain publikálta A Relational Model of Data for Large Shared Data Banks című cikkét. Négy gondolata máig meghatározó.

Codd 1985-ben a Computerworldben közölte híres szabályait, hogy megvédje a relációs címkét a felhígulástól. A 0. szabállyal együtt 13 tételről van szó.

#SzabályMit jelent ma a gyakorlatban
0AlapszabályA rendszer kizárólag relációs eszközökkel kezeli az adatot
1Információ szabályMinden információ táblában, értékként jelenik meg
2Garantált hozzáférésTábla neve, kulcs és oszlopnév elég bármely érték eléréséhez
3NULL szisztematikus kezeléseA hiányzó érték egységes jelölése, típustól függetlenül
4Online katalógusA séma maga is lekérdezhető tábla (information_schema, pg_catalog)
5Átfogó adatnyelvEgy nyelv a definícióra, lekérdezésre, jogosultságra, tranzakcióra
6Nézet frissítéseAz elméletileg frissíthető nézet frissíthető is
7Halmazszintű INSERT, UPDATE, DELETENem csak soronként
8Fizikai adatfüggetlenségIndex, tárolás változik, a program nem
9Logikai adatfüggetlenségTábla szétbontása nézettel elrejthető
10Integritás függetlenségeA kényszerek a katalógusban élnek, nem az alkalmazásban
11Elosztás függetlenségeA program nem tudja, hol van fizikailag az adat
12Nem megkerülhetőségAlacsony szintű interfész nem kerülheti meg az integritási szabályokat

A 10. és a 12. szabály ma a legfontosabb. Ha az integritás csak az alkalmazás kódjában él (például egy Next.js API route ellenőrzi, hogy az email egyedi), akkor egy második kliens, egy SQL konzol, egy edge function vagy egy AI ügynök megkerüli. Supabase alatt a kliens közvetlenül a PostgREST API-n át ír az adatbázisba, ezért ott a kényszer és az RLS az egyetlen megbízható hely.

1.4 Chen ER modellje (1976)

Peter Chen 1976-os cikke (The Entity-Relationship Model. Toward a Unified View of Data, ACM TODS) az entitás, a kapcsolat és az attribútum fogalmi szintű leírását adja, adatbázistól függetlenül.

ER modellezés lépésenként.

  1. A szakterület leírása mondatokban. Például egy résztvevő több képzésre jelentkezik, minden képzésnek egy vagy több trénere van, a jelentkezésnek állapota és díja van.
  2. A főnevek kigyűjtése. Ezek az entitásjelöltek (résztvevő, képzés, tréner, jelentkezés).
  3. Az igék kigyűjtése. Ezek a kapcsolatjelöltek (jelentkezik, tart).
  4. A kardinalitás meghatározása mindkét irányból. Hány képzésre jelentkezhet egy résztvevő, és hány résztvevő lehet egy képzésen? Ha mindkét irányban több, kapcsolótábla kell.
  5. A saját attribútummal bíró kapcsolat (állapot, díj, dátum) önálló, úgynevezett asszociatív entitás. Ilyen a jelentkezés is.
  6. Az azonosító kiválasztása. Van-e természetes kulcs (adószám, ISBN), és stabil-e? Ha nem, helyettesítő kulcs (surrogate) kell.
  7. A kötelezőség jelölése. Lehet-e jelentkezés képzés nélkül? Nem, tehát not null idegen kulcs kell.
  8. Csak ezután jön a táblákra fordítás.

Tipikus hiba. A főnévgyűjtés a felület képernyőiből indul (Dashboard, Beállítások), nem a szakterületből. Gyakori az is, hogy a kapcsolat attribútumai az egyik entitásba kerülnek (a résztvevő táblába kerül a fizetett oszlop, holott az a jelentkezéshez tartozik).

1.5 System R, SQL és Ingres

Az IBM San Jose kutatóközpontjában 1974 és 1979 között épült a System R, Codd elméletének első komoly megvalósítása. Donald Chamberlin és Raymond Boyce 1974-ben publikálta a SEQUEL nyelvet (később védjegy okokból SQL). A System R hozta a költség alapú lekérdezés optimalizálót (Selinger és társai, 1979), ami ma is minden relációs motor alapja. Párhuzamosan Berkeleyn Michael Stonebraker és Eugene Wong építette az Ingrest, QUEL nyelvvel. Stonebraker későbbi projektje a Postgres (post Ingres, 1986), ebből lett a PostgreSQL. Az első kereskedelmi SQL termék az Oracle volt 1979-ben.

Gyakorlati tanulság. A Postgres optimalizáló statisztikákból becsül. Ha a statisztika elavult (nagy betöltés után nem futott ANALYZE), a terv rossz lesz. Lekérdezéseinket deklaratívan írjuk, és az EXPLAIN mutatja meg, mit választott a motor.

1.6 A tranzakció fogalma és az ACID

Jim Gray 1981-es VLDB cikke (The Transaction Concept. Virtues and Limitations) a tranzakciót szerződésként írja le, ami vagy teljesen végbemegy, vagy semmi hatása nincs. Theo Härder és Andreas Reuter 1983-ban vezette be az ACID mozaikszót (Principles of Transaction Oriented Database Recovery, ACM Computing Surveys). Gray 1998-ban Turing díjat kapott.

BetűJelentésKi garantálja Postgresben
A, atomicityHiba esetén minden visszagörgetődikWAL, BEGIN és COMMIT
C, consistencyAz invariánsok tranzakció után is igazakRészben a motor (kényszerek), részben az alkalmazás
I, isolationPárhuzamos tranzakciók nem zavarják egymástIzolációs szint, alapból READ COMMITTED
D, durabilityA commitolt adat nem veszik elWAL fsync, replika, mentés

Kleppmann a DDIA-ban megjegyzi, hogy a C valójában az alkalmazás tulajdonsága, a betű főként a mozaikszó kedvéért került be. Az I alapbeállításon a gyakorlatban szinte soha nem jelent teljes sorosíthatóságot. Ezért tévedés azt hinni, hogy egy tranzakcióba csomagolt kód automatikusan versenyhelyzet mentes (lásd 3.7).

1.7 Date és Darwen

Chris Date (An Introduction to Database Systems, 1975 óta nyolc kiadás) és Hugh Darwen a relációs modell legkövetkezetesebb őrzői. The Third Manifesto (1995, később könyv formában) a relációs modell tiszta formáját követeli, és több ponton kritizálja az SQL-t.

Ami ma is hasznos a kritikájukból.

2. Normalizálás mint eljárás

A normalizálás célja, hogy minden tény egyetlen helyen legyen tárolva. Ha egy tény két helyen van, előbb vagy utóbb eltér, és akkor nem tudni, melyik igaz. Ezek a módosítási anomáliák (update, insert, delete anomaly).

2.1 Fogalmak

Funkcionális függőség (FD). X → Y, ha X értéke egyértelműen meghatározza Y értékét. Ha két sorban X azonos, Y is azonos. Ilyen például az adoszam → cegnev.

Szuperkulcs. Olyan attribútumhalmaz, amelytől minden más attribútum függ. Kulcs (candidate key) a minimális szuperkulcs, elsődleges kulcs a kiválasztott kulcs.

Prím attribútum. Valamelyik kulcs része.

Teljes függés. Y a kulcs egészétől függ, nem csak egy részétől.

Tranzitív függés. K → X és X → Y, ahol X nem kulcs. Y így közvetve függ a kulcstól.

Többértékű függőség (MVD). X →→ Y, ha X egy értékéhez Y értékeinek egy halmaza tartozik, ami független a többi attribútumtól.

FD-k felderítése a gyakorlatban. A mintaadatból nem érdemes következtetni, mert az véletlenül is mutathat függést. Az üzleti szabályra kell rákérdezni. Lehet-e egy adószámhoz két cégnév? Ha a cég átnevezi magát, a régi jelentkezéseken is változzon a név? Ez utóbbi kérdés gyakran kideríti, hogy valójában pillanatkép kell (lásd 2.3).

2.2 Futó példa. Képzési jelentkezések egy táblában

A kiinduló tábla úgy néz ki, ahogy egy táblázatkezelőből vagy egy gyorsan generált prototípusból érkezik.

jelentkezesresztvevo_emailresztvevo_nevceg_adoszamceg_nevkepzes_kodkepzes_cimkepzes_datumtrenerekdij_huf
1[email protected]Kiss Anna12345678Alfa KftAI101AI alapok2026-11-05Kalmár D., Nagy P.90000
2[email protected]Tóth Béla87654321Béta ZrtAI101AI alapok2026-11-05Kalmár D., Nagy P.90000
3[email protected]Kiss Anna12345678Alfa KftAI201Ügynökök2026-12-03Kalmár D.120000

1NF. Atomi értékek, nincs ismétlődő csoport

Szabály. Minden cellában egyetlen érték van a tartományból, nincs lista, nincs ismétlődő oszlopcsoport (trener1, trener2), és a táblának van kulcsa.

Lépés. A trenerek oszlop vesszővel elválasztott lista, ez sérti az 1NF-et (Karwin ezt Jaywalking néven tárgyalja). Külön sorokra vagy külön táblába bontjuk. Átmenetileg így néz ki az 1NF reláció.

jelentkezes(jelentkezes_id, resztvevo_email, resztvevo_nev, ceg_adoszam, ceg_nev,
            kepzes_kod, kepzes_cim, kepzes_datum, dij_huf)
kepzes_trener(kepzes_kod, trener_nev)

Tipikus hiba. Postgres text[] vagy JSONB tömb a trénereknek. Technikailag nem szöveges lista, de a probléma ugyanaz. Idegen kulcs nem tehető rá, és a tréner átnevezése minden sort érint.

2NF. Nincs részleges függés összetett kulcstól

Szabály. 1NF, és minden nem prím attribútum a kulcs egészétől függ. Csak összetett kulcsnál releváns.

A példában a jelentkezés természetes kulcsa (resztvevo_email, kepzes_kod). Ekkor a függőségek így alakulnak.

A név csak a kulcs egyik felétől függ, ez részleges függés. Lépés. Minden részleges függés külön relációba kerül, amelynek a determináns lesz a kulcsa.

resztvevo(resztvevo_email, resztvevo_nev, ceg_adoszam, ceg_nev)
kepzes(kepzes_kod, kepzes_cim, kepzes_datum)
jelentkezes(resztvevo_email, kepzes_kod, dij_huf)

3NF. Nincs tranzitív függés

Szabály. 2NF, és egyetlen nem prím attribútum sem függ tranzitívan a kulcstól. Bill Kent közismert emlékeztetője szerint minden nem kulcs attribútumnak a kulcsról, a teljes kulcsról és kizárólag a kulcsról kell szólnia.

A resztvevo táblában resztvevo_email → ceg_adoszam → ceg_nev. Ez a név a cégről szól, nem a résztvevőről. Lépés. Külön relációba emeljük.

ceg(ceg_adoszam, ceg_nev)
resztvevo(resztvevo_email, resztvevo_nev, ceg_adoszam)

BCNF. Minden determináns kulcs

Szabály. Minden nem triviális X → Y függőségnél X szuperkulcs. Erősebb a 3NF-nél, a különbség ott jön elő, ahol több, egymást átfedő összetett kulcs van.

Példa. A teremfoglalásnál az üzleti szabály szerint egy tréner mindig ugyanabban a teremben tart, és adott teremben egy idősávban egy képzés van. Ennek relációja foglalas(kepzes_kod, idosav, trener, terem), a függések (kepzes_kod, idosav) → trener, terem és trener → terem. Itt a trener nem szuperkulcs, mégis determinál. Ez 3NF-ben megengedett (ha a terem prím), de BCNF-et sért. Lépés. A trener_terem(trener, terem) külön táblába kerül, a foglalásból pedig kikerül a terem.

Mikor érdemes 3NF-nél megállni. A BCNF bontás néha elveszít egy függőséget, amit utána csak triggerrel vagy kényszerrel lehet ellenőrizni (dependency preservation). Ha ez fáj, a 3NF plusz egy célzott unique kényszer gyakran jobb kompromisszum.

4NF. Nincs független többértékű függőség egy táblában

Szabály. BCNF, és minden nem triviális MVD bal oldala szuperkulcs.

Példa. kepzes_tulajdonsag(kepzes_kod, trener, nyelv), ahol egy képzésnek több trénere és több nyelve van, és ezek egymástól függetlenek. Ha az AI101 képzésnek két trénere és két nyelve van, négy sort kell tárolni, és egy új nyelv felvétele két új sort jelent. Lépés. Két tábla, kepzes_trener(kepzes_kod, trener) és kepzes_nyelv(kepzes_kod, nyelv).

Felismerés. Ha egy kapcsolótáblában három idegen kulcs van, érdemes megvizsgálni, hogy a három együtt alkot-e tényt (ez a tréner ezt a képzést ezen a nyelven tartja), vagy két független tény van összegyúrva.

5NF (projekció és join normálforma)

Szabály. Minden join függőség a kulcsokból következik. Ha egy tábla három kisebb projekcióra bontható és azokból veszteség nélkül visszaállítható, és ez nem a kulcsok miatt van így, bontani kell.

Példa. Ciklikus üzleti szabály. Ha egy tréner tud egy témát, egy ügyfél kéri azt a témát, és a tréner dolgozik az ügyfélnek, akkor a tréner tartja azt a témát annak az ügyfélnek. Ekkor (trener, tema, ugyfel) helyett három kétoszlopos tábla a helyes modell. A gyakorlatban ritka, a jele az, hogy a három oszlopos táblába beszúráskor mindig további sorokat is be kell szúrni a konzisztencia miatt.

A végeredmény Postgresben

create table ceg (
  adoszam    text primary key check (adoszam ~ '^\d{8}$'),
  nev        text not null
);

create table resztvevo (
  id         bigint generated always as identity primary key,
  email      citext not null unique,          -- create extension citext;
  nev        text not null,
  ceg_adoszam text references ceg(adoszam)
);

create table trener (
  id   bigint generated always as identity primary key,
  nev  text not null
);

create table kepzes (
  id      bigint generated always as identity primary key,
  kod     text not null unique,
  cim     text not null,
  kezdes  timestamptz not null,
  alapdij numeric(12,2) not null check (alapdij >= 0)
);

create table kepzes_trener (
  kepzes_id bigint not null references kepzes(id) on delete cascade,
  trener_id bigint not null references trener(id),
  primary key (kepzes_id, trener_id)
);
create index on kepzes_trener (trener_id);  -- az FK másik irányához

create table jelentkezes (
  id           bigint generated always as identity primary key,
  resztvevo_id bigint not null references resztvevo(id),
  kepzes_id    bigint not null references kepzes(id),
  dij          numeric(12,2) not null check (dij >= 0), -- pillanatkép, lásd 2.3
  allapot      text not null default 'fuggoben'
               check (allapot in ('fuggoben','fizetve','lemondva')),
  letrehozva   timestamptz not null default now(),
  unique (resztvevo_id, kepzes_id)
);
create index on jelentkezes (kepzes_id);

Postgres nem indexeli automatikusan az idegen kulcs oszlopot. A hivatkozott oldalon (primary key) van index, a hivatkozó oldalon nincs. Ennek hiánya lassú joint és lassú delete műveletet okoz a szülő táblán.

2.3 Tudatos denormalizálás

A denormalizálás döntés, nem lustaság. Csak akkor indokolt, ha meg tudjuk nevezni, melyik lekérdezés lassú, mennyire, és ki tartja szinkronban a másolatot.

Lépések.

  1. A normalizált változat mérése. EXPLAIN (ANALYZE, BUFFERS) a valódi lekérdezésen, valósághű adatmennyiséggel.
  2. Kísérlet index és lekérdezés átírással. Az esetek többségében ez elég.
  3. A legolcsóbb szinkronizálási mechanizmus kiválasztása, az alábbi táblázat sorrendjében.
  4. Dokumentálás a séma kommentjében, hogy az oszlop származtatott, és mi a forrása.
SorrendMechanizmusMikor jó
1Generált oszlopUgyanazon a soron belüli származtatás
2Nézet vagy materializált nézetrefresh materialized view concurrently, ehhez unique index kell
3TriggerUgyanabban a tranzakcióban frissít
4Aszinkron frissítés (CDC, háttérfeladat)Ha a késés elfogadható
comment on column kepzes.jelentkezok_szama is
  'Denormalizált számláló. Forrás: count(jelentkezes). Karbantartja: trg_jelentkezes_szamlalo.';

Pillanatkép és denormalizálás két külön dolog. A jelentkezes.dij nem a kepzes.alapdij másolata, hanem a jelentkezés pillanatában érvényes ár, ami üzleti tény. Ugyanígy üzleti tény a számlán a vevő neve és címe a kiállítás pillanatában. Ha ezt hivatkozással oldjuk meg, a cég átnevezése visszamenőleg átírja a régi számlákat, ami jogilag is hiba. A döntő kérdés, hogy a forrás változásának visszamenőleg is látszania kell-e.

Tipikus hiba. Számláló oszlop (likes_count) trigger nélkül, az alkalmazásból count + 1 írással. Párhuzamos kérésnél elveszett frissítést okoz (lásd 3.7). Ha mégis kell, atomikus update ... set n = n + 1 a megoldás.

3. Designing Data-Intensive Applications (Kleppmann és Riccomini, 2. kiadás, O'Reilly, 2026)

Az első kiadás 2017-ben jelent meg Kleppmann egyedüli szerzőként, a második 2026 márciusában, Chris Riccomini társszerzővel. Kleppmann szerint a szerkezet és a szemlélet megmaradt, a részletek frissültek. A legnagyobb változás a felhő szerepe. Az első fejezet címe Trade-offs in Data Systems Architecture lett, és a felhő kontra saját üzemeltetés, az elosztott kontra egygépes rendszer, valamint az operatív kontra analitikai rendszer kompromisszumaival indít. A particionálásról szóló rész a sharding elnevezést használja. Új vagy erősen bővült téma az object storage alapú architektúra, az oszlopos formátumok és a lakehouse, a vektoros keresés és az embedding indexek, valamint az adatvédelem és a felelős adatkezelés. A könyv fejezetszerkezete a 2026 októberi állapot szerint a kiadó oldalán ellenőrizendő, ha egy hivatkozás fejezetszámra épül.

3.1 Nem funkcionális követelmények

Architektúra döntés előtt a terhelést és a teljesítményt számokban rögzítjük.

AI asszisztált fejlesztésben. Ha a megrendelő vagy a kódgeneráló modell skálázhatóságot emleget, előbb a számokra kérdezünk rá. Száz felhasználónál egy Postgres példány a teljes rendszer, nincs szükség üzenetsorra, cache rétegre, külön keresőre.

3.2 Adatmodellek. Relációs, dokumentum, gráf

SzempontRelációsDokumentumGráf
ErősségeTöbb a többhöz, join, kényszerek, ad hoc lekérdezésÖnálló, fa alakú aggregátum egyben olvasvaSok, változatos típusú kapcsolat, tetszőleges mélységű bejárás
SémaÍrásnál ellenőrzött (schema on write)Olvasáskor értelmezett (schema on read)Általában rugalmas
LokalitásTöbb táblából áll összeEgy dokumentum egy helyenCsúcsonként
GyengeségeObjektum és tábla közötti eltérés (impedance mismatch)Hivatkozás más dokumentumra, join, részleges frissítésAggregáló analitika
PostgresbenAlapJSONBRekurzív CTE, Apache AGE kiterjesztés

Döntési eljárás.

  1. Felrajzoljuk az entitásokat és a kapcsolatokat.
  2. Ha minden kapcsolat egy a többhöz, és mindig a gyökérrel együtt olvassuk (űrlap beküldés teljes tartalma, beállítások), a dokumentum modell jó.
  3. Ha bármely beágyazott elemre más entitás hivatkozik, önállóan keresünk rá, vagy több a többhöz kapcsolatba kerül, relációs modell kell.
  4. Ha a kérdések tipikusan azt firtatják, hány lépésben jutunk el A-tól B-ig, gráf modell kell. Postgresben előbb rekurzív CTE-vel érdemes próbálkozni.
-- Szervezeti hierarchia bejárása rekurzív CTE-vel
with recursive alarendeltek as (
  select id, nev, vezeto_id, 1 as szint from munkatars where id = 42
  union all
  select m.id, m.nev, m.vezeto_id, a.szint + 1
  from munkatars m join alarendeltek a on m.vezeto_id = a.id
  where a.szint < 20                         -- körbefutás elleni védelem
)
select * from alarendeltek;

A DDIA a lekérdezőnyelveket is tárgyalja (SQL, Cypher, SPARQL, Datalog, és a 2. kiadásban a DataFrame szemlélet). Tanulságuk, hogy a deklaratív nyelv előnye az optimalizálhatóság és a párhuzamosíthatóság.

3.3 Tárolómotorok

Napló alapú tárolás. A legegyszerűbb adatbázis egy csak hozzáfűzhető fájl (append only log). Írni olcsó, olvasáshoz index kell. Minden index lassítja az írást, ez az alapvető kompromisszum.

LSM tree (log structured merge tree). Az írás memóriabeli rendezett szerkezetbe (memtable) kerül, ami betelve rendezett, megváltoztathatatlan fájlként (SSTable) íródik lemezre. A háttérben a fájlok összefésülődnek (compaction). Olvasáskor több szinten kell keresni, ezt Bloom filter gyorsítja. Írásra optimalizált, ilyen a RocksDB, a Cassandra és a ScyllaDB.

B-tree. Fix méretű lapok (Postgresben 8 kB) fa szerkezetben, jellemző rá a helyben frissítés. Az összeomlás elleni védelmet a write ahead log (WAL) adja. Olvasásra és tartományos keresésre optimalizált, kiszámítható. Ezt használja a Postgres, a MySQL InnoDB és az SQL Server.

SzempontB-treeLSM tree
Írási erősítésLap újraírás, WALCompaction miatt többszöri újraírás
Írási átviteli sebességKözepesMagas
Olvasási késésKiszámíthatóVáltozó, a compaction állapotától függ
HelyfoglalásTöredezettségTömörebb

Postgres sajátosság, MVCC és VACUUM. Postgresben az update új sorverziót (tuple) ír, a régi halott sor marad, amíg a VACUUM fel nem szabadítja. Ezért a gyakran frissített széles sor táblapuffadást (bloat) okoz. Ha egy tábla egyik oszlopa nagyon gyakran változik (számláló, last_seen_at), a többi pedig ritkán, érdemes külön táblába tenni. A HOT update (heap only tuple) akkor működik, ha a módosított oszlop nincs indexelve, és a lapon van hely (fillfactor).

Oszlopos tárolás. Analitikánál egy lekérdezés kevés oszlopot és rengeteg sort olvas. Oszloponkénti tárolásnál csak a szükséges oszlopok jönnek be, és az azonos típusú értékek jól tömöríthetők (run length, dictionary, bitmap). Ilyen formátum és motor a Parquet, az ORC, a ClickHouse, a DuckDB, a BigQuery és a Snowflake. A Postgres sor alapú. Analitikához ott a materializált nézet, a pg_duckdb vagy a Citus columnar, nagyobb méretnél külön analitikai réteg.

Memóriabeli adatbázisok. Gyorsak, de nem azért, mert nem olvasnak lemezről (az operációs rendszer cache-e ezt amúgy is megoldja), hanem mert nem kell lemezre alkalmas formátumba kódolni az adatszerkezetet.

3.4 Kódolás és sémaevolúció

Élő rendszerben a régi és az új kód egyszerre fut (rolling deploy, mobilkliens, amit a végfelhasználó nem frissít). Ezért kétféle kompatibilitás kell.

Formátumok.

FormátumSémaEvolúciós szabály
JSON, XML, CSVNincs vagy külön (JSON Schema)Számok pontossága bizonytalan (2^53 felett), bináris adat base64
Protocol Buffers.proto, mezőszámmalÚj mező új számmal, opcionális. Régi szám nem használható újra. Kötelező mező nem vezethető be
AvroÍró és olvasó séma összevetéseCsak alapértelmezett értékkel rendelkező mező adható hozzá vagy törölhető

Az előre kompatibilitás csapdája. Egy régi kódverzió beolvas egy rekordot, az ismeretlen mezőt eldobja a modellje, majd visszaírja. Az új mező elveszett. JSONB-nél és API-knál ugyanez történik, ha a kód teljes objektumot ír vissza PATCH helyett.

Adatáramlás módjai. Adatbázison keresztül (a régi adat évekig ott marad, ezért a migrációnak ezt kezelnie kell), szolgáltatáshívással (REST, RPC, a hívó és a hívott külön verziózódik), és üzenettel (üzenetsor, esemény, a fogyasztó később is olvashatja). A DDIA 2. kiadása a tartós végrehajtást (durable execution, például Temporal) is ebben a körben tárgyalja.

Sémaevolúciós szabályok.

  1. Oszlopot vagy mezőt mindig opcionálisként vagy alapértelmezéssel adunk hozzá.
  2. Átnevezés nincs, csak új mező, másolás és a régi kivezetése (lásd 7.3, expand és contract).
  3. Az enum bővíthető, szűkíteni csak akkor lehet, ha egyetlen író és olvasó sem használja a kivett értéket.
  4. Típusváltás új mezőként történik.
  5. Az API válaszban az ismeretlen mezőt a kliens figyelmen kívül hagyja, nem dob hibát.

3.5 Replikáció

Cél. Rendelkezésre állás, olvasási skálázás, földrajzi közelség.

Single leader (leader és follower). Minden írás a leaderre megy, a followerek a replikációs naplót játsszák vissza. Így működik a Postgres streaming replikáció és a Supabase read replica.

Replikációs késés anomáliái és ellenszerük.

AnomáliaMi történikEllenszer
Read your writes hiányaA felhasználó ment, frissít, és nem látja a saját módosítását, mert a replikáról olvasottSaját adat olvasása a leaderről egy ideig írás után, vagy LSN alapú várakozás
Monotonic reads hiányaKét egymás utáni olvasás két különböző késésű replikáról, az adat visszaugrik az időbenEgy felhasználó mindig ugyanarról a replikáról olvas (hash a user id-ra)
Consistent prefix hiányaA válasz előbb látszik, mint a kérdés, mert különböző partícióból jönnekOk és okozat ugyanabba a partícióba, vagy explicit ok okozati függőség követés

Supabase gyakorlat. Read replica használatakor az írás utáni képernyőt (mentés, majd átirányítás a részletes oldalra) a primary felé irányítjuk. A legtöbb kis projektnek nincs szüksége replikára.

Multi leader. Több adatközpont, offline kliens, kollaboratív szerkesztés. Az írási ütközés elkerülhetetlen. Feloldási lehetőség az utolsó író nyer elv (last write wins, ami adatot veszít), az egyedi azonosító szerinti sorrend, az alkalmazásszintű összefésülés vagy a CRDT (Kleppmann kutatási területe, Automerge). Offline first alkalmazásnál ez a terület releváns, és a megoldás legtöbbször egy kész szinkronizáló réteg (PowerSync, ElectricSQL), nem saját fejlesztés.

Leaderless (Dynamo stílus). Az író több replikára ír, az olvasó több replikáról olvas. Kvórum. Van n replika, w írási és r olvasási visszaigazolás, és ha w + r > n, az olvasás jellemzően lát legalább egy friss másolatot. Ez nem ad linearizálhatóságot (sloppy quorum, egyidejű írások, részleges hibák). Javítás olvasás közben (read repair) és háttér szinkron (anti entropy) tartja karban. Ilyen a Cassandra, a ScyllaDB és a DynamoDB.

3.6 Particionálás (sharding)

Cél. Az adat és a terhelés egyenletes elosztása több gépre.

MódszerElőnyHátrány
Kulcstartomány szerintTartományos lekérdezés hatékonyHotspot, ha a kulcs időbélyeg
Kulcs hash szerintEgyenletes eloszlásTartományos lekérdezés minden partíciót érint
Bérlő szerint (tenant id)Egy ügyfél adata együtt, izolációEgy nagy ügyfél kinőheti a partíciót

Másodlagos indexek. Lokális (dokumentum szerint particionált, olvasáskor minden partíciót kérdezni kell, scatter gather) vagy globális (kifejezés szerint particionált, az írás több partíciót érint).

Újraelosztás (rebalancing). A hash mod N szerinti elosztás kerülendő, mert N változásakor szinte minden kulcs vándorol. Jobb a fix számú, sok kis partíció, ami csomópontok között mozog, vagy a konzisztens hash.

Gyakorlat. Egy Postgres példány nagyon messzire elvisz. A sharding előtt sorrendben ezek jönnek szóba, vagyis indexek, lekérdezés optimalizálás, nagyobb gép, read replica, deklaratív partícionálás egy gépen belül (lásd 6.7) és archiválás. Elosztott Postgreshez a Citus (Microsoft) a bevett megoldás, tenant id szerinti elosztással.

3.7 Tranzakciók és izolációs szintek

A tranzakciókezelés okozza a legtöbb valós hibát az AI generált kódban, mert a hiba csak párhuzamos terhelés alatt jelentkezik, fejlesztés közben soha.

Az anomáliák

AnomáliaLeírásPélda
Dirty readMás tranzakció még nem commitolt adatát látjukLátszik a fizetve állapot, ami utána rollbackel
Dirty writeMás tranzakció nem commitolt adatát írjuk felülKét vásárlás, az autó az egyiké, a számla a másiké
Read skew (nonrepeatable read)Egy tranzakción belül két olvasás közt más commitol, inkonzisztens kép áll előKét számla egyenlege összeadva 100 forinttal kevesebb, mert közben utalás volt
Lost updateKét olvas, módosít, ír ciklus, az egyik felülírja a másikatKét kérés olvassa az n = 5 értéket, mindkettő 6-ot ír
Write skewKét tranzakció ugyanazt a feltételt olvassa, különböző sorokat ír, és együtt megsértik az invariánstLegalább egy orvos ügyeletben legyen. Ketten egyszerre jelentenek beteget, mindketten látják, hogy a másik bent van
PhantomEgy tranzakció írása megváltoztatja egy másik keresési feltételének eredményétTeremfoglalás ütközés ellenőrzése, közben valaki beszúr egy átfedő foglalást

Postgres viselkedése szintenként

SzintPostgres megvalósításMitől védMitől nem
READ UNCOMMITTEDUgyanaz, mint READ COMMITTEDDirty read, dirty writeLásd a következő sort
READ COMMITTED (alapértelmezés)Utasításonként új pillanatképDirty read, dirty writeRead skew, lost update (alkalmazásbeli olvas, módosít, ír), write skew, phantom
REPEATABLE READSnapshot isolation, tranzakciónként egy pillanatképAz előzők, read skew, phantom olvasás, lost update (konkurens módosításnál hibát dob)Write skew
SERIALIZABLESerializable Snapshot Isolation (SSI, Cahill, Röhm és Fekete 2008, Postgres 9.1 óta)Minden sorosíthatósági sértésSemmi, de újrapróbálás kell

REPEATABLE READ és SERIALIZABLE szinten a Postgres ütközéskor 40001 serialization_failure hibát dob. Ez nem hiba, hanem a protokoll része. Az alkalmazásnak az egész tranzakciót újra kell futtatnia. Újrapróbálási logika nélkül ezeket a szinteket nem érdemes bekapcsolni.

Az anomáliák kivédése

Lost update, 1. Atomikus utasítás. A legjobb, ha az adatbázis végzi a számítást.

-- ROSSZ (alkalmazásban): n = select ...; update ... set keszlet = n - 1
-- JÓ:
update termek set keszlet = keszlet - 1
where id = $1 and keszlet > 0
returning keszlet;
-- 0 érintett sor = elfogyott, nem negatív készlet

Lost update, 2. Explicit zárolás. Akkor kell, ha a logika nem fér egy utasításba.

begin;
select * from kepzes where id = $1 for update;  -- sorzár a commitig
-- alkalmazás logika, ellenőrzés
update kepzes set ... where id = $1;
commit;

Lost update, 3. Optimista zárolás verziószámmal. Hosszú, felhasználói interakción átnyúló szerkesztésnél (űrlap megnyitva tíz percig).

update dokumentum
set tartalom = $2, verzio = verzio + 1
where id = $1 and verzio = $3;   -- $3 a betöltéskori verzió
-- 0 sor: valaki közben módosította, az ütközést jelezni kell

Write skew és phantom. A feltétel olyan sorokra vonatkozik, amiket nem írunk, vagy még nem léteznek, ezért a for update nem segít. Megoldások erősség szerinti sorrendben.

  1. Kényszer, ha kifejezhető. Ez a legerősebb, mert a motor garantálja.
-- Teremfoglalás átfedés tiltása (btree_gist kiterjesztés kell)
create extension if not exists btree_gist;
create table foglalas (
  id      bigint generated always as identity primary key,
  terem_id bigint not null references terem(id),
  idoszak tstzrange not null,
  exclude using gist (terem_id with =, idoszak with &&)
);
-- Postgres 18-tól alternatíva:
-- primary key (terem_id, idoszak without overlaps)
  1. Zárolás egy közös szülő sorra (materializing conflicts). Az orvos példánál a műszak sorát zároljuk for update-tel, így a két tranzakció sorba áll.
  2. Tanácsadó zár (advisory lock) egy logikai erőforrásra.
select pg_advisory_xact_lock(hashtextextended('ugyelet:' || $1::text, 0));
  1. SERIALIZABLE szint újrapróbálással. Általános megoldás, ha sok ilyen invariáns van.

Egyedi beszúrás versenyhelyzete. Előzetes select ellenőrzés helyett unique kényszer és insert ... on conflict a helyes megoldás.

insert into hirlevel_feliratkozo (email) values ($1)
on conflict (email) do nothing
returning id;

Supabase specifikum. A supabase-js kliens hívásai külön HTTP kérések, mindegyik saját tranzakcióban fut. Két egymás utáni .update() hívás között nincs tranzakció. Ha több utasításnak atominak kell lennie, Postgres függvény kell, amit az rpc() hív meg, vagy edge functionből közvetlen Postgres kapcsolattal dolgozunk.

create or replace function jelentkezes_letrehoz(p_kepzes bigint)
returns bigint
language plpgsql
security invoker               -- az RLS a hívó jogaival fut
set search_path = ''
as $$
declare v_id bigint;
begin
  perform 1 from public.kepzes where id = p_kepzes for update;
  if (select count(*) from public.jelentkezes where kepzes_id = p_kepzes
      and allapot <> 'lemondva') >= (select letszam from public.kepzes where id = p_kepzes) then
    raise exception 'betelt' using errcode = 'P0001';
  end if;
  insert into public.jelentkezes (resztvevo_id, kepzes_id, dij)
  select r.id, k.id, k.alapdij
  from public.resztvevo r, public.kepzes k
  where r.auth_user_id = auth.uid() and k.id = p_kepzes
  returning id into v_id;
  return v_id;
end $$;

Tipikus AI hiba. A létszám ellenőrzés és a beszúrás két külön kliens hívásban, zár nélkül fut. Tesztben működik, élesben a meghirdetés pillanatában túlfoglalás lesz. Az AI generált kód további jellemző buktatóit az AI ügynökök és vibecoding kézikönyv tárgyalja.

3.8 Az elosztott rendszerek bajai

Hálózat. Egy kérésnél nem lehet megkülönböztetni, hogy a kérés veszett el, a távoli fél lassú vagy halott, vagy a válasz veszett el. Időkorlát után nem tudni, végrehajtódott-e a művelet. Ebből következik az idempotencia szükségessége (lásd 8.).

Órák. A falióra (wall clock, now(), Date.now()) ugorhat előre és hátra (NTP korrekció), és gépek között eltér. Ezzel szemben a monoton óra (performance.now(), clock_gettime(CLOCK_MONOTONIC)) időtartam mérésére jó, gépek között nem összevethető. Emiatt az események sorrendjét különböző gépek időbélyege alapján nem lehet megbízhatóan megállapítani. Az időbélyeg alapú last write wins csendben adatot veszít. Sorrendhez egyetlen helyen kiosztott sorszám (szekvencia, Postgres LSN) vagy logikai óra (Lamport, vektor óra, hibrid logikai óra) kell.

Folyamat szünet. GC szünet, VM felfüggesztés, lemez várakozás bármikor megállíthatja a folyamatot másodpercekre. Egy zár (lease) birtokosa felébred, és azt hiszi, még övé a zár. Ellenszer a fencing token. A zár kiadója minden kiadáskor növekvő számot ad, a tároló elutasítja a régebbi tokennel érkező írást.

Bizánci hibák. Akkor számítanak, ha a résztvevők hazudhatnak (blokklánc, nyílt peer to peer rendszer). Saját adatközponton belül általában nem modellezzük, de bemeneti validáció kell, mert a kliens bármit küldhet.

Rendszermodellek. A rendszer lehet szinkron, részlegesen szinkron (ez a valóság) vagy aszinkron. Hibamodellként az összeomlás és leállás, az összeomlás és helyreállás, illetve a bizánci hiba jön szóba. Az algoritmusok helyességét biztonsági (safety, rossz dolog sosem történik) és élőségi (liveness, jó dolog végül megtörténik) tulajdonságokkal írjuk le.

3.9 Linearizálhatóság és konszenzus

Linearizálhatóság. A rendszer úgy viselkedik, mintha egyetlen másolat lenne, és minden művelet egy pillanatban, valós idő szerinti sorrendben hatna. Szükség van rá vezetőválasztáshoz, egyediségi kényszerhez, zárhoz, és ahol két csatorna között versenyhelyzet van (feltöltött kép és a feldolgozó üzenet).

A CAP tétel helyes olvasata. Hálózati partíció esetén a rendszer vagy linearizálható, vagy elérhető. Partíció nélkül nem kell választani, de a linearizálhatóságnak mindig van késleltetési ára. Kleppmann a CAP tételt túl szűknek tartja a gyakorlati döntésekhez.

Ok okozati konzisztencia (causal consistency). Gyengébb, de partíció alatt is elérhető, és sok alkalmazásnak elég.

Konszenzus. Több csomópont megegyezik egy értékben (vezető, commit, sorrend). Ismert algoritmusai a Raft (etcd, Consul), a Paxos és a Zab (ZooKeeper). Egyenértékű probléma a teljes sorrendű üzenetszórás (total order broadcast), a linearizálható compare and set és az atomi commit. A kétfázisú commit (2PC) atomi commit, de a koordinátor kiesésekor a résztvevők blokkolva várnak, ezért nem hibatűrő konszenzus.

Gyakorlat. Konszenzust nem implementálunk magunk. Olyan rendszert használunk, ami már megoldja (Postgres egy primaryval, etcd, a felhőszolgáltató menedzselt szolgáltatása). Elosztott tranzakció helyett outbox és saga a megoldás (lásd 8.).

3.10 Batch, stream, CDC, event sourcing

Batch. Korlátos bemenet, determinisztikus feldolgozás, a kimenet újra előállítható. Mintája a Unix filozófia (kis eszközök, csővezeték, változatlan bemenet), ez a MapReduce, majd a Spark és a dataflow motorok alapja. A 2. kiadás a mai felhős batch eszközöket és a DataFrame szemléletet is tárgyalja. Kulcsgondolata, hogy ha a bemenet megváltoztathatatlan és a feldolgozás tiszta függvény, a hibás eredmény egyszerűen újraszámolható.

Stream. Korlátlan bemenet, eseményenként. Lehet üzenetközvetítő (RabbitMQ, SQS, amely a fogyasztás után törli az üzenetet) vagy napló alapú (Kafka, Redpanda, Kinesis, ahol az üzenet megmarad, a fogyasztó offsettel halad, és újrajátszható).

Change data capture (CDC). Az adatbázis replikációs naplóját olvassuk (Postgres logikai dekódolás, WAL), és minden változást eseményként továbbítunk. Így a keresőindex, a cache és az adattárház az adatbázisból származtatott, és nincs kettős írásból (dual write) eredő versenyhelyzet. Ismert eszközök a Debezium, a Supabase Realtime (Postgres changes), a Sequin és a felhős adattárház betöltők.

A kettős írás csapdája. Az alkalmazás ír az adatbázisba és a keresőbe is. Ha az egyik sikertelen, eltérnek. Két párhuzamos kérésnél pedig a két rendszerbe eltérő sorrendben érkezhetnek az írások, és tartósan eltérő végállapot marad. Megoldás a CDC vagy az outbox.

Event sourcing. A változásokat megváltoztathatatlan üzleti eseményként tároljuk (JelentkezesLetrehozva, FizetesBeerkezett), az aktuális állapot ebből származtatott nézet. Előnye a teljes audit, az újrajátszhatóság, és hogy új nézet utólag is építhető. Hátránya a bonyolultság, az évekig élő eseménysémák és a nehéz törlés (GDPR), ami kriptográfiai törléssel (crypto shredding) oldható meg.

Mikor event sourcing. Pénzügyi főkönyvnél, ahol a történet maga az adat, és összetett, audit kötelezettséggel járó munkafolyamatnál. Felesleges átlagos CRUD alkalmazásban, ott elég egy audit_log tábla triggerrel és a mezőszintű updated_at.

Stream joinok és idő. Az eseményidő és a feldolgozási idő eltér, a késve érkező eseményeket ablakokkal (tumbling, hopping, sliding, session) és vízjellel (watermark) kezeljük.

A 2. kiadás záró gondolatköre. Az adatrendszerek egységesítése a származtatott adat elve mentén (egy rendszer a forrás, minden más belőle számított), a helyesség végponttól végpontig (end to end argument, az idempotencia azonosítója a klienstől az adatbázisig ér), és az adatkezelés etikai, adatvédelmi felelőssége.

3.11 Felhő, object storage, vektoros keresés

Felhős architektúra. A tárolás és a számítás szétválik. Az adat object storage-ban van (S3, GCS, Supabase Storage, R2), nagyon olcsón és tartósan, a számítás igény szerint skálázódik. Erre épül a lakehouse (lásd 9.4), a szerver nélküli Postgres (Neon, ahol a lapok object storage-ból töltődnek) és a Kafka kompatibilis, object storage alapú streaming (WarpStream). Az object storage írása nem helyben frissítés, hanem teljes objektumcsere, a listázás pedig drága, ezért a táblaformátumok (Iceberg, Delta, Hudi) saját metaadat réteget visznek fölé.

Vektoros keresés. A szöveget, képet embedding modell vektorrá alakítja, a keresés a legközelebbi szomszédok megtalálása (koszinusz, belső szorzat, euklideszi távolság). Nagy adaton a teljes keresés lassú, ezért közelítő indexek (ANN) kellenek.

IndexMűködésMikor
HNSWTöbbrétegű közelségi gráfAlapértelmezés. Jó felidézés, gyors lekérdezés, lassabb építés, több memória
IVFFlatKlaszterezés, a legközelebbi klaszterekben keresGyorsabb építés, a klasztereket az adat birtokában kell tanítani
DiskANN, StreamingDiskANNLemezre optimalizált gráfNagyon nagy adat (pgvectorscale kiterjesztés)

Hibrid keresés. A vektoros és a teljes szöveges (BM25 vagy Postgres FTS) keresés eredményének összefésülése, tipikusan reciprocal rank fusion módszerrel. Konkrét terméknévre, kódra, azonosítóra a lexikális keresés jobb, jelentésre a vektoros. A gyakorlati beállítást a 6.4 pont mutatja.

4. SQL antiminták (Bill Karwin, SQL Antipatterns, 2. kiadás, Pragmatic Bookshelf, 2022)

A táblázat minden antimintához megadja a felismerés jelét és a javítást.

AntimintaFelismerésJavítás
JaywalkingVesszővel elválasztott azonosítók egy oszlopbanKapcsolótábla
Naive TreesCsak parent_id, és mélységfüggetlen lekérdezés kellRekurzív CTE, closure table, ltree kiterjesztés
ID RequiredMinden táblán id, a kapcsolótáblán is, a természetes kulcs nem egyediKapcsolótáblán összetett elsődleges kulcs, vagy unique kényszer a természetes kulcsra
Keyless EntryNincs idegen kulcs, mert lassít vagy rugalmatlanIdegen kulcs mindig, plusz index a hivatkozó oszlopon
Entity Attribute Valueattributum(entitas_id, nev, ertek text)Rendes oszlopok, altípus táblák, vagy JSONB sémavalidációval
Polymorphic Associationscommentable_type és commentable_id, nincs FKKülön kapcsolótábla típusonként, közös szülőtábla, vagy több nullable FK plusz check (num_nonnulls(a,b,c) = 1)
Multicolumn Attributestag1, tag2, tag3Függő tábla
Metadata TribblesTábla vagy oszlop évenként (rendeles_2025), kézzelDeklaratív partícionálás
Rounding Errorsfloat pénzrenumeric
31 Flavorscheck (x in (...)) vagy enum gyakran változó listáraHivatkozott lookup tábla
Phantom FilesFájl csak útvonalként, a fájl és a sor élete függetlenObject storage plusz életciklus kezelés, a törlés tranzakciója és a fájl törlése összehangolva (outbox)
Index ShotgunIndex mindenre vagy semmireMérés (MENTOR módszer, measure, explain, nominate, test, optimize, rebuild)
Fear of the UnknownNULL helyett üres string vagy -1, vagy = NULLis null, is distinct from, coalesce, tudatos not null
Ambiguous Groupsgroup by nem csoportosított, nem aggregált oszloppalMinden oszlop csoportosított vagy aggregált, distinct on
Random Selectionorder by random() limit 1 nagy táblántablesample, vagy véletlen kulcs plusz index
Poor Man's Search Enginelike '%szo%'tsvector és GIN, vagy pg_trgm
Spaghetti QueryEgy óriási lekérdezés mindent kiszámol, Descartes szorzatTöbb lekérdezés, vagy CTE lépésekkel, aggregálás join előtt
Implicit Columnsselect *, insert oszloplista nélkülExplicit oszloplista
Readable PasswordsJelszó visszafejthető formábanSupabase Auth, vagy argon2id, bcrypt
SQL InjectionSzöveg összefűzésParaméteres lekérdezés, format('%I', ...) dinamikus azonosítóhoz
Pseudokey Neat FreakValakit zavarnak a hézagok az id sorozatbanA hézag normális, az id nem sorszám
See No EvilHibakód figyelmen kívül hagyásaMinden kliens hívásnál az error ellenőrzése
Diplomatic ImmunityAz SQL kód nincs verziókezelve, tesztelveMigrációs fájlok gitben, pgTAP vagy integrációs teszt
Standard Operating ProceduresMinden logika tárolt eljárásban, vagy éppen semmiAmi invariáns, az adatbázisban, ami munkafolyamat, az alkalmazásban
Magic BeansAz ORM Active Record osztálya maga a tartománymodellA tartománymodell fölötte, az ORM csak adathozzáférés

A 2. kiadás a példákat modern SQL-re (MySQL 8, ablakfüggvények, CTE) frissítette, és rövidebb mini antimintákkal bővült. Javításai Postgresre is érvényesek, a táblázat javítás oszlopa már Postgres szerint készült. A jelszókezelés és az injekciós támadások részletei a kiberbiztonság kézikönyvben szerepelnek.

5. Indexek (Markus Winand, Use the Index, Luke, és SQL Performance Explained)

Winand ingyenesen elérhető online könyve (use-the-index-luke.com) a fejlesztői indexelés legjobb rövid tananyaga. Fő üzenete, hogy az indexelés a fejlesztő feladata, nem az adminisztrátoré, mert a lekérdezés ismerete kell hozzá.

5.1 Hogyan működik a B-tree index

Ezért egy index keresés lassú lehet, ha sok levelet és sok táblasort kell érinteni. Ilyenkor szűkebb hozzáférési predikátum segít, vagy covering index (include), ami index only scant tesz lehetővé.

5.2 Összetett index oszlopsorrendje

Szabály. Az összetett index balról jobbra használható. Egy (a, b, c) index kiszolgálja az a, az a, b és az a, b, c feltételt, a b vagy c oszlopot önmagában viszont csak korlátozottan (a Postgres 18 skip scan ezen enyhít, ha a oszlopnak kevés különböző értéke van).

A sorrend eldöntése.

  1. Kiírjuk a gyakori lekérdezéseket a where, order by és join feltételekkel.
  2. Egyenlőségi feltételek előre, tartományos feltétel utoljára. Ami a tartomány után jön, az már csak szűrőként (filter) működik, nem hozzáférési predikátumként.
  3. Az egyenlőségi oszlopok közül az kerül előre, amit több lekérdezés önmagában is használ. A szelektivitás itt másodlagos, mert egyenlőségnél a végeredmény ugyanaz.
  4. Ha order by is van, az index sorrendje egyezzen vele, így elmarad a rendezés (pipelined order by), és a limit korán leáll.
  5. Ha a lekérdezés kevés oszlopot ad vissza, include oszlopokkal covering index készül.
-- Lekérdezés: egy bérlő függőben lévő jelentkezései, legújabb elöl
select id, kepzes_id, letrehozva
from jelentkezes
where tenant_id = $1 and allapot = 'fuggoben'
order by letrehozva desc
limit 20;

-- Jó index: egyenlőségek, majd a rendezés oszlopa, covering mezővel
create index concurrently jelentkezes_tenant_allapot_ido_idx
  on jelentkezes (tenant_id, allapot, letrehozva desc) include (kepzes_id);

-- Részleges index, ha szinte mindig csak a függőben lévők kellenek
create index concurrently jelentkezes_fuggo_idx
  on jelentkezes (tenant_id, letrehozva desc) where allapot = 'fuggoben';

Tipikus hibák.

select id, cim, letrehozva from cikk
where (letrehozva, id) < ($utolso_ido, $utolso_id)
order by letrehozva desc, id desc
limit 20;
-- index: (letrehozva desc, id desc)

5.3 EXPLAIN olvasása Postgresben

explain (analyze, buffers, verbose, settings)
select ...;

Az analyze ténylegesen lefuttatja a lekérdezést, írási utasításnál ezért tranzakcióba csomagoljuk és visszagörgetjük.

Eljárás.

  1. Olvasás belülről kifelé, alulról felfelé. A legbeljebb lévő csomópont fut először.
  2. A becsült és a tényleges sorszám összevetése (rows= a becslésben és actual ... rows=). Tízszeres vagy nagyobb eltérés rossz statisztikát jelez. Ilyenkor ANALYZE tabla kell, összefüggő oszlopokra pedig create statistics.
  3. Szorzó a loops érték. A tényleges idő és sorszám egy ciklusra vonatkozik, a teljeshez szorozni kell.
  4. A drága csomópont megkeresése. Ahol a legnagyobb az actual time különbség a gyerekeihez képest.
  5. Buffers. A shared hit a cache-ből jön, a read a lemezről. Sok read hideg cache-t vagy túl nagy olvasást jelez.
  6. Szűrés utáni eldobás. A Rows Removed by Filter nagy értéke azt jelzi, hogy a predikátum nem hozzáférési, hanem szűrő. Ilyenkor jobb index kell.
CsomópontJelentésMikor gyanús
Seq ScanTeljes tábla végigolvasásaNagy táblán szelektív feltétellel
Index ScanIndex plusz táblasorokSok sornál (akkor a bitmap vagy seq jobb)
Index Only ScanCsak index, a Heap Fetches alacsony legyenHa a Heap Fetches magas, VACUUM kell (visibility map)
Bitmap Heap ScanIndex alapján lapok gyűjtése, majd olvasáslossy blokkok, kevés work_mem
Nested LoopKülső soronként belső keresésHa a külső oldal sok sor és a belső nem indexelt
Hash JoinHash tábla a kisebb oldalbólBatches > 1, kevés memória
Merge JoinKét rendezett bemenetHa előtte drága rendezés kell
SortRendezésexternal merge Disk

Supabase eszközök. A dashboard Query Performance nézete (pg_stat_statements alapján), az Index Advisor (index_advisor kiterjesztés), valamint a Security és Performance Advisor ellenőrzései (hiányzó FK index, használatlan index, RLS nélküli tábla).

6. Gyakorlati Postgres (Supabase környezetben)

6.1 Kényszerek mint végrehajtható specifikáció

create table szamla (
  id            uuid primary key default uuidv7(),       -- PG18, előtte lásd 12.3
  tenant_id     uuid not null references tenant(id),
  sorszam       text not null,
  vevo_nev      text not null check (length(trim(vevo_nev)) > 0),
  netto         numeric(14,2) not null check (netto >= 0),
  afa_kulcs     numeric(5,2) not null check (afa_kulcs in (0, 5, 18, 27)),
  brutto        numeric(14,2) generated always as (round(netto * (1 + afa_kulcs/100), 2)) stored,
  penznem       char(3) not null default 'HUF' check (penznem ~ '^[A-Z]{3}$'),
  kiallitva     date not null,
  esedekes      date not null,
  allapot       text not null default 'piszkozat',
  constraint esedekes_kiallitas_utan check (esedekes >= kiallitva),
  constraint sorszam_egyedi unique (tenant_id, sorszam)
);

Kényszertípusok és céljuk. A not null minden kötelező mezőre kell. Tartományra és mezők közötti szabályra a check való. Természetes kulcsra unique kerül, akár részleges indexként (create unique index ... where deleted_at is null). Hivatkozásra a foreign key szolgál, az on delete viselkedést tudatosan választva (restrict alapból, cascade csak valódi rész és egész viszonynál, set null ritkán). Az exclude az átfedés tiltására szolgál.

Új kényszer nagy táblán. Előbb alter table ... add constraint ... not valid;, majd külön lépésben validate constraint. A validálás gyengébb zárat tart, a forgalom nem áll meg.

6.2 Generált oszlopok

Postgres 12 óta stored, Postgres 18 óta virtual változat is van, és a 18-ban a virtuális az alapértelmezés, ha nem adjuk meg. A tárolt oszlop indexelhető és lemezt foglal, a virtuális olvasáskor számolódik.

Mikor igen. Ugyanazon sor más oszlopaiból determinisztikusan számolható érték (bruttó, keresési tsvector, normalizált email). Kerülendő más tábla adatából vagy now() alapján (nem engedélyezett, mert nem determinisztikus).

6.3 JSONB mikor igen, mikor nem

Igen.

Nem.

Ha mégis JSONB, akkor így.

-- Sémaellenőrzés check kényszerrel (egyszerű esetben)
alter table urlap_bekuldes add constraint payload_objektum
  check (jsonb_typeof(payload) = 'object' and payload ? 'email');

-- Supabase-en a pg_jsonschema kiterjesztéssel teljes JSON Schema:
-- check (extensions.jsonb_matches_schema('{...}'::json, payload))

-- Index tartalmazás keresésre
create index on urlap_bekuldes using gin (payload jsonb_path_ops);
-- where payload @> '{"forras":"linkedin"}'

-- Gyakran használt mező kiemelése oszlopba
alter table urlap_bekuldes
  add column email text generated always as (payload->>'email') stored;
create index on urlap_bekuldes (email);

6.4 pgvector

create extension if not exists vector with schema extensions;

create table dokumentum_resz (
  id          bigint generated always as identity primary key,
  tenant_id   uuid not null,
  dokumentum_id bigint not null references dokumentum(id) on delete cascade,
  tartalom    text not null,
  embedding   extensions.vector(1536) not null,       -- a modell dimenziója
  modell      text not null,                          -- melyik embedding modell
  fts         tsvector generated always as (to_tsvector('simple', tartalom)) stored
);

create index on dokumentum_resz using hnsw (embedding vector_cosine_ops)
  with (m = 16, ef_construction = 64);
create index on dokumentum_resz using gin (fts);
create index on dokumentum_resz (tenant_id);

Szabályok.

  1. A modell nevét és verzióját is tároljuk. Modellváltáskor minden embeddinget újra kell számolni, mert különböző modellek vektorai nem összevethetők.
  2. Egyezzen a távolság operátor az index ops osztályával. A <=> koszinusz (vector_cosine_ops), a <#> negatív belső szorzat, a <-> euklideszi. Ha nem egyezik, az index nem használódik.
  3. Csapda a szűrt keresésnél. A HNSW először a közelítő top k elemet adja, utána szűr. Erős szűrésnél (egy bérlő a sok közül) kevés vagy nulla találat jön. A pgvector 0.8 óta a set hnsw.iterative_scan = relaxed_order; segít. Alternatíva a bérlőnkénti részleges index vagy partíció.
  4. Felidézés kontra sebesség. A set hnsw.ef_search = 100; (alapértelmezés 40) jobb felidézést ad, lassabban.
  5. Méret. 1536 dimenzió float4 típussal soronként 6 kB. A halfvec fele akkora, gyakran elhanyagolható minőségvesztéssel.
  6. Hibrid keresés reciprocal rank fusionnel, az alábbi minta szerint.
with vektoros as (
  select id, row_number() over (order by embedding <=> $1) as rang
  from dokumentum_resz where tenant_id = $2
  order by embedding <=> $1 limit 40
), lexikalis as (
  select id, row_number() over (order by ts_rank_cd(fts, q) desc) as rang
  from dokumentum_resz, websearch_to_tsquery('simple', $3) q
  where tenant_id = $2 and fts @@ q
  order by ts_rank_cd(fts, q) desc limit 40
)
select coalesce(v.id, l.id) as id,
       coalesce(1.0/(60 + v.rang), 0) + coalesce(1.0/(60 + l.rang), 0) as pont
from vektoros v full join lexikalis l using (id)
order by pont desc limit 10;

6.5 Teljes szöveges keresés

create text search configuration magyar_ekezetnelkul (copy = hungarian);
alter text search configuration magyar_ekezetnelkul
  alter mapping for hword, hword_part, word with unaccent, hungarian_stem;

6.6 Row Level Security (RLS)

Supabase-en a kliens a publishable (anon) kulccsal közvetlenül a PostgREST API-n át éri el a public sémát. Ezért minden public táblán kötelező az RLS, és policy nélkül a tábla zárva van, ami jó alapértelmezés. A hozzáférés szervezeti oldaláról (jogosultsági koncepció, naplózás) az adatbiztonság és adatvédelem kézikönyv szól.

Eljárás új táblánál.

  1. alter table x enable row level security;
  2. Műveletenként külön policy (select, insert, update, delete), szerepkörhöz kötve (to authenticated).
  3. Az update policynél using (melyik sort módosíthatja) és with check (milyen állapotba).
  4. Bérlők elválasztása a tagság táblán keresztül.
  5. Tesztelés több felhasználóval, nem a service role kulccsal (az megkerüli az RLS-t).
alter table jelentkezes enable row level security;

create policy jelentkezes_sajat_olvasas on jelentkezes
  for select to authenticated
  using (resztvevo_id in (select id from resztvevo where auth_user_id = (select auth.uid())));

create policy tenant_tag_olvas on szamla
  for select to authenticated
  using (tenant_id in (select tenant_id from tagsag where user_id = (select auth.uid())));

create policy tenant_admin_modosit on szamla
  for update to authenticated
  using (exists (select 1 from tagsag t where t.tenant_id = szamla.tenant_id
                 and t.user_id = (select auth.uid()) and t.szerep = 'admin'))
  with check (exists (select 1 from tagsag t where t.tenant_id = szamla.tenant_id
                 and t.user_id = (select auth.uid()) and t.szerep = 'admin'));

Teljesítmény. Az auth.uid() hívást (select auth.uid()) formában írjuk, így a tervező egyszer értékeli ki (initPlan), nem soronként. A policyben használt oszlopra (tenant_id, user_id) index kell.

Tipikus hibák.

6.7 Deklaratív partícionálás

Mikor. Nagy, időben növő tábla (napló, esemény, mérés) tízmillió sor felett, ahol a lekérdezések időtartományra szűrnek, és a régi adatot egészében töröljük vagy archiváljuk. Kis táblán felesleges bonyolultság.

create table esemeny (
  id          bigint generated always as identity,
  tenant_id   uuid not null,
  tipus       text not null,
  tortent     timestamptz not null,
  adat        jsonb,
  primary key (id, tortent)          -- a partíciókulcsnak benne kell lennie
) partition by range (tortent);

create table esemeny_2026_10 partition of esemeny
  for values from ('2026-10-01') to ('2026-11-01');
-- Automatizálás: pg_partman kiterjesztés, vagy pg_cron feladat
-- Régi adat: alter table esemeny detach partition esemeny_2025_10 concurrently;

Csapdák. Az egyedi kényszernek tartalmaznia kell a partíciókulcsot. A partíciókulcs nélküli lekérdezés minden partíciót érint. Túl sok partíció (ezres nagyságrend) lassítja a tervezést.

6.8 Connection pooling

Postgresben minden kapcsolat külön folyamat, nagyjából néhány MB memóriával. A szerver nélküli környezet (edge function, Vercel függvény) sok rövid életű kapcsolatot nyit, ami kimeríti a max_connections értéket.

MódSupabaseMireKorlát
Közvetlen kapcsolatdb.<ref>.supabase.co:5432Migráció, hosszú életű szerverIPv6, kevés kapcsolat
Session poolerSupavisor, 5432Hosszú életű kliens IPv4-enKapcsolatonként egy szerver kapcsolat
Transaction poolerSupavisor, 6543Szerver nélküli függvényekNincs session állapot, prepared statement, set, listen, session advisory lock

Transaction módban a kliensnél ki kell kapcsolni a prepared statementeket (Prisma ?pgbouncer=true, postgres.js prepare: false). Session szintű set helyett set local kell a tranzakción belül. Supabase-en dedikált PgBouncer is elérhető a fizetős csomagokban (a 2026 októberi állapot szerint, a csomagleírásban ellenőrizendő). A supabase-js kliens HTTP-n keresztül a PostgREST-en megy, az nem nyit Postgres kapcsolatot klienskénként.

6.9 Egyéb hasznos Postgres eszközök

7. Migrációk

7.1 Alapszabályok

  1. Minden sémaváltozás migrációs fájlban, verziókezelve. Supabase-en supabase migration new nev, a fájl a supabase/migrations/ alá kerül. A dashboardon kattintott változás a következő környezetben elveszik.
  2. Egy migráció egy logikai változás. Kicsi, visszakövethető.
  3. A lefutott migrációt nem szerkesztjük. Ami már lefutott valahol, azt új migráció követi, nem módosítás.
  4. Először lokálisan vagy branch adatbázison fut (supabase db reset, Supabase branching), csak utána élesen.
  5. Visszaút terv. Nem feltétlenül down migráció, de ismert, hogyan állunk vissza (előre irányú javító migráció, PITR).

7.2 Zárak, amik leállítják a forgalmat

MűveletZárBiztonságos változat
add column alapérték nélkül vagy konstans alapértékkelRövid ACCESS EXCLUSIVE (Postgres 11 óta nem írja újra a táblát)Rendben, lock_timeout beállítással
add column ... default volatile()Tábla újraírásOszlop alapérték nélkül, backfill, majd default
create indexÍrást blokkolcreate index concurrently (tranzakción kívül)
add foreign keyMindkét táblán zár, teljes ellenőrzésnot valid, majd validate constraint
set not nullTeljes tábla olvasás zár alattElőbb check (x is not null) not valid, validate, majd set not null (Postgres 12 óta felhasználja a kényszert)
alter column typeÁltalában tábla újraírásÚj oszlop, expand és contract
rename columnRövid zár, de a régi kódot azonnal eltöriExpand és contract

Minden migráció elején.

set lock_timeout = '3s';       -- ne álljon sorba végtelenül egy hosszú lekérdezés mögött
set statement_timeout = '60s';

A legveszélyesebb nem maga a zár, hanem a sorban állás. Ha az ACCESS EXCLUSIVE zár egy hosszan futó lekérdezésre vár, mögötte minden más kérés is vár, és az alkalmazás percekre megáll.

7.3 Expand és contract (parallel change)

Danilo Sato a martinfowler.com oldalon írta le parallel change néven. Minden nem visszafelé kompatibilis változást fázisokra bontunk, és minden fázis után működik a rendszer.

Példa. A resztvevo.nev oszlopot vezeteknev és keresztnev oszlopokra bontjuk.

LépésAdatbázisAlkalmazás
1. ExpandÚj oszlopok nullable formábanVáltozatlan
2. Kettős írásSzükség esetén szinkronizáló triggerAz új kód a régi és az új oszlopot is írja
3. BackfillRégi sorok kitöltése kötegekbenVáltozatlan
4. Olvasás átállításaVáltozatlanAz új oszlopból olvas
5. Kényszernot null biztonságos módonVáltozatlan
6. Régi írás leállításaVáltozatlanCsak az új oszlopot írja
7. ContractRégi oszlop törlése (külön deployban, napokkal később)Változatlan
-- 1. expand
alter table resztvevo add column vezeteknev text, add column keresztnev text;

-- 2. trigger alapú szinkron, amíg régi kód is fut
create or replace function resztvevo_nev_szinkron() returns trigger
language plpgsql as $$
begin
  if new.vezeteknev is null and new.nev is not null then
    new.vezeteknev := split_part(new.nev, ' ', 1);
    new.keresztnev := nullif(substr(new.nev, length(split_part(new.nev, ' ', 1)) + 2), '');
  end if;
  return new;
end $$;
create trigger trg_resztvevo_nev before insert or update on resztvevo
  for each row execute function resztvevo_nev_szinkron();

7.4 Backfill

Szabályok.

  1. Kötegekben, nem egyetlen update utasítással a teljes táblán. Egy óriási update hosszú zárat, rengeteg WAL-t, replikációs késést és táblapuffadást okoz.
  2. Kulcs szerint haladva, nem offset alapon.
  3. Idempotensen. Csak a még nem kitöltött sorokat érinti, így megszakítás után újraindítható.
  4. Kötegenként commit. Egy do blokk egyetlen tranzakció, ezért Postgres 11 óta eljárásban (procedure) commit utasítással, vagy külső szkriptből.
  5. Szünet a kötegek között, a replikációs késés és a terhelés figyelésével.
create or replace procedure backfill_nev(batch int default 5000)
language plpgsql as $$
declare n int;
begin
  loop
    update resztvevo r set
      vezeteknev = split_part(r.nev, ' ', 1),
      keresztnev = nullif(substr(r.nev, length(split_part(r.nev, ' ', 1)) + 2), '')
    where r.id in (
      select id from resztvevo
      where vezeteknev is null and nev is not null
      order by id limit batch
      for update skip locked
    );
    get diagnostics n = row_count;
    commit;
    exit when n = 0;
    perform pg_sleep(0.2);
  end loop;
end $$;

call backfill_nev();

7.5 Zero downtime migráció lépései összefoglalva

  1. Leírjuk, melyik kódverzió milyen sémával működik. A deploy alatt a régi és az új kód egyszerre fut.
  2. A változást expand, migrate, contract lépésekre bontjuk.
  3. Minden DDL lock_timeout beállítással fut, az indexek concurrently, a kényszerek not valid plusz validate formában.
  4. A backfill kötegekben, idempotensen fut.
  5. Az olvasást feature flaggel váltjuk át.
  6. Figyeljük a hibaarányt, a lekérdezési késést és a replikációs késést.
  7. A contract lépés külön deployban jön, miután biztos, hogy egyetlen futó kód sem használja a régi szerkezetet (a régi mobilkliens, az ütemezett feladat, a riport és az adattárház betöltő sem).

8. Idempotencia, outbox, inbox

8.1 Az exactly once mítosz

Hálózaton nem garantálható, hogy egy üzenet egyetlenegyszer kézbesül, mert a küldő időkorlát után nem tudja, megérkezett-e (lásd 3.8). A választás legfeljebb egyszer (at most once, elveszhet) vagy legalább egyszer (at least once, duplikálódhat). Amit a rendszerek exactly once néven hirdetnek (Kafka tranzakciók, Flink), az a gyakorlatban effectively once, vagyis at least once kézbesítés plusz idempotens vagy deduplikált feldolgozás, egy zárt rendszeren belül. Amint a feldolgozás külső mellékhatással jár (email, fizetés, harmadik fél API), az idempotencia a mi felelősségünk.

8.2 Idempotencia kulccsal

Eljárás.

  1. A kliens műveletenként egyedi kulcsot generál (UUID), és újrapróbálásnál ugyanazt küldi (Idempotency-Key fejléc, a Stripe mintája).
  2. A szerver egyedi kényszerrel rögzíti a kulcsot, a művelettel azonos tranzakcióban.
  3. Ismételt kérésnél a tárolt választ adja vissza, nem hajtja végre újra a műveletet.
  4. A kulcs mellé a kérés tartalmának hash értékét is tárolja, és az eltérő tartalmú, azonos kulcsú kérést elutasítja.
create table idempotencia (
  kulcs        text primary key,
  user_id      uuid not null,
  keres_hash   text not null,
  valasz       jsonb,
  allapot      text not null default 'folyamatban',
  letrehozva   timestamptz not null default now()
);

-- A kérés elején, ugyanabban a tranzakcióban, mint az üzleti írás
insert into idempotencia (kulcs, user_id, keres_hash)
values ($1, $2, $3)
on conflict (kulcs) do nothing
returning kulcs;
-- Ha nincs visszatérő sor: már volt ilyen kérés, a tárolt választ kell visszaadni

Természetes idempotencia. Sokszor nem kell külön kulcs. Elég egy insert ... on conflict do nothing természetes kulcsra, egy update ... set allapot = 'fizetve' where id = $1 and allapot = 'fuggoben', vagy abszolút érték beállítása növelés helyett.

8.3 Webhook fogadás (inbox minta)

A szolgáltatók (Stripe, Számlázz.hu, Barion, GitHub) legalább egyszer kézbesítenek, és nem garantálnak sorrendet.

create table webhook_inbox (
  szolgaltato   text not null,
  esemeny_id    text not null,          -- a szolgáltató eseményazonosítója
  tipus         text not null,
  payload       jsonb not null,
  erkezett      timestamptz not null default now(),
  feldolgozva   timestamptz,
  hiba          text,
  probalkozas   int not null default 0,
  primary key (szolgaltato, esemeny_id)
);

Lépések.

  1. Az aláírás ellenőrzése a nyers törzsön.
  2. insert ... on conflict do nothing az inboxba, és azonnal 200-as válasz. A lassú feldolgozás ne tartsa a kérést, mert a szolgáltató időkorlát után újraküld.
  3. Egy háttérfeldolgozó (pg_cron, pgmq, ütemezett edge function) a fel nem dolgozott sorokat for update skip locked zárolással veszi fel.
  4. Sorrendfüggetlen logika. A szolgáltató objektumának aktuális állapotát lekérjük vagy verziót hasonlítunk, és nem feltételezzük, hogy a paid esemény a created után jön.

8.4 Transactional outbox

Probléma. Az adatbázisba írunk és üzenetet küldünk (email, esemény, másik szolgáltatás hívása). Commit utáni sikertelen küldésnél az üzenet elveszik. Ha előbb küldünk és a commit hiúsul meg, az üzenet olyan eseményről szól, ami meg sem történt.

Megoldás. Az üzenetet ugyanabban a tranzakcióban egy outbox táblába írjuk, egy külön folyamat küldi ki és jelöli késznek.

create table outbox (
  id           bigint generated always as identity primary key,
  aggregatum   text not null,
  aggregatum_id text not null,
  tipus        text not null,
  payload      jsonb not null,
  letrehozva   timestamptz not null default now(),
  elkuldve     timestamptz,
  probalkozas  int not null default 0,
  kovetkezo_probalkozas timestamptz not null default now()
);
create index outbox_fuggo_idx on outbox (kovetkezo_probalkozas) where elkuldve is null;

-- Üzleti tranzakció
begin;
update jelentkezes set allapot = 'fizetve' where id = $1;
insert into outbox (aggregatum, aggregatum_id, tipus, payload)
values ('jelentkezes', $1::text, 'JelentkezesFizetve', jsonb_build_object('id', $1));
commit;

-- Kiküldő (relay), párhuzamosan több példány is futhat
with tetel as (
  select id from outbox
  where elkuldve is null and kovetkezo_probalkozas <= now()
  order by id limit 50
  for update skip locked
)
select o.* from outbox o join tetel using (id);
-- küldés után: update outbox set elkuldve = now() where id = ...
-- hiba után: probalkozas + 1, kovetkezo_probalkozas = now() + exponenciális várakozás

Az outbox legalább egyszer kézbesít. A relay a küldés után, de a késznek jelölés előtt is összeomolhat. Ezért a fogadó oldalnak idempotensnek kell lennie (inbox vagy idempotencia kulcs az outbox id alapján). Az email szolgáltatóknak sokszor van saját idempotencia kulcsa, érdemes azt használni.

Alternatíva a CDC. Az outbox tábla változásait Debezium vagy logikai replikáció olvassa, így nem kell lekérdező relay.

8.5 Saga

Több szolgáltatáson átívelő művelet elosztott tranzakció nélkül. Lépések sorozata, mindegyikhez kompenzáló lépéssel (foglalás, majd sikertelen fizetésnél a foglalás felszabadítása). Megvalósítható koreográfiával (eseményekre reagáló szolgáltatások) vagy orkesztrációval (egy koordinátor, például Temporal, Inngest vagy Trigger.dev munkafolyamat). Kis rendszerben a saga helyett jobb, ha a műveletek egy adatbázisban, egy tranzakcióban maradnak.

9. Adatmérnökség

9.1 Fundamentals of Data Engineering (Joe Reis és Matt Housley, O'Reilly, 2022)

Az adatmérnökség életciklusa öt szakaszból áll, és mindegyiket hat áthatoló terület (undercurrent) kíséri.

SzakaszKérdések
Generálás (source systems)Ki a forrás gazdája? Milyen gyakran változik a sémája? CDC vagy API?
TárolásMelyik réteg, milyen hozzáférési minta, milyen költség?
Betöltés (ingestion)Batch vagy stream? Push vagy pull? Mi történik a késő vagy duplikált adattal?
TranszformációHol történik (ELT a raktárban, dbt), ki felel a logikáért?
Kiszolgálás (serving)Analitika, ML, reverse ETL vissza az operatív rendszerekbe

Áthatoló területek. Biztonság, adatkezelés (data management), DataOps, adatarchitektúra, orkesztráció, szoftvermérnökség.

Gyakorlati tanácsai. Egyszerű kezdés menedzselt eszközökkel. Visszafordítható döntések (kétirányú ajtó). A teljes költség számolása, a csapat idejével együtt. Az önéletrajz vezérelt fejlesztés (resume driven development) kerülése.

Kis projektnél a Supabase Postgres maga az operatív és az analitikai réteg, egy külön analytics sémával, nézetekkel vagy materializált nézetekkel. Külön adattárház akkor kell, ha az analitikai lekérdezések terhelik az éles adatbázist, vagy több forrást kell egyesíteni.

9.2 Kimball dimenzionális modellezés (The Data Warehouse Toolkit, Kimball és Ross, 3. kiadás, 2013)

A négy lépéses tervezés.

  1. Az üzleti folyamat kiválasztása (jelentkezés, fizetés, oldalletöltés), nem a riporté vagy az osztályé.
  2. A szemcsézettség (grain) deklarálása. Mit jelent a tényrekord egy sora? Például egy jelentkezés egy résztvevő egy képzésre. Ez a legfontosabb döntés, és utána nem keverhető.
  3. A dimenziók azonosítása. Ezek a ki, mi, hol, mikor, miért kérdések (résztvevő, cég, képzés, dátum, csatorna).
  4. A tények azonosítása. Számszerű, általában additív mérőszámok a szemcsézettségen (díj, kedvezmény, darab).

Ténytábla típusok. Tranzakciós (eseményenként egy sor), periodikus pillanatkép (napi készlet), halmozódó pillanatkép (egy folyamat mérföldkövei egy sorban, például a jelentkezés, a fizetés és a részvétel dátuma), tény nélküli ténytábla (részvétel, ahol maga az esemény a tény).

Csillag séma. Középen a ténytábla, körülötte denormalizált dimenziótáblák. Analitikára szándékosan nem 3NF, mert az elemző egyszerű joinokat akar, és a dimenzió ritkán változik. Dátum dimenzió mindig van (ünnepnap, hét, negyedév, üzleti év).

Lassan változó dimenziók (SCD).

TípusMűködésMikor
0Sosem változikSzületési dátum, eredeti forrás
1Felülírás, nincs történetElírás javítása
2Új sor új helyettesítő kulccsal, érvényességi dátumokkalHa a riportnak a tény idején érvényes értéket kell mutatnia (a cég régiója a vásárláskor)
3Előző érték külön oszlopbanEgy korábbi állapot elég
4Történet külön táblában (mini dimenzió)Gyakran változó attribútumok
6Az 1, 2 és 3 kombinációjaAktuális és történeti érték egyszerre
create table dim_ceg (
  ceg_kulcs      bigint generated always as identity primary key,  -- helyettesítő
  adoszam        text not null,                                    -- természetes
  nev            text not null,
  regio          text not null,
  ervenyes_tol   timestamptz not null,
  ervenyes_ig    timestamptz not null default 'infinity',
  aktualis       boolean not null default true
);
create unique index on dim_ceg (adoszam) where aktualis;

-- SCD2 frissítés: régi sor lezárása, új sor beszúrása egy tranzakcióban
begin;
update dim_ceg set ervenyes_ig = now(), aktualis = false
where adoszam = $1 and aktualis and (nev, regio) is distinct from ($2, $3);
insert into dim_ceg (adoszam, nev, regio, ervenyes_tol)
select $1, $2, $3, now()
where not exists (select 1 from dim_ceg where adoszam = $1 and aktualis);
commit;

Egyeztetett dimenziók (conformed dimensions) és a busz mátrix. Ugyanaz a dimenzió (ügyfél, dátum) több ténytáblával kapcsolódik, így a folyamatok között összevethető. A busz mátrixban soronként egy folyamat, oszloponként egy dimenzió áll.

9.3 Data Vault (Dan Linstedt, Data Vault 2.0)

Integrációs réteg sok, változó forrásrendszerhez, három elemmel. A hub az üzleti kulcs (ügyfél adószám), a link a kulcsok közötti kapcsolat, a satellite a leíró attribútumok időbélyeggel és forrással. Csak beszúrás történik, a történet teljes, a betöltés párhuzamosítható. A riport réteg ebből épül csillag sémaként. Kis és közepes projektre túl nehéz, ott a dbt staging, majd egy Kimball modell elég.

9.4 Lakehouse

Az adattó (data lake, olcsó object storage, bármilyen fájl) és az adattárház (séma, ACID, SQL teljesítmény) egyesítése. A nyílt táblaformátumok (Apache Iceberg, Delta Lake, Apache Hudi) Parquet fájlok fölött tranzakciót, sémaevolúciót, időutazást (time travel) és partícionálást adnak. Ugyanazt az adatot több motor olvashatja (Spark, Trino, DuckDB, Snowflake, BigQuery). Az Iceberg 2024 és 2026 között vált de facto szabvánnyá, katalógusokkal (Polaris, Unity Catalog, AWS S3 Tables). A medallion architektúra bronz (nyers), ezüst (tisztított) és arany (üzleti) rétegei a szokásos szervezési minta. Kis csapat is lekérdezhet Parquet és Iceberg adatot Postgresből, a DuckDB és a pg_duckdb révén.

9.5 Data Mesh (Zhamak Dehghani, 2019 cikk, 2022 könyv)

Szervezeti és architektúra minta nagy cégekre, ahol a központi adatcsapat szűk keresztmetszet lett. Négy elvre épül.

  1. Tartományi tulajdon (domain ownership). Az adatot az a csapat birtokolja és szolgálja ki, amelyik létrehozza.
  2. Adat mint termék (data as a product). Felfedezhető, címezhető, megbízható, önleíró, interoperábilis, biztonságos, és van gazdája.
  3. Önkiszolgáló adatplatform. Egy központi platformcsapat ad eszközt, hogy a tartományok maguk tudjanak adatterméket építeni.
  4. Föderált, számítással támogatott irányítás (federated computational governance). Közös szabályok (azonosítók, adatvédelem, minőség), amiket a platform automatikusan kényszerít ki.

Mikor nem. Néhány fős csapatnál vagy egyetlen terméknél a data mesh szervezeti túlépítés. Az adat mint termék gondolkodás (gazda, leírás, minőségi ígéret) viszont kis méretben is hasznos. A szervezeti adatirányítás tágabb kereteiről az irányítási rendszerek és governance kézikönyv szól.

9.6 Data contracts

Az adat előállítója és fogyasztója közötti formális megállapodás (Andrew Jones, Driving Data Quality with Data Contracts, 2023, és Chad Sanderson írásai). Tartalma a séma (mezők, típusok, kötelezőség), a szemantika (mit jelent a mező, mi a mértékegység), a minőségi ígéret (frissesség, teljesség, egyediség), a gazda és a változtatási szabály (verziózás, értesítés). Az Open Data Contract Standard (ODCS, Bitol projekt, Linux Foundation) gépileg olvasható YAML formátumot ad.

Bevezetés lépései.

  1. A legtöbbet fogyasztott, legtöbbször törő adatfolyamnál kezdünk.
  2. A szerződés a forrás repójában él, és a forrás CI ellenőrzi (a sématörő változás elbukik).
  3. A fogyasztó oldalon a betöltés validál, és a szerződést sértő adatot karanténba teszi, nem tölti be csendben.
  4. Postgres oldalon a kényszerek és a JSON Schema validáció a szerződés végrehajtható része.

10. Adatminőség, data lineage, metaadat

10.1 Adatminőségi dimenziók

DimenzióKérdésPostgres vagy pipeline ellenőrzés
Pontosság (accuracy)Megfelel a valóságnak?Összevetés forrással, mintavételes ellenőrzés
Teljesség (completeness)Megvan minden kötelező érték és rekord?not null, sorszám összevetés forrással
KonzisztenciaRendszerek és táblák között egyezik?FK, egyeztető lekérdezés
Érvényesség (validity)Formátum, tartomány, szabály szerinti?check, domain típus, JSON Schema
EgyediségNincs duplikátum?unique, deduplikáló lekérdezés
Időszerűség (timeliness, freshness)Elég friss?max(betoltve) figyelése, riasztás
IntegritásA kapcsolatok épek?FK, árva sor keresés

A formális kereteket a DAMA DMBOK (2. kiadás, 2017), az ISO 8000 és az ISO/IEC 25012 adja. Tágabb megfelelési kérdéseiket az ISO megfelelés és audit kézikönyv tárgyalja.

Eljárás.

  1. Az adat belépési pontján validálunk (kényszer, séma).
  2. Ami nem fejezhető ki kényszerrel, arra ütemezett, riasztó ellenőrző lekérdezés fut (dbt tests, Great Expectations, Soda, vagy egy pg_cron feladat, ami egy minosegi_hiba táblába ír).
  3. Trendet mérünk, nem csak pillanatképet (nő-e a duplikátumok aránya).
  4. A hibás adatot nem a riportban javítjuk csendben, hanem a forrásnál.
-- Árva jelentkezések, duplikált emailek, elavult betöltés
select 'arva_jelentkezes' as ellenorzes, count(*) from jelentkezes j
  left join kepzes k on k.id = j.kepzes_id where k.id is null
union all
select 'dupla_email', count(*) from (
  select lower(email) from resztvevo group by 1 having count(*) > 1) d
union all
select 'friss_betoltes_hianyzik', count(*) from betoltes_naplo
  where forras = 'crm' having max(befejezve) < now() - interval '25 hours';

10.2 Data lineage

Az adat származási útja a forrástól a riportig, mezőszintig. Szükség van rá hibakereséshez (honnan jön a rossz szám), hatáselemzéshez (mi törik el, ha ezt az oszlopot átnevezzük) és megfelelőséghez (GDPR, hol van személyes adat). Ismert eszköz az OpenLineage szabvány (Marquez), a dbt docs, a DataHub és az OpenMetadata. Kis projektben a migrációs fájlok, a nézetek definíciója és a comment on dokumentáció maga a lineage. A személyes adatok nyilvántartásáról az adatbiztonság és adatvédelem kézikönyv ad részletes útmutatást.

10.3 Metaadat

Három rétege van. Technikai (séma, típus, index, méret), üzleti (definíció, gazda, érzékenységi besorolás) és működési (betöltés ideje, sorszám, hibák). Postgresben a comment on table/column az üzleti metaadat olcsó, mindig naprakész helye, és a Supabase dashboard, a generált TypeScript típusok és az AI asszisztens is látja.

comment on table resztvevo is 'Képzési résztvevő. Gazda: értékesítés. Személyes adat, GDPR.';
comment on column resztvevo.email is 'Belépési és értesítési cím. PII. Egyedi, kis és nagybetű független.';

11. Mentés és helyreállítás

11.1 Fogalmak

RPO (Recovery Point Objective). Mennyi adatvesztés fogadható el, időben mérve. Napi mentésnél ez akár 24 óra.

RTO (Recovery Time Objective). Mennyi idő alatt kell újra működnie a rendszernek.

PITR (point in time recovery). Alapmentés plusz a folyamatosan archivált WAL, amiből tetszőleges időpontra visszaállítható az adatbázis. Postgresben a pgBackRest, a Barman és a WAL G a bevett eszköz. Supabase-en fizetős kiegészítő, másodperces nagyságrendű RPO-val. Az alapcsomagban napi mentés van, ingyenes projekten a mentés nem érhető el letölthetően. Ez a 2026 októberi állapot, az aktuális csomagleírásban ellenőrizendő.

3-2-1 szabály. Három példány, két különböző adathordozón vagy szolgáltatásnál, ebből egy telephelyen kívül. Ma gyakran 3-2-1-1-0 a cél, vagyis egy példány megváltoztathatatlan vagy elszigetelt (immutable, air gapped, zsarolóvírus ellen), és nulla hiba a visszatöltési próbán.

11.2 Eljárás

  1. Az RPO és az RTO meghatározása az üzleti gazdával. Ez dönti el a mentési módszert és a költséget.
  2. A mechanizmus kiválasztása. Logikai mentés (pg_dump) kis adatra és hordozhatóságra, fizikai mentés plusz WAL archiválás PITR-hez.
  3. Egy példány a szolgáltatón kívül. Ütemezett pg_dump saját object storage-ba, titkosítva, más fiókban. Supabase-en a Storage fájlok külön mentést igényelnek, a Postgres mentés nem tartalmazza a fájlokat, csak a metaadatukat.
  4. Ütemezett visszatöltési próba (legalább negyedévente). Egy sosem visszatöltött mentés csak remény. A próba méri a valódi RTO-t, és ellenőrzi, hogy a kiterjesztések, a szerepkörök, az RLS policyk és az auth séma is visszajönnek.
  5. Runbook. Lépésről lépésre leírt visszaállítás, benne az is, kinek van hozzáférése, honnan és milyen jelszóval (a jelszó a jelszókezelőben, nem a runbookban).
  6. A mentés sikerességének figyelése. Riasztás, ha egy mentés nem futott le, vagy gyanúsan kicsi.
# Logikai mentés Supabase projektről (közvetlen vagy session pooler kapcsolat)
pg_dump "$DATABASE_URL" --format=custom --no-owner --no-privileges \
  --schema=public --file="mentes_$(date +%F).dump"
# Visszatöltési próba külön adatbázisba
pg_restore --dbname="$PROBA_URL" --no-owner --clean --if-exists mentes_2026-10-20.dump

Tipikus hiba. A replika nem mentés, egy véletlen delete from szamla azonnal replikálódik. Soft delete sem mentés (lásd 12.4). A Supabase CLI supabase db dump parancsa a sémát és az adatot is tudja menteni, de az auth és a storage sémákra külön figyelni kell.

12. Idő, pénz, azonosítók, soft delete

12.1 Idő

Szabályok.

  1. Időpont mindig timestamptz. A Postgres UTC-ben tárol, és a kapcsolat TimeZone beállítása szerint jeleníti meg. Időzóna nélküli timestamp csak akkor kell, ha szándékosan falióra időt tárolunk (lásd a 3. szabályt).
  2. Dátum, ami nem időpont (születésnap, számla kiállítás napja, esedékesség), date típus. Éjfél UTC időpontként tárolva Budapesten egy nappal eltolódhat.
  3. Jövőbeli helyi esemény (képzés november 5-én 9:00-kor budapesti idő szerint). A helyi időt (timestamp) és az IANA időzónát (Europe/Budapest) külön tároljuk, mert az időzóna szabályok változhatnak a rögzítés és az esemény között. Ebből számoljuk a timestamptz értéket.
  4. Időtartam interval típusban, vagy egész számként, a nevében egyértelmű mértékegységgel (idotartam_perc).
  5. Tartomány tstzrange vagy daterange típusban, félig nyitott [kezdet, veg) formában. Így a szomszédos időszakok nem fednek át, és nincs köztük rés.
  6. Napi aggregálás helyi idő szerint. date_trunc('day', ido at time zone 'Europe/Budapest'). UTC nap szerint a késő esti események rossz napra kerülnek.
  7. A now() a tranzakció kezdetét adja, a clock_timestamp() a valós pillanatot. Hosszú tranzakcióban ez különbség.
  8. Nyári időszámítás. Tavasszal a vasárnap hajnali 2 és 3 közötti időpont nem létezik, ősszel egy óra kétszer létezik. Ütemezőnél ezt kezelni kell.
  9. Bitemporális adat. Ha tudni kell, mikor volt igaz valami a valóságban (valid time) és mikor rögzítettük (transaction time), két tartomány oszlop kell. A Postgres 18 WITHOUT OVERLAPS és PERIOD idegen kulcs ezt támogatja.

12.2 Pénz

  1. numeric(p, s), soha float vagy real. A bináris lebegőpontos szám nem tudja hűen ábrázolni a 0.1 értéket, összegzésnél fillérek vesznek el vagy keletkeznek.
  2. Vagy egész szám a legkisebb egységben (bigint, fillér vagy cent), ahogy a Stripe API dolgozik. Forintnál óvatosság kell, mert a forint váltópénz nélküli készpénzes kerekítéssel (5 forintra) és a számlán két tizedessel is megjelenhet.
  3. Kerülendő a Postgres money típusa. A tizedesjegyek száma az lc_monetary beállítástól függ, és pénznemet nem tárol.
  4. Mindig pénznem mellé (char(3), ISO 4217). Különböző pénznemű összegeket árfolyam nélkül nem adunk össze.
  5. A kerekítési szabály explicit, és egy helyen él. Az ÁFA tételes és összesített kerekítése más eredményt ad, ezt a jogszabály és az üzleti döntés határozza meg.
  6. Árfolyam pillanatképként, dátummal és forrással tárolva.
  7. JavaScript oldalon a Postgres numeric stringként érkezik (supabase-js, pg), ezért számításhoz nem alakítjuk Number típusra. Decimális könyvtár (decimal.js, big.js) vagy egész fillér kell.
  8. Pénzügyi mozgás kettős könyvvitellel. Egyenleg oszlop helyett megváltoztathatatlan tételek (debit és kredit), az egyenleg ezek összege. Javítás storno tétellel, nem update utasítással.

12.3 Azonosítók

TípusElőnyHátrányMikor
bigint generated always as identityKicsi (8 bájt), gyors, rendezett, olvashatóKitalálható és megszámlálható, elosztott generálás nehéz, összefésülésnél ütközikBelső táblák, kapcsolótáblák, naplók
serial, bigserialRégi szintaxisAz identity a szabványos, jogosultság és dump szempontból tisztábbÚj kódban nem
UUID v4 (gen_random_uuid())Kliensen generálható, nem kitalálható, összefésülhető16 bájt, véletlen sorrend, ami B-tree indexnél lapfelosztást és cache szétszórást okozHa a rendezettség nem kell
UUID v7 (uuidv7() Postgres 18 óta, RFC 9562, 2024)Időrendezett, jó index lokalitás, kliensen is generálható16 bájt, a létrehozás ideje kiolvasható belőleAlapértelmezés új táblák nyilvános azonosítójára
ULID, KSUID, SnowflakeIdőrendezettNem natív típusHa a környezet már ezt használja

Postgres 18 előtt a UUID v7 kliens oldali könyvtárral (az uuid npm csomag v7() függvényével), egy kis SQL függvénnyel vagy a pg_uuidv7 kiterjesztéssel generálható, ha elérhető. Supabase-en ehhez érdemes ellenőrizni a Postgres verziót.

Szabályok.

  1. Külső felületen (URL, API) ne legyen sorszámozott egész id, ha az kitalálással más rekordhoz vezet, vagy üzleti adatot árul el (hány rendelés van). Az RLS véd a hozzáférés ellen, de a számosság így is kiolvasható.
  2. Gyakori minta a belső bigint elsődleges kulcs plusz egy uuid nyilvános azonosító unique indexszel, vagy egyszerűen UUID v7 elsődleges kulcs.
  3. Supabase-en az auth.users.id UUID. A profil tábla definíciója id uuid primary key references auth.users(id) on delete cascade.
  4. Változó természetes kulcs (email, név) nem lehet elsődleges kulcs. Az adószám stabilabb, de cégátalakulásnál az is változhat.
  5. A UUID v7 időbélyege információt szivárogtat. Ha a létrehozás ideje érzékeny, v4 kell.
  6. A nem nyilvános sorszám (számlaszám) nem azonos az id-val. Saját, hézagmentes sorszámozás kell, amit tranzakción belül, zárolt számláló sorral osztunk ki, mert a szekvencia rollbacknél hézagot hagy.

12.4 Soft delete csapdái

A deleted_at timestamptz oszlop egyszerűnek tűnik, de rengeteg hiba forrása.

Csapdák.

  1. Minden lekérdezésbe be kell írni a szűrést. Egy elfelejtett where deleted_at is null törölt adatot mutat. Joinnál mindkét oldalra kell.
  2. Az egyedi kényszer eltörik. A törölt felhasználó email címe blokkolja az újraregisztrációt. Megoldás egy részleges unique index where deleted_at is null feltétellel.
  3. Az idegen kulcs nem tud róla. Törölt szülőre élő gyerek mutat, a cascade nem fut le.
  4. Az index és a tábla duzzad, a lekérdezések egyre lassabbak.
  5. GDPR és adatvédelem. Törlési kérelemnél a soft delete nem törlés. A személyes adatot ténylegesen törölni vagy anonimizálni kell, a mentésekből is, a megőrzési idő végén.
  6. Az RLS policyben is szűrni kell, különben a kliens API-n a törölt sor elérhető.

Alternatívák, sorrendben.

create table torolt_rekord (
  id         bigint generated always as identity primary key,
  tabla      text not null,
  rekord_id  text not null,
  adat       jsonb not null,
  torolte    uuid,
  torolve    timestamptz not null default now()
);

create or replace function archival_torleskor() returns trigger
language plpgsql security definer set search_path = '' as $$
begin
  insert into public.torolt_rekord (tabla, rekord_id, adat, torolte)
  values (tg_table_name, old.id::text, to_jsonb(old), auth.uid());
  return old;
end $$;

create trigger trg_kepzes_archiv before delete on kepzes
  for each row execute function archival_torleskor();

13. A gyakorlatban

Sématervezésnél, migráció írásakor, lekérdezés generálásakor és Supabase projekt módosításakor ugyanazok a szabályok érvényesek, akár ember írja a kódot, akár AI asszisztens.

13.1 Munkarend adatbázis feladatnál

  1. Előbb a meglévő sémát nézzük meg (táblalista, migrációs fájlok, \d tabla), nem feltételezünk.
  2. Az entitásokat és a kapcsolatokat mondatokban írjuk le, kardinalitással (lásd 1.4), mielőtt SQL születik.
  3. Minden táblánál kimondjuk, ki olvashatja és ki írhatja, ebből lesz az RLS.
  4. Migrációs fájlba írunk. Nem a dashboardon kattintunk, és élő sémát nem módosítunk közvetlen SQL futtatással.
  5. Lokálisan vagy branch adatbázison futtatjuk, utána a Supabase biztonsági és teljesítmény ellenőrzésével (Security és Performance Advisor) vizsgáljuk.
  6. Sémaváltozás után újrageneráljuk a típusokat (supabase gen types).

13.2 Sématervezési checklist

13.3 Migráció írási checklist

13.4 Az AI által generált séma tipikus hibái

A csapat ezeket a hibákat a saját és az AI asszisztens által generált kódban egyaránt keresi, generálás után és kódátnézéskor is. Az AI asszisztált fejlesztés tágabb kockázatait az AI ügynökök és vibecoding kézikönyv tárgyalja.

HibaMiért történikJavítás
RLS kikapcsolva vagy using (true) policyA gyors működés a cél, a hiba nem látszikPolicy műveletenként, valódi feltétellel, teszt két felhasználóval
Service role kulcs a kliensbenMegkerüli a jogosultsági hibátPublishable kulcs a kliensen, service role csak szerveren
Hiányzó FK indexA Postgres nem teszi automatikusancreate index minden hivatkozó oszlopra
float pénzre, timestamp időpontraA modell az alapértelmezett típust választjanumeric, timestamptz
Ellenőrzés select, majd insert, kényszer nélkülEgyfelhasználós fejlesztői tesztunique és on conflict, vagy zár
Olvasás, módosítás, írás két kliens hívásbanA supabase-js hívások nem közös tranzakcióban futnakAtomikus update, vagy rpc() függvény
JSONB minden bizonytalan mezőreGyors prototípusOszlopok és kényszerek, JSONB csak a nyers külső adatra
Tömb oszlop kapcsolat helyettKevesebb tábla egyszerűbbnek tűnikKapcsolótábla
Soft delete részleges unique index nélkülA mintaprojektekből átvett szokásA 12.4 alatti alternatívák
user_metadata alapú jogosultságA dokumentációban könnyen elérhető mezőSaját szerepkör tábla vagy app_metadata
security definer függvény search_path nélkülMűködik, a kockázat nem látszikPrivát séma, set search_path = ''
Webhook feldolgozás deduplikáció nélkülA szolgáltató tesztje egyszer küldInbox tábla eseményazonosítóval (lásd 8.3)
Email vagy fizetés hívása a tranzakción belül vagy után, outbox nélkülEgyszerű sorrendnek tűnikOutbox (lásd 8.4)
Offset lapozásA legegyszerűbb APIKeyset lapozás (lásd 5.2)
select * és teljes objektum visszaírásaKényelmes ORM használatExplicit oszlopok, részleges update
Migráció helyett közvetlen sémamódosításAz eszköz megengediMigrációs fájl, verziókezelés
Index nélküli RLS policy oszlopA policy logikailag helyesIndex a tenant_id, user_id oszlopokra
Időzóna nélküli napi bontásUTC alapértelmezésat time zone 'Europe/Budapest'
Hiányzik az embedding modell neveCsak a vektor tűnik adatnakmodell oszlop, újraszámolási terv
Szekvencia mint számlaszámKézenfekvőnek tűnikHézagmentes, zárolt számláló bérlőnként

13.5 Lekérdezés és teljesítmény szabályok

  1. Lassú lekérdezésnél előbb explain (analyze, buffers) jön, utána index, és csak a mérés után denormalizálás.
  2. Az összetett index sorrendje egyenlőség, majd rendezés, végül tartomány.
  3. Indexelt oszlopon nincs függvény a where feltételben, vagy van hozzá kifejezés index.
  4. A lapozás keyset módszerrel történik.
  5. N+1 helyett join vagy egyetlen lekérdezés in listával (supabase-js beágyazott select, select('*, jelentkezes(*)')).
  6. Szerver nélküli környezetből transaction pooler, prepared statement nélkül.
  7. Analitikai lekérdezés nem fut percekig az éles adatbázison csúcsidőben, erre materializált nézet vagy külön réteg való.

13.6 Elosztott és integrációs szabályok

  1. Minden külső mellékhatás (email, fizetés, webhook kiküldés) at least once kézbesítésű és idempotens.
  2. Az adatbázis írás és az üzenetküldés együtt mindig outboxszal történik.
  3. A bejövő webhook inboxba kerül, aláírás ellenőrzéssel és sorrendfüggetlen feldolgozással.
  4. Különböző gépek óráira épülő, időbélyeg alapú utolsó író nyer logikát nem írunk.
  5. Kettős írás (adatbázis plusz kereső vagy cache) helyett CDC vagy outbox.
  6. Read replica használatánál a saját írás utáni olvasás a primaryról történik.

13.7 Mentés és üzemeltetés szabályok

  1. Új projektnél rögzítjük az RPO és az RTO értékét.
  2. Ügyféladatot kezelő éles rendszernél PITR vagy legalább napi, szolgáltatón kívüli mentés kell.
  3. A Storage fájlok mentését külön tervezzük.
  4. A visszatöltési próba ütemezett és dokumentált.
  5. Romboló műveletet (drop, truncate, tömeges delete) élő adatbázison senki nem futtat jóváhagyás nélkül, AI ügynök sem, és előtte mindig ellenőrizni kell, mikor volt az utolsó mentés.

13.8 Rövid döntési szabályok

Források

Állapot 2026 októberében. A verzióhoz és szolgáltatási csomaghoz kötött részleteket (Postgres, Supabase, pgvector) a gyártó aktuális dokumentációjában érdemes ellenőrizni.