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
| Feladat | Releváns szakasz |
|---|---|
| Új séma, új tábla, új entitás | 2. Normalizálás, 6. Gyakorlati Postgres, 12. Idő, pénz, azonosítók, 13.2 checklist |
| Lassú lekérdezés | 5. Indexek és EXPLAIN |
| Versenyhelyzet, duplikált sor, elveszett frissítés | 3.7 Tranzakciók és izoláció |
| Séma módosítása élő rendszeren | 7. Migrációk |
| Webhook, fizetés, email küldés, háttérfeladat | 8. Idempotencia és outbox |
| Riport, dashboard, analitika | 9. Adatmérnökség, Kimball |
| Mennyi adatvesztés fér bele | 11. Mentés és helyreállítás |
| AI keresés, embedding, RAG | 3.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ó.
- Adatfüggetlenség. A program logikai kérdést tesz fel, nem fizikai útvonalat jár be. Az indexek, a tárolási sorrend és a fizikai elrendezés változhat anélkül, hogy a program változna.
- Reláció mint halmaz. Sorok halmaza, nincs sorrend, nincs duplikátum, minden attribútum atomi értékkészletből (domain) vesz értéket.
- Deklaratív lekérdezés. Relációs algebra és relációs kalkulus. Az optimalizáló dönti el, hogyan hajtja végre.
- Kulcsok és hivatkozások értékkel, nem mutatóval. Az idegen kulcs érték, nem fizikai cím.
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ály | Mit jelent ma a gyakorlatban |
|---|---|---|
| 0 | Alapszabály | A rendszer kizárólag relációs eszközökkel kezeli az adatot |
| 1 | Információ szabály | Minden információ táblában, értékként jelenik meg |
| 2 | Garantált hozzáférés | Tábla neve, kulcs és oszlopnév elég bármely érték eléréséhez |
| 3 | NULL szisztematikus kezelése | A hiányzó érték egységes jelölése, típustól függetlenül |
| 4 | Online katalógus | A séma maga is lekérdezhető tábla (information_schema, pg_catalog) |
| 5 | Átfogó adatnyelv | Egy nyelv a definícióra, lekérdezésre, jogosultságra, tranzakcióra |
| 6 | Nézet frissítése | Az elméletileg frissíthető nézet frissíthető is |
| 7 | Halmazszintű INSERT, UPDATE, DELETE | Nem csak soronként |
| 8 | Fizikai adatfüggetlenség | Index, tárolás változik, a program nem |
| 9 | Logikai adatfüggetlenség | Tábla szétbontása nézettel elrejthető |
| 10 | Integritás függetlensége | A kényszerek a katalógusban élnek, nem az alkalmazásban |
| 11 | Elosztás függetlensége | A program nem tudja, hol van fizikailag az adat |
| 12 | Nem megkerülhetőség | Alacsony 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.
- 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.
- A főnevek kigyűjtése. Ezek az entitásjelöltek (résztvevő, képzés, tréner, jelentkezés).
- Az igék kigyűjtése. Ezek a kapcsolatjelöltek (jelentkezik, tart).
- 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.
- 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.
- 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.
- A kötelezőség jelölése. Lehet-e jelentkezés képzés nélkül? Nem, tehát
not nullidegen kulcs kell. - 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és | Ki garantálja Postgresben |
|---|---|---|
| A, atomicity | Hiba esetén minden visszagörgetődik | WAL, BEGIN és COMMIT |
| C, consistency | Az invariánsok tranzakció után is igazak | Részben a motor (kényszerek), részben az alkalmazás |
| I, isolation | Párhuzamos tranzakciók nem zavarják egymást | Izolációs szint, alapból READ COMMITTED |
| D, durability | A commitolt adat nem veszik el | WAL 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.
- Háromértékű logika a NULL miatt. A
NULL = NULLeredménye NULL, nem igaz.WHERE x <> 'a'nem adja vissza a NULL sorokat, aNOT INpedig egy NULL elemet tartalmazó részlekérdezéssel üres eredményt ad. Ezért minden nullable oszlopnál dönteni kell, mit jelent a hiány (ismeretlen, nem alkalmazható, még nincs megadva), és ahol lehet,not nullplusz értelmes alapérték kell. - Duplikátum sorok. Az SQL tábla multihalmaz, a reláció halmaz. Minden táblának legyen kulcsa, különben a duplikátum csendben megjelenik.
- Temporális adat. Date, Darwen és Lorentzos könyve (Temporal Data and the Relational Model, 2002) a mai
tstzrangeés a Postgres 18WITHOUT OVERLAPSkényszerek elméleti háttere.
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.
| jelentkezes | resztvevo_email | resztvevo_nev | ceg_adoszam | ceg_nev | kepzes_kod | kepzes_cim | kepzes_datum | trenerek | dij_huf |
|---|---|---|---|---|---|---|---|---|---|
| 1 | [email protected] | Kiss Anna | 12345678 | Alfa Kft | AI101 | AI alapok | 2026-11-05 | Kalmár D., Nagy P. | 90000 |
| 2 | [email protected] | Tóth Béla | 87654321 | Béta Zrt | AI101 | AI alapok | 2026-11-05 | Kalmár D., Nagy P. | 90000 |
| 3 | [email protected] | Kiss Anna | 12345678 | Alfa Kft | AI201 | Ügynökök | 2026-12-03 | Kalmá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.
resztvevo_email → resztvevo_nev, ceg_adoszamkepzes_kod → kepzes_cim, kepzes_datum(resztvevo_email, kepzes_kod) → dij_huf
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.
- A normalizált változat mérése.
EXPLAIN (ANALYZE, BUFFERS)a valódi lekérdezésen, valósághű adatmennyiséggel. - Kísérlet index és lekérdezés átírással. Az esetek többségében ez elég.
- A legolcsóbb szinkronizálási mechanizmus kiválasztása, az alábbi táblázat sorrendjében.
- Dokumentálás a séma kommentjében, hogy az oszlop származtatott, és mi a forrása.
| Sorrend | Mechanizmus | Mikor jó |
|---|---|---|
| 1 | Generált oszlop | Ugyanazon a soron belüli származtatás |
| 2 | Nézet vagy materializált nézet | refresh materialized view concurrently, ehhez unique index kell |
| 3 | Trigger | Ugyanabban a tranzakcióban frissít |
| 4 | Aszinkron 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.
- Terhelés leírása. Kérés per másodperc, írás és olvasás aránya, egy entitás átlagos és maximális mérete, a legnagyobb felhasználó mérete (a fan out probléma, aminek klasszikus példája a Twitter idővonal).
- Válaszidő percentilisekben. p50, p95, p99. Az átlag elrejti a farokkésést, és egy oldalbetöltés több háttérkérésből áll, így a lassú farok gyakoribb, mint elsőre várnánk (tail latency amplification).
- Megbízhatóság. Hardver, szoftver és emberi hiba. A legtöbb kiesés emberi konfigurációs hibából jön, ezért a visszaállíthatóság és a kis lépésű deploy többet ér, mint a redundáns hardver.
- Karbantarthatóság. Üzemeltethetőség, egyszerűség, fejleszthetőség.
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
| Szempont | Relációs | Dokumentum | Gráf |
|---|---|---|---|
| Erőssége | Több a többhöz, join, kényszerek, ad hoc lekérdezés | Önálló, fa alakú aggregátum egyben olvasva | Sok, 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ás | Több táblából áll össze | Egy dokumentum egy helyen | Csúcsonként |
| Gyengesége | Objektum és tábla közötti eltérés (impedance mismatch) | Hivatkozás más dokumentumra, join, részleges frissítés | Aggregáló analitika |
| Postgresben | Alap | JSONB | Rekurzív CTE, Apache AGE kiterjesztés |
Döntési eljárás.
- Felrajzoljuk az entitásokat és a kapcsolatokat.
- 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ó.
- 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.
- 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.
| Szempont | B-tree | LSM tree |
|---|---|---|
| Írási erősítés | Lap újraírás, WAL | Compaction miatt többszöri újraírás |
| Írási átviteli sebesség | Közepes | Magas |
| Olvasási késés | Kiszámítható | Változó, a compaction állapotától függ |
| Helyfoglalás | Töredezettség | Tö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.
- Visszafelé kompatibilitás (backward). Az új kód olvassa a régi kód által írt adatot.
- Előre kompatibilitás (forward). A régi kód olvassa az új kód által írt adatot, és nem töri el azt, amit nem ismer.
Formátumok.
| Formátum | Séma | Evolúciós szabály |
|---|---|---|
| JSON, XML, CSV | Nincs 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ése | Csak 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.
- Oszlopot vagy mezőt mindig opcionálisként vagy alapértelmezéssel adunk hozzá.
- Átnevezés nincs, csak új mező, másolás és a régi kivezetése (lásd 7.3, expand és contract).
- Az enum bővíthető, szűkíteni csak akkor lehet, ha egyetlen író és olvasó sem használja a kivett értéket.
- Típusváltás új mezőként történik.
- 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.
- Szinkron replikáció. A commit megvárja legalább egy follower visszaigazolását. Tartós, de egy lassú replika megállítja az írást. Gyakori kompromisszum az egy szinkron, a többi aszinkron felállás (semi synchronous).
- Aszinkron replikáció. Gyors, de leader kiesésnél a még nem replikált írások elvesznek.
- Failover kockázatai. Elveszett írások, két leader egyszerre (split brain), rosszul megválasztott időkorlát.
Replikációs késés anomáliái és ellenszerük.
| Anomália | Mi történik | Ellenszer |
|---|---|---|
| Read your writes hiánya | A felhasználó ment, frissít, és nem látja a saját módosítását, mert a replikáról olvasott | Saját adat olvasása a leaderről egy ideig írás után, vagy LSN alapú várakozás |
| Monotonic reads hiánya | Két egymás utáni olvasás két különböző késésű replikáról, az adat visszaugrik az időben | Egy felhasználó mindig ugyanarról a replikáról olvas (hash a user id-ra) |
| Consistent prefix hiánya | A válasz előbb látszik, mint a kérdés, mert különböző partícióból jönnek | Ok é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ódszer | Előny | Hátrány |
|---|---|---|
| Kulcstartomány szerint | Tartományos lekérdezés hatékony | Hotspot, ha a kulcs időbélyeg |
| Kulcs hash szerint | Egyenletes eloszlás | Tartomá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ália | Leírás | Példa |
|---|---|---|
| Dirty read | Más tranzakció még nem commitolt adatát látjuk | Látszik a fizetve állapot, ami utána rollbackel |
| Dirty write | Más tranzakció nem commitolt adatát írjuk felül | Ké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 update | Két olvas, módosít, ír ciklus, az egyik felülírja a másikat | Két kérés olvassa az n = 5 értéket, mindkettő 6-ot ír |
| Write skew | Két tranzakció ugyanazt a feltételt olvassa, különböző sorokat ír, és együtt megsértik az invariánst | Legalább egy orvos ügyeletben legyen. Ketten egyszerre jelentenek beteget, mindketten látják, hogy a másik bent van |
| Phantom | Egy tranzakció írása megváltoztatja egy másik keresési feltételének eredményét | Teremfoglalás ütközés ellenőrzése, közben valaki beszúr egy átfedő foglalást |
Postgres viselkedése szintenként
| Szint | Postgres megvalósítás | Mitől véd | Mitől nem |
|---|---|---|---|
| READ UNCOMMITTED | Ugyanaz, mint READ COMMITTED | Dirty read, dirty write | Lásd a következő sort |
| READ COMMITTED (alapértelmezés) | Utasításonként új pillanatkép | Dirty read, dirty write | Read skew, lost update (alkalmazásbeli olvas, módosít, ír), write skew, phantom |
| REPEATABLE READ | Snapshot isolation, tranzakciónként egy pillanatkép | Az előzők, read skew, phantom olvasás, lost update (konkurens módosításnál hibát dob) | Write skew |
| SERIALIZABLE | Serializable Snapshot Isolation (SSI, Cahill, Röhm és Fekete 2008, Postgres 9.1 óta) | Minden sorosíthatósági sértés | Semmi, 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.
- 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)
- 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. - Tanácsadó zár (advisory lock) egy logikai erőforrásra.
select pg_advisory_xact_lock(hashtextextended('ugyelet:' || $1::text, 0));
- 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.
| Index | Működés | Mikor |
|---|---|---|
| HNSW | Többrétegű közelségi gráf | Alapértelmezés. Jó felidézés, gyors lekérdezés, lassabb építés, több memória |
| IVFFlat | Klaszterezés, a legközelebbi klaszterekben keres | Gyorsabb építés, a klasztereket az adat birtokában kell tanítani |
| DiskANN, StreamingDiskANN | Lemezre optimalizált gráf | Nagyon 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.
| Antiminta | Felismerés | Javítás |
|---|---|---|
| Jaywalking | Vesszővel elválasztott azonosítók egy oszlopban | Kapcsolótábla |
| Naive Trees | Csak parent_id, és mélységfüggetlen lekérdezés kell | Rekurzív CTE, closure table, ltree kiterjesztés |
| ID Required | Minden táblán id, a kapcsolótáblán is, a természetes kulcs nem egyedi | Kapcsolótáblán összetett elsődleges kulcs, vagy unique kényszer a természetes kulcsra |
| Keyless Entry | Nincs idegen kulcs, mert lassít vagy rugalmatlan | Idegen kulcs mindig, plusz index a hivatkozó oszlopon |
| Entity Attribute Value | attributum(entitas_id, nev, ertek text) | Rendes oszlopok, altípus táblák, vagy JSONB sémavalidációval |
| Polymorphic Associations | commentable_type és commentable_id, nincs FK | Kü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 Attributes | tag1, tag2, tag3 | Függő tábla |
| Metadata Tribbles | Tábla vagy oszlop évenként (rendeles_2025), kézzel | Deklaratív partícionálás |
| Rounding Errors | float pénzre | numeric |
| 31 Flavors | check (x in (...)) vagy enum gyakran változó listára | Hivatkozott lookup tábla |
| Phantom Files | Fájl csak útvonalként, a fájl és a sor élete független | Object storage plusz életciklus kezelés, a törlés tranzakciója és a fájl törlése összehangolva (outbox) |
| Index Shotgun | Index mindenre vagy semmire | Mérés (MENTOR módszer, measure, explain, nominate, test, optimize, rebuild) |
| Fear of the Unknown | NULL helyett üres string vagy -1, vagy = NULL | is null, is distinct from, coalesce, tudatos not null |
| Ambiguous Groups | group by nem csoportosított, nem aggregált oszloppal | Minden oszlop csoportosított vagy aggregált, distinct on |
| Random Selection | order by random() limit 1 nagy táblán | tablesample, vagy véletlen kulcs plusz index |
| Poor Man's Search Engine | like '%szo%' | tsvector és GIN, vagy pg_trgm |
| Spaghetti Query | Egy óriási lekérdezés mindent kiszámol, Descartes szorzat | Több lekérdezés, vagy CTE lépésekkel, aggregálás join előtt |
| Implicit Columns | select *, insert oszloplista nélkül | Explicit oszloplista |
| Readable Passwords | Jelszó visszafejthető formában | Supabase Auth, vagy argon2id, bcrypt |
| SQL Injection | Szöveg összefűzés | Paraméteres lekérdezés, format('%I', ...) dinamikus azonosítóhoz |
| Pseudokey Neat Freak | Valakit zavarnak a hézagok az id sorozatban | A hézag normális, az id nem sorszám |
| See No Evil | Hibakód figyelmen kívül hagyása | Minden kliens hívásnál az error ellenőrzése |
| Diplomatic Immunity | Az SQL kód nincs verziókezelve, tesztelve | Migrációs fájlok gitben, pgTAP vagy integrációs teszt |
| Standard Operating Procedures | Minden logika tárolt eljárásban, vagy éppen semmi | Ami invariáns, az adatbázisban, ami munkafolyamat, az alkalmazásban |
| Magic Beans | Az ORM Active Record osztálya maga a tartománymodell | A 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
- A levélszint egy rendezett, kétszeresen láncolt lista az indexelt értékekről és a sor helyéről (Postgresben ctid).
- Fölötte kiegyensúlyozott fa vezet a levélhez, a mélysége millió sornál is 3 vagy 4.
- Egy index keresés három lépés. Fa bejárás (gyors), levelek követése (lehet hosszú), tábla hozzáférés soronként (lehet nagyon drága).
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.
- Kiírjuk a gyakori lekérdezéseket a
where,order byésjoinfeltételekkel. - 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.
- 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.
- Ha
order byis van, az index sorrendje egyezzen vele, így elmarad a rendezés (pipelined order by), és alimitkorán leáll. - Ha a lekérdezés kevés oszlopot ad vissza,
includeoszlopokkal 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.
- Függvény az oszlopon. A
where lower(email) = $1nem használja azemailindexet. Kifejezés index kell (create index on felhasznalo (lower(email))) vagycitext. - Típuseltérés. A
where id = '42'szöveges paraméterrel, vagy egytimestamptzoszlopdateértékkel összevetve implicit konverziót okozhat. - Dátum tartomány függvénnyel. A
where date(letrehozva) = '2026-10-20'helyettwhere letrehozva >= '2026-10-20' and letrehozva < '2026-10-21'a helyes. like '%x%'B-tree indexszel nem gyorsítható,pg_trgmGIN index kell hozzá.orkét különböző oszlopon gyakran bitmap or lesz, néhaunion alljobb.- Offset alapú lapozás. Az
offset 100000százezer sort olvas és eldob. A helyes megoldás a keyset lapozás (seek method).
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.
- Olvasás belülről kifelé, alulról felfelé. A legbeljebb lévő csomópont fut először.
- A becsült és a tényleges sorszám összevetése (
rows=a becslésben ésactual ... rows=). Tízszeres vagy nagyobb eltérés rossz statisztikát jelez. IlyenkorANALYZE tablakell, összefüggő oszlopokra pedigcreate statistics. - Szorzó a
loopsérték. A tényleges idő és sorszám egy ciklusra vonatkozik, a teljeshez szorozni kell. - A drága csomópont megkeresése. Ahol a legnagyobb az
actual timekülönbség a gyerekeihez képest. - Buffers. A
shared hita cache-ből jön, areada lemezről. Sokreadhideg cache-t vagy túl nagy olvasást jelez. - Szűrés utáni eldobás. A
Rows Removed by Filternagy értéke azt jelzi, hogy a predikátum nem hozzáférési, hanem szűrő. Ilyenkor jobb index kell.
| Csomópont | Jelentés | Mikor gyanús |
|---|---|---|
| Seq Scan | Teljes tábla végigolvasása | Nagy táblán szelektív feltétellel |
| Index Scan | Index plusz táblasorok | Sok sornál (akkor a bitmap vagy seq jobb) |
| Index Only Scan | Csak index, a Heap Fetches alacsony legyen | Ha a Heap Fetches magas, VACUUM kell (visibility map) |
| Bitmap Heap Scan | Index alapján lapok gyűjtése, majd olvasás | lossy blokkok, kevés work_mem |
| Nested Loop | Külső soronként belső keresés | Ha a külső oldal sok sor és a belső nem indexelt |
| Hash Join | Hash tábla a kisebb oldalból | Batches > 1, kevés memória |
| Merge Join | Két rendezett bemenet | Ha előtte drága rendezés kell |
| Sort | Rendezés | external 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.
- Külső rendszer nyers válasza (webhook payload, LLM válasz naplózása), változatlanul megőrizve.
- Ritkán lekérdezett, kliensenként változó beállítások.
- Ritka, sok változatú attribútumok, amikre nem kell kényszer és join.
- Űrlap beküldés, ahol az űrlap sémája változik, és a mezőkre nem épül üzleti logika.
Nem.
- Amire idegen kulcs hivatkozik, vagy ami idegen kulcs lenne.
- Amire gyakran szűrünk, rendezünk, aggregálunk (ez oszlop legyen).
- Pénz, dátum, állapot. Ezeknek típus és kényszer kell.
- Gyakran részlegesen frissített nagy dokumentum (minden
updateaz egészet újraírja, TOAST).
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.
- 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.
- 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. - 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ó. - Felidézés kontra sebesség. A
set hnsw.ef_search = 100;(alapértelmezés 40) jobb felidézést ad, lassabban. - Méret. 1536 dimenzió float4 típussal soronként 6 kB. A
halfvecfele akkora, gyakran elhanyagolható minőségvesztéssel. - 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
- A
tsvectorgenerált oszlopba kerül, GIN indexszel. - A nyelvi konfiguráció határozza meg a szótövezést. Magyarhoz a Postgres beépített
hungariankonfigurációja (Snowball stemmer) elérhető, ezt aselect cfgname from pg_ts_config;lekérdezés mutatja. Ékezetfüggetlen kereséshez azunaccentkiterjesztés kell egy saját konfigurációban. - Felhasználói bemenethez
websearch_to_tsqueryvaló, mert nem dob szintaktikai hibát. - Elírástűréshez és részszóhoz
pg_trgm(similarity,%operátor, GINgin_trgm_ops). - Súlyozás
setweightfüggvénnyel, címre (A) és törzsre (B).
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.
alter table x enable row level security;- Műveletenként külön policy (
select,insert,update,delete), szerepkörhöz kötve (to authenticated). - Az
updatepolicynélusing(melyik sort módosíthatja) éswith check(milyen állapotba). - Bérlők elválasztása a tagság táblán keresztül.
- 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.
security definerfüggvénysearch_pathbeállítás nélkül, vagy apublicsémában, ami RLS megkerülést nyit. Ha definer kell, privát sémába kerüljön,set search_path = ''beállítással.- Nézet, ami a tulajdonos jogaival fut. Postgres 15 óta erre a
create view ... with (security_invoker = true)való. user_metadatahasználata jogosultságra. Ezt a felhasználó maga módosíthatja, a jogosultság saját táblából vagyapp_metadatamezőből jöjjön.- Service role kulcs a böngészőben.
updatepolicywith checknélkül, így a felhasználó átírhatja atenant_idértékét egy másik bérlőre.
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ód | Supabase | Mire | Korlát |
|---|---|---|---|
| Közvetlen kapcsolat | db.<ref>.supabase.co:5432 | Migráció, hosszú életű szerver | IPv6, kevés kapcsolat |
| Session pooler | Supavisor, 5432 | Hosszú életű kliens IPv4-en | Kapcsolatonként egy szerver kapcsolat |
| Transaction pooler | Supavisor, 6543 | Szerver nélküli függvények | Nincs 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
pg_cronütemezett SQL feladatokhoz,pgmqegyszerű üzenetsorhoz Postgresen belül (Supabase Queues).merge(Postgres 15,returninga 17 óta) összetett upserthez.returning old.*, new.*Postgres 18 óta, auditáláshoz hasznos.pg_stat_statementsa lassú lekérdezések listájához.- Postgres 18 aszinkron I/O, ami a szekvenciális olvasást gyorsítja.
7. Migrációk
7.1 Alapszabályok
- Minden sémaváltozás migrációs fájlban, verziókezelve. Supabase-en
supabase migration new nev, a fájl asupabase/migrations/alá kerül. A dashboardon kattintott változás a következő környezetben elveszik. - Egy migráció egy logikai változás. Kicsi, visszakövethető.
- A lefutott migrációt nem szerkesztjük. Ami már lefutott valahol, azt új migráció követi, nem módosítás.
- Először lokálisan vagy branch adatbázison fut (
supabase db reset, Supabase branching), csak utána élesen. - 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űvelet | Zár | Biztonságos változat |
|---|---|---|
add column alapérték nélkül vagy konstans alapértékkel | Rö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ás | Oszlop alapérték nélkül, backfill, majd default |
create index | Írást blokkol | create index concurrently (tranzakción kívül) |
add foreign key | Mindkét táblán zár, teljes ellenőrzés | not valid, majd validate constraint |
set not null | Teljes tábla olvasás zár alatt | Elő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 column | Rövid zár, de a régi kódot azonnal eltöri | Expand é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és | Adatbázis | Alkalmazás |
|---|---|---|
| 1. Expand | Új oszlopok nullable formában | Változatlan |
| 2. Kettős írás | Szükség esetén szinkronizáló trigger | Az új kód a régi és az új oszlopot is írja |
| 3. Backfill | Régi sorok kitöltése kötegekben | Változatlan |
| 4. Olvasás átállítása | Változatlan | Az új oszlopból olvas |
| 5. Kényszer | not null biztonságos módon | Változatlan |
| 6. Régi írás leállítása | Változatlan | Csak az új oszlopot írja |
| 7. Contract | Ré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.
- Kötegekben, nem egyetlen
updateutasítással a teljes táblán. Egy óriásiupdatehosszú zárat, rengeteg WAL-t, replikációs késést és táblapuffadást okoz. - Kulcs szerint haladva, nem
offsetalapon. - Idempotensen. Csak a még nem kitöltött sorokat érinti, így megszakítás után újraindítható.
- Kötegenként commit. Egy
doblokk egyetlen tranzakció, ezért Postgres 11 óta eljárásban (procedure)commitutasítással, vagy külső szkriptből. - 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
- 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.
- A változást expand, migrate, contract lépésekre bontjuk.
- Minden DDL
lock_timeoutbeállítással fut, az indexekconcurrently, a kényszereknot validpluszvalidateformában. - A backfill kötegekben, idempotensen fut.
- Az olvasást feature flaggel váltjuk át.
- Figyeljük a hibaarányt, a lekérdezési késést és a replikációs késést.
- 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.
- A kliens műveletenként egyedi kulcsot generál (UUID), és újrapróbálásnál ugyanazt küldi (
Idempotency-Keyfejléc, a Stripe mintája). - A szerver egyedi kényszerrel rögzíti a kulcsot, a művelettel azonos tranzakcióban.
- Ismételt kérésnél a tárolt választ adja vissza, nem hajtja végre újra a műveletet.
- 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.
- Az aláírás ellenőrzése a nyers törzsön.
insert ... on conflict do nothingaz 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.- Egy háttérfeldolgozó (pg_cron, pgmq, ütemezett edge function) a fel nem dolgozott sorokat
for update skip lockedzárolással veszi fel. - 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
paidesemény acreatedutá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.
| Szakasz | Ké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ás | Melyik 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.
- Az üzleti folyamat kiválasztása (jelentkezés, fizetés, oldalletöltés), nem a riporté vagy az osztályé.
- 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ő.
- 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).
- 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ípus | Működés | Mikor |
|---|---|---|
| 0 | Sosem változik | Születési dátum, eredeti forrás |
| 1 | Felülírás, nincs történet | Elírás javítása |
| 2 | Új sor új helyettesítő kulccsal, érvényességi dátumokkal | Ha a riportnak a tény idején érvényes értéket kell mutatnia (a cég régiója a vásárláskor) |
| 3 | Előző érték külön oszlopban | Egy korábbi állapot elég |
| 4 | Történet külön táblában (mini dimenzió) | Gyakran változó attribútumok |
| 6 | Az 1, 2 és 3 kombinációja | Aktuá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.
- Tartományi tulajdon (domain ownership). Az adatot az a csapat birtokolja és szolgálja ki, amelyik létrehozza.
- Adat mint termék (data as a product). Felfedezhető, címezhető, megbízható, önleíró, interoperábilis, biztonságos, és van gazdája.
- Önkiszolgáló adatplatform. Egy központi platformcsapat ad eszközt, hogy a tartományok maguk tudjanak adatterméket építeni.
- 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.
- A legtöbbet fogyasztott, legtöbbször törő adatfolyamnál kezdünk.
- 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).
- 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.
- 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és | Postgres 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 |
| Konzisztencia | Rendszerek é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ég | Nincs duplikátum? | unique, deduplikáló lekérdezés |
| Időszerűség (timeliness, freshness) | Elég friss? | max(betoltve) figyelése, riasztás |
| Integritás | A 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.
- Az adat belépési pontján validálunk (kényszer, séma).
- 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_hibatáblába ír). - Trendet mérünk, nem csak pillanatképet (nő-e a duplikátumok aránya).
- 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
- 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.
- 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. - Egy példány a szolgáltatón kívül. Ütemezett
pg_dumpsajá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. - Ü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
authséma is visszajönnek. - 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).
- 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.
- Időpont mindig
timestamptz. A Postgres UTC-ben tárol, és a kapcsolatTimeZonebeállítása szerint jeleníti meg. Időzóna nélkülitimestampcsak akkor kell, ha szándékosan falióra időt tárolunk (lásd a 3. szabályt). - Dátum, ami nem időpont (születésnap, számla kiállítás napja, esedékesség),
datetípus. Éjfél UTC időpontként tárolva Budapesten egy nappal eltolódhat. - 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 atimestamptzértéket. - Időtartam
intervaltípusban, vagy egész számként, a nevében egyértelmű mértékegységgel (idotartam_perc). - Tartomány
tstzrangevagydaterangetípusban, félig nyitott[kezdet, veg)formában. Így a szomszédos időszakok nem fednek át, és nincs köztük rés. - 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. - A
now()a tranzakció kezdetét adja, aclock_timestamp()a valós pillanatot. Hosszú tranzakcióban ez különbség. - 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.
- 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ésPERIODidegen kulcs ezt támogatja.
12.2 Pénz
numeric(p, s), sohafloatvagyreal. 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.- 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. - Kerülendő a Postgres
moneytípusa. A tizedesjegyek száma azlc_monetarybeállítástól függ, és pénznemet nem tárol. - Mindig pénznem mellé (
char(3), ISO 4217). Különböző pénznemű összegeket árfolyam nélkül nem adunk össze. - 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.
- Árfolyam pillanatképként, dátummal és forrással tárolva.
- JavaScript oldalon a Postgres
numericstringként érkezik (supabase-js, pg), ezért számításhoz nem alakítjukNumbertípusra. Decimális könyvtár (decimal.js, big.js) vagy egész fillér kell. - 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
updateutasítással.
12.3 Azonosítók
| Típus | Előny | Hátrány | Mikor |
|---|---|---|---|
bigint generated always as identity | Kicsi (8 bájt), gyors, rendezett, olvasható | Kitalálható és megszámlálható, elosztott generálás nehéz, összefésülésnél ütközik | Belső táblák, kapcsolótáblák, naplók |
serial, bigserial | Régi szintaxis | Az 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 okoz | Ha 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őle | Alapértelmezés új táblák nyilvános azonosítójára |
| ULID, KSUID, Snowflake | Időrendezett | Nem natív típus | Ha 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.
- 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ó.
- Gyakori minta a belső
bigintelsődleges kulcs plusz egyuuidnyilvános azonosító unique indexszel, vagy egyszerűen UUID v7 elsődleges kulcs. - Supabase-en az
auth.users.idUUID. A profil tábla definíciójaid uuid primary key references auth.users(id) on delete cascade. - 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.
- A UUID v7 időbélyege információt szivárogtat. Ha a létrehozás ideje érzékeny, v4 kell.
- 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.
- Minden lekérdezésbe be kell írni a szűrést. Egy elfelejtett
where deleted_at is nulltörölt adatot mutat. Joinnál mindkét oldalra kell. - 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 nullfeltétellel. - Az idegen kulcs nem tud róla. Törölt szülőre élő gyerek mutat, a
cascadenem fut le. - Az index és a tábla duzzad, a lekérdezések egyre lassabbak.
- 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.
- 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.
- Valódi törlés plusz audit vagy archív tábla. Egy
before deletetrigger JSONB formában átmásolja a sort egytorolt_rekordtáblába. Az élő tábla tiszta marad. - Állapot mező, ha a törlés valójában üzleti állapot (archivált, lemondott). Ilyenkor az állapotnak neve és jelentése van.
- Ha mégis soft delete, akkor nézet az élő sorokra, RLS policy, részleges indexek és részleges unique kényszerek, plusz ütemezett végleges törlés a megőrzési idő után.
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
- 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. - Az entitásokat és a kapcsolatokat mondatokban írjuk le, kardinalitással (lásd 1.4), mielőtt SQL születik.
- Minden táblánál kimondjuk, ki olvashatja és ki írhatja, ebből lesz az RLS.
- Migrációs fájlba írunk. Nem a dashboardon kattintunk, és élő sémát nem módosítunk közvetlen SQL futtatással.
- 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.
- Sémaváltozás után újrageneráljuk a típusokat (
supabase gen types).
13.2 Sématervezési checklist
- Minden táblának van elsődleges kulcsa, a típusa tudatos döntés (lásd 12.3).
- A természetes kulcsokon
uniquekényszer van (email, sorszám a bérlőn belül, kapcsolótábla páros). - Minden kötelező oszlop
not null, a nullable oszlopoknál ismert, mit jelent a NULL. - Minden hivatkozás idegen kulcs, tudatos
on deleteviselkedéssel, és a hivatkozó oszlopon index van. - A tartomány és állapot szabályok
checkkényszerben élnek, nem csak a frontendben. - A 3NF a kiindulás. Minden eltérés indokolt és kommentben dokumentált (lásd 2.3).
- A pillanatkép értékek (ár, név a számlán) tudatosan, másolatként tárolva.
- Időpont
timestamptz, dátumdate, pénznumericvagy egész fillér, plusz pénznem. - Nincs lista szövegben, nincs
tag1, tag2, nincs EAV, nincs FK nélküli polimorf hivatkozás (lásd 4.). - JSONB csak indokolt helyen, és ha van, sémaellenőrzéssel (lásd 6.3).
- RLS bekapcsolva minden
publictáblán, policy műveletenként,(select auth.uid())formában, indexelt policy oszlopokkal. - Multi tenant rendszerben minden bérlői adatot tartalmazó táblán
tenant_id not null, és a policy erre szűr. created_at, és szükség esetén triggerrel karbantartottupdated_at.- A személyes adat jelölve (
comment on), a megőrzési és törlési szabály ismert. - A versenyhelyzetek kezelve (egyediség kényszerrel, készlet és létszám atomikus utasítással vagy zárral, lásd 3.7).
- A gyakori lekérdezésekhez index terv van, összetett indexnél tudatos oszlopsorrenddel (lásd 5.2).
13.3 Migráció írási checklist
- Egy logikai változás egy fájlban, beszédes névvel.
set lock_timeoutaz elején.- Index
create index concurrentlyformában (külön migrációban, mert tranzakción kívül fut). - Új FK és check
not valid, majdvalidate constraint. not nullnagy táblán check kényszeren keresztül.- A nem visszafelé kompatibilis változás expand és contract lépésekre bontva (lásd 7.3).
- Backfill kötegekben, idempotensen, nem a sémamigrációban egy nagy
updateutasítással. - Átnevezés és törlés csak akkor, ha egyetlen futó kód sem használja.
- A migráció újrafuttatható, vagy legalább egyértelműen hibázik (
if not existsott, ahol értelmes). - Új táblánál RLS és policy ugyanabban a migrációban.
security definerfüggvény privát sémában,set search_path = ''beállítással.- Éles méretű adatmásolaton vagy branchen lefuttatva, mért futási idővel.
- A visszaút leírva (javító migráció vagy PITR időpont).
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.
| Hiba | Miért történik | Javítás |
|---|---|---|
RLS kikapcsolva vagy using (true) policy | A gyors működés a cél, a hiba nem látszik | Policy műveletenként, valódi feltétellel, teszt két felhasználóval |
| Service role kulcs a kliensben | Megkerüli a jogosultsági hibát | Publishable kulcs a kliensen, service role csak szerveren |
| Hiányzó FK index | A Postgres nem teszi automatikusan | create index minden hivatkozó oszlopra |
float pénzre, timestamp időpontra | A modell az alapértelmezett típust választja | numeric, timestamptz |
Ellenőrzés select, majd insert, kényszer nélkül | Egyfelhasználós fejlesztői teszt | unique és on conflict, vagy zár |
| Olvasás, módosítás, írás két kliens hívásban | A supabase-js hívások nem közös tranzakcióban futnak | Atomikus update, vagy rpc() függvény |
| JSONB minden bizonytalan mezőre | Gyors prototípus | Oszlopok és kényszerek, JSONB csak a nyers külső adatra |
| Tömb oszlop kapcsolat helyett | Kevesebb tábla egyszerűbbnek tűnik | Kapcsolótábla |
| Soft delete részleges unique index nélkül | A mintaprojektekből átvett szokás | A 12.4 alatti alternatívák |
user_metadata alapú jogosultság | A 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ül | Működik, a kockázat nem látszik | Privát séma, set search_path = '' |
| Webhook feldolgozás deduplikáció nélkül | A szolgáltató tesztje egyszer küld | Inbox 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ül | Egyszerű sorrendnek tűnik | Outbox (lásd 8.4) |
| Offset lapozás | A legegyszerűbb API | Keyset lapozás (lásd 5.2) |
select * és teljes objektum visszaírása | Kényelmes ORM használat | Explicit oszlopok, részleges update |
| Migráció helyett közvetlen sémamódosítás | Az eszköz megengedi | Migrációs fájl, verziókezelés |
| Index nélküli RLS policy oszlop | A policy logikailag helyes | Index a tenant_id, user_id oszlopokra |
| Időzóna nélküli napi bontás | UTC alapértelmezés | at time zone 'Europe/Budapest' |
| Hiányzik az embedding modell neve | Csak a vektor tűnik adatnak | modell oszlop, újraszámolási terv |
| Szekvencia mint számlaszám | Kézenfekvőnek tűnik | Hézagmentes, zárolt számláló bérlőnként |
13.5 Lekérdezés és teljesítmény szabályok
- 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. - Az összetett index sorrendje egyenlőség, majd rendezés, végül tartomány.
- Indexelt oszlopon nincs függvény a
wherefeltételben, vagy van hozzá kifejezés index. - A lapozás keyset módszerrel történik.
- N+1 helyett join vagy egyetlen lekérdezés
inlistával (supabase-js beágyazott select,select('*, jelentkezes(*)')). - Szerver nélküli környezetből transaction pooler, prepared statement nélkül.
- 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
- Minden külső mellékhatás (email, fizetés, webhook kiküldés) at least once kézbesítésű és idempotens.
- Az adatbázis írás és az üzenetküldés együtt mindig outboxszal történik.
- A bejövő webhook inboxba kerül, aláírás ellenőrzéssel és sorrendfüggetlen feldolgozással.
- Különböző gépek óráira épülő, időbélyeg alapú utolsó író nyer logikát nem írunk.
- Kettős írás (adatbázis plusz kereső vagy cache) helyett CDC vagy outbox.
- 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
- Új projektnél rögzítjük az RPO és az RTO értékét.
- Ügyféladatot kezelő éles rendszernél PITR vagy legalább napi, szolgáltatón kívüli mentés kell.
- A Storage fájlok mentését külön tervezzük.
- A visszatöltési próba ütemezett és dokumentált.
- Romboló műveletet (
drop,truncate, tömegesdelete) é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
- Kétség esetén relációs, 3NF, kényszerekkel. A denormalizálás később könnyebb, mint a szétszedés.
- Kétség esetén egy Postgres. Külön kereső, vektor adatbázis, üzenetsor vagy cache csak mért szükséglet esetén kell. Postgresben van FTS, pgvector, pgmq, pg_cron, JSONB.
- Kétség esetén az adatbázis kényszeríti ki az invariánst, nem az alkalmazás (Codd 10. és 12. szabálya).
- Kétség esetén
timestamptz,numeric,not null, UUID v7 vagy identity. - Kétség esetén valódi törlés plusz archív tábla, nem soft delete.
- Kétség esetén expand és contract, kis rendszerben is, mert a régi és az új kód együttélése minden deploynál fennáll.
Források
- Bachman, Charles W. The Programmer as Navigator. Turing díj előadás, Communications of the ACM, 1973.
- Chamberlin, Donald D. és Boyce, Raymond F. SEQUEL. A Structured English Query Language. ACM SIGFIDET Workshop, 1974.
- Chen, Peter P. The Entity-Relationship Model. Toward a Unified View of Data. ACM Transactions on Database Systems, 1976.
- Codd, Edgar F. A Relational Model of Data for Large Shared Data Banks. Communications of the ACM, 1970.
- Codd, Edgar F. A relációs adatbázis kezelők szabályairól szóló cikkei. Computerworld, 1985.
- Selinger, Patricia G. és társai. Access Path Selection in a Relational Database Management System. ACM SIGMOD, 1979.
- Gray, Jim. The Transaction Concept. Virtues and Limitations. VLDB, 1981.
- Härder, Theo és Reuter, Andreas. Principles of Transaction Oriented Database Recovery. ACM Computing Surveys, 1983.
- Date, Chris J. An Introduction to Database Systems. 8. kiadás, Addison Wesley, 2003.
- Date, Chris J. és Darwen, Hugh. The Third Manifesto, 1995, illetve Databases, Types and the Relational Model, 3. kiadás, 2006.
- Date, Chris J., Darwen, Hugh és Lorentzos, Nikos. Temporal Data and the Relational Model. Morgan Kaufmann, 2002.
- Kent, William. A Simple Guide to Five Normal Forms in Relational Database Theory. Communications of the ACM, 1983.
- Kleppmann, Martin. Designing Data-Intensive Applications. O'Reilly, 2017.
- Kleppmann, Martin és Riccomini, Chris. Designing Data-Intensive Applications. 2. kiadás, O'Reilly, 2026.
- Cahill, Michael J., Röhm, Uwe és Fekete, Alan D. Serializable Isolation for Snapshot Databases. ACM SIGMOD, 2008.
- Karwin, Bill. SQL Antipatterns, Volume 1. 2. kiadás, Pragmatic Bookshelf, 2022.
- Winand, Markus. Use the Index, Luke (use-the-index-luke.com) és SQL Performance Explained, 2012.
- Sato, Danilo. ParallelChange. martinfowler.com, 2014.
- Reis, Joe és Housley, Matt. Fundamentals of Data Engineering. O'Reilly, 2022.
- Kimball, Ralph és Ross, Margy. The Data Warehouse Toolkit. 3. kiadás, Wiley, 2013.
- Linstedt, Dan és Olschimke, Michael. Building a Scalable Data Warehouse with Data Vault 2.0. Morgan Kaufmann, 2015.
- Dehghani, Zhamak. How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh. martinfowler.com, 2019, valamint Data Mesh. O'Reilly, 2022.
- Jones, Andrew. Driving Data Quality with Data Contracts. Packt, 2023.
- Bitol projekt (Linux Foundation). Open Data Contract Standard (ODCS).
- DAMA International. DAMA DMBOK. Data Management Body of Knowledge. 2. kiadás, 2017.
- ISO 8000 szabványsorozat (adatminőség) és ISO/IEC 25012 (adatminőségi modell).
- IETF RFC 9562. Universally Unique IDentifiers (UUIDs), 2024.
- PostgreSQL Global Development Group. PostgreSQL 18 dokumentáció és kiadási jegyzetek, 2025.
- Supabase dokumentáció (Row Level Security, Supavisor, mentés és PITR, branching).
- pgvector projekt dokumentációja (HNSW, IVFFlat, iteratív keresés).
Á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.