Wykład 11

Komentarze

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