De SQL taal is zeer intuïtief. Totdat het dat niet is.
In de loop der jaren hebben veel mensen de SQL taal om verschillende redenen bekritiseerd. Bijvoorbeeld: IDE’s kunnen niet gemakkelijk raden welke auto completion opties ze moeten aanbieden, want zolang je de FROM
clause niet specificeert, zijn er (nog) geen tabellen in scope:
-- Don't you wish this would be completed to first_name?SELECT first_na...-- Aaah, now it works:SELECT first_na...FROM customer
Deze dingen zijn raar, omdat de lexicale volgorde van operaties niet overeenkomt met de logische volgorde van operaties. Wij mensen kunnen dit verschil in volgorde soms (vaak) intuïtief begrijpen. We weten bijvoorbeeld dat we gaan selecteren uit de klantentabel. Maar de IDE weet dit niet.
GROUP BY draagt de meeste verwarring bij
Wanneer een junior ontwikkelaar / SQL-beginner met SQL begint te werken, zullen ze vrij snel achter aggregatie en GROUP BY
komen. En ze zullen snel dingen schrijven als:
SELECT count(*)FROM customer
Ja, we hebben 200 klanten!
En dan:
SELECT count(*)FROM customerWHERE first_name = 'Steve'
Wauw, 90 van hen heten Steve! Interessant. Laten we eens kijken hoeveel we er per naam hebben…
SELECT first_name, count(*)FROM customerGROUP BY first_name
Ahaa!
FIRST_NAME COUNT------------------Steve 90Jane 80Joe 20Janet 10
Zeer leuk. Maar zijn ze allemaal hetzelfde? Laten we ook eens naar de achternaam kijken
SELECT first_name, last_name, count(*)FROM customerGROUP BY first_name
Oeps!
ORA-00979: not a GROUP BY expression
Jemig, wat betekent dat? (let op, helaas, MySQL gebruikers die niet de STRICT modus gebruiken zullen hier nog steeds een resultaat krijgen met willekeurige achternamen!, dus een nieuwe MySQL gebruiker zal hun fout niet begrijpen)
Hoe leg je dit eenvoudig uit aan een SQL newbie? Voor “pro’s” lijkt het duidelijk, maar is het ook echt duidelijk? Is het duidelijk genoeg dat je het gemakkelijk aan een beginner kunt uitleggen? Denk er eens over na. Waarom is elk van deze verklaringen semantisch juist of onjuist?
Het probleem heeft te maken met de syntax
De SQL syntax werkt op een vergelijkbare manier als de Engelse taal. Het is een commando. We beginnen commando’s met werkwoorden. Het werkwoord is SELECT
(of INSERT
, UPDATE
, DELETE
, CREATE
, DROP
, enz. enz.)
Helaas is de menselijke taal ongelooflijk slecht geschikt voor de veel formelere wereld van het programmeren. Hoewel het enige troost biedt aan nieuwe gebruikers (mogelijk niet-programmeurs) die absolute beginners zijn, maakt het de dingen alleen maar moeilijk voor alle anderen. Alle verschillende SQL-clausules hebben zeer complexe onderlinge afhankelijkheden. Bijvoorbeeld:
- In aanwezigheid van een
GROUP BY
clausule, kunnen alleen expressies die zijn opgebouwd uitGROUP BY
expressies (of functionele afhankelijkheden daarvan), of aggregate functies worden gebruikt inHAVING
,SELECT
, enORDER BY
clausules. - Voor de eenvoud, laten we het zelfs niet hebben over
GROUPING SETS
- In feite zijn er zelfs een paar gevallen waarin
GROUP BY
wordt geïmpliceerd. Bijv. als u een “naakte”HAVING
clausule schrijft - Een enkele aggregate functie in de
SELECT
clausule (bij afwezigheid vanGROUP BY
) zal aggregatie in een enkele rij afdwingen - In feite kan dit ook worden geïmpliceerd door die aggregate functie in
ORDER BY
te zetten (om welke reden dan ook) - U kunt
ORDER BY
heel wat expressies die verwijzen naar kolommen uit deFROM
clausule zonderSELECT
ze te gebruiken. Maar dat is niet meer waar als jeSELECT DISTINCT
- …
schrijft De lijst is eindeloos. Als u geïnteresseerd bent, kunt u de SQL standaarddocumenten lezen en nagaan hoeveel rare en ingewikkelde onderlinge afhankelijkheden er bestaan tussen de vele clausules van het SELECT
statement.
Kan dit ooit begrepen worden?
Gelukkig, ja! Er is een eenvoudige truc, die ik altijd uitleg aan de afgevaardigden die mijn SQL Masterclass bezoeken. De lexicale (syntactische) volgorde van SQL-bewerkingen (clausules) komt helemaal niet overeen met de logische volgorde van bewerkingen (hoewel ze dat soms toevallig wel doen). Dankzij moderne optimizers komt de volgorde ook niet overeen met de werkelijke volgorde van bewerkingen, zodat we eigenlijk hebben: syntactische -> logische -> werkelijke volgorde, maar dat laten we nu even buiten beschouwing.
De logische volgorde van bewerkingen is de volgende (voor de “eenvoud” laat ik verkoperspecifieke dingen als CONNECT BY
, MODEL
, MATCH_RECOGNIZE
, PIVOT
, UNPIVOT
en al het andere weg):
-
FROM
: Dit is eigenlijk het eerste dat gebeurt, logisch gezien. Voordat iets anders gebeurt, laden we alle rijen van alle tabellen en voegen ze samen. Voordat je schreeuwt en boos wordt: Nogmaals, dit is wat er eerst logischerwijs gebeurt, niet feitelijk. De optimiser zal zeer waarschijnlijk niet eerst deze operatie doen, dat zou dom zijn, maar een index openen op basis van deWHERE
clausule. Maar nogmaals, logisch gezien gebeurt dit als eerste. Ook: alleJOIN
clausules zijn eigenlijk onderdeel van dezeFROM
clausule.JOIN
is een operator in de relationele algebra. Net zoals+
en-
operatoren zijn in de rekenkunde. Het is geen onafhankelijke clausule, zoalsSELECT
ofFROM
-
WHERE
: Als we alle rijen uit de bovenstaande tabellen hebben geladen, kunnen we ze nu weer weggooien metWHERE
-
GROUP BY
: Als je wilt, kun je de rijen die overblijven naWHERE
nemen en ze in groepen of emmers zetten, waarbij elke groep dezelfde waarde bevat voor deGROUP BY
expressie (en alle andere rijen in een lijst voor die groep worden gezet). In Java zou je zoiets krijgen als:Map<String, List<Row>>
. Als je wel eenGROUP BY
clausule specificeert, dan bevatten je feitelijke rijen alleen de groepskolommen, niet langer de overige kolommen, die nu in die lijst staan. Die kolommen in de lijst zijn alleen zichtbaar voor aggregaatfuncties die op die lijst kunnen werken. Zie hieronder. - aggregaties: Dit is belangrijk om te begrijpen. Het maakt niet uit waar u uw aggregatiefunctie syntactisch plaatst (d.w.z. in de
SELECT
-clausule, of in deORDER BY
-clausule), dit hier is de stap waar aggregatiefuncties worden berekend. Direct naGROUP BY
. (Onthoud: logisch. Slimme databases kunnen ze al eerder berekend hebben, eigenlijk). Dit verklaart waarom je een aggregate functie niet in deWHERE
clausule kunt zetten, omdat de waarde ervan nog niet kan worden benaderd. DeWHERE
-clausule gebeurt logischerwijs vóór de aggregatiestap. Aggregatiefuncties hebben toegang tot kolommen die je in “deze lijst” hebt gezet voor elke groep, hierboven. Na de aggregatie zal “deze lijst” verdwijnen en niet langer beschikbaar zijn. Als je geenGROUP BY
clausule hebt, zal er gewoon één grote groep zijn zonder enige sleutel, die alle rijen bevat. -
HAVING
: … maar nu heb je toegang tot de waarden van de aggregatiefunctie. U kunt bijvoorbeeld controleren datcount(*) > 1
in deHAVING
clausule. OmdatHAVING
naGROUP BY
komt (ofGROUP BY
impliceert), hebben we geen toegang meer tot kolommen of uitdrukkingen die geenGROUP BY
-kolommen waren. -
WINDOW
: Als u de geweldige vensterfunctie-functie gebruikt, is dit de stap waar ze allemaal worden berekend. Alleen nu. En het leuke is, omdat we alle aggregate functies al hebben berekend (logisch!), kunnen we aggregate functies nestelen in window functies. Het is dus perfect in orde om dingen te schrijven alssum(count(*)) OVER ()
ofrow_number() OVER (ORDER BY count(*))
. Dat vensterfuncties nu pas logisch worden berekend, verklaart ook waarom je ze alleen in deSELECT
ofORDER BY
clausules kunt zetten. Ze zijn niet beschikbaar voor deWHERE
clausule, wat voorheen wel het geval was. Merk op dat PostgreSQL en Sybase SQL Anywhere een echteWINDOW
clausule hebben! -
SELECT
: Eindelijk. We kunnen nu alle rijen gebruiken die uit de bovenstaande clausules voortkomen en er nieuwe rijen/tupels van maken metSELECT
. We hebben toegang tot alle vensterfuncties die we hebben berekend, alle aggregaatfuncties die we hebben berekend, alle groeperingskolommen die we hebben opgegeven, of als we niet hebben gegroepeerd/geaggregeerd, kunnen we alle kolommen uit onzeFROM
-clausule gebruiken. Onthoud: Zelfs als het lijkt alsof we dingen aggregeren binnenSELECT
, is dit allang gebeurd, en de zoete zoetecount(*)
functie is niets meer dan een verwijzing naar het resultaat. -
DISTINCT
: Ja!DISTINCT
gebeurt naSELECT
, zelfs als het voor uwSELECT
kolom lijst is gezet, syntaxis-wise. Maar denk er eens over na. Het is volkomen logisch. Hoe kunnen we anders afzonderlijke rijen verwijderen, als we nog niet alle rijen (en hun kolommen) kennen? -
UNION, INTERSECT, EXCEPT
: Dit is een no-brainer. EenUNION
is een operator die twee subqueries verbindt. Alles waar we het tot nu toe over hebben gehad was een subquery. De output van een union is een nieuwe query met dezelfde rijtypes (d.w.z. dezelfde kolommen) als de eerste subquery. Gewoonlijk. Want in wacko Oracle is de voorlaatste subquery de juiste om de kolomnaam te definiëren. Oracle database, de syntactische trol 😉 -
ORDER BY
: Het is volkomen logisch om de beslissing om een resultaat te ordenen uit te stellen tot het eind, omdat alle andere operaties hashmaps kunnen gebruiken, intern, dus elke tussenliggende ordening kan weer verloren gaan. Dus kunnen we nu het resultaat ordenen. Normaal gesproken kun je veel rijen benaderen vanuit deORDER BY
clausule, inclusief rijen (of expressies) die je nietSELECT
hebt opgegeven. Maar wanneer jeDISTINCT
hebt opgegeven, kun je niet meer ordenen op rijen / expressies die niet geselecteerd waren. Waarom? Omdat de volgorde dan nogal ongedefinieerd zou zijn. -
OFFSET
: Gebruik geen offset -
LIMIT, FETCH, TOP
: Nu zetten verstandige databases deLIMIT
(MySQL, PostgreSQL) ofFETCH
(DB2, Oracle 12c, SQL Server 2012) clausule helemaal aan het eind, syntactisch. Vroeger dachten Sybase en SQL Server dat het een goed idee zou zijn omTOP
als een sleutelwoord inSELECT
te hebben. Alsof de juiste volgorde vanSELECT DISTINCT
al niet verwarrend genoeg was.
Daar, we hebben het. Het is volkomen logisch. En als je ooit iets wilt doen dat niet in de “juiste volgorde” staat, is de eenvoudigste truc altijd je toevlucht te nemen tot een afgeleide tabel. Als je bijvoorbeeld wilt groeperen op een vensterfunctie:
Waarom werkt dat? Omdat:
- In de afgeleide tabel,
FROM
gebeurt eerst, en dan wordt deWINDOW
berekend, dan wordt de emmerSELECT
ed. - De buitenste
SELECT
kan nu het resultaat van deze vensterfunctie berekening behandelen als elke gewone tabel in deFROM
clausule, danGROUP BY
een gewone kolom, dan aggregeren, danSELECT
Laten we onze oorspronkelijke voorbeelden nog eens bekijken met een uitleg waarom ze werken of waarom ze niet werken.
Denk altijd aan de logische volgorde van bewerkingen
Als u niet vaak SQL schrijft, kan de syntax inderdaad verwarrend zijn. Vooral GROUP BY
en aggregaties “besmetten” de rest van de hele SELECT
clausule, en de dingen worden echt raar. Wanneer we geconfronteerd worden met deze vreemdheid, hebben we twee opties:
- Worden boos en schreeuwen tegen de ontwerpers van de SQL-taal
- Begrijpen ons lot, sluiten onze ogen, vergeten de snytax en onthouden de logische bewerkingsvolgorde
Ik raad over het algemeen het laatste aan, omdat de dingen dan een stuk logischer beginnen te worden, met inbegrip van de prachtige cumulatieve dagelijkse inkomstenberekening hieronder, die de dagelijkse inkomsten (SUM(amount)
aggregatiefunctie) nestelt binnen de cumulatieve inkomsten (SUM(...) OVER (...)
vensterfunctie):
SELECT payment_date, SUM(SUM(amount)) OVER (ORDER BY payment_date) AS revenueFROM paymentGROUP BY payment_date
… omdat aggregaties logischerwijs vóór vensterfuncties komen.
Caveat: ORDER BY-clausule
Er zijn enkele caveats rond de ORDER BY
-clausule, die tot verdere verwarring zouden kunnen bijdragen. Standaard wordt er verder van uitgegaan dat de logische volgorde van bewerkingen correct is. Maar dan zijn er enkele speciale gevallen, in het bijzonder:
- In de afwezigheid van een
DISTINCT
clausule - In de afwezigheid van set operaties zoals
UNION
U kunt expressies in ORDER BY
refereren, die niet door SELECT
zijn geprojecteerd. De volgende query is perfect in orde in de meeste databases:
SELECT first_name, last_nameFROM actorORDER BY actor_id
Er is een “virtuele” / impliciete ACTOR_ID
projectie, alsof we hadden geschreven:
SELECT first_name, last_name, actor_idFROM actorORDER BY actor_id
Maar dan de ACTOR_ID
kolom weer uit het resultaat hebben verwijderd. Dit is erg handig, hoewel het kan leiden tot enige verwarring over de semantiek en de volgorde van bewerkingen. In het bijzonder kunt u in een dergelijke situatie geen gebruik maken van bijvoorbeeld DISTINCT
. De volgende query is ongeldig:
SELECT DISTINCT first_name, last_nameFROM actorORDER BY actor_id -- Oops
Want, wat als er twee actoren zijn met dezelfde naam maar met heel verschillende ID’s? De ordening zou nu ongedefinieerd zijn.
Met set operations is het nog duidelijker waarom dit niet is toegestaan:
SELECT first_name, last_nameFROM actorUNIONSELECT first_name, last_nameFROM customerORDER BY actor_id -- Oops
In dit geval is de kolom ACTOR_ID
niet aanwezig in de tabel CUSTOMER
, dus de query heeft geen enkele zin.
Verder lezen
Wilt u meer weten? We hebben ook deze artikelen voor u om te lezen:
- SQL GROUP BY en functionele afhankelijkheden: Een zeer nuttige functie
- Hoe SQL GROUP BY had moeten worden ontworpen – Net als Neo4j’s Implicit GROUP BY
- Hoe SQL GROUP BY en Aggregaties te vertalen naar Java 8
- Begrijpt u SQL’s GROUP BY en HAVING clausules echt?
- GROUP BY ROLLUP / CUBE