No dobrze, umiemy wypisywać już całkiem sporo, ale jak na przykład wypisać wszystkich (niekoniecznie bezpośrednich) podwładnych danego pracownika, niezależnie od liczby poziomów hierarchii?
Aby zapisać takie zapytania, trzeba mieć możliwość policzenia domknięcia przechodniego. Inaczej mówiąc, potrzebna jest rekursja. SQL dość długo nie pozwalał na zapytania rekurencyjne: stwarzają kłopoty zarówno teoretyczne (negacja) jak i praktyczne (wydajność).
Zacznijmy od perspektywy: konstrukcji, która nie zwiększa mocy zapytań, ale pozwala wygodniej je zapisywać. Przypuśćmy, że chcemy znaleźć imię najcięższego zwierzaka dla każdego gatunku
SELECT imie, Zwierzaki.gatunek FROM Zwierzaki JOIN (SELECT gatunek, MAX(waga) mw FROM Zwierzaki GROUP BY gatunek) Maksy ON Zwierzaki.gatunek = Maksy.gatunek WHERE waga = mw;
Jeśli takiego wewnętrznego zapytania używamy w wielu miejscach, to warto zdefiniować perspektywę
CREATE VIEW Maksy AS SELECT gatunek, MAX(waga) mw FROM Zwierzaki GROUP BY gatunek;
Powstaje wirtualna tabela Maksy
, której możemy używać
jak zwykłej tabeli
SELECT imie, Zwierzaki.gatunek FROM Zwierzaki JOIN Maksy ON Zwierzaki.gatunek = Maksy.gatunek WHERE waga = mw;
Jednak materialnie tabela taka nie istnieje -- jest wyliczana przy każdorazowym użyciu, podobnie jak procedury czy funkcje w konwencjonalnych językach programowania.
Perspektywy są bardzo wygodne, ale zaśmiecają przestrzeń nazw, ponieważ są definiowane globalnie w schemacie. Jeśli chcemy perspektywy użyć tylko przez chwilę, trzeba po sobie posprzątać
DROP VIEW Maksy;
Z pomocą przychodzi konstrukcja WITH
, pozwalającą definiować
perspektywy lokalne (jak bloki w zwykłych językach programowania).
Czasem nazywa się ją Common Table Expression (CTE).
Podstawowa składnia:
WITH lokalna-relacja AS ( definicja -- najczęściej wyrażenie SELECT )[, ...)] zapytanie-z-lokalna-relacją;
Banalny przykład: jeśli globalną perspektywę zapisujemy używając
CREATE VIEW gatavg(gat, m) AS (SELECT gatunek, avg(waga) FROM zwierzaki GROUP BY gatunek);
i potem możemy napisać
SELECT gat FROM gatavg WHERE m = (SELECT max(m) FROM gatmaks);
To samo z perspektywą lokalną (CTE) wygląda następująco
WITH gatavg(gat, m) AS (SELECT gatunek,avg(waga) FROM Zwierzaki GROUP BY gatunek) SELECT gat FROM gatavg WHERE m = (SELECT max(m) FROM gatavg);
Inny przykład
WITH stud_mat AS ( SELECT * FROM students WHERE wydz='MIMUW' ) SELECT * FROM stud_mat WHERE rok != 1 ORDER BY nazwisko, imie;
Równoważna postać bez CTE
SELECT * FROM (SELECT * FROM students WHERE wydz = 'MIMUW') AS stud_mat WHERE rok != 1 ORDER BY nazwisko, imie;
Perspektyw lokalnych może być więcej
WITH gatmaks(gat,kont,m) AS (SELECT nazwa,kontynent,max(waga) FROM zwierzaki JOIN gatunki ON nazwa = gatunek GROUP BY nazwa), kontmaks(kont,mk) AS (SELECT kont, max(m) from gatmaks group by kont) SELECT kont FROM kontmaks WHERE mk = (SELECT max(mk) FROM kontmaks);
a także
WITH stud_mat AS ( SELECT * FROM students WHERE wydz='MIMUW' ), stud_mat_inf AS ( SELECT * from stud_mat WHERE kierunek = 'Informatyka' ) SELECT * FROM stud_mat_inf WHERE rok != 1 ORDER BY nazwisko, imie;
Przejdźmy do obiecanych zapytań rekurencyjnych. Potrzebna nam będzie baza danych z zależnościami hierarchicznymi. Taka ,,oficjalna'' demonstracyjna baza danych Oracle znajduje się na pliku demobld.sql.
Aby zdefiniować zapytanie rekurencyjne należy dodać po
WITH
słowo kluczowe RECURSIVE
.
Najprostszy format wygląda tak:
WITH RECURSIVE nazwa ([nazwy kolumn]) AS ( zapytanie inicjujące UNION zapytanie poszerzające, korzystające z nazwa ) zapytanie korzystające z nazwa;
Koncepcyjnie działa to wtedy następująco. Najpierw do lokalnej perspektywy
Prześledźmy ten proces na przykładzie zapytania liczącego sumę liczb
od 1 do 100:
Jeżeli chcemy wypisać wszystkie osoby, za które odpowiada KING
(czyli jego podwładnych i jego samego) możemy napisać coś takiego:
W każdym kolejnym kroku dodajemy teraz kolejny poziom podwładnych.
1. Wypisz wszystkich podwładnych Kinga bez niego samego
2. Wypisz wszystkich podwładnych Kinga bez Blake'a i jego podwładnych
3. Wypisz wszystkich pracowników którzy mają ,,pod sobą'' SALESMANa
4. Wypisz dla każdego pracownika sumę zarobków jego
i jego podwładnych
5. [Trudne!] Wypisz imiona wszystkich podwładnych Kinga (razem z nim)
w taki sposób aby uzyskać strukturę drzewa:
Może się przydać funkcja
WITH RECURSIVE t (liczba) AS (
SELECT 1
UNION
SELECT liczba + 1 FROM t WHERE liczba < 100
)
SELECT sum(liczba) FROM t;
WITH RECURSIVE sub (empno, ename) AS (
SELECT empno, ename FROM emp WHERE ename = 'King'
UNION
SELECT emp.empno, emp.ename FROM emp, sub WHERE emp.mgr = sub.empno
)
SELECT * FROM sub;
Zadanie 1
King
Jones
Scott
Adams
Ford
Smith
...
rpad()
.
Odpowiedzi
with recursive sub (empno, ename) as (
select empno, ename from emp where ename = 'King'
union
select emp.empno, emp.ename from emp, sub
where emp.mgr = sub.empno
)
select * from sub where ename != 'King';
with recursive sub (empno, ename) as (
select empno, ename from emp where ename = 'King'
union
select emp.empno, emp.ename from emp, sub
where emp.mgr = sub.empno and emp.ename != 'Blake')
select * from sub;
WITH RECURSIVE sub (empno, ename, mgr, job) AS (
SELECT empno, ename, mgr, job FROM emp WHERE job = 'SALESMAN'
UNION
SELECT emp.empno, emp.ename, emp.mgr, emp.job FROM emp, sub
WHERE sub.mgr = emp.empno
)
SELECT * FROM sub;
SELECT p.ename,
(WITH RECURSIVE sub (empno, ename, sal) AS (
SELECT empno, ename, sal FROM emp WHERE ename =p.ename
UNION
SELECT emp.empno, emp.ename, emp.sal FROM emp, sub
WHERE emp.mgr = sub.empno)
SELECT SUM(sal) FROM sub) FROM emp p;
albo
with recursive foo as (select empno as p,empno as c,sal from emp
union
select foo.p, emp.empno, emp.sal
from foo join emp on foo.c = emp.mgr)
select p, sum(sal) from foo group by p;
p | sum
------+----------
7844 | 1500.00
7900 | 950.00
7369 | 800.00
7566 | 10875.00
7876 | 1100.00
7782 | 3750.00
7902 | 3800.00
7788 | 4100.00
7654 | 1250.00
7934 | 1300.00
7521 | 1250.00
7839 | 29025.00
7499 | 1600.00
7698 | 9400.00
(14 wierszy)