SELECT IV - poddotazy
Tato kapitola bude trochu náročnější mozkové cvičení. Proberu zde poddotazy a korelované dotazy, které se vám budou v praxi hodit častěji, než by se mohlo zdát.
Protože je toto poslední kapitola ze základního kurzu o SQL,
využiju teď chvilku k tomu, abych udělal reklamu svým dalším
tutoriálům. Například byste se mohli podívat na programovací jazyk
Python, který umožňuje pomocí
modulu psycopg2 s databází
PostgreSQL komunikovat. Pokud nevíte o programování zhola nic, pak
to budete mít trochu těžší. Programování je vysvětleno od úplných
začátků v článku o programovacím jazyku C.
Konec reklamního bloku. A teď zpět k Postgresu :-)
Poddotazy
Poddotazem se myslí příkaz SELECT
, jehož výsledek se použije buď jako tabulka
pro další SELECT v rámci jednoho SQL dotazu, nebo se vrácená hodnota (hodnoty) použíjí třeba
v podmínce WHERE
.
Podívejte se nejdříve na využítí SELECTu vracejícího jednu hodnotu. Takovým příkazem bude typicky nějaká statistická hodnota, například průměrný plat. Pokud vás například budou zajímat všichni zaměstnanci s průměrným a nadprůměrným platem, docílíte toho následujícím poddotazem (tabulku zamestnanci jsem vytvořil v kapitole Vytváření relací). Pro zajímavost jsem výsledek setřídil sestupně dle výše platu.
WHERE plat >= (SELECT AVG(plat) FROM zamestnanci)
ORDER BY plat DESC;
jmeno | prijmeni | plat | oddeleni_id | rodne_cislo
-------+----------+-------+-------------+-------------
Tomas | Mann | 22000 | 3 | 8001010608
Vasek | Trn | 16000 | 3 | 8001010609
Lenka | Pavova | 15000 | 2 | 8001010604
Tom | Jerry | 15000 | 2 | 8001010605
(4 řádky)
Poddotaz musí být v závorkách a musí vrátit jen jednu hodnotu (jeden řádek s jedním sloupcem), jinak by tato podmínka nedávala smysl.
Teď řekněme, že mě zajímá průmerný plat z předchozího výsledku.
Jednou z možností by bylo udělat z předchozího SELECTu pohled (VIEW)
a nad ním zavolat AVG
.
Nebo se s vytvářením pohledu nebudu zdržovat a udělám rovnou SELECT
z předchozího SELECTu.
Všiměte si, že jsem vnitřní SELECT
pojmenoval xxx.
Respektive jsem tak pojmenoval výsledek vnitřního SELECTu, kterým je vždy
tabulka. Takže jsem „stvořil“ tabulku xxx,
ze které jsem provedl SELECT AVG(xxx.plat)
Pokud používám takto poddotaz jako tabulku, vždycky musí být pojmenována.
Použití tečkové notace s názvem tabulky (xxx.plat
) už v příkladu nebylo nezbytné,
použil jsem ho jen pro příklad.
SELECTů jako tabulek můžete v jednom příkazu použít více, můžete je pak JOINovat a dělat s nimi cokoliv, co s pohledem (VIEW). Poddotaz není vlastně nic jiného, než ad-hoc VIEW.
Porovnávání hodnoty s polem hodnot
V následujících odstavcích popíšu operátory, které se používají k porovnávání s polem hodnot. Můžete je používat s poddotazy i bez nich, ale teprve s poddotazy oceníte jejích sílu :-)
IN
IN
je operátor, který říká, zda je proměnná v nějaké
množině hodnot nebo ne (alespoň jednou), a používá se v podmínce WHERE
(stejně jako třeba = atp.). Množinou můžou být hodnoty oddělené
čárkou uzavřené v závorkách, nebo výsledek poddotazu >
1 sloupcec s libovolným počtem
řádků.
Například budu chtít zjistit zaměstnance, kteří pracují ve všech právních odděleních.
Můžu na to jít takto:
prim_klic
-----------
2
3
(2 řádky)
rimmer1=> SELECT * FROM zamestnanci WHERE oddeleni_id IN (2, 3);
jmeno | prijmeni | plat | oddeleni_id | rodne_cislo
---------+----------+-------+-------------+-------------
Lenka | Pavova | 15000 | 2 | 8001010604
Tom | Jerry | 15000 | 2 | 8001010605
Martin | Luter | 12000 | 2 | 8001010606
Leopold | King | 13000 | 2 | 8001010607
Tomas | Mann | 22000 | 3 | 8001010608
Vasek | Trn | 16000 | 3 | 8001010609
Stary | Osel | 9000 | 3 | 8001010610
(7 řádek)
Nebo rovnou takto:
SELECT prim_klic FROM oddeleni WHERE nazev = 'Pravni oddeleni'
);
jmeno | prijmeni | plat | oddeleni_id | rodne_cislo
---------+----------+-------+-------------+-------------
Lenka | Pavova | 15000 | 2 | 8001010604
Tom | Jerry | 15000 | 2 | 8001010605
Martin | Luter | 12000 | 2 | 8001010606
Leopold | King | 13000 | 2 | 8001010607
Tomas | Mann | 22000 | 3 | 8001010608
Vasek | Trn | 16000 | 3 | 8001010609
Stary | Osel | 9000 | 3 | 8001010610
(7 řádek)
Jak vidíte, použití poddotazu vám ušetří ruční přepisování výsledku prvního SELECTu do podmínky druhého.
Opakem IN
je NOT IN
.
NOT IN
vyhoví tehdy, pokud hodnota není v množině ani jednou.
ALL
ALL
spolupracuje s dalším operátorem (dle vašeho výběru),
který aplikuje na celou množinu.
Řádek je do výsledku zahrnut pouze
tehdy, vrátí-li operátor pro všechny hodnoty v množině true.
Příklad: vypište všechny zaměstnance, kteří mají maximální plat.
Pouze maximální plat je roven nebo větší než všechny platy:
Všiměte si podmínky WHERE plat IS NOT NULL
, bez které by
ALL
použil operátor na NULL
hodnotu (která se vyskytuje v jednom řádku
tabulky zamestnanci) a tím pádem by bez milosti vrátil pro každý řádek NULL
.
Výsledkem by bylo, že podmínka nikdy nevyhovuje. (Nic není >= NULL.)
S ALL
můžete použít jakýkoliv operátor (v příkladu >=
),
který vrací booleovskou hodnotu (true nebo false).
Opakem ALL
není výraz operátor NOT ALL (…)
,
ale NOT výraz operator ALL (…)
.
ANY/SOME
ANY
je podobné jako ALL
, jen s tím rozdílem,
že vrací true, když operátor vrátí true alespoň pro jednu hodnotu
z množiny.
V příkladu vyberu všechny platy, které jsou větší nebo rovny dvojnásobku nejmenšího platu. (Nejmenší plat je 9000. Takže podmínku splní každý plat který je větší nebo roven 18000).
Podmínka ANY
požaduje splnění podmínky (>=) alespoň
u jednoho záznamu. V příkladě tedy stačí, aby byl plat větší než
dvojnásobek toho nejmenšího.
Všiměte si, že ve vnořeném SELECTu není podmínka NOT NULL
.
ANY vrací true, když vyhoví alespoň jedna hodnota z množiny. Když jsou některé
hodnoty NULL, tak sice nevyhoví, ale to nevadí.
SOME
je jen synonymum pro ANY
.
Operátor IN
je vlastně jen synonymum pro = ANY
.
WHERE oddeleni_id = ANY (
SELECT prim_klic FROM oddeleni WHERE nazev = 'Pravni oddeleni'
);
jmeno | prijmeni | plat | oddeleni_id | rodne_cislo
---------+----------+-------+-------------+-------------
Lenka | Pavova | 15000 | 2 | 8001010604
...
Stary | Osel | 9000 | 3 | 8001010610
(7 řádek)
Porovnávání řádků
V SQL existuje něco, čemu se říká řádkový konstruktor.
Řádkový konstruktor vypadá takto: ROW(hodnota, hodnota, …)
Klíčové slovo ROW
není povinné, pokud konstruujete řádek s více jak
jedním sloupcem. Takže jako řádkový konstruktor
slouží i jen hodnoty oddělené čárkou uzavřené do závorek.
Příklad použití:
A teď k čemu je to dobré: řádky můžete porovnávat. Porovnávají se z leva do prava po jednotlivých sloupcích (hodnotách). Aby se dva řádky rovnali, musí být všechny hodnoty stejné. Aby byl jeden řádek větší než druhý, musí mít větí první nestejnou hodnotu (z leva).
Praktičtější využití může být toto (oba SELECTy dělají totéž):
Zábava příjde s poddotazy. Jako příklad zkusím najít všechny telefony, které mají předvolbu a operatora jinou, než je v tabulce operator. Abych něco našel, tak si nejdřív něco takového do tabulky vložím:
Teď onen SELECT
s poddotazem:
WHERE (t.predvolba,t.operator_id) NOT IN (SELECT p.predvolba, p.operator_id FROM predvolba p);
predvolba | telefon | operator_id | tarif_id | kontakt_id | poznamka
-----------+---------+-------------+----------+------------+----------
908 | 666666 | 2 | | |
(1 řádka)
Předchozí SELECT se dá přepsat pomocí JOINu. Není z něho možná tak „čitelné“ o co se snažím, ale JOIN verze SELECTů bývají v praxi o dost rychlejší než SELECTy s poddotazy. (V tabulce s pár tisíci záznami rozdíl obvykle nepoznáte.)
Pozor! Následující SQL dotaz dělá něco jiného a nevrátí žádnou řádku:
Korelované dotazy
Při vytváření korelovaného dotazu se využívá jednak poddotazů a jednak aliasů. Korelované dotazy jsou dotazy několika SELECTů do jedné a té samé tabulky (pomocí poddotazů) v rámci jednoho SQL příkazu. Aliasy slouží k tomu, aby šlo rozeznat z jakého SELECTu (poddotazu) pochází sloupec, se kterým se pracuje.
Korelované dotazy nejsou opravdu nic jiného než poddotazy nad stejnou tabulkou a několik příkladů jste už vyděli s tabulou zamestnanci výše. Jen se navíc navzájem mezi se bou odkazují a ovlivňují tak svůj výsledek.
Pomocí korelovaného dotazu můžu například vypsat všechny zaměstnance, kteří mají maximální plat v oddělení, ve kterém pracují.
FROM zamestnanci z1
WHERE z1.plat = (
SELECT MAX(z2.plat) FROM zamestnanci z2
WHERE z1.oddeleni_id = z2.oddeleni_id
);
prijmeni | plat | oddeleni_id
----------+-------+-------------
Mala | 12000 | 1
Pavova | 15000 | 2
Jerry | 15000 | 2
Mann | 22000 | 3
(4 rows)
Podívejte se, kde je zakopaný pes. První a druhá řádka příkazu je obyčený
SELECT
s aliasem tabulky zamestnanci z1.
V tuto chvíli by se vybrali všechny
řádky z tabulky zamestnanci (alias z1
).
Mě však zajímají jen ty řádky z tabulky z1, kde je maximální
plat pro řádky se stejnou hodnotu v sloupci oddeleni_id.
Tato podmínka je podstatou tohoto korelovaného dotazu. Moc dobře se to pochopit nedá, že? Ale jde to :-).
Ve výsledku to funguje asi takto: Pro každý řádek z tabulky z1
se udělá vnořený SELECT nad tabulkou z2, do jehož podmínky
se za z1.oddeleni_id
dosadí hodnota aktuálně zpracovávaného řádku z z1,
pro který se korelovaný dotaz provádí.
Teoreticky to znamená, že se pro každý řádek ze z1 provede extra
poddotaz, což není zrovna nejvýkonější záležitost. V našem příkladě by si mohl
optimalizátor DBMS zapamatovat výsledky pro stejná z1.oddeleni_id
a nedělat
stejný dotaz znovu, ale i tak je lepší se korelovaným dotazům vyhýbat
(pokud je to teda možné).
Není důvod, proč si to ještě trochu nezkomplikovat. Místo oddeleni_id bude jistě lepší mít ve výsledku název oddělení. Název oddělení je uložen v tabulce oddeleni, takže se musí předcházející korelovaný dotaz propojit (JOINovat) s touto tabulkou.
FROM zamestnanci z1, oddeleni
WHERE z1.oddeleni_id = oddeleni.prim_klic AND
z1.plat = (
SELECT max(z2.plat) FROM zamestnanci z2
WHERE z1.oddeleni_id = z2.oddeleni_id
);
prijmeni | plat | nazev
----------+-------+-----------------
Mala | 12000 | Sekretariat
Pavova | 15000 | Pravni oddeleni
Jerry | 15000 | Pravni oddeleni
Mann | 22000 | Pravni oddeleni
(4 rows)
MySQL
MySQL zachází poněkud podivně s hodnotou NULL
u operátoru ALL
.
Náslecující SELECT vrátí maximální plat, i když existuje řádek, kde je plat NULL
.
V PostgreSQL i Oracle je potřeba přidat do poddotazu podmínku plat IS NOT NULL.
MySQL neumí zacházet s řádkem jako by to byl sloupec (nemá datový typ „řádek“), takže tento příklad nefunguje:
Jinak funguje vše jako v Postgresu, včetně tohoto:
SQLite
SQLite nemá operátory ALL
a ANY/SOME
.
Nemá ani konstruktor řádku (ROW
), takže celou část kapitolu o něm
můžte, pokud jde o SQLite, zapomenout. (Což není taková hrůza, protože víte, jak
všechny SQL dotazy přepsat bez použití řádkového konstruktoru.)
Vše ostatní z této kapitoly funguje tak, jako v PostgreSQL.
Oracle
Oracle umí řádkový konstruktor tak trochu. Umí ho jen v části WHERE
a jen s poddotazy. Proto toto v Oracle nefunguje:
Zatímco první dva SELECTy byste museli nějak přepsat bez použití řádkového konstruktoru, ten třetí jde za použití poddotazu. Toto v Oracle funguje:
Nicméně, asi by bylo lepší i toto přepsat bez řádkového konstruktoru (viz výše, kde je to ukázáno).
Protože funguje řádkový konstruktor jen v klauzuli WHERE
, nefunguje ani toto:
A musí se to přepsat takto:
Vše ostatní z této kapitoly funguje tak, jako v PostgreSQL.