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 występuja w nim dziury, to znaczy czy elementy ciagu sa kolejnymi liczbami naturalnymi. Jeżeli jakieś dziury występuja, wskazać gdzie. Tego typu problem i problemy pokrewne moga mieć poważne zastosowania praktyczne. Copyright c 2011-13 P. F. Góra 11 2
Na przykład jeśli mamy tabelę 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? Copyright c 2011-13 P. F. Góra 11 3
Przede wszystkim znajdźmy następniki poszczególnych liczb. Następnikiem jest najmniejsza liczba spośród liczb większych 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) Następnikiem liczby 1 jest 2, następnikiem liczby 13 jest 17. Copyright c 2011-13 P. F. Góra 11 4
Żeby znaleźć następniki wszystkich liczb, trzeba użyć samozłaczenia jedno wystapienie tabeli jest potrzebne do przejrzenia jej wszystkich elementów, drugie do znalezienia następnika bieżacego elementu. mysql> SELECT L1.I AS I, MIN(L2.I) AS Następnik -> FROM liczby AS L1 JOIN liczby AS L2 ON L2.I > L1.I -> GROUP BY L1.I; +----+-----------+ I Następnik +----+-----------+ 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) Copyright c 2011-13 P. F. Góra 11 5
Dalej jest już bardzo łatwo: wystarczy odrzucić te pary liczba-następnik, w której następnik jest o 1 większy 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. Copyright c 2011-13 P. F. Góra 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) Copyright c 2011-13 P. F. Góra 11 7
Chcemy stwozryć ranking uczestników: ten, kto ma największy 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 Copyright c 2011-13 P. F. Góra 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) Copyright c 2011-13 P. F. Góra 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 zewnętrznego: 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 +----------+------+ Copyright c 2011-13 P. F. Góra 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). Copyright c 2011-13 P. F. Góra 11 11
3. Druga Największa wartość Znalezienie największej 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) Copyright c 2011-13 P. F. Góra 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 się na poczatku w oczy srtowanie jest operacja kosztowna (w porównaniu z przegladaniem tabeli) i nie należy go niepotrzebnie wykonywać. Copyright c 2011-13 P. F. Góra 11 13
Zadajmy pytanie pomocnicze: Jaki jest drugi co do wartości wynik? Jest to największy wynik spośród wyników mniejszych od największego 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 występujaca tu tabela pochodna musi mieć swój alias. Copyright c 2011-13 P. F. Góra 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 się w czasie liniowym, co może być bardzo istotne dla dużych tabel. Copyright c 2011-13 P. F. Góra 11 15
4. Suma narastajaca Suma narastajaca jest często potrzebna w rozmaitych rozliczeniach księgowych 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. Copyright c 2011-13 P. F. Góra 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) Copyright c 2011-13 P. F. Góra 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) Copyright c 2011-13 P. F. Góra 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 miesięcy itp. Należy obliczyć średnia ruchoma y i = 1 p + 1 i+p/2 x j j=i p/2 Obliczenie tego po stronie aplikacji jest proste. Jak to zrobić w SQL? Copyright c 2011-13 P. F. Góra 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> SET @p = 3; 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 -> GROUP BY x.nr; Dwukrotnie odwolujemy się do tej samej tabeli, a więc musi ona występować pod różnymi aliasami. Wystapienie x służy do identyfikowania wierszy, więc występuje także w klauzuli GROUP BY. Z odpowiednich wystapień y obliczana jest średnia. Copyright c 2011-13 P. F. Góra 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. Copyright c 2011-13 P. F. Góra 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) Copyright c 2011-13 P. F. Góra 11 22
6. Zmienne w czasie wartości Przypuśćmy, że w pewnej tabeli zgromadziliśmy informację o sprzedaży: Imię sprzedawcy, datę transakcji, numer (symbol) artykułu i liczbę 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) Copyright c 2011-13 P. F. Góra 11 23
Ceny przechowywane sa w osobnej tabeli. Rzecz w tym, że ceny moga się 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 następujaco: 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ć tę odpowiedź za pomoca SQL? Copyright c 2011-13 P. F. Góra 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) Copyright c 2011-13 P. F. Góra 11 25
Wydaje się, że możemy teraz łatwo poznać cenę 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 grupę (zdefiniowana w klauzuli WHERE) i stosuje funkcję MAX do tej grupy. Atrybut Cena odnosi się 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 będzie 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 się do grup i do indywidualnych krotek. Copyright c 2011-13 P. F. Góra 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ęta przez poszczególnych sprzedawców. Zapytanie to powinno mieć strukturę SELECT Imie, SUM(Ilosc*CenaDnia) AS JakaSprzedaz FROM TSprzedaz Group BY Imie; Copyright c 2011-13 P. F. Góra 11 27
CenęDnia 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; Copyright c 2011-13 P. F. Góra 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) Copyright c 2011-13 P. F. Góra 11 29