Az SQL nyelv nagyon intuitív. Egészen addig, amíg nem az.
Az évek során sokan kritizálták az SQL nyelvet különböző okokból. Például: Az IDE-k nem tudják könnyen kitalálni, hogy milyen automatikus kitöltési lehetőségeket kínáljanak, mert amíg nem adjuk meg a FROM
záradékot, addig (még) nincsenek táblázatok a hatókörben:
-- Don't you wish this would be completed to first_name?SELECT first_na...-- Aaah, now it works:SELECT first_na...FROM customer
Ezek a dolgok azért furcsák, mert a műveletek lexikai sorrendje nem egyezik a műveletek logikai sorrendjével. Mi emberek néha (gyakran) intuitíve megérthetjük ezt a sorrendi különbséget. Pl. tudjuk, hogy az ügyfél táblából akarunk szelektálni. De az IDE ezt nem tudja.
GROUP BY járul hozzá a legnagyobb zűrzavarhoz
Amikor egy junior fejlesztő / SQL kezdő elkezd SQL-el dolgozni, elég hamar rájön az aggregációra és GROUP BY
. És gyorsan írni fognak olyan dolgokat, mint:
SELECT count(*)FROM customer
Jó, 200 ügyfelünk van!
És aztán:
SELECT count(*)FROM customerWHERE first_name = 'Steve'
Váó, közülük 90-et Steve-nek hívnak! Érdekes. Nézzük meg, hányan vannak egy névre vetítve…
SELECT first_name, count(*)FROM customerGROUP BY first_name
Ahaa!
FIRST_NAME COUNT------------------Steve 90Jane 80Joe 20Janet 10
Nagyon szép. De vajon mind egyforma? Nézzük meg a vezetéknevet is
SELECT first_name, last_name, count(*)FROM customerGROUP BY first_name
Oops!
ORA-00979: not a GROUP BY expression
Jej, mit jelent ez? (megjegyzés, sajnos a MySQL felhasználók, akik nem használják a STRICT módot, itt is tetszőleges vezetéknevekkel kapnak eredményt!, tehát egy új MySQL felhasználó nem fogja érteni a hibájukat)
Hogyan lehet ezt egy SQL kezdőnek könnyen elmagyarázni? A “profik” számára egyértelműnek tűnik, de tényleg egyértelmű? Elég nyilvánvaló ahhoz, hogy egy kezdőnek könnyen el tudd magyarázni? Gondolj csak bele. Miért szemantikailag helyes vagy helytelen az egyes kijelentések mindegyike?
A probléma a szintaxissal kapcsolatos
Az SQL szintaxis hasonlóan működik, mint az angol nyelv. Ez egy parancs. A parancsokat igékkel kezdjük. Az ige SELECT
(vagy INSERT
, UPDATE
, DELETE
, CREATE
, DROP
, stb. stb. stb.)
Az emberi nyelv sajnos hihetetlenül alkalmatlan a programozás sokkal formálisabb világára. Bár némi vigaszt nyújt az új felhasználóknak (esetleg nem programozóknak), akik abszolút kezdők, de mindenki másnak csak megnehezíti a dolgát. A különböző SQL-klauzulák rendkívül összetett függőségi viszonyban állnak egymással. Például:
- A
GROUP BY
záradék jelenlétében aHAVING
,SELECT
ésORDER BY
záradékokban csakGROUP BY
kifejezésekből (vagy azok funkcionális függőségeiből) felépített kifejezések, illetve aggregált függvények használhatók. - Az egyszerűség kedvéért ne is beszéljünk a
GROUPING SETS
- Sőt, van néhány olyan eset is, amikor a
GROUP BY
implikált. Pl. ha “csupasz”HAVING
záradékot írunk - Egyetlen aggregáló függvény a
SELECT
záradékban (GROUP BY
hiányában) egyetlen sorba való aggregálást kényszerít ki - Sőt, ezt implikálhatjuk azzal is, ha ezt az aggregáló függvényt
ORDER BY
-ba tesszük (bármilyen okból) - Meglehet
ORDER BY
elég sok olyan kifejezés, amelySELECT
nélkül hivatkozik aFROM
záradék bármely oszlopára. De ez már nem igaz, ha azt írjaSELECT DISTINCT
- …
A lista végtelen. Ha érdekel, elolvashatod az SQL szabványdokumentumokat, és megnézheted, mennyi furcsa és bonyolult kölcsönös függőség van a SELECT
utasítás számos záradéka között.
Meg lehet ezt valaha is érteni?
Szerencsére igen! Van egy egyszerű trükk, amit mindig elmagyarázok az SQL mesterkurzusomat látogató küldötteknek. Az SQL műveletek (záradékok) lexikai (szintaktikai) sorrendje egyáltalán nem felel meg a műveletek logikai sorrendjének (bár néha véletlenül mégis). A modern optimalizálóknak köszönhetően a sorrend szintén nem felel meg a műveletek tényleges sorrendjének, így valójában: szintaktikai -> logikai -> tényleges sorrend, de ezt most hagyjuk félre.
A műveletek logikai sorrendje a következő (az “egyszerűség kedvéért” kihagyom az olyan beszállítóspecifikus dolgokat, mint CONNECT BY
, MODEL
, MATCH_RECOGNIZE
, PIVOT
, UNPIVOT
és az összes többit):
-
FROM
: Ez valójában az első dolog, ami történik, logikailag. Minden más előtt betöltjük az összes sort az összes táblából, és összekapcsoljuk őket. Mielőtt sikítanál és dühbe gurulnál: Ismétlem, ez történik először logikailag, nem ténylegesen. Az optimalizáló nagyon valószínű, hogy nem ezt a műveletet fogja először elvégezni, az butaság lenne, hanem aWHERE
záradék alapján hozzáfér valamilyen indexhez. De ismétlem, logikailag ez történik először. Továbbá: az összesJOIN
záradék valójában ennek aFROM
záradéknak a része. AJOIN
egy operátor a relációs algebrában. Ugyanúgy, mint ahogy az+
és-
operátorok az aritmetikában. Ez nem egy önálló záradék, mint aSELECT
vagy aFROM
-
WHERE
: Miután betöltöttük az összes sort a fenti táblázatokból, most újra kidobhatjuk őket aWHERE
-
GROUP BY
használatával: Ha akarjuk, akkor aWHERE
után megmaradt sorokat foghatjuk, és csoportokba vagy vödrökbe rakhatjuk őket, ahol minden csoport tartalmazza aGROUP BY
kifejezés azonos értékét (és az összes többi sor az adott csoporthoz tartozó listába kerül). Java nyelven valami ilyesmit kapnánk:Map<String, List<Row>>
. Ha mégis megadja aGROUP BY
záradékot, akkor a tényleges sorok csak a csoport oszlopait tartalmazzák, a többi oszlopot már nem, amelyek most már ebben a listában vannak. A listában lévő oszlopokat csak azok az aggregáló függvények láthatják, amelyek képesek a listával operálni. Lásd alább: - aggregációk: Ezt fontos megérteni. Nem számít, hogy szintaktikailag hová helyezi az aggregáló függvényt (azaz a
SELECT
záradékba vagy aORDER BY
záradékba), ez itt az a lépés, ahol az aggregáló függvények kiszámításra kerülnek. Közvetlenül aGROUP BY
után. (ne feledjük: logikailag. Az okos adatbázisok valójában már korábban is kiszámíthatták őket). Ez megmagyarázza, hogy miért nem lehet azWHERE
záradékba aggregált függvényt tenni, mert annak értékéhez még nem lehet hozzáférni. AWHERE
záradék logikailag az aggregációs lépés előtt történik. Az aggregáló függvények elérhetik azokat az oszlopokat, amelyeket a fentiekben az egyes csoportok “this list”-jába tettél. Az aggregálás után a “this list” eltűnik, és többé nem lesz elérhető. Ha nincsGROUP BY
záradék, akkor csak egy nagy csoport marad kulcs nélkül, amely az összes sort tartalmazza. -
HAVING
: … de most már hozzáférhet az aggregációs függvények értékeihez. Például ellenőrizheti, hogycount(*) > 1
aHAVING
záradékbanHAVING
. Mivel aHAVING
aGROUP BY
után van (vagy feltételezi aGROUP BY
-ot), már nem tudunk hozzáférni olyan oszlopokhoz vagy kifejezésekhez, amelyek nem voltakGROUP BY
oszlopok. -
WINDOW
: Ha a fantasztikus ablakfüggvény funkciót használja, ez az a lépés, ahol az összes kiszámításra kerül. Csak most. És az a menő, hogy mivel már kiszámítottuk (logikailag!) az összes aggregált függvényt, az aggregált függvényeket ablakfüggvényekbe fészkelhetjük. Így teljesen rendben van, ha olyan dolgokat írunk, mintsum(count(*)) OVER ()
vagyrow_number() OVER (ORDER BY count(*))
. Az, hogy az ablakfüggvények csak most számítódnak ki logikailag, megmagyarázza azt is, hogy miért csak aSELECT
vagyORDER BY
záradékokba tehetjük őket. AWHERE
záradékhoz nem állnak rendelkezésre, ami korábban megtörtént. Vegyük észre, hogy a PostgreSQL és a Sybase SQL Anywhere rendelkezik ténylegesWINDOW
záradékkal! -
SELECT
: Végre. Most már felhasználhatjuk a fenti záradékok által előállított összes sort, és aSELECT
használatával új sorokat/tuplikat hozhatunk létre belőlük. Hozzáférhetünk az összes kiszámított ablakfüggvényhez, az összes kiszámított aggregált függvényhez, az összes megadott csoportosító oszlophoz, vagy ha nem csoportosítottunk/aggregáltunk, akkor használhatjuk az összes oszlopot aFROM
záradékunkból. Ne feledje! Még ha úgy is tűnik, mintha aSELECT
belsejében aggregálnánk dolgokat, ez már régen megtörtént, és az édes-édescount(*)
függvény nem más, mint egy hivatkozás az eredményre. -
DISTINCT
: Igen! ADISTINCT
aSELECT
után történik, még akkor is, ha szintaxisilag aSELECT
oszloplista elé kerül. De gondolj bele! Ennek tökéletes értelme van. Hogyan tudnánk különálló sorokat eltávolítani, ha még nem ismerjük az összes sort (és azok oszlopait)? -
UNION, INTERSECT, EXCEPT
: Ez nem is kérdéses. AUNION
egy olyan operátor, amely két részkérdést kapcsol össze. Minden, amiről eddig beszéltünk, alkérdés volt. Az egyesítés kimenete egy új lekérdezés, amely ugyanazokat a sortípusokat (azaz ugyanazokat az oszlopokat) tartalmazza, mint az első alkérdés. Általában. Mert a wacko Oracle-ben az utolsó előtti alkérdés a megfelelő az oszlopnév meghatározására. Oracle adatbázis, a szintaktikai troll 😉 -
ORDER BY
: Teljesen logikus az eredmény rendezésének eldöntését a végére halasztani, mert minden más művelet használhat hashmapset, belsőleg, így minden közbenső rendezés újra elveszhet. Tehát most már rendezhetjük az eredményt. Normális esetben aORDER BY
záradékból rengeteg sorhoz hozzáférhetünk, köztük olyan sorokhoz (vagy kifejezésekhez) is, amelyeket nemSELECT
. De ha korábbanDISTINCT
-t adtunk meg, akkor már nem tudunk rendezni olyan sorok/kifejezések szerint, amelyek nem voltak kijelölve. Miért? Mert a sorrend eléggé meghatározhatatlan lenne. -
OFFSET
: -
LIMIT, FETCH, TOP
: Most az épeszű adatbázisok aLIMIT
(MySQL, PostgreSQL) vagyFETCH
(DB2, Oracle 12c, SQL Server 2012) záradékot a legvégére teszik, szintaktikailag. A régi időkben a Sybase és az SQL Server úgy gondolta, hogy aSELECT
kulcsszókéntTOP
. Mintha aSELECT DISTINCT
helyes sorrendje nem lenne már így is elég zavaró.
Tessék, megvan. Teljesen érthető. És ha valaha is olyasmit akarsz csinálni, ami nem a “helyes sorrendben” van, a legegyszerűbb trükk mindig az, hogy származtatott táblázathoz folyamodsz. Pl. amikor egy ablakfüggvényre akarsz csoportosítani:
Miért működik? Mert:
- A származtatott táblázatban először a
FROM
történik, majd aWINDOW
kerül kiszámításra, utána pedig a vödörSELECT
. - A külső
SELECT
most már úgy kezelheti ennek az ablakfüggvény-számításnak az eredményét, mint bármelyik közönséges táblázatot aFROM
záradékban, majdGROUP BY
egy közönséges oszlopot, majd aggregál, majdSELECT
Mondjuk újra az eredeti példáinkat azzal, hogy miért működnek vagy miért nem működnek.
Mindig gondoljunk a műveletek logikai sorrendjére
Ha nem gyakran írunk SQL-t, a szintaxis valóban zavaró lehet. Különösen a GROUP BY
és az aggregációk “fertőzik” az egész SELECT
záradék többi részét, és a dolgok nagyon furcsává válnak. Amikor ezzel a furcsasággal szembesülünk, két lehetőségünk van:
- Dühbe gurulni és üvöltözni az SQL nyelv tervezőivel
- Elvállalni a sorsunkat, becsukni a szemünket, elfelejteni a snytaxot és emlékezni a logikai műveletek sorrendjére
Általában az utóbbit javaslom, mert akkor a dolgoknak sokkal több értelme lesz, beleértve az alábbi gyönyörű kumulatív napi bevétel számítást, amely a napi bevételt (SUM(amount)
aggregátum függvény) a kumulatív bevételbe (SUM(...) OVER (...)
ablak függvény) fészkeli be:
SELECT payment_date, SUM(SUM(amount)) OVER (ORDER BY payment_date) AS revenueFROM paymentGROUP BY payment_date
… mert az aggregációk logikailag az ablakfüggvények előtt történnek.
Caveat: ORDER BY záradék
A ORDER BY
záradék körül van néhány caveat, ami további zavart okozhat. Alapértelmezés szerint továbbra is feltételezi, hogy a műveletek logikai sorrendje helyes. De aztán van néhány speciális eset, különösen:
- A
DISTINCT
záradék hiányában - A halmazműveletek hiányában, mint
UNION
A ORDER BY
-ban hivatkozhatunk olyan kifejezésekre, amelyeket a SELECT
nem vetít előre. A következő lekérdezés tökéletesen megfelel a legtöbb adatbázisban:
SELECT first_name, last_nameFROM actorORDER BY actor_id
Ez egy “virtuális” / implicit ACTOR_ID
vetítés, mintha azt írtuk volna:
SELECT first_name, last_name, actor_idFROM actorORDER BY actor_id
De aztán a ACTOR_ID
oszlopot ismét eltávolítottuk volna az eredményből. Ez nagyon kényelmes, bár némi zavarhoz vezethet a szemantikát és a műveletek sorrendjét illetően. Konkrétan nem használhatod pl. a DISTINCT
-t ilyen helyzetben. A következő lekérdezés érvénytelen:
SELECT DISTINCT first_name, last_nameFROM actorORDER BY actor_id -- Oops
Mert mi van akkor, ha két azonos nevű, de nagyon különböző azonosítóval rendelkező szereplő van? A sorrend ekkor meghatározhatatlan lenne.
A halmazműveleteknél még világosabb, hogy miért nem megengedett:
SELECT first_name, last_nameFROM actorUNIONSELECT first_name, last_nameFROM customerORDER BY actor_id -- Oops
Ez esetben a ACTOR_ID
oszlop nincs jelen a CUSTOMER
táblában, így a lekérdezésnek egyáltalán nincs értelme.
További olvasmány
Még többet szeretne megtudni? Ezeket a cikkeket is elolvashatja:
- SQL GROUP BY és funkcionális függőségek: Egy nagyon hasznos funkció
- Hogyan kellett volna megtervezni az SQL GROUP BY-t – mint a Neo4j implicit GROUP BY-jét
- Hogyan fordítsuk le az SQL GROUP BY-t és az aggregációkat Java 8-ra
- Tényleg érti az SQL GROUP BY és HAVING záradékokat?
- GROUP BY ROLLUP / CUBE