</> SQL & Data Modeling

0 / 20 section completed
Section 00

SQL alapok Parquet

Az SQL (Structured Query Language) a data engineering legfontosabb alapnyelve – szinte minden adatfeldolgozási folyamat valamilyen formában SQL-t használ. Legyen szó adatkinyerésről, transzformációról vagy analitikáról, az SQL az a közös nyelv, amit minden adatbázis, data warehouse és big data rendszer ért.

Ebben a kurzusban a WebShop Pro projekten keresztül tanulod meg az SQL alapokat és az adatmodellezést. Egy fiktív webáruház teljes adatfolyamát fogjuk felépíteni: a táblák létrehozásától kezdve az összetett analitikai lekérdezéseken át a star schema adatmodellig. Minden fogalmat közvetlenül gyakorlati kódpéldákon keresztül fogsz megérni.

A kurzus három nagy blokkra épül: először az SQL lekérdező nyelv alapjait vesszük át (SELECT, JOIN, aggregáció, window functions), majd az adatmódosítást és a tranzakciókezelést vizsgáljuk, végül rátérünk az analitikai adatmodellezésre (star schema, SCD, normalizálás). A végére képes leszel önállóan felépíteni egy teljes analitikai adatmodellt.

Tartalom

SELECT, JOIN, Aggregáció, Window Functions, CTE, DML/DDL, Tranzakciók, Star Schema, SCD, Normalizálás, Parquet, Teljes analitikai modell

Projekt

Felépítjük a WebShop Pro teljes analitikai adatmodelljét.

Szójegyzék

CTE · SCD · Star Schema

Section 01

Táblák és adattípusok

A relációs adatbázisok az adatokat táblákban (tables) tárolják, ahol minden tábla egy adott entitást – például vásárlókat, termékeket vagy rendeléseket – ír le. Minden táblának van egy sémája (schema), ami meghatározza, milyen oszlopokból áll és milyen adattípusú értékeket tartalmazhatnak azok. A séma a tervrajz, a sorok (rows) pedig a konkrét adatok.

A fenti kódrészlet egy customers táblát hoz létre a WebShop Pro rendszerben. A CREATE TABLE utasítás határozza meg a tábla nevét és oszlopait. Minden oszlopnak meg kell adni az adattípust: INT egész számokhoz, VARCHAR(n) szövegekhez, DECIMAL(p,s) pénzértékekhez, TIMESTAMP dátum+idő kombinációkhoz, BOOLEAN pedig igaz/hamis értékekhez. A PRIMARY KEY biztosítja, hogy minden vásárló azonosítója egyedi legyen.

A NOT NULL megkötés garantálja, hogy a név mező nem maradhat üresen, az UNIQUE pedig megakadályozza, hogy két vásárló ugyanazzal az e-mail címmel regisztráljon. A DEFAULT kulcsszó automatikus értéket rendel: a created_at a rekord létrehozásának időpontját kapja, is_active pedig alapértelmezetten igaz lesz.

Gyakori hiba: ne felejts el PRIMARY KEY-t definiálni – enélkül nehéz lesz a táblát más táblákhoz kapcsolni, és az adatbázis sem tudja hatékonyan indexelni a sorokat.

TípusLeírásPélda
INTEgész számcustomer_id
VARCHAR(n)Szövegname
DECIMAL(p,s)Tizedes számprice
TIMESTAMPDátum és időcreated_at
BOOLEANIgaz/hamis értékis_active
[1]
-- Create customers table
CREATE TABLE customers (
    customer_id   INT PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    email         VARCHAR(255) UNIQUE,
    city          VARCHAR(50),
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_active     BOOLEAN DEFAULT TRUE
);
Output:
Query OK, 0 rows affected
Section 02

INSERT, SELECT, WHERE

Az INSERT INTO utasítás új sorokat ad hozzá egy táblához – ebben az esetben három vásárlót szúrunk be a customers táblába. Minden érték sorrendben kell, hogy megfeleljen a tábla oszlopainak: azonosító, név, e-mail, város, regisztráció dátuma és aktív státusz. A VALUES kulcsszó után vesszővel elválasztva több sort is felvihetünk egyetlen utasítással.

A SELECT az SQL leggyakoribb utasítása – ezzel kérdezünk le adatokat. A SELECT name, city azt mondja meg, hogy csak a név és a város oszlopot szeretnénk látni. A FROM customers határozza meg a forrástáblát, a WHERE city = 'Budapest' pedig egy szűrőfeltétel: csak azokat a sorokat adja vissza, ahol a város pontosan Budapest. Az eredményből jól látszik, hogy két budapesti vásárlónk van.

Ez a két művelet – adatfelvitel és adatlekérdezés – a data engineering mindennapi alapja. Az ETL folyamatokban is ezek kellenek: a nyers adatokat először betöltjük (INSERT), utána transzformáljuk és lekérdezzük (SELECT). A WHERE feltétel kulcsfontosságú, mert ritkán van szükségünk az összes adatra – általában egy részét akarjuk csak feldolgozni.

Praktikus tipp: az INSERT-nél mindig ellenőrizd, hogy az értékek száma megegyezik-e az oszlopok számával – a leggyakoribb hiba az, amikor egy oszlopot kihagysz vagy feleslegesen beírsz.

[2]
INSERT INTO customers VALUES
    (1, 'Kovacs Anna', 'anna@email.hu', 'Budapest', '2024-01-15', TRUE),
    (2, 'Nagy Peter', 'peter@email.hu', 'Debrecen', '2024-02-20', TRUE),
    (3, 'Szabo Kati', 'kati@email.hu', 'Budapest', '2024-03-10', FALSE);

SELECT name, city FROM customers WHERE city = 'Budapest';
Output:
name        | city
------------+----------
Kovacs Anna | Budapest
Szabo Kati  | Budapest

2 rows
Section 03

ORDER BY, LIMIT, DISTINCT

A ORDER BY utasítás rendezni tudja a lekérdezés eredményét egy vagy több oszlop alapján. Alapértelmezetten növekvő (ASC) sorrendben rendez, de a DESC kulcsszóval csökkenő sorrendet is kérhetünk. A példában a legdrágább termékeket keressük, ezért az árat csökkenő sorrendbe rendezzük, a LIMIT 5 pedig csak az első 5 sort tartja meg – így megkapjuk a „top 5 legdrágább termék" listát.

A DISTINCT kulcsszó egyedi (ismétlődés nélküli) értékeket ad vissza. Amikor a SELECT DISTINCT category FROM products lekérdezést futtatjuk, az adatbázis minden kategóriát csak egyszer listáz – akkor is, ha több száz termék tartozik ugyanabba a kategóriába. Ez nagyon hasznos, ha meg akarjuk tudni, milyen értékek szerepelnek egy oszlopban, vagy ha egyedi értékek számát akarjuk megtudni (a COUNT(DISTINCT category) kombinációval).

A LIMIT (vagy TOP SQL Serverben) a data engineeringben az egyik leggyakrabban használt eszköz az adatfeltárás (data exploration) során. Mielőtt egy több millió soros táblán futtatsz nehéz lekérdezést, érdemes LIMIT 10-nel megnézni, hogy az adatok struktúrája megfelelő-e. A ORDER BY és LIMIT együttes használata tökéletes „top N" jellegű kérdések megválaszolására: ki a legjobb vevő, melyik a legnépszerűbb termék, stb.

Figyelem: ha ORDER BY nélkül használod a LIMIT-et, az adatbázis tetszőleges sorokat adhat vissza – a sorrend nem garantált explicit rendezés nélkül!

[3]
SELECT name, price FROM products ORDER BY price DESC LIMIT 5;

SELECT DISTINCT category FROM products ORDER BY category;
Output:
name            | price
----------------+-------
Laptop Pro 16   | 549.99
Phone Ultra     | 449.99

Audio, Laptop, Phone, Tablet
Section 04

Aggregáció: GROUP BY, HAVING

Az aggregáció azt jelenti, hogy több sor adataiból egyetlen összesített értéket készítünk. A leggyakoribb aggregáló függvények: COUNT(*) (sorok száma), SUM() (összeg), AVG() (átlag), MIN() és MAX() (minimum és maximum). Ezek önmagukban az egész táblára vonatkoznak, de a GROUP BY utasítással csoportonként tudjuk alkalmazni őket.

A példában kategóriánként számoljuk ki a rendelések számát (COUNT(*)), az összes bevételt (SUM(oi.quantity * oi.unit_price)) és az átlagos egyséárat (AVG(oi.unit_price)). A GROUP BY p.category mondja meg, hogy a products tábla category oszlopa szerint kell csoportosítani. A ROUND(..., 2) két tizedesjegyre kerekít, hogy az eredmény olvashatóbb legyen.

A HAVING olyan, mint a WHERE, de a csoportosítás után szűr. A WHERE még az aggregáció előtt fut (egyes sorokat szűri), a HAVING pedig a már aggregált eredményeken. A példában HAVING revenue > 1000 csak azokat a kategóriákat tartja, ahol a bevétel meghaladja az 1000-et. A végén ORDER BY revenue DESC csökkenő sorrendbe rendezi az eredményt.

Gyakori hiba: ha a SELECT-ben egy nem aggregált oszlop szerepel (pl. p.category), annak szerepelnie kell a GROUP BY-ban is – különben az adatbázis nem tudja, melyik csoport értékét mutassa.

[4]
SELECT p.category, COUNT(*) AS order_count,
       SUM(oi.quantity * oi.unit_price) AS revenue,
       ROUND(AVG(oi.unit_price), 2) AS avg_price
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.category
HAVING revenue > 1000
ORDER BY revenue DESC;
Output:
category | order_count | revenue  | avg_price
---------+-------------+----------+----------
Laptop   |          45 | 24749.55 |    549.99
Phone    |          62 | 18949.38 |    399.99
Section 05

JOIN: Táblák összekapcsolása

A JOIN az SQL egyik legfontosabb művelete: táblákat kapcsol össze közös kulcsok alapján. A való életben az adatok szét vannak osztva több táblába (ez a normalizálás lényege), de a lekérdezéskor össze kell fűzni őket. A WebShop Pro rendszerben például a vásárlók adatai a customers táblában vannak, a rendeléseik a orders táblában, a tételek az order_items táblában, és a termékek a products táblában – a JOIN segítségével mindezt egyetlen eredménytáblává fűzhetjük össze.

A fenti lekérdezés négy táblát kapcsol össze. Az INNER JOIN csak azokat a sorokat tartja meg, amelyekhez van pár a másik táblában. Az ON feltétel adja meg, mely oszlopok egyezését vizsgálja: a customers.customer_id összeköti a rendelésekkel, a orders.order_id a tételsorokkal, és a order_items.product_id a termékekkel. A c, o, oi, p aliasok (rövidítések) teszik olvashatóbbá a kódot – ahelyett, hogy mindenhol kiírnánk a teljes táblanevet.

A lekérdezés eredménye megmutatja, melyik vásárló melyik terméket vette, mennyiért és milyen mennyiségben. A line_total egy számított oszlop: oi.quantity * oi.unit_price – azaz a tétel teljes értéke. Ezt az adatbázis „menet közben" számolja ki, nem kell külön tárolni.

Fontos szabály: ha LEFT JOIN-t használsz, a bal oldali tábla összes sora megmarad – még akkor is, ha a jobb oldalon nincs egyezés (ilyenkor NULL értékeket kapsz). Ezt gyakran használják arra, hogy megtaláljuk azokat a vásárlókat, akik még nem rendeltek semmit.

JOIN típusLeírás
INNER JOINCsak azok a sorok jelennek meg, amelyek mindkét táblában egyeznek.
LEFT JOINA bal oldali tábla összes sora megmarad, a jobb oldali táblából pedig az egyező sorok kerülnek mellé.
FULL JOINMindkét tábla összes sora megmarad; ahol nincs pár, ott NULL értékek jelennek meg.
[5]
SELECT c.name AS customer, p.name AS product,
       oi.quantity, oi.unit_price,
       oi.quantity * oi.unit_price AS line_total
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
ORDER BY c.name;
Output:
customer    | product       | quantity | unit_price | line_total
------------+---------------+----------+------------+-----------
Kovacs Anna | Laptop Pro 16 |        1 |     549.99 |    549.99
Section 06

Subquery és CTE (WITH)

A CTE (Common Table Expression) a WITH kulcsszóval definiált névleges allekérdezés, amely olvashatóbbá teszi az összetett SQL kódot. Ahelyett, hogy egy hatalmas, egymásba ágyazott lekérdezést írnánk, a CTE-vel logikai blokkokra bonthatjuk a kódot – mintha változókat hoznánk létre a lekérdezésen belül. A CTE eredménye csak az azt követő fő lekérdezésben használható, nem tárolódik el tartósan.

A példában a customer_revenue CTE kiszámítja minden vásárló teljes költését úgy, hogy a customers, orders és order_items táblákat összekapcsolja és csoportosít. Ezt az eredményt a fő SELECT utasítás úgy használja, mintha egy valódi tábla lenne – de valójában csak egy átmeneti, memóriában lévő eredményhalmaz. A WHERE feltétel egy allekérdezéssel (subquery) szűri ki az átlag felettieket: SELECT AVG(total_spent) FROM customer_revenue.

A CTE-k különösen fontosak a data engineeringben, ahol a lekérdezések gyakran több száz sorosak. A WITH blokkok segítségével a kód modularizálható: minden transzformációs lépés saját CTE-be kerülhet (stg_orders, int_customer, mart_revenue stb.), ami megkönnyíti a hibakeresést és a karbantartást is.

Tipp: ha egy allekérdezést több helyen is használsz, mindenképpen emeld ki CTE-be – nemcsak olvashatóbb lesz, de bizonyos adatbázisok a CTE eredményt cache-elik, így gyorsabb is lehet.

Tipp

Ha egy részlekérdezést több helyen használsz, vagy nehezen olvashatóvá válik a query, emeld ki CTE-be.

[6]
WITH customer_revenue AS (
    SELECT c.customer_id, c.name,
           SUM(oi.quantity * oi.unit_price) AS total_spent
    FROM customers c
    JOIN orders o ON c.customer_id = o.customer_id
    JOIN order_items oi ON o.order_id = oi.order_id
    GROUP BY c.customer_id, c.name
)
SELECT name, total_spent
FROM customer_revenue
WHERE total_spent > (SELECT AVG(total_spent) FROM customer_revenue)
ORDER BY total_spent DESC;
Output:
name         | total_spent
-------------+------------
Kovacs Anna  |    1349.97
Section 07

Window Functions

A window function (ablakfüggvény) olyan függvény, amely soronként eredményt ad, de a számítás egy kapcsolódó sorhalmazt (az „ablakot") vesz figyelembe – ellentétben a GROUP BY-val, amely a sorokat összevonja egyetlen értékké. Ez azt jelenti, hogy minden sor megmarad az eredményben, de kiegészül egy számított értékkel (pl. futó összeg, sorszám, rangsor).

A példában a SUM(SUM(total_amount)) OVER (ORDER BY ...) egy futó összeget (running total) számol havi bontásban. A belső SUM(total_amount) a GROUP BY miatt havonta összegzi a bevételelt, a külső SUM(...) OVER (...) pedig ezeket a havi összegeket halmozottan összeadja. Januárban a futó összeg megegyezik a havi bevétellel, februárban már a január+február összeg jelenik meg, és így tovább.

Az OVER() határozza meg az ablakot: ORDER BY DATE_TRUNC('month', order_date) mondja meg, hogy milyen sorrendben halmozunk. A DATE_TRUNC('month', ...) függvény a dátumot havi szintre kerekíti, így minden hónap egyetlen csoportot képez. A PARTITION BY kulcsszóval további csoportosítás is lehetséges – például termékkategóriánkénti futó összeg számításához.

Ablakfüggvények a data engineeringben: a ROW_NUMBER(), RANK(), LAG(), LEAD() és SUM() OVER mind-mind elengedhetetlenek az analitikai ETL folyamatokban, például a deduplikáció, az SCD Type 2 kezelés vagy a cohort elemzések során.

[7]
SELECT DATE_TRUNC('month', order_date) AS month,
       SUM(total_amount) AS revenue,
       SUM(SUM(total_amount)) OVER (ORDER BY DATE_TRUNC('month', order_date)) AS running_total
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;
Output:
month   | revenue  | running_total
--------+---------+---------------
2024-01 | 2349.96 |       2349.96
2024-02 | 3199.94 |       5549.90
2024-03 | 4299.91 |       9849.81
Section 08

CASE WHEN és transzformáció

A CASE WHEN az SQL feltételes kifejezése – hasonló a programozási nyelvek if-else szerkezetéhez. Lehetővé teszi, hogy egy oszlop értékét feltételek alapján alakítsuk ki. A példában a vásárlókat szegmentáljuk aszerint, mennyit költöttek: VIP (2000 felett), Regular (500-2000 között) vagy Occasional (500 alatt).

A CASE WHEN sorrendben értékeli ki a feltételeket, és az első igaz feltételnek megfelelő THEN ágat adja vissza. A végén az ELSE az a „kapunyitás" ág, amely akkor fut, ha egyik feltétel sem teljesül. A kódban a SUM(oi.quantity * oi.unit_price) eredményét vizsgáljuk – ez a teljes költés összege vásárlónként.

A COALESCE(SUM(...), 0) függvény NULL értékeket kezel: ha egy vásárló nem rendelt semmit, a LEFT JOIN miatt a SUM eredménye NULL lenne – a COALESCE ezt nullára cseréli. Ez azért fontos, mert a CASE WHEN összehasonlítása NULL értékkel mindig hamis eredményt ad, és a vásárló „eltűnhetne" a szegmentálásból.

A LEFT JOIN-t használunk (nem INNER JOIN-t), hogy azok a vásárlók is megjelenjenek az eredményben, akik még nem rendeltek – náluk a total_spent nulla lesz, és az „Occasional" szegmensbe kerülnek.

[8]
SELECT c.name,
       COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS total_spent,
       CASE WHEN SUM(oi.quantity * oi.unit_price) >= 2000 THEN 'VIP'
            WHEN SUM(oi.quantity * oi.unit_price) >= 500 THEN 'Regular'
            ELSE 'Occasional' END AS segment
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY c.customer_id, c.name
ORDER BY total_spent DESC;
Output:
name         | total_spent | segment
-------------+------------+----------
Kovacs Anna  |    1349.97 | Regular
Szabo Kati   |       0.00 | Occasional
Section 09

DML: MERGE, UPSERT

A MERGE (más néven upsert) a data engineering egyik legfontosabb művelete: egyetlen utasításban frissít vagy beszúr sortokat attól függően, hogy már léteznek-e a céltáblában. Ez különösen fontos az ETL folyamatokban, ahol a napi adatbetöltés során az új rekordokat be kell szúrni, a meglévőket pedig frissíteni kell.

A példa három lépést mutat. Az első az UPDATE, amely az SCD Type 2 (Slowly Changing Dimension) mintára lezárja a régi rekordot: a valid_to mezőt az aktuális dátumra állítja, az is_current flag-et pedig hamisra. A második lépés egy INSERT, amely új sort szúr be az új értékekkel (pl. a vásárló új városa: Szeged). Így a dimenziótáblában a vásárlónak két sora lesz – a régi (lezárt) és az új (aktuális) –, és a történeti változások nyomon követhetők.

A harmadik lépés a MERGE INTO utasítás: a USING határozza meg a forrásadatokat, az ON a kapcsolási feltételt. Ha talál egyezést (WHEN MATCHED), akkor UPDATE-eli a meglévő sort; ha nem talál (WHEN NOT MATCHED), akkor INSERT-tel új sort hoz létre. Ez az „insert or update" logika iparági szabvány a növekményes (incremental) adatbetöltésben.

Praktikus tipp: a MERGE előtt mindig teszteld le egy SELECT utasítással, hány sort fog érinteni – egy rossz ON feltétel miatt az egész táblát felülírhatod!

[9]
-- SCD Type 2: Close old + insert new
UPDATE dim_customer 
SET valid_to = CURRENT_DATE, is_current = FALSE
WHERE customer_id = 1 AND is_current = TRUE;

INSERT INTO dim_customer VALUES
(101, 1, 'Kovacs Anna', 'Szeged', 'Regular', CURRENT_DATE, '9999-12-31', TRUE);

-- MERGE (upsert)
MERGE INTO products AS target
USING (VALUES (10, 'New Tablet', 'Tablet', 299.99, 50)) AS src (id, name, cat, price, stock)
ON target.product_id = src.id
WHEN MATCHED THEN UPDATE SET price = src.price
WHEN NOT MATCHED THEN INSERT VALUES (src.id, src.name, src.cat, src.price, src.stock);
Output:
Query OK, 3 rows affected
Section 10

DDL, Indexek, Partitioning

A DDL (Data Definition Language) a táblaszerkezetet módosító utasítások gyűjtőneve: ide tartozik a CREATE TABLE, ALTER TABLE, DROP TABLE és az CREATE INDEX. Míg a DML (INSERT, UPDATE, SELECT) az adatokkal dolgozik, a DDL magát a struktúrát definiálja – ez az adatbázis „alapja", amit az adatbetöltés előtt kell megtervezni.

Az indexek (indexes) jelentősen felgyorsítják a lekérdezéseket azáltal, hogy az adatbázisnak nem kell végigpásztáznia az egész táblát. A CREATE INDEX idx_orders_customer ON orders(customer_id) egy indexet hoz létre a customer_id oszlopra – ettől kezdve a WHERE customer_id = 1 jellegű lekérdezések sokkal gyorsabbak lesznek. Hasonlóan, a CREATE INDEX idx_orders_date ON orders(order_date) a dátum alapú szűréseket gyorsítja, ami gyakori az analitikai jelentésekben.

A particionálás (partitioning) nagy táblák esetén elengedhetetlen: a táblát logikai részekre (partíciókra) bontja, például hónap vagy régió szerint. Így a lekérdezések csak az érintett partíciókat olvassák, nem az egész táblát. A példa az information_schema.tables rendszertáblából kérdezi le a táblák méretét – ez hasznos a monitoring során, hogy lásd, melyik tábla fogyasztja a legtöbb tárhelyet.

Fontos: az indexek felgyorsítják az olvasást, de lassítják az írást (mert minden INSERT/UPDATE/DELETE esetén az indexet is frissíteni kell). Csak ott hozz létre indexet, ahol tényleg gyakori a szűrés!

[10]
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_date ON orders(order_date);

SELECT table_name, table_rows,
       ROUND(data_length/1024/1024, 2) AS size_mb
FROM information_schema.tables
WHERE table_schema = 'webshop_pro' ORDER BY data_length DESC;
Output:
table_name  | table_rows | size_mb
------------+-----------+--------
orders      |    500000 | 125.50
order_items |   1850000 | 342.10
Section 11

Tranzakciók: ACID

Az ACID rövidítés a megbízható tranzakciókezelés négy alapelve: Atomicity (atomosság), Consistency (konzisztencia), Isolation (izoláció) és Durability (tartósság). Ezek garantálják, hogy az adatbázis még hiba, összeomlás vagy párhuzamos hozzáférés esetén is helyes és tartós állapotban maradjon – kritikus követelmény minden pénzügyi és üzleti tranzakciónál.

A példa egy egyszerű átutalást mutat: az egyik számláról levonunk 500 dollárt, a másikra feltöltjük ugyanennyit. A BEGIN TRANSACTION elindítja a tranzakciót, a két UPDATE módosítja az egyenlegeket, a COMMIT pedig véglegesíti a változtatásokat. Ha a két UPDATE között hiba történik (pl. áramkimaradás), az Atomicity miatt az adatbázis automatikusan visszavonja (rollback) az első UPDATE-et is – tehát nem történik meg, hogy az egyik számláról levontuk a pénzt, de a másikra nem került rá.

A ROLLBACK utasítással manuálisan is visszavonható egy tranzakció – ezt gyakran használják hiba esetén, hogy az adatbázis ne maradjon inkonzisztens állapotban. A data engineeringben a tranzakciók különösen fontosak a dimenziótáblák frissítésekor (SCD Type 2), ahol a régi sor lezárása és az új sor beszúrása egyetlen atomi műveletként kell, hogy történjen.

Fontos: a modern big data rendszerek (pl. Spark, Kafka) gyakran csak eventual consistency-t garantálnak az ACID helyett – a tranzakciós garanciák a relációs adatbázisok erősségei.

TulajdonságLeírás
AtomicityA tranzakció vagy teljes egészében lefut, vagy semmi nem marad belőle.
ConsistencyA tranzakció után az adatbázis érvényes állapotban marad.
IsolationA párhuzamos tranzakciók nem zavarják össze egymás köztes állapotait.
DurabilityA véglegesített adatok tartósan tárolódnak.
[11]
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
Output:
Query OK, COMMIT successful
Section 12

VIEW, UDF

A VIEW (nézet) egy mentett lekérdezés, amit úgy használhatsz, mintha egy valódi tábla lenne. Nem tárol fizikailag adatot – minden alkalommal, amikor lekérdezed, a mögötte lévő SQL kód fut le. Ez nagyon hasznos az ismétlődő lekérdezések egyszerűsítésére, az adathozzáférések szabályozására és az üzleti logika központosítására.

A példában a vw_customer_orders nézet minden vásárló rendelésszámát és teljes költését (lifetime value) tartalmazza. A LEFT JOIN biztosítja, hogy a rendeléstelen vásárlók is megjelennek – náluk a COALESCE nullát ad a lifetime_value helyett. A nézet létrehozása után a SELECT * FROM vw_customer_orders WHERE order_count > 3 már sokkal egyszerűbb, mint az eredeti több táblás JOIN lekérdezés.

Az UDF (User Defined Function) egyedi függvény, amelyet te írsz és regisztrálsz az adatbázisban. Például egy calculate_discount(category, amount) függvényt létrehozva a kedvezényszámítási logikát egy helyen tarthatod, és minden lekérdezésből meghívhatod. A data engineeringben az UDF-ek segítenek a komplex üzleti szabályok (pl. adószámítás, konverzió) kiszervezésében.

Praktikus tipp: a nézeteket rétegenként érdemes szervezni – staging nézetek a nyers adatokhoz, intermediate nézetek a transzformált adatokhoz, mart nézetek a végleges riportokhoz. Ezt a „view-on-view" mintát az Section 18-ban részletesen is bemutatjuk.

[12]
CREATE VIEW vw_customer_orders AS
SELECT c.customer_id, c.name,
       COUNT(o.order_id) AS order_count,
       COALESCE(SUM(o.total_amount), 0) AS lifetime_value
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name;

SELECT * FROM vw_customer_orders WHERE order_count > 3;
Output:
customer_id | name        | order_count | lifetime_value
------------+-------------+-------------+---------------
1           | Kovacs Anna |           5 |       1349.97
Section 13

Adatmodellezés alapjai

Az adatmodellezés a folyamat, amelynek során a valós világ entitásait (vásárlók, termékek, rendelések) és kapcsolataikat adatbázis-struktúrává alakítjuk. Három szinten dolgozunk: a konceptuális szint az üzleti fogalmakat és kapcsolataikat írja le (üzleti elemzőknek), a logikai szint már táblák, oszlopok és kulcsok formájában jeleníti meg ezeket (adatmodellezőknek), a fizikai szint pedig az implementáció részleteit (indexek, partíciók, adattípusok) határozza meg (DBA-knak és data engineereknek).

A WebShop Pro rendszerben a fő entitások: Customers (vásárlók), Products (termékek), Orders (rendelések), Order Items (tételsorok), Categories (kategóriák), Payments (fizetések), Shipments (szállítások) és Reviews (értékelések). Ezek között a kapcsolat típusos: egy vásárló több rendelést adhat le, egy rendelés több tételt tartalmazhat, egy termék egy kategóriához tartozik.

A jó adatmodell alapja a helyes kapcsolatok meghatározása: 1:1 (egy ügyfél egy profil), 1:N (egy vásárló több rendelés), N:M (több termék több kategóriában – ezt köztáblával oldjuk meg). A kulcsok (primary key, foreign key) biztosítják a referenciális integritást – tehát nem lehet olyan rendelés, amelyhez nem létezik vásárló.

A legnagyobb hiba: rögtön a fizikai szinttel kezdeni, anélkül, hogy a konceptuális és logikai modellt tisztáztad volna. Mindig először értsd meg az üzleti logikát!

SzintLeírásKinek hasznos
KonceptuálisÜzleti entitások és kapcsolataikÜzleti elemzők
LogikaiTáblák, oszlopok, kulcsokAdatmodellezők
FizikaiIndexek, partíciók, tárolási döntésekDBA-k és data engineerek
WebShop Pro

Customers, Products, Orders, Order Items, Categories, Payments, Shipments, Reviews.

Section 14

Star Schema

A Star Schema (csillagséma) az analitikai adatmodellezés leggyakoribb mintája. A középen helyezkedik el a fact tábla (tények táblája), amely a mérhető eseményeket tárolja (pl. rendelések, fizetések, kattintások), és körülötte a dimenzió táblák (dim) adják meg a kontextust (kicsoda, mi, mikor, hol). A sémának a neve azért „csillag", mert a fact tábla és a dimenziók kapcsolata csillag alakú mintát ad az ER diagramon.

A fenti kódban a fact_orders a központi fact tábla: rendelési tételeket tárol, és idegen kulcsokkal (foreign key) hivatkozik a dimenziókra – customer_sk, product_sk, date_sk. A dimenziók (dim_customer, dim_product, dim_date) a leíró információkat tartalmazzák. A mértékek (metrics) – quantity, unit_price, net_amount – a fact táblában vannak, mert ezek azok a számok, amiket aggregálni akarunk (összeg, átlag, stb.).

Fontos különbség: a dimenziók surrogate key-t (helyettes kulcs, sk) használnak, nem az eredeti üzleti azonosítót. Ez azért van, mert az SCD Type 2 miatt egy üzleti entitásnak (pl. vásárló) több sora lehet a dimenzióban – a surrogate key egyedileg azonosítja mindegyiket. A REFERENCES megkötés biztosítja a referenciális integritást a fact és a dimenziók között.

A star schema előnye: a lekérdezések egyszerűek és gyorsak – csak a fact táblát kell JOIN-olni a szükséges dimenziókkal, nincs bonyolult többutas kapcsolat. Ezt a mintát használják a Snowflake, BigQuery, Redshift és a legtöbb modern data warehouse.

[14]
-- Fact table
CREATE TABLE fact_orders (
    order_sk    BIGINT PRIMARY KEY,
    customer_sk BIGINT REFERENCES dim_customer(sk),
    product_sk  BIGINT REFERENCES dim_product(sk),
    date_sk     BIGINT REFERENCES dim_date(sk),
    quantity    INT,
    unit_price  DECIMAL(10,2),
    net_amount  DECIMAL(10,2)
);

-- Dimension: dim_customer
CREATE TABLE dim_customer (
    sk BIGINT PRIMARY KEY, customer_id INT,
    name VARCHAR(100), city VARCHAR(50),
    segment VARCHAR(20), is_current BOOLEAN
);
Output:
Query OK, 4 tables created
Section 15

SCD: Lassan változó dimenziók

Az SCD (Slowly Changing Dimension) stratégiák azt írják le, hogyan kezeljük a dimenziótáblákban az idővel változó adatokat. „Lassan változó" alatt azt értjük, hogy a változások ritkák (pl. a vásárló átköltözik egy másik városba), de nyomon kell követni őket – az analitikai jelentésekben gyakran kell tudni, hogy egy adott időpontban mi volt a vásárló címe vagy ügyfélszegmense.

A SCD Type 2 a leggyakoribb stratégia: minden változásnál új sort szúrunk be érvényességi időintervallummal (valid_from, valid_to) és egy is_current flaggel. A példában Kovacs Anna Budapestről Szegedre költözött: az UPDATE lezárja a régi sort (valid_to = '2024-06-01', is_current = FALSE), az INSERT pedig új sort hoz létre az új várossal és nyitott időintervallummal (valid_to = '9999-12-31'). Így az eredményben két sor látható: a régi (Budapest, lezárt) és az új (Szeged, aktuális).

A 9999-12-31 az úgynevezett „végtelen jövő" dátum – ezt használjuk, amíg a sor aktuális. Amikor változás történik, ezt felülírjuk a tényleges záródátummal. A SELECT utasítás a végén ellenőrzi az eredményt: mindkét sor megjelenik, és a is_current flag egyértelműen mutatja, melyik az érvényes.

Gyakori buktató: az SCD Type 2 implementálásakor gondoskodni kell arról, hogy a fact táblában a megfelelő surrogate key hivatkozzon a dimenzió megfelelő verziójára – különben a riportok téves kontextusban mutatják az adatokat.

SCD típusStratégia
Type 0Az érték nem változik, a korábbi állapotot megőrizzük.
Type 1Felülírjuk a régi értéket, nem tartunk történetet.
Type 2Új sort szúrunk be érvényességi időintervallummal.
Type 3A régi és az új érték külön oszlopban szerepel.
[15]
-- SCD Type 2
UPDATE dim_customer SET valid_to = '2024-06-01', is_current = FALSE
WHERE customer_id = 1 AND is_current = TRUE;

INSERT INTO dim_customer VALUES
(101, 1, 'Kovacs Anna', 'Szeged', 'Regular', '2024-06-01', '9999-12-31', TRUE);

SELECT customer_id, name, city, valid_from, is_current
FROM dim_customer WHERE customer_id = 1;
Output:
customer_id | name       | city    | valid_from | is_current
------------+-----------+---------+------------+----------
1           | Kovacs Anna| Budapest| 2024-01-15 | FALSE
1           | Kovacs Anna| Szeged  | 2024-06-01 | TRUE
Section 16

Normalizálás

A normalizálás az a folyamat, amely során az adatmodellt úgy szervezzük át, hogy minimalizáljuk a redundanciát és elkerüljük az anomáliákat (beszúrási, módosítási, törlési). A normálformák (NF) szabályrendszerek: az 1NF megköveteli, hogy minden mező atomi (egy értéket tartalmazzon, ne például egy listát), a 2NF kiküszöböli a részleges függőségeket (összetett kulcs esetén minden nem kulcs mező a teljes kulcstól függjön), a 3NF pedig megszünteti a tranzitív függőségeket (nem kulcs mezők ne függjenek egymástól).

A gyakorlatban a normalizálás azt jelenti, hogy a terméknév, kategória és ár nem szerepel minden rendelési tételben külön, hanem egy külön products táblában tároljuk őket, és a tételek csak egy product_id hivatkozással hivatkoznak rá. Ha a termék ára változik, csak egy helyen kell frissíteni – ez a normalizálás legnagyobb előnye. Normalizálatlan modellnél ugyanezt százával vagy millióval kellene frissíteni.

A denormalizálás a normalizálás ellentéte: szándékosan engedünk a redundanciából az olvasási teljesítmény javítása érdekében. Az analitikai rendszerekben (OLAP) gyakori, mert a jelentések gyors lekérdezéseket igényel – a star schema például denormalizált modell, ahol a dimenzióadatok (pl. város, szegmens) közvetlenül elérhetők a fact táblából, bonyolult JOIN nélkül.

Szabály: OLTP rendszerekben (tranzakciós, pl. webshop rendelésfelvétel) normalizálj (3NF), OLAP rendszerekben (analitikai jelentések) denormalizálj (star schema). A kettő között az ETL folyamat hidat képez.

NFSzabály
1NFMinden mező atomi, vagyis egyetlen értéket tartalmaz.
2NFNincs részleges függőség összetett kulcs esetén.
3NFNincs tranzitív függőség a nem kulcs mezők között.
Mikor mit?

OLTP rendszerekben jellemzően normalizált modellt, OLAP rendszerekben gyakran denormalizált star schemát használunk.

Section 17

Parquet és DuckDB Parquet

A Parquet egy oszlopos tárolási formátum, amelyet az Apache projekt fejlesztett. A hagyományos sor-alapú formátumokkal (pl. CSV) ellentétben a Parquet oszloponként tárolja az adatokat, ami két hatalmas előnnyel jár: (1) ha csak néhány oszlopra van szükséged, nem kell az összeset beolvasni, és (2) az azonos típusú adatok jobban tömöríthetők. Ez a data engineering alaptechnológiája – a legtöbb modern data lake Parquet fájlokat használ.

A DuckDB egy beágyazható, in-process analitikai adatbázis – olyan, mint az „SQLite analitikai feladatokra". Nem kell szerver telepíteni, egy Python importálással (import duckdb) azonnal használható, és natívan képes Parquet fájlokat olvasni. A read_parquet('gold/fact_orders.parquet') függvény egy Parquet fájlt úgy olvas be, mintha egy SQL tábla lenne – a DuckDB automatikusan felismeri a sémát és az adattípusokat.

A példában a conn.execute(...) egy SQL lekérdezést futtat közvetlenül Parquet fájlokon: a fact_orders.parquet és a dim_product.parquet fájlokat JOIN-olja, kategóriánként aggregál, és az eredményt egy Pandas DataFrame-ként (.fetchdf()) adja vissza. Ez az úgynevezett „lakehouse" megközelítés: a data lake fájlokon (Parquet) SQL lekérdezéseket futtatunk, dedikált data warehouse nélkül.

Miért fontos ez? A Parquet + DuckDB kombináció a modern data engineering egyik leggyakoribb eszköztára. Olcsó (nem kell szerver), gyors (oszlopos tárolás + vektorizált végrehajtás), és skálázható (a Parquet fájlok végtelenül particionálhatók). A dbt, a Spark és a legtöbb ETL eszköz is Parquet-ot használ köztes formátumként.

[17]
import duckdb

conn = duckdb.connect()
result = conn.execute("""
    SELECT p.category, COUNT(*) AS orders,
           SUM(oi.quantity * oi.unit_price) AS revenue
    FROM read_parquet('gold/fact_orders.parquet') oi
    JOIN read_parquet('gold/dim_product.parquet') p ON oi.product_sk = p.sk
    GROUP BY p.category ORDER BY revenue DESC
""").fetchdf()
print(result.to_string(index=False))
Output:
category | orders |  revenue
---------+--------+---------
Laptop   |     45 | 24749.55
Phone    |     62 | 18949.38
Section 18

Teljes analitikai modell

A modern data engineeringben az adatfolyamatot rétegekbe (layers) szervezzük, amelyeket a Medallion architektúra (bronze-silver-gold) vagy a dbt stílusú elnevezés (staging-intermediate-mart) írnak le. Ez a megközelítés modularizálja a transzformációkat: minden réteg egy jól definiált feladatot lát el, és a hibakeresés, a tesztelés és a karbantartás sokkal könnyebb lesz.

A staging réteg (stg_orders) a nyers adatokat tisztítja és szűri: a példában a raw_orders táblából kiszűrjük a törölt rendeléseket (WHERE status != 'cancelled'), és csak a szükséges oszlopokat tartjuk meg. Ez a „bronze" szint – itt még csak szűrés és átnevezés történik, nincs komplex transzformáció.

Az intermediate réteg (int_customer_orders) összekapcsolja a staging adatokat a dimenziókkal: a stg_orders nézetet JOIN-olja a dim_customer táblával, így már rendelkezésre állnak az üzleti attribútumok (név, szegmens). Ez a „silver" szint – ahol az adatok már értelmezhetők, de még nem véglegesek.

A mart réteg (mart_monthly_revenue) a végleges, riportolásra kész adatokat tartalmazza: szegmensenként aggregálva a rendelések számát és az árbevételt. Ez a „gold" szint – ezeket a nézeteket közvetlenül a BI eszközök (Metabase, Tableau, Looker) olvassák.

[18]
-- Staging
CREATE VIEW stg_orders AS
SELECT order_id, customer_id, order_date, total_amount
FROM raw_orders WHERE status != 'cancelled';

-- Intermediate
CREATE VIEW int_customer_orders AS
SELECT c.name, c.segment, o.order_date, o.total_amount
FROM stg_orders o
JOIN dim_customer c ON o.customer_id = c.customer_id;

-- Mart
CREATE VIEW mart_monthly_revenue AS
SELECT segment, COUNT(*) AS orders, SUM(total_amount) AS revenue
FROM int_customer_orders GROUP BY segment;
Output:
segment    | orders | revenue
-----------+--------+-------
VIP        |     28 | 18249.80
Regular    |     67 | 23499.65
Section 19

Összefoglalás

Gratulálunk! Végigcsináltad az SQL & Data Modeling kurzust – ez egy komoly eredmény. Végigmentünk az SQL lekérdező nyelv alapjaitól (SELECT, WHERE, JOIN, GROUP BY) a haladó technikákon át (window functions, CTE, CASE WHEN) egészen az adatmodellezésig (star schema, SCD, normalizálás). A végére már fel tudod építeni a WebShop Pro teljes analitikai adatmodelljét, és érted, hogyan működnek a réteges adatfolyamatok (staging → intermediate → mart).

A megtanult fogalmak nemcsak az SQL-re korlátozódnak: a JOIN logikája, az aggregáció, az ablakfüggvények és a star schema univerzális koncepciók, amelyeket minden adatbázisban és big data rendszerben (Spark, Snowflake, BigQuery, Databricks) megtalálsz. Az SQL a data engineering lingua franca – ha ezt jól érted, a többi eszközt is gyorsan el fogod sajátítani.

A következő lépés a Python for Data Engineering kurzus, ahol megtanulod, hogyan automatizáld ezeket a folyamatokat Pythonnal: Pandas DataFrame-ökkel dolgozol, Parquet fájlokat írsz és olvasol, Delta Lake-et használsz, és PyArrow-ral dolgozol nagy adathalmazokon. A Python + SQL kombináció a data engineer legfontosabb eszköztára.

Tanács: gyakorolj! Építs saját projekteket – például egy online store analitikai modelljét –, és használd a kurzusban tanult sémákat. A legjobb módja a tanulásnak, ha saját adatokon kísérletezel.

MegtanultukKövetkező
SELECT, JOIN, GROUP BYPython for DE
Window functions, CTEPandas DataFrame
Star schema, SCDDelta Lake
DuckDB + ParquetPyArrow
Következő

Python for Data Engineering!

Quiz: Melyik JOIN adja vissza az összes sort mindkét táblából?

Quiz: Mi az SCD Type 2 stratégia?