Slovník | Vyhledávání | Mapa webu
 
Základy informatiky pro biologyDatabázové systémy v biomedicíně Pokročilé SQL Vnořené dotazy

Logo Matematická biologie

Vnořené dotazy

Připomeňme si, co s pomocí SQL jazyka již umíme. Získat konkrétní hodnotu zvoleného sloupce, filtrovat konkrétní řádek z libovolné tabulky, pomocí agregačních funkcí a klauzule GROUP BY získat sumární přehled o obsahu zvolených sloupců a umíme propojit záznamy ve více tabulkách pomocí výrazu JOIN ON. Pro čerpání primárních dat z databáze pro další zpracování například ve statistickém software je to zcela dostačující. Zdaleka to ale není vše, co relační databáze a standard SQL nabízejí. Pokročilé SQL začíná možností vnořených (nested) dotazů.

Vnořený dotaz má stejnou strukturu jabo běžný dotaz, pouze je uzavřen v kulatých závorkách a umístěn v nadřazeném dotazu na jednom z těchto míst:

  • Na místě výčtu sloupců mezi slovy SELECT a FROM
  • Na místě názvu tabulky za FROM
  • Jako součást podmínky za slovem WHERE

Za klíčovým slovem SELECT můžeme použít zanořený dotaz, který vrátí právě jeden sloupec a právě jeden řádek. Tuto možnost využijeme, pokud chceme do přehledu či do výpočtu získat výsledek agregační funkce, např. chceme procentické zastoupení. Představme si tabulku s daty o denní spotřebě léků, ze které chceme získat přehled, kolik procent se vyčerpalo daný den.


SELECT datum, mnozstvi, mnozstvi / (SELECT SUM(mnozstvi) FROM spotreba) * 100  FROM spotreba

Uvedený dotaz je složen ze dvou částí. Základem je prostý SELECT do tabulky spotreba, odkud získáme data sloupců datum a mnozstvi. Do tohoto dotazu je vložen vnořený dotaz, který pomocí agregační funkce SUM získá celkové spotřebované množství. Tímto číslem dělíme hodnotu každého řádku tabulky spotřeby a násobíme stem, čímž získáme spotřebu daného dne v procentech.  

Vnořený dotaz je také možné uvést za klíčové slovo FROM a použít ho tak místo názvu tabulky. Tento postup použijeme při sestavování složitých dotazů, kdy začneme jednodušším dotazem, jehož výsledek použijeme v nadřízeném dotazu k další manipulaci. Tento typ zanoření použijeme také v případě, kdy potřebujeme rychle získat počet řádků, které vrací náš dotaz. Mějme dotaz: 

SELECT * FROM spotreba WHERE mnozstvi > 100

Pokud tabulka spotreba obsahuje tisíce a více řádků, netušíme, kolik řádků dotaz vrátil, dokud nenecháme všechny výsledné řádky zobrazit, což je při ladění dotazů velmi neefektivní. Nejrychlejší způsob, jak získat počet řádků laděného dotazu, je jeho zapouzdření do vnořeného dotazu a aplikace agregační funkce COUNT:

SELECT COUNT(*) FROM (
            SELECT * FROM spotreba WHERE mnozstvi > 100)  

Platí, že vnořené dotazy na pozici za FROM je možné vždy spustit samostatně, tedy nezávisle na nadřízeném dotazu. Zanoření je možné opakovat na další vyšší úrovni. Počet možných zanoření je závislé na limitech daného databázového systému.

Třetím a nejčastějším umístěním vnořeného dotazu je v podmínce za WHERE. Zde může být využit jako operand podmínky nebo v kombinaci s výrazem (NOT) EXISTS jako samostatná podmínka. Dotaz vkládáný jako operand může být umístěn buď přímo za operátor (=, <, >, <>), nebo s použitím modifikátoru ANY nebo ALL. Pokud je vnořený dotaz přímo za operátorem, musí dotaz vracet právě jeden sloupec a právě jeden řádek. Typicky se zde používají dotazy s agregační funkcí. Pokud použijeme kromě operátoru také modifikátor, zůstává omezení na jeden sloupec, ale řádků může dotaz vracet 0 až N. Poslední možností je umístění vnořeného dotazů za výraz EXISTS. V této variantě není počet sloupců vnořeného dotazu významný, používá se buď * nebo jakákoliv konstanta (1). Podle počtu vrácených řádků se vyhodnotí pravdivost výrazu EXISTS. Pokud dotaz nevrátí žádný řádek, je výsledek FALSE, pokud vrátí 1 až N řádků, je výsledek výrazu TRUE. Pokud použijeme negaci v podobě výrazu NOT EXISTS je výsledek opačný.

U vnořených dotazů umístěných za WHERE budeme až na výjimky definovat podmínku, která prováže vnořený dotaz s rodičovským dotazem. Mějme dvě tabulky, jedna s názvem student obsahuje jména studentů, druhá tabulka s názvem zkoušky obsahuje informace o složených zkouškách jednotlivých studentů. Pomocí spojení (JOIN) těchto tabulek můžeme získat přehled o absolvovaných zkouškách jednotlivých studentů. Co ale v případě, že chceme získat seznam studentů, kteří doposud žádnou zkoušku nesložili a nemají žádný řádek v tabulce zkouska. Právě v těchto případech využijeme vnořený dotaz s výrazem NOT EXISTS. 

SELECT * FROM student WHERE NOT EXIST (SELECT 1 FROM zkouska WHERE student.uco = zkouska.uco)

Všimněme si podmínky student.uco = zkouska.uco. Pokud bychom ji vynechali, dostali bychom neprázdný výsledek jen v případě, kdyby tabulka zkouska byla prázdná. Vložená podmínka zajistí, že se bude tabulka zkouska prohledávat pro každé uco studenta zvlášť.  Častou chybou bývá opomenutí nebo chybná definice propojovací podmínky, což má za následek zcela chybný výsledek dotazu. V propojovací podmínce spojujeme sloupce z nadřazeného dotazu se sloupci vnořeného dotazu. Platí, že ve vnořeném dotazu se můžeme odkazovat na všechny sloupce dotazu nadřízeného, ale nikoliv naopak, v nadřízeném dotazu nesmí být žádný odkaz na v něm vnořené dotazy. 

V SQL vede ke stejnému výsledku často několik cest. Pokud neřešíme rychlost dotazu, záleží na našich preferencích, kterou cestu zvolíme. Například hledání nejstaršího studenta můžeme řešit minimálně třemi způsoby:

SELECT * FROM student WHERE datum_narozeni = (
            SELECT MIN (datum_narozeni) FROM student) 

Tímto způsobem hledáme studenty, jejichž datum narození se rovná nejmenšímu (nejstaršímu) datu v tabulce. 

SELECT * FROM student WHERE datum_narozeni <= ALL (
            SELECT datum_narozeni FROM student)

Tímto způsobem hledáme studenty, jejichž datum narození je menší nebo rovno než všechny datumy v tabulce. Pokud nemá nikdo menší datum narození než já, jsem nejstarší.

SELECT * FROM student ridici WHERE NOT EXIST (
            SELECT 1 FROM student vnoreny
            WHERE ridici.datum_narozeni > vnoreny.datum_narozeni)

Pro každý řádek řídícího dotazu je prohledávána tabulka vnořeného dotazu (v našem případě stejná tabulka student), zda obsahuje záznam s menším datem narození. Pokud takový řádek neexistuje je splněna podmínka NOT EXISTS a daný řádek je zobrazen. 

Jako složitější příklad můžeme uvést požadavek, kdy chceme vidět jména studentů, kteří již absolvovali alespoň tři zkoušky  a všechny na první pokus. Toto je opět příklad, kde k výsledku povede více cest, podívejme se na jednu z nich. Potřebujeme nejprve vybrat ty studenty, kteří mají v tabulce zkouska alespoň 3 řádky s různým kódem předmětu. Pokud se spokojíme s uco studenta, vystačíme si s tabulkou zkouska.

SELECT uco, COUNT(DISTINCT predmet) FROM zkouska GROUP BY uco

Tento dotaz nám vrátí přehled o počtu zkoušek jednotlivých studentů.  Klíčové slovo DISTINCT zajistí, že se bude každý předmět počítat jen jednou. Studenti, kteří doposud žádnou zkoušku nesložili, v seznamu nebudou, protože žádný záznam v tabulce nemají. To nám nevadí, protože nás zajímají pouze studenti s alespoň třemi zkouškami. Abychom vyfiltrovali studenty s jednou a dvěmi zkouškami, doplníme dotaz o podmínku. Podmínku uvedeme nikoliv za WHERE, ale za klíčové slovo HAVING, protože již pracujeme s agregovaným výsledkem (počet zkoušek v primární tabulce není). Doplněný dotaz vypadá takto:

SELECT uco, COUNT(DISTINCT predmet) FROM zkouska GROUP BY uco

HAVING COUNT(DISTINCT predmet) >= 3 

Nyní ověříme, že v našem seznamu není student s neúspěšnou zkouškou (žádný řádek s F):

SELECT uco, COUNT(DISTINCT predmet) FROM zkouska

WHERE NOT EXIST (
            SELECT 1 FROM zkouska vnoreny
            WHERE vnoreny.uco = ridici.uco and vnoreny.znamka = 'F') 

GROUP BY uco

HAVING COUNT(DISTINCT predmet) >= 3

Pokud se ptáte, proč jsme místo vnořeného dotazu nevložili přímo do původního dotazu za WHERE podmínku znamka <> 'F', uvědomte si, že v tomto případě by nám v seznamu zůstal student, který má 3 a více úspěšných zkoušek a libovolný počet neúspěšných. Jednoduchá podmínka by pouze odfiltrovala jeho neúspěšné pokusy ještě před provedením operace GROUP BY.  My však chceme studenty bez F, proto je nutné podmínku prověřit v zanořeném dotazu. 
Nyní známe uco hledaných studentů a potřebujeme doplnit jméno. Připojíme k výslednému dotazu tabulku student.

SELECT jmeno FROM student JOIN

(SELECT uco, COUNT(DISTINCT predmet) FROM zkouska

WHERE NOT EXIST (
            SELECT 1 FROM zkouska vnoreny
            WHERE vnoreny.uco = ridici.uco and vnoreny.znamka = 'F') 

GROUP BY uco

HAVING COUNT(DISTINCT predmet) >= 3

) filtr ON student.uco = filtr.uco

Připojení jsme provedli tak, že jsme náš připravený dotaz zanořili a umístili místo názvu tabulky na pozici za JOIN. Za uzavírací závorkou jsme si tento dotaz pojmenovali jako "filtr", abychom mohli definovat spojovací podmínku za ON.

 
vytvořil Institut biostatistiky a analýz Lékařské fakulty Masarykovy univerzity