Wykład 11
Transkrypt
Wykład 11
Bazy danych 11. Kilka przykładów P. F. Góra http://th-www.if.uj.edu.pl/zfs/gora/ 2012 2. Dziury w sekwencjach Jest to klasyczny problem programistyczny w SQL: Majac ˛ dany rosnacy ˛ ciag ˛ liczb naturalnych, sprawdzić, czy wystepuj ˛ a˛ w nim “dziury”, to znaczy czy elementy ciagu ˛ sa˛ kolejnymi liczbami naturalnymi. Jeżeli jakieś dziury wystepuj ˛ a, ˛ wskazać gdzie. Tego typu problem — i problemy pokrewne — moga˛ mieć poważne zastosowania praktyczne. c 2011-13 P. F. Góra Copyright 11–2 Na przykład jeśli mamy tabele˛ mysql> SELECT * FROM liczby; +----+ | I | +----+ | 1 | | 2 | | 3 | | 4 | | 6 | | 7 | | 10 | | 11 | | 13 | | 17 | | 18 | +----+ 11 rows in set (0.00 sec) możemy powiedzieć, że brakujacymi ˛ liczbami sa˛ 5, 8, 9, 12, 14, 15, 16. Jak to sprawdzić w SQL? c 2011-13 P. F. Góra Copyright 11–3 Przede wszystkim znajdźmy nastepniki ˛ poszczególnych liczb. Nastepnikiem ˛ jest najmniejsza liczba spośród liczb wiekszych ˛ od danej. mysql> SELECT MIN(I) FROM liczby -> WHERE I > 1; +--------+ | MIN(I) | +--------+ | 2 | +--------+ 1 row in set (0.00 sec) mysql> SELECT MIN(I) FROM liczby -> WHERE I > 13; +--------+ | MIN(I) | +--------+ | 17 | +--------+ 1 row in set (0.00 sec) Nastepnikiem ˛ liczby 1 jest 2, nastepnikiem ˛ liczby 13 jest 17. c 2011-13 P. F. Góra Copyright 11–4 Żeby znaleźć nastepniki ˛ wszystkich liczb, trzeba użyć samozłaczenia ˛ — jedno wystapienie ˛ tabeli jest potrzebne do przejrzenia jej wszystkich elementów, drugie do znalezienia nastepnika ˛ “bieżacego” ˛ elementu. mysql> SELECT L1.I AS I, MIN(L2.I) AS Nast˛ epnik -> FROM liczby AS L1 JOIN liczby AS L2 ON L2.I > L1.I -> GROUP BY L1.I; +----+-----------+ | I | Nast˛ epnik | +----+-----------+ | 1 | 2 | | 2 | 3 | | 3 | 4 | | 4 | 6 | | 6 | 7 | | 7 | 10 | | 10 | 11 | | 11 | 13 | | 13 | 17 | | 17 | 18 | +----+-----------+ 10 rows in set (0.00 sec) c 2011-13 P. F. Góra Copyright 11–5 Dalej jest już bardzo łatwo: wystarczy odrzucić te pary liczba-nastepnik, ˛ w której nastepnik ˛ jest o 1 wiekszy ˛ od liczby (to znaczy, że w danym miejscu nie ma dziury): mysql> SELECT L1.I AS I, MIN(L2.I) AS J -> FROM liczby AS L1 JOIN liczby AS L2 ON L2.I > L1.I -> GROUP BY L1.I -> HAVING J > I + 1; +----+------+ | I | J | +----+------+ | 4 | 6 | | 7 | 10 | | 11 | 13 | | 13 | 17 | +----+------+ 4 rows in set (0.02 sec) W ten sposób wyznaczyliśmy granice obszarów, które sa˛ “dziurami”. Zauważmy, że w klauzuli HAVING wolno użyć aliasów zdefiniowanych na liście SELECT. c 2011-13 P. F. Góra Copyright 11–6 2. Ranking Tabela zawiera wyniki pewnego współzawodnictwa: mysql> SELECT * FROM Rezultaty; +----------+-------+ | Imie | Wynik | +----------+-------+ | Alicja | 15.5 | | Bogdan | 12 | | Cecylia | 8 | | Dorota | 15.5 | | Eryk | 17 | | Feliks | 8 | | Grzegorz | 7 | | Helena | 14.5 | | Irena | 13 | | Jan | 7 | | Karolina | 14 | +----------+-------+ 11 rows in set (0.00 sec) c 2011-13 P. F. Góra Copyright 11–7 Chcemy stwozryć ranking uczestników: ten, kto ma najwiekszy ˛ wynik, ma ranking 1, ten, kto ma drugi wynik, ma ranking 2 itd. Ranking jest równy liczbie zawodników, którzy maja˛ wynik lepszy od danego, plus jeden (najlepszy nie ma lepszych od siebie, a jego rankingiem ma być 1, nie 0). Próbujemy zatem c 2011-13 P. F. Góra Copyright 11–8 mysql> SELECT R1.Imie, COUNT(R2.Imie)+1 AS Rank -> FROM Rezultaty AS R1 JOIN Rezultaty AS R2 ON R2.Wynik > R1.Wynik -> GROUP BY R1.Imie; +----------+------+ | Imie | Rank | +----------+------+ | Alicja | 2 | | Bogdan | 7 | | Cecylia | 8 | | Dorota | 2 | | Feliks | 8 | | Grzegorz | 10 | | Helena | 4 | | Irena | 6 | | Jan | 10 | | Karolina | 5 | +----------+------+ 10 rows in set (0.02 sec) c 2011-13 P. F. Góra Copyright 11–9 Coś jest nie tak: Brakuje osoby o najlepszym wyniku. Dlaczego? Ponieważ nie ma nikogo lepszego, warunek złaczenia ˛ nie był dla tej osoby nigdy spełniony. Trzeba użyć złaczenia ˛ zewnetrznego: ˛ mysql> SELECT R1.Imie, COUNT(R2.Imie)+1 AS Rank -> FROM Rezultaty AS R1 LEFT JOIN Rezultaty AS R2 ON R2.Wynik > R1.Wynik -> GROUP BY R1.Imie -> ORDER BY Rank; +----------+------+ | Imie | Rank | +----------+------+ | Eryk | 1 | | Alicja | 2 | | Dorota | 2 | | Helena | 4 | | Karolina | 5 | | Irena | 6 | | Bogdan | 7 | | Cecylia | 8 | | Feliks | 8 | | Grzegorz | 10 | | Jan | 10 | +----------+------+ c 2011-13 P. F. Góra Copyright 11–10 Zauważmy, że jest to ranking “sportowy”: Alicja i Dorota zajmuja˛ ex aequo drugie miejsce, a kolejna osoba jest na miejscu czwartym (nie ma miejsca trzeciego). c 2011-13 P. F. Góra Copyright 11–11 3. Druga Najwieksza ˛ wartość Znalezienie najwiekszej ˛ wartości w tabeli oraz odpowiadajacych ˛ jej wierszy jest bardzo proste: mysql> SELECT MAX(Wynik) FROM Rezultaty; +------------+ | MAX(Wynik) | +------------+ | 17 | +------------+ 1 row in set (0.01 sec) mysql> SELECT * FROM Rezultaty -> WHERE Wynik = (SELECT MAX(Wynik) FROM Rezultaty); +------+-------+ | Imie | Wynik | +------+-------+ | Eryk | 17 | +------+-------+ 1 row in set (0.00 sec) c 2011-13 P. F. Góra Copyright 11–12 Ale co z druga˛ wartościa? ˛ Bardzo naiwnie można spróbować zrobić tak: mysql> SELECT * FROM Rezultaty -> ORDER BY Wynik DESC -> LIMIT 2; +--------+-------+ | Imie | Wynik | +--------+-------+ | Eryk | 17 | | Alicja | 15.5 | +--------+-------+ 2 rows in set (0.01 sec) Widać, że to podejście nie zadziałało, gdzyż powinniśmy otrzymać trzy wiersze (Alicja i Dorota zremisowały!), nie zaś dwa, jak tego zażadaliśmy. ˛ Dodatkowo — co nie rzuca sie˛ na poczatku ˛ w oczy — srtowanie jest operacja˛ kosztowna˛ (w porównaniu z przegladaniem ˛ tabeli) i nie należy go niepotrzebnie wykonywać. c 2011-13 P. F. Góra Copyright 11–13 Zadajmy pytanie pomocnicze: Jaki jest drugi co do wartości wynik? Jest to najwiekszy ˛ wynik spośród wyników mniejszych od najwiekszego ˛ z całości: mysql> SELECT MAX(Wynik) AS Druga FROM -> (SELECT Wynik FROM Rezultaty -> WHERE Wynik < (SELECT MAX(Wynik) FROM Rezultaty)) AS RRR; +-------+ | Druga | +-------+ | 15.5 | +-------+ 1 row in set (0.01 sec) Zauważmy, ze wystepuj ˛ aca ˛ tu tabela pochodna musi mieć swój alias. c 2011-13 P. F. Góra Copyright 11–14 Wobec tego wskazanie krotek odpowiadajacym ˛ dwu najwieszym wynikom jest już bardzo proste: mysql> SELECT * FROM Rezultaty -> WHERE wynik >= -> (SELECT MAX(Wynik) FROM -> (SELECT Wynik FROM Rezultaty -> WHERE Wynik < (SELECT MAX(Wynik) FROM Rezultaty)) AS RRR); +--------+-------+ | Imie | Wynik | +--------+-------+ | Alicja | 15.5 | | Dorota | 15.5 | | Eryk | 17 | +--------+-------+ 3 rows in set (0.01 sec) Zapytanie to zawiera co prawda cztery polecenia SELECT, ale w odróznieniu od sortowania wykona sie˛ w czasie liniowym, co może być bardzo istotne dla dużych tabel. c 2011-13 P. F. Góra Copyright 11–15 4. Suma narastajaca ˛ Suma narastajaca ˛ jest cz˛esto potrzebna w rozmaitych rozliczeniach ksiegowych ˛ — ale także w innych zastosowaniach, na przykład w wyznaczaniu dystrybuanty dyskretnego rozkładu prawdopodobieństwa. Należy po prostu wysumować kolejne (numerowane jakimś “indeksem”) wartości od poczatku ˛ do pozycji bieżacej. ˛ c 2011-13 P. F. Góra Copyright 11–16 mysql> CREATE TABLE W -> (Nr SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, -> Wartosc FLOAT NOT NULL); Query OK, 0 rows affected (0.12 sec) Załóżmy, że mamy takie dane: mysql> SELECT * FROM W; +----+---------+ | Nr | Wartosc | +----+---------+ | 1 | 1 | | 2 | 5 | | 3 | 2.2 | | 4 | 0.8 | | 5 | 4 | | 6 | 3.1 | | 7 | 3.9 | | 8 | 2.7 | | 9 | 3.1 | | 10 | 0.9 | +----+---------+ 10 rows in set (0.00 sec) c 2011-13 P. F. Góra Copyright 11–17 Problem sumy narastajacej ˛ rozwiazujemy ˛ przez grupowanie i samozłaczenie ˛ (self–join). mysql> SELECT x.Nr, x.wartosc, FORMAT(SUM(y.wartosc),1) AS RunningTotal -> FROM W AS x, W AS y -> WHERE y.Nr <= x.Nr -> GROUP BY x.Nr; +----+---------+--------------+ | Nr | wartosc | RunningTotal | +----+---------+--------------+ | 1 | 1 | 1.0 | | 2 | 5 | 6.0 | | 3 | 2.2 | 8.2 | | 4 | 0.8 | 9.0 | | 5 | 4 | 13.0 | | 6 | 3.1 | 16.1 | | 7 | 3.9 | 20.0 | | 8 | 2.7 | 22.7 | | 9 | 3.1 | 25.8 | | 10 | 0.9 | 26.7 | +----+---------+--------------+ 10 rows in set (0.00 sec) c 2011-13 P. F. Góra Copyright 11–18 5. Średnia ruchoma W tabeli mamy zgromadzone dane numeryczne, indeksowane “czasem” (szereg czasowy) — na przykład kolejne kursy akcji, kolejne odczyty temperatury, dane o sprzedaży z kolejnych miesiecy ˛ itp. Należy obliczyć średnia˛ ruchoma˛ i+p/2 X 1 xj yi = p + 1 j=i−p/2 Obliczenie tego po stronie aplikacji jest proste. Jak to zrobić w SQL? c 2011-13 P. F. Góra Copyright 11–19 Użyjemy tych samych danych (tej samej tabeli), co w poprzednim przykładzie. Problem także rozwiażemy ˛ za pomoca˛ samozłaczenia. ˛ mysql> mysql> -> -> -> SET @p = 3; SELECT x.wartosc, AVG(y.wartosc) AS Srednia FROM W AS x, W AS y WHERE y.Nr >= x.Nr - @p/2 AND y.Nr <= x.Nr + @p/2 GROUP BY x.Nr; Dwukrotnie odwolujemy sie˛ do tej samej tabeli, a wiec ˛ musi ona wystepować ˛ pod różnymi aliasami. Wystapienie ˛ x służy do identyfikowania wierszy, wiec ˛ wystepuje ˛ także w klauzuli GROUP BY. Z odpowiednich wystapie ˛ ń y obliczana jest średnia. c 2011-13 P. F. Góra Copyright 11–20 +---------+-----------------+ | wartosc | Srednia | +---------+-----------------+ | 1 | 3 | | 5 | 2.7333333492279 | | 2.2 | 2.6666666865349 | | 0.8 | 2.3333333532015 | | 4 | 2.6333333055178 | | 3.1 | 3.6666666666667 | | 3.9 | 3.2333333492279 | | 2.7 | 3.2333333492279 | | 3.1 | 2.2333333094915 | | 0.9 | 1.9999999403954 | +---------+-----------------+ 10 rows in set (0.94 sec) Skrajne wartości sa˛ źle obsługiwane. c 2011-13 P. F. Góra Copyright 11–21 mysql> SET @p = 3; mysql> SET @gorny = (SELECT MAX(Nr) FROM W) + 1; mysql> SET @dolny = (SELECT MIN(Nr) FROM W) - 1; mysql> SELECT x.wartosc, AVG(y.wartosc) AS Srednia -> FROM W as x, W as y -> WHERE y.Nr >= x.Nr - @p/2 AND y.Nr <= x.Nr + @p/2 -> AND x.Nr - @p/2 >= @dolny AND x.Nr + @p/2 <= @gorny -> GROUP BY x.Nr; +---------+-----------------+ | wartosc | Srednia | +---------+-----------------+ | 5 | 2.7333333492279 | | 2.2 | 2.6666666865349 | | 0.8 | 2.3333333532015 | | 4 | 2.6333333055178 | | 3.1 | 3.6666666666667 | | 3.9 | 3.2333333492279 | | 2.7 | 3.2333333492279 | | 3.1 | 2.2333333094915 | +---------+-----------------+ 8 rows in set (0.01 sec) c 2011-13 P. F. Góra Copyright 11–22 6. Zmienne w czasie wartości Przypuśćmy, że w pewnej tabeli zgromadziliśmy informacje˛ o sprzedaży: Imie˛ sprzedawcy, date˛ transakcji, numer (symbol) artykułu i liczbe˛ sprzedanych sztuk. mysql> SELECT * FROM TSprzedaz; +----------+------------+------------+-------+ | Imie | Data | NrArtykulu | Ilosc | +----------+------------+------------+-------+ | Celina | 2012-09-30 | 10 | 2 | | Jan | 2012-09-02 | 7 | 1 | | Alicja | 2012-10-28 | 5 | 1 | | Bogdan | 2012-11-10 | 6 | 1 | | Feliks | 2012-09-09 | 6 | 1 | | Alicja | 2012-11-03 | 13 | 1 | .............................................. | Celina | 2012-12-06 | 4 | 1 | +----------+------------+------------+-------+ 346 rows in set (0.02 sec) c 2011-13 P. F. Góra Copyright 11–23 Ceny przechowywane sa˛ w osobnej tabeli. Rzecz w tym, że ceny moga˛ sie˛ zmieniać. W tabeli przechowywana jest informacja o zmianach cen, nie zaś wszystkie ceny dzienne. Przypuśćmy, że zmiany cen artykułu numer 6 wygladały ˛ nastepuj ˛ aco: ˛ mysql> SELECT * FROM TCeny -> WHERE NrArtykulu=6; +------------+------------+------+ | NrArtykulu | Data | Cena | +------------+------------+------+ | 6 | 2012-08-15 | 27 | | 6 | 2012-09-24 | 28 | | 6 | 2012-12-18 | 27 | +------------+------------+------+ 3 rows in set (0.05 sec) Ile wynosiła cena tego artykułu 14 listopada 2012? Człowiek bez trudu odpowie, że 28: taka cena obowiazywała ˛ od 24 września do 17 grudnia. Jak uzyskać te˛ odpowiedź za pomoca˛ SQL? c 2011-13 P. F. Góra Copyright 11–24 Data 24 września 2012 jest istotna w naszym przykładzie, była to bowiem ostatnia zmiana ceny badanego artykułu przed wskazana˛ data. ˛ Zacznijmy zatem od wyszukania ostatniej zmiany cen wskazanego artykułu przed wskazana˛ data˛ (słaba nierówność oznacza, że zmiana cen obowiazuje ˛ już od dnia wprowadzenia): mysql> SELECT MAX(Data) FROM TCeny -> WHERE NrArtykulu = 6 AND Data <= "2012-11-14"; +------------+ | MAX(Data) | +------------+ | 2012-09-24 | +------------+ 1 row in set (0.02 sec) c 2011-13 P. F. Góra Copyright 11–25 Wydaje sie, ˛ że możemy teraz łatwo poznać cene˛ obowiazuj ˛ ac ˛ a˛ we wskazanym dniu: mysql> SELECT Cena, MAX(Data) FROM Tceny -> WHERE NrArtykulu = 6 AND Data <= "2012-11-14"; +------+------------+ | Cena | MAX(Data) | +------+------------+ | 27 | 2012-09-24 | +------+------------+ 1 row in set (0.05 sec) To jest nieprawidłowy wynik! Dlaczego? Funkcja MAX jest funkcja˛ agregujac ˛ a. ˛ Oznacza to, że SQL tworzy pewna˛ grupe˛ (zdefiniowana˛ w klauzuli WHERE) i stosuje funkcje˛ MAX do tej grupy. Atrybut Cena odnosi sie˛ natomiast do konkretnych krotek, nie do grup. Wobec tego SQL wział˛ jakaś ˛ przypadkowa˛ wartość tego atrybutu (zapewne z ostatniej wczytanej krotki) — nie ma żadnej gwarancji, że bedzie ˛ on odpowiadał tej dacie, która˛ znajduje funkcja MAX. Trzeba tego zażadać ˛ explicite. Na jednej liście SELECT nie wolno umieszczać wielkości odnoszacych ˛ sie˛ do grup i do indywidualnych krotek. c 2011-13 P. F. Góra Copyright 11–26 mysql> SELECT Cena FROM TCeny -> WHERE NrArtykulu = 6 AND -> Data = (SELECT MAX(Data) FROM TCeny -> WHERE NrArtykulu = 6 AND Data <= "2012-11-14"); +------+ | Cena | +------+ | 28 | +------+ 1 row in set (0.06 sec) Skoro potrafimy już ustalić jaka cena danego artykułu obowiazywała ˛ we wskazanym dniu, możemy zadawać zapytania w rodzaju: Jaka była sumaryczna wartość sprzedaży osiagni ˛ eta ˛ przez poszczególnych sprzedawców. Zapytanie to powinno mieć strukture˛ SELECT Imie, SUM(Ilosc*CenaDnia) AS JakaSprzedaz FROM TSprzedaz Group BY Imie; c 2011-13 P. F. Góra Copyright 11–27 CeneDnia ˛ ustalimy za pomoca˛ tego, co zrobiliśmy powyżej. Ostatecznie otrzymujemy zapytanie ze skorelowanymi podzapytaniami: mysql> SELECT Imie, SUM(Ilosc* -> (SELECT Cena FROM TCeny -> WHERE NrArtykulu = TSprzedaz.NrArtykulu AND -> Data = (SELECT MAX(Data) FROM TCeny -> WHERE NrArtykulu = TSprzedaz.NrArtykulu AND -> Data <= TSprzedaz.Data) -> )) AS JakaSprzedaz FROM TSprzedaz GROUP BY Imie; c 2011-13 P. F. Góra Copyright 11–28 +----------+--------------+ | Imie | JakaSprzedaz | +----------+--------------+ | Alicja | 2039 | | Bogdan | 1470 | | Celina | 1614 | | Damian | 1293 | | Ewelina | 1836 | | Feliks | 1749 | | Grzegorz | 1155 | | Helena | 1005 | | Irena | 1868 | | Jan | 2253 | +----------+--------------+ 10 rows in set (0.44 sec) c 2011-13 P. F. Góra Copyright 11–29