Oskar Skibski, grupa nr 4, sala 2043
Bazy danych 2015/16
Laboratorium 4 (26.10.2015) - SQL / ZADANIA
Kilka trików
Trik 1: grupy z identycznymi wartościami
Czasem grupując wiersze wiemy, że w danej grupie wszystkie wartości w kolumnie po której nie grupujemy też są sobie równe. Na przykład jeżeli pogrupujemy tabelę (chociażby po złączeniu ją z inną) po kluczu głownym (takim jakim jest empno
w emp
) to wiemy, że w każdej grupie pola z tej tabeli będa sobie równe. W skrajnym przypadku możemy wiedzieć, że grupy mają jeden wiersz (np. dodając warunek HAVING COUNT(*) = 1
). Oracle jednak nie jest taki domyślny i nie pozwoli na wypisanie wartości z pól po których nie grupujemy. Jak sobie z tym poradzić? Możemy zrobić to na dwa sposoby. Pierwszym jest pogrupowanie także po tych kolumnach - skoro we wszystkich grupach wartości w nich są równe grupy nie rozpadną się bardziej, a Oracle przestanie się pluć.
SELECT dept.deptno, dname, loc, COUNT(*) immanuel
FROM dept LEFT JOIN emp ON dept.deptno = emp.deptno
GROUP BY dept.deptno, dname, loc;
Drugim sposobem jest użycie funkcji agregujacej MIN
lub MAX
która działa na wszystkich typach danych.
SELECT dept.deptno, MIN(dname), MIN(loc), COUNT(*) immanuel
FROM dept LEFT JOIN emp ON dept.deptno = emp.deptno
GROUP BY dept.deptno;
Trik 2: wiersz naj
Inną częstą sytuacją jest potrzeba wypisania danych z wiersza który zawiera największą/najmniejszą wartość w kolumnie. Przykładowo możemy szukać najbogatszego pracownika - największą pensję łatwo wybrać za pomocą funkcji agregującej, ale jak wybrać inne pola z tego wiersza? Można to zrobić podzapytaniem, ale ten efekt uzyskamy też sprytnie uzywając klasycznej konstrukcji łączenia - w tym celu wystarczy złączyć tabelę samą ze sobą (LEFT/RIGHT JOIN
) dla każdego wiersza szukając innych z wartościami większymi; wiersz bez dopasowania jest tym z największą wartością.
SELECT A.* FROM emp A LEFT JOIN emp B ON A.sal < B.sal WHERE B.sal IS NULL;
Warto zwrócić uwagę, że nie musieliśmy tu używać grupowania.
Trik 3: rankingi
Wiemy jak wypisać najbogatszego pracowika, ale jak wypisać np. trzech pierwszych? A uogólniając, załóżmy że tworzymy listę rankingową pracowników podług ich płac. Jak teraz przypisać każdemu pracownikowi jego pozycję w tym rankingu? Umiemy posortować wyświetlane wiersze w odpowiedniej kolejności i pierwszy pomysł wiąże się z konstrukcją ORDER BY
i specjalną zmienną ROWNUM
, która każdemu wierszowi wyniku przypisuje kolejny numer:
SELECT A.*, ROWNUM pozycja FROM (SELECT ename, sal FROM emp ORDER BY sal) A; # trochę źle!!
Co jednak się stanie jeżeli pracownicy będa zarabiali tyle samo? Konstrukcja ROWNUM
niedeterministycznie przypisze któremuś z nich niższą pozycję! (Ten sam problem pojawiłby się nam gdybyśmy próbowali jej użyć w problemie "wiersz naj" powyżej). Wiedząc, że konstrukcja ROWNUM
jest przereklamowana przypominamy sobie ponownie o złączeniu tabel i grupowaniu i piszemy coś takiego:
SELECT A.*, COUNT(B.sal)+1 pozycja FROM emp A LEFT JOIN emp B ON A.sal < B.sal GROUP BY A.empno;
Zapytanie to wygląda prosto, ale warto zwrócić uwagę na parę niuansów: każdy pracownik dostaje w nim grupę z osobami które zarabiają (ŚCIŚLE!!) więcej niż on. Najbogatszy pracownik dostanie sztuczną grupę z wierszem nullowy, ilość osób zliczamy zatem nie COUNT(*)
, a COUNT(B.sal)
. Alternatywnie można by też było dodać samego pracownika do wyszukiwanej grupy - unikniemy wtedy sztucznej grupy - jednak trzeba to zrobić ostrożnie i porównać klucz główny:
SELECT A.*, COUNT(*) FROM emp A LEFT JOIN emp B ON A.sal < B.sal OR A.empno = B.empno GROUP BY A.empno;
...a nie zmienić < na <=, bo przy równych pensjach dwaj najbogatsi pracownicy znaleźliby się na drugiej, a nie pierwszej pozycji (i pewnie ich by to nie ucieszyło).
Zadanie 1
Pobierz i wgraj bazę danych zwierzeta.sql.
- wypisz dla każdej osoby ile posiada żywych zwierząt (nawet jak ta wartość jest równa 0)
- dla każdego gatunku wypisz ile jest maksymalnie zwierząt o tym samym imieniu
- wypisz dla każdej matki jej imię, nazwisko, ilość dzieci ze znanym ojcem i ilość dzieci z nieznanym ojcem
- wypisz wszystkie różne imiona kotów osoby o nazwisku 'Makota' oraz jej dzieci
- wypisz dane (imie, nazwisko) ojca osoby z największą ilością (żyjących) zwierząt
- dla każdej osoby wypisz imię jednego z jego żywych zwierząt lub '-' jeżeli takiego zwięrzęcia nie ma
- dla każdego bezpańskiego zwierzęcia wypisz jego imię, gatunek oraz ilość żyjących zwierząt w jego gatunku
- napisz ilu różnych ojców mają właściciele kotów
- dla każdego zwierzęcia napisz którym zwierzęciem swojego właściciela jest
- wypisz wszystkie dzieci osoby której zwierze umarło najdawniej
Rozwiązania
Ad.1.
Brzydko, bo mało efektywnie - dla każdej osoby oblicz innego COUNT'a.
pokaż
SELECT osoba.*,
(SELECT COUNT(*) FROM posiadanie NATURAL JOIN zwierze
WHERE imiewlasciciela=osoba.imie AND datasmierci IS NULL) ile_zwierzat
FROM osoba;
A teraz ładnie - użyj LEFT JOIN i pogrupuj wiersze. Jak wówczas wypisać nazwisko?
pokaż
SELECT osoba.imie, osoba.nazwisko, COUNT(idzwierzecia)-COUNT(datasmierci) ile_zwierzat
FROM osoba LEFT JOIN posiadanie NATURAL JOIN zwierze ON osoba.imie = posiadanie.imiewlasciciela
GROUP BY osoba.imie, osoba.nazwisko;
Ad.2.
Żeby policzyć ile jest maksymalnie takich samych musimy policzyć ile jest zwierząt z danego gatunku o danym imieniu i wybrać dla każdego gatunku liczbę największą. Najłatwiej zrobić to podzapytaniem w sekcji FROM.
pokaż
SELECT gatunek, MAX(ile) max_imiennikow
FROM (SELECT imie, gatunek, COUNT(*) ile FROM zwierze GROUP BY imie, gatunek)
GROUP BY gatunek;
Ad.3.
Standardowe połączenie tabeli osoba z samą sobą daje nam zbiór dzieci, pozostaje odpowiednio użyć funkcji agregujących.
pokaż
SELECT mama.imie, COUNT(dziecko.tata) znanych, COUNT(*)-COUNT(dziecko.tata) nieznanych
FROM osoba mama JOIN osoba dziecko ON mama.imie = dziecko.mama
GROUP BY mama.imie;
Ad.4.
Osoby jakie nas interesują:
pokaż
SELECT B.imie FROM osoba A, osoba B
WHERE A.nazwisko = 'Makota' AND (A.imie = B.imie OR A.imie = B.mama);
Wszystkie koty dla osoby o imieniu ?????:
pokaż
SELECT DISTINCT imie FROM zwierze NATURAL JOIN posiadanie
WHERE gatunek = 'kot' AND imiewlasciciela = '?????';
Razem z użyciem podzapytania:
pokaż
SELECT DISTINCT imie FROM zwierze NATURAL JOIN posiadanie
WHERE gatunek = 'kot' AND imiewlasciciela IN
(SELECT B.imie FROM osoba A, osoba B
WHERE A.nazwisko = 'Makota' AND (A.imie = B.imie OR A.imie = B.mama));
Razem bez użycia podzapytania:
pokaż
SELECT DISTINCT Z.imie FROM osoba A, osoba B, posiadanie P, zwierze Z
WHERE A.nazwisko = 'Makota' AND (A.imie = B.imie OR A.imie = B.mama)
AND B.imie = P.imiewlasciciela AND P.idzwierzecia = Z.idzwierzecia AND gatunek = 'kot';
Ad.5.
Uwaga, będzie trochę strasznie!
Ile każda osoba ma żywych zwierząt?
pokaż
SELECT imiewlasciciela, COUNT(*) ile_zywych FROM posiadanie NATURAL JOIN zwierze
WHERE datasmierci IS NULL GROUP BY imiewlasciciela;
Oznaczmy to zapytanie jako X. Kto ma najwięcej żywych zwierząt?
pokaż
SELECT imiewlasciciela FROM X WHERE ile_zywych = (SELECT MAX(ile_zywych) FROM X);
Jak teraz wybrać ojca tej osoby (wybierzemy tylko imię)?
pokaż
SELECT tata FROM osoba WHERE imie =
(SELECT imiewlasciciela FROM X WHERE ile_zywych = (SELECT MAX(ile_zywych) FROM X));
Ad.6.
Wystarczy złączyć tabelę osób ze złączonymi naturalnie tabelami zwierz i posiadanie.
pokaż
SELECT osoba.imie, NVL(MAX(zywe_zwierzeta.imie), '-') zywy FROM osoba LEFT JOIN
(SELECT * FROM posiadanie NATURAL JOIN zwierze WHERE zwierze.datasmierci IS NULL) zywe_zwierzeta
ON osoba.imie = zywe_zwierzeta.imiewlasciciela GROUP BY osoba.imie;
Czemu nie działa to poniżej?
pokaż
SELECT osoba.imie, NVL(MAX(zwierze.imie), '-') zywy
FROM osoba LEFT JOIN posiadanie NATURAL JOIN zwierze ON osoba.imie = posiadanie.imiewlasciciela
WHERE zwierze.datasmierci IS NULL GROUP BY osoba.imie;
Ad.7.
Najłatwiej użyć dwóch podzapytań - jednego w sekcji SELECT i drugiego po WHERE (najlepiej po NOT IN)
pokaż
SELECT imie, gatunek,
(SELECT COUNT(*) FROM zwierze inne WHERE inne.gatunek = zwierze.gatunek AND datasmierci IS NULL) inne
FROM zwierze WHERE idzwierzecia NOT IN (SELECT idzwierzecia FROM posiadanie);
A czy da się to zadanie wykonać bez podzapytań? Oczywiście!
pokaż
SELECT A.imie, A.gatunek, COUNT(B.imie) ile_innych
FROM zwierze A LEFT JOIN zwierze B ON A.gatunek = B.gatunek AND B.datasmierci IS NULL
LEFT JOIN posiadanie P ON A.idzwierzecia = P.idzwierzecia
WHERE P.idzwierzecia IS NULL GROUP BY A.imie, A.gatunek;
Ad.8.
Właściciele kotów.
pokaż
SELECT DISTINCT osoba.*
FROM zwierze NATURAL JOIN posiadanie JOIN osoba ON posiadanie.imiewlasciciela = osoba.imie
WHERE gatunek = 'kot';
Można oczywiście wybrać ojców za pomocą podzapytania i następnie policzyć ich COUNT-em. Postąpimy jednak sprytniej i dodamy tylko jedną funkcję agregującą.
pokaż
SELECT COUNT(DISTINCT tata) roznych_ojcow
FROM zwierze NATURAL JOIN posiadanie JOIN osoba ON posiadanie.imiewlasciciela = osoba.imie
WHERE gatunek = 'kot';
Ad.9.
Którym zwierzęciem jest zwierze przygarnięcie przez ??kto?? w dniu ??data?? ?
pokaż
SELECT COUNT(*) FROM posiadanie
WHERE dataprzygarniecia <= '??data??' AND imiewlasciciela = '??kto??';
Całe zapytanie możemy więc skonstruować na przykład umieszczając tą liczbę jako podzapytanie w sekcji SELECT:
pokaż
SELECT imie, imiewlasciciela, dataprzygarniecia,
(SELECT COUNT(*) FROM posiadanie inne WHERE inne.dataprzygarniecia <= posiadanie.dataprzygarniecia
AND inne.imiewlasciciela = posiadanie.imiewlasciciela) ktory
FROM zwierze NATURAL JOIN posiadanie
ORDER BY imiewlasciciela, dataprzygarniecia;
Ad. 10.
Osoba której zwierze umarło najdawniej (dopuszczamy wiele rekordów):
pokaż
SELECT DISTINCT imiewlasciciela FROM posiadanie NATURAL JOIN zwierze
WHERE datasmierci = (SELECT MIN(datasmierci) FROM zwierze);
Wybór dzieci możemy zrealizować na parę sposobów, na przykład używając dwa razy IN w sekcji WHERE. A jak zrobić to umieszczając powyższe podzapytanie w sekcji FROM?
pokaż
SELECT osoba.* FROM osoba,
(SELECT DISTINCT imiewlasciciela FROM posiadanie NATURAL JOIN zwierze
WHERE datasmierci = (SELECT MIN(datasmierci) FROM zwierze)) najdawniejsi
WHERE mama = imiewlasciciela OR tata = imiewlasciciela;
Powyższe rozwiązania są tylko przykładami, jest dużo więcej sposobów uzyskania takiego samego rezultatu.
Rekurencja - WITH RECURSIVE [#]
Na wykładzie poznaliśmy jednak inny - bardziej zbliżony do datalogu - sposób pisania zapytań rekurencyjnych. Niestety, nie działa on w Oraclu :(. Aby go odpalić logujemy się do Postgresa:
> psql -h labbd bd login
gdzie login to nasz login. (Powinien działać, jeżeli nie działa możemy korzystać ze wspólnego konta scott z hasłem tiger (nie mylić z hasłem "tiger tiger")). Teraz możemy pisać zapytania rekurencyjne. Ogólny format wygląda tak:
WITH RECURSIVE nazwa AS (
zapytanie inicjujące
UNION [ALL]
zapytanie poszerzające zbiór
)
zapytanie korzystające z nazwa
Działa to tak: zawartość tabeli nazwa jest najpierw wypełniana wierszami z zapytania inicjującego. Następnie korzystając z aktualnej wersji tabeli wyliczamy nowe wiersze i zastępujemy aktualną zawartość nimi. Wykonujemy te kroki aż tabela nazwa będzie pusta. Po zakończeniu procedury, tabela nazwa zawiera wiersze z zapytania inicjującego oraz wszystkie wygenerowane wiersze w tym procesie.
Prześledźmy ten proces na przykładzie zapytania liczącego sumę liczb od 1 do 100:
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 100
)
SELECT sum(n) FROM t;
Najpierw do roboczej tabeli oraz do wyniku trafia wiersz z wartością 1. Następnie wyliczane jest zapytanie w oparciu o robocza tabelę: wynikiem jest wiersz (2). Wiersz idzie zatem do wyniku i zastępuje roboczą tabelę: ma ona zatem tylko wiersz (2). Ponownie wyliczamy i dostajemy (3). W rezultacie robocza tabela ma zawsze jeden wiersz. W końcu po setnym kroku zapytanie jest puste, a zatem na koniec wynikiem są wszystkie wygenerowane wiersze włącznie z inicjacją, czyli (1,2,3,...,100).
Jeżeli nie dodamy słowa ALL
, wówczas na każdym kroku eliminowane będą duplikaty. Dokładniej - po wygenerowaniu nowych wierszy (także przy inicjacji) usuwamy duplikaty z tej tabeli, ale także wiersze które już wcześniej wypisaliśmy na wynik. Jest to dodatkowa robota, więc jeżeli zapytanie nie generuje duplikatów to lepiej użyć ALL
.
Jeżeli chcemy wypisać wszystkie osoby za które odpowiada KING (czyli jego podwładnych i jego samego) możemy napisać coś takiego:
WITH RECURSIVE sub AS (
SELECT * FROM emp WHERE ename = 'KING'
UNION ALL
SELECT emp.* FROM emp, sub WHERE emp.mgr = sub.empno
)
SELECT * FROM sub;
W każdym kolejnym kroku dostajemy teraz kolejny poziom podwładnych.
Warto też wiedzieć, że jeżeli zapytanie nam się zapętla, to możemy do ostatniego zapytania dodać LIMIT
(konstrukcja LIMIT 100
wypisuje maksymalnie 100 wierszy zapytania). Postres jest na tyle mądry, że będzie wyliczał tak długo nowe wiersze jak długo musi, np. poniższe zapytanie zwróci 10 wierszy z jedynką:
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n FROM t
)
SELECT * FROM t LIMIT 10;