Limbajul SQL este foarte intuitiv. Până când nu mai este.
De-a lungul anilor, o mulțime de oameni au criticat limbajul SQL dintr-o varietate de motive. De exemplu: IDE-urile nu pot ghici cu ușurință ce opțiuni de autocompletare să ofere, deoarece atâta timp cât nu specificați clauza FROM
, nu există (încă) tabele în domeniul de aplicare:
-- Don't you wish this would be completed to first_name?SELECT first_na...-- Aaah, now it works:SELECT first_na...FROM customer
Aceste lucruri sunt ciudate, deoarece ordinea lexicală a operațiilor nu se potrivește cu ordinea logică a operațiilor. Noi, oamenii, putem înțelege uneori (adesea) intuitiv această diferență de ordonare. De exemplu, știm că suntem pe cale să selectăm din tabelul de clienți. Dar IDE-ul nu știe acest lucru.
GROUP BY contribuie la cea mai mare confuzie
Când un dezvoltator junior / începător SQL începe să lucreze cu SQL, destul de repede, va afla despre agregare și GROUP BY
. Și vor scrie rapid lucruri de genul:
SELECT count(*)FROM customer
Yay, avem 200 de clienți!
Și apoi:
SELECT count(*)FROM customerWHERE first_name = 'Steve'
Wow, 90 dintre ei se numesc Steve! Interesant. Hai să aflăm câți avem pe nume…
SELECT first_name, count(*)FROM customerGROUP BY first_name
Ahaa!
FIRST_NAME COUNT------------------Steve 90Jane 80Joe 20Janet 10
Frumos. Dar sunt toate la fel? Să verificăm și numele de familie
SELECT first_name, last_name, count(*)FROM customerGROUP BY first_name
Oops!
ORA-00979: not a GROUP BY expression
Doamne, ce înseamnă asta? (notă, din păcate, utilizatorii MySQL care nu folosesc modul STRICT vor obține în continuare un rezultat aici cu nume de familie arbitrare!, așa că un utilizator nou de MySQL nu va înțelege greșeala lor)
Cum explicați cu ușurință acest lucru unui începător SQL? Pare evident pentru „profesioniști”, dar este cu adevărat evident? Este suficient de evident încât să-l poți explica cu ușurință unui începător? Gândiți-vă la asta. De ce fiecare dintre aceste afirmații sunt corecte sau greșite din punct de vedere semantic?
Problema este legată de sintaxă
Sintaxa SQL funcționează într-un mod similar cu limba engleză. Este o comandă. Noi începem comenzile cu verbe. Verbul este SELECT
(sau INSERT
, UPDATE
, DELETE
, CREATE
, DROP
, etc. etc.)
Din păcate, limbajul uman este incredibil de nepotrivit pentru lumea mult mai formală a programării. Deși oferă o oarecare consolare noilor utilizatori (eventual neprogramatori) care sunt începători absoluți, nu face decât să îngreuneze lucrurile pentru toți ceilalți. Toate diferitele clauze SQL au interdependențe extrem de complexe. De exemplu:
- În prezența unei clauze
GROUP BY
, numai expresiile construite din expresiiGROUP BY
(sau dependențe funcționale ale acestora), sau funcțiile agregate pot fi folosite în clauzeleHAVING
,SELECT
șiORDER BY
. - Din motive de simplitate, să nu vorbim nici măcar despre
GROUPING SETS
- De fapt, există chiar câteva cazuri în care
GROUP BY
este implicită. De ex. dacă scrieți o clauzăHAVING
„goală” - O singură funcție de agregare în clauza
SELECT
(în absența luiGROUP BY
) va forța agregarea într-un singur rând - De fapt, acest lucru poate fi implicat și prin plasarea acelei funcții de agregare în
ORDER BY
(indiferent de motiv) - Puteți
ORDER BY
destule expresii care fac referire la orice coloane din clauzaFROM
fără a leSELECT
. Dar acest lucru nu mai este valabil dacă scriețiSELECT DISTINCT
- …
Lista este nesfârșită. Dacă vă interesează, puteți citi documentele standard SQL și să verificați câte interdependențe ciudate și complicate există între numeroasele clauze ale instrucțiunii SELECT
.
Poate fi aceasta înțeleasă vreodată?
Din fericire, da! Există un truc simplu, pe care îl explic mereu delegaților care îmi vizitează Masterclass-ul SQL. Ordinea lexicală (sintactică) a operațiilor (clauzelor) SQL nu corespunde deloc cu ordinea logică a operațiilor (deși, uneori, ele coincid întâmplător). Mulțumită optimizatorilor moderni, ordinea nu corespunde nici cu ordinea reală a operațiilor, așa că avem de fapt: ordine sintactică -> logică -> ordine reală, dar să lăsăm asta deoparte pentru moment.
Ordinea logică a operațiilor este următoarea (pentru „simplitate” las la o parte lucrurile specifice furnizorilor, cum ar fi CONNECT BY
, MODEL
, MATCH_RECOGNIZE
, PIVOT
, UNPIVOT
și toate celelalte):
-
FROM
: Acesta este de fapt primul lucru care se întâmplă, din punct de vedere logic. Înainte de orice altceva, încărcăm toate rândurile din toate tabelele și le unim. Înainte să țipați și să vă enervați: Din nou, acesta este primul lucru care se întâmplă logic, nu de fapt. Foarte probabil că optimizatorul nu va face mai întâi această operațiune, ar fi o prostie, ci va accesa un index bazat pe clauzaWHERE
. Dar, din nou, din punct de vedere logic, acest lucru se întâmplă primul. De asemenea: toate clauzeleJOIN
fac de fapt parte din această clauzăFROM
.JOIN
este un operator în algebra relațională. La fel cum+
și-
sunt operatori în aritmetică. Nu este o clauză independentă, precumSELECT
sauFROM
-
WHERE
: După ce am încărcat toate rândurile din tabelele de mai sus, acum le putem arunca din nou folosindWHERE
-
GROUP BY
: Dacă doriți, puteți lua rândurile care rămân dupăWHERE
și să le puneți în grupuri sau găleți, unde fiecare grup conține aceeași valoare pentru expresiaGROUP BY
(și toate celelalte rânduri sunt puse într-o listă pentru acel grup). În Java, ați obține ceva de genul:Map<String, List<Row>>
. Dacă specificați o clauzăGROUP BY
, atunci rândurile efective conțin doar coloanele grupului, nu și celelalte coloane, care sunt acum în acea listă. Acele coloane din listă sunt vizibile doar pentru funcțiile de agregare care pot opera pe acea listă. A se vedea mai jos. - agregări: Acest lucru este important de înțeles. Indiferent unde ați plasat funcția de agregare din punct de vedere sintactic (de exemplu, în clauza
SELECT
sau în clauzaORDER BY
), acesta este pasul în care se calculează funcțiile de agregare. Imediat dupăGROUP BY
. (țineți minte: din punct de vedere logic. Este posibil ca bazele de date inteligente să le fi calculat înainte, de fapt). Așa se explică de ce nu puteți pune o funcție agregată în clauzaWHERE
, pentru că valoarea ei nu poate fi accesată încă. ClauzaWHERE
are loc, în mod logic, înainte de etapa de agregare. Funcțiile de agregare pot accesa coloanele pe care le-ați pus în „această listă” pentru fiecare grup, mai sus. După agregare, „această listă” va dispărea și nu va mai fi disponibilă. Dacă nu aveți o clauzăGROUP BY
, va exista doar un singur grup mare, fără nicio cheie, care va conține toate rândurile. -
HAVING
: … dar acum puteți accesa valorile funcției de agregare. De exemplu, puteți verifica căcount(*) > 1
în clauzaHAVING
. DeoareceHAVING
este dupăGROUP BY
(sau implicăGROUP BY
), nu mai putem accesa coloane sau expresii care nu au fost coloaneGROUP BY
. -
WINDOW
: Dacă folosiți minunata funcție de funcție de fereastră, acesta este pasul în care sunt calculate toate. Numai acum. Și ceea ce este grozav este că, deoarece am calculat deja (logic!) toate funcțiile agregate, putem anina funcții agregate în funcții fereastră. Astfel, este perfect în regulă să scriem lucruri precumsum(count(*)) OVER ()
saurow_number() OVER (ORDER BY count(*))
. Faptul că funcțiile de fereastră sunt calculate logic abia acum explică și de ce le puteți pune doar în clauzeleSELECT
sauORDER BY
. Ele nu sunt disponibile pentru clauzaWHERE
, ceea ce se întâmpla înainte. Rețineți că PostgreSQL și Sybase SQL Anywhere au o clauzăWINDOW
reală! -
SELECT
: În cele din urmă. Acum putem utiliza toate rândurile care sunt produse de clauzele de mai sus și putem crea noi rânduri / tuple din ele folosindSELECT
. Putem accesa toate funcțiile de fereastră pe care le-am calculat, toate funcțiile de agregare pe care le-am calculat, toate coloanele de grupare pe care le-am specificat sau, dacă nu am grupat/agregat, putem utiliza toate coloanele din clauza noastrăFROM
. Rețineți: Chiar dacă pare că agregăm lucruri în interiorul luiSELECT
, acest lucru s-a întâmplat cu mult timp în urmă, iar funcția dulce dulcecount(*)
nu este nimic mai mult decât o referință la rezultat. -
DISTINCT
: Da!DISTINCT
se întâmplă dupăSELECT
, chiar dacă este pusă înaintea listei de coloaneSELECT
, din punct de vedere sintactic. Dar gândiți-vă la asta. Este perfect logic. Cum altfel putem elimina rândurile distincte, dacă nu cunoaștem încă toate rândurile (și coloanele lor)? -
UNION, INTERSECT, EXCEPT
: Acest lucru este de la sine înțeles. UnUNION
este un operator care conectează două subinterogări. Tot ceea ce am discutat până acum a fost o subîntrebare. Rezultatul unei uniuni este o nouă interogare care conține aceleași tipuri de rânduri (adică aceleași coloane) ca și prima subinterogare. De obicei. Pentru că, în Oracle, penultima subinterogare este cea corectă pentru a defini numele coloanei. Baza de date Oracle, trolul sintactic 😉 -
ORDER BY
: Are sens total să se amâne decizia de ordonare a unui rezultat până la sfârșit, deoarece toate celelalte operații ar putea folosi hashmaps, la nivel intern, astfel încât orice ordine intermediară ar putea fi pierdută din nou. Așadar, acum putem ordona rezultatul. În mod normal, puteți accesa o mulțime de rânduri din clauzaORDER BY
, inclusiv rânduri (sau expresii) pe care nu le-ațiSELECT
. Dar când ați specificatDISTINCT
, înainte, nu mai puteți ordona după rândurile/expresiile care nu au fost selectate. De ce? Pentru că ordonarea ar fi destul de nedefinită. -
OFFSET
: Nu folosiți offsetul -
LIMIT, FETCH, TOP
: Acum, bazele de date sănătoase pun clauzaLIMIT
(MySQL, PostgreSQL) sauFETCH
(DB2, Oracle 12c, SQL Server 2012) chiar la sfârșit, din punct de vedere sintactic. Pe vremuri, Sybase și SQL Server au crezut că ar fi o idee bună să aibăTOP
ca un cuvânt cheie înSELECT
. De parcă ordonarea corectă aSELECT DISTINCT
nu era deja suficient de confuză.
Aici, am ajuns. Are sens în totalitate. Și dacă doriți vreodată să faceți ceva care nu este în „ordinea corectă”, cel mai simplu truc este întotdeauna să recurgeți la un tabel derivat. De exemplu, atunci când doriți să grupați pe o funcție de fereastră:
De ce funcționează? Pentru că:
- În tabelul derivat,
FROM
se întâmplă mai întâiFROM
, apoi se calculeazăWINDOW
, după care găleata esteSELECT
editată. - Cele exterioare
SELECT
pot trata acum rezultatul calculului acestei funcții de fereastră ca orice tabel obișnuit în clauzaFROM
, apoiGROUP BY
o coloană obișnuită, apoi agregarea, apoiSELECT
Să trecem în revistă exemplele noastre originale cu o explicație de ce funcționează sau de ce nu.
Gândiți-vă întotdeauna la ordinea logică a operațiilor
Dacă nu sunteți un scriitor SQL frecvent, sintaxa poate fi într-adevăr confuză. În special GROUP BY
și agregările GROUP BY
și agregările „infectează” restul întregii clauze SELECT
, iar lucrurile devin foarte ciudate. Atunci când ne confruntăm cu această ciudățenie, avem două opțiuni:
- Să ne enervăm și să țipăm la proiectanții limbajului SQL
- Să ne acceptăm soarta, să închidem ochii, să uităm de snytaxă și să ne amintim ordinea operațiilor logice
În general, recomand cea de-a doua variantă, pentru că atunci lucrurile încep să aibă mult mai mult sens, inclusiv frumosul calcul al veniturilor zilnice cumulate de mai jos, care înglobează veniturile zilnice (SUM(amount)
funcția de agregare) în interiorul veniturilor cumulate (SUM(...) OVER (...)
funcția fereastră):
SELECT payment_date, SUM(SUM(amount)) OVER (ORDER BY payment_date) AS revenueFROM paymentGROUP BY payment_date
… pentru că agregările au loc, în mod logic, înaintea funcțiilor de fereastră.
Caveat: clauza ORDER BY
Există câteva caveat-uri în jurul clauzei ORDER BY
, care ar putea contribui la o confuzie suplimentară. În mod implicit, continuă să presupună că ordinea logică a operațiilor este corectă. Dar apoi, există câteva cazuri speciale, în special:
- În absența unei clauze
DISTINCT
- În absența unor operații de set cum ar fi
UNION
Puteți face referire la expresii în ORDER BY
, care nu sunt proiectate de SELECT
. Următoarea interogare este perfect corectă în majoritatea bazelor de date:
SELECT first_name, last_nameFROM actorORDER BY actor_id
Există o proiecție „virtuală” / implicită ACTOR_ID
, ca și cum am fi scris:
SELECT first_name, last_name, actor_idFROM actorORDER BY actor_id
Dar apoi, am eliminat din nou coloana ACTOR_ID
din rezultat. Acest lucru este foarte convenabil, deși ar putea duce la unele confuzii cu privire la semantica și ordinea operațiilor. Mai exact, nu puteți utiliza, de exemplu, DISTINCT
într-o astfel de situație. Următoarea interogare nu este valabilă:
SELECT DISTINCT first_name, last_nameFROM actorORDER BY actor_id -- Oops
Pentru că, ce se întâmplă dacă există doi actori cu același nume, dar cu ID-uri foarte diferite? Ordinea ar fi acum nedefinită.
În cazul operațiilor cu seturi, este și mai clar de ce nu este permisă:
SELECT first_name, last_nameFROM actorUNIONSELECT first_name, last_nameFROM customerORDER BY actor_id -- Oops
În acest caz, coloana ACTOR_ID
nu este prezentă în tabelul CUSTOMER
, așa că interogarea nu are niciun sens.
Citește mai departe
Vrei să afli mai multe? Avem, de asemenea, aceste articole pe care le puteți citi:
- SQL GROUP BY și dependențe funcționale: O caracteristică foarte utilă
- Cum ar fi trebuit să fie proiectat SQL GROUP BY – Ca și Neo4j’s Implicit GROUP BY
- Cum să traduci SQL GROUP BY și agregările în Java 8
- Înțelegi cu adevărat clauzele GROUP BY și HAVING din SQL?
- GROUP BY ROLLUP / CUBE
.