Pořád dokola mě překvapuje, jak málo programátorů zná množinové operace v SQL. Možná ještě méně jich zná “common table expressions”. A pokud už znají obojí, nevědí, jak vše použít s rekurzí. A jen malá hrstka ví, jak to vše funguje uvnitř. Proto tento malý přehled (od jednoduššího ke složitějšímu).
Množinové operace
Základy teorie množin zná asi každý. Ostatně SQL nenabízí o mnoho více než průnik, sjednocení a rozdíl. Jsou popsány v každém úvodu do SQL, ale uvedu zde příklad, protože bude potřeba dále. Mějme tedy tabulku edges se sloupci a a b, jež reprezentuje hrany nějakého orientovaného grafu. Získat SELECTem hrany začínající ve vrcholu 1 je triviální. Jak ale získat ty, které ve vrcholu 1 nezačínají? Zde pomůže rozdíl množin:
SELECT a, b FROM edges
EXCEPT
SELECT a, b FROM edges WHERE a = 1
Je jasné, že obě tabulky musí mít stejnou signaturu (počet sloupců a jejich typ). Jak říkám, je to triviální příklad, ale poslouží jako výchozí bod pro složitější aspekty dotazů do relačních databází.
Common table expressions (CTEs) a rekurze
CTEs jsou náhledy (views) existující pouze po dobu vyhodnocování dotazu. Pokud se nevyužije rekurze (zavedená v SQL3), jde jen o syntaktický cukr. S rekurzí jde ale o mocný nástroj. Pokud chceme získat všechny cesty v grafu (tj. tranzitivní uzávěr), použijeme právě CTE:
WITH paths AS (
SELECT a, b FROM edges
UNION
SELECT e.a, p.b FROM edges e, paths p WHERE e.b = p.a
) SELECT a, b FROM paths
První SELECT znamená, že hrana z a do b je cesta z a do b (to je dno rekurze). Druhý SELECT říká, že pokud máme hranu z a do b a cestu z b do c, máme také cestu z a do c. Sjednocením množin (UNION odstraní případné duplicity) získáme konečný výsledek dotazu.
Vyhodnocování rekurzívních dotazů
Nyní se konečně dostáváme k něčemu zajímavému. Jak se rekurzívní dotaz vyhodnucuje? Nejjednodušší by bylo vyhodnotit dotaz prostou rekurzí (backtrackingem). Pro názornost uvedu odpovídající pravidla v Prologu:
path(X, Y) :- edge(X, Y).
path(X, Z) :- edge(X, Y), path(Y, Z).
To je z pohledu logiky prvního řádu naprosto správně, ovšem pokud graf obsahuje cyklus, výpočet se zacyklí. A protože žádná databáze se na uvedeném příkladu nezacyklí, je zřejmé, že použitý algoritmus je poněkud (v některých databázích značně) složitější.
Jen o trochu komplikovanější je vyhodnocení dotazu výpočtem pevného bodu (tak funguje například sqlite). Začne se od nerekurzívní části dotazu (první argument operace UNION) a postupně se v cyklech přidávají další výsledky podle rekurzívní definice. Duplicity se odstraňují a pokud již není do přidat, je výsledek dotazu kompletní (z pohledu logiky se jedná o stabilní model uvažovaných relací a pravidel).
Ani tento algoritmus nelze bohužel použít vždy, nefungují s ním dotazy s operací EXCEPT. V nejobecnějším případě se používá algoritmus založený na principu “producer/consumer” (některé publikace uvádějí “server/requester”). Každý (rekurzivní) poddotaz se vyhodnocuje pouze jednou a mezivýsledky se ukládají do pomocných tabulek. Pokud nějaký poddotaz obsahuje rekurzívní “podpoddotaz”, zapíše se k němu jako “consumer”, resp. “requester”. Odpovídající pomocná tabulka následně funguje jako “producer” a posílá zprávy a nových mezivýsledcích. Někdy se tento postup nazývá “delayed evaluation”, protože vyhodnocování poddotazů se pozastavuje (v angličtině se poddotaz nazývá “suspended”) a pokračuje se později, jakmile je k dispozici další mezivýsledek. Implementace je značně komplikovaná, ale uvedený algoritmus zaručuje, že se výpočet na žádném dotaze nezacyklí (v obecnější verzi pro logické programování s negací se nazacyklí na žádném programu bez funkčních symbolů, jenž je “non-floundering”).
Pro zajímavost uveďme, že z pohledu výpočetní složitosti (a ta je u velkých databází extrémně důležitá) je řádově výhodnější použít levou rekurzi. V Prologu tedy něco, na čemž se většina implementací zacyklí:
path(X, Z) :- path(X, Y), edge(Y, Z).
path(X, Y) :- edge(X, Y).
jaka je u prvniho prikladu vyhoda oproti klasickemu
"SELECT a,b FROM edges WHERE a != 1;"?
Nenapada me priklad ve kterem bych nemohl podminku vyjadrit WHEREm primo. Mozna jedine u klauzuli typu UPDATE, ktere MySQL neumi vyhodnotit s vnorenym dotazem nad tou samou tabulkou "UPDATE ... WHERE foo = (SELECT ... LIMIT 1)".
[1] To jste se nedostal před první odstavec. Tam je ten příklad skutečně špatný :-)
A tady zjevně nešlo o where podmínku, ale o rekurzi. Jednodušší úloha: máte tabulku "directories", kde je sloupec s názvem adresáře (primární klíč) a názvem nadřazeného adresáře (shodný s názvem adresáře, pokud je to kořen). A pak tabulku "files", kde je název souboru (primární klíč) a název adresáře, kde je soubor uložen. A úloha zní vypsat seznam souborů a ke každému seznam adresářů z cesty k němu. Případně vyšší dívčí - seznam souborů a ke každému textový údaj s celou cestou k souboru. A vypořádat se pak i s chybou, když se adresáře zacyklí (A / B / C / D / B).
[9] Pokud by šlo čistě o iteraci, tak program v C nad daty v paměti bude cca 20x rychlejší - a pokud by rekurzivní prohledávání dat byla primární úloha, tak existují vhodnější nástroje než je relační SQL databáze. Nicméně rekurzivní prohledávání dat může být jen jednou z mnoha úloh, které chcete s daty v databázi dělat - např. operacemi nad číselníky získat seznam idček a pak dál už pracujete klasicky relačně - nebo rekurzí získáte popisky, které přijoinujete k agregovaným hodnotám - a pak se může hodit, že SQL databáze rekurzi umí. I když je relační databáze pomalejší než specializovaný sw, pořád umí vracet (i s rekurzí) desítky tisíc řádků za vteřinu, což na hodně úloh bohatě stačí. Určitě nemá relační a nemůže relační databáze s CTE zastoupit specializované grafové databáze nebo algoritmy, které jedou čistě nad maticemi v paměti - primárním účelem relačních databází je management dat - a CTE je aby se některé věci dělali pohodlněji.
Pokud byste chtěl dělat rekurzi nad daty uloženými v databázi externě, tak pak musíte počítat s režií exportu dat mimo databázi - network, interproces communication
[10] Programování v DB má význam například v okamžiku, kdy se vám mezivýsledky nevejdou do paměti a musíte mezivýsledky odkládat na disk. (BTW: Osobně jsem odpůrcem uložených procedur a triggerů jako standardního vývojového nástroje. Aplikační logika do DB dle mého názoru nepatří.)
CTE bez rekurze nemusí být jen syntaktický cukr. Jejich správné použití se může projevit na rychlosti výpočtu. Např:
with COMPLEX_QUERY as ( select ..... )
select ...
from COMPLEX_QUERY as A
join COMPLEX_QUERY as B on ....
Bez CTE většina databází vyhodnotí COMPLEX_QUERY 2x (bohužel).
Poznam to, riesenie cez Oracle forms bolo ponuknute zakaznikovi ako rozhranie, a ked sa to naucil pouzivat tak tam pridaval vypocty a na vypocitane hodnoty dalsie vypocty a tak asi 5 rokov. Potom bol DB server tak zatazeny ze niekedy jednoducho nieco nevypocital, bohuzial bolo nahodne co. Potom bolo treba zistit kde je diera v datach a dopocitat to.
Ale uvedomujem si ze to je ojedinely pripad, na jednej oracle konferencii sa prednasajuci spytal publika aby zodvihli ruku ludia co je ich server zatazeny nad 20% a zodvihol som ruku len ja.
[15] Triggery jistě, ačkoliv je to bez nich občas hodně velké zesložitění. Nicméně bez procedur si nedovedu představit rozumnou aplikaci, kde programátora zajímá integrita dat. Zvlášť v případě, že do databáze dělají zásahy aplikace od více dodavatelů.
Mimochodem, řeší článek nějakou konkrétní implementaci, nebo jde o obecné SQL? Protože to v něm není uvedeno. Řešit obecné SQL je dle mého názoru víceméně zbytečné (bohužel).
[17] Celý frk spočívá v tom, že vyjadřovací schopnosti vyšších jazyků (a jejich ladicích nástrojů) jsou někde úplně jinde než PL/SQL. Takže ono jde veškerou doménovou logiku nacpat do DB, ale prcačka je to neskutečná. Takže jestliže to jde, můžou cizí aplikace využívat ne rozhraní DB, ale rozhraní vyššího jazyku (když to zvládnou).
Druhým problémem je umístění dom. logiky do persistentní vrstvy, v případě, že ji potřebujete prohodit (např. za dokumentovou DB), můžete to všechno předělat.
Autor se zabývá vývojem kompilátorů a knihoven pro objektově-orientované programovací jazyky.
Přečteno 36 200×
Přečteno 25 361×
Přečteno 23 793×
Přečteno 20 177×
Přečteno 17 872×