Przetwarzanie sumarycznych, zgrupowanych wyników bezpośrednio w zawołaniach MySQL

Tytuł notki być może niewiele mówi. Głowiłem się długo jak go zredagować, by jasno i klarownie, bez zbędnego rozwleczenia, przedstawiał zawartość notki. Nie wiem czy się udało, więc najpierw wytłumaczę o co chodzi.

Przy tworzeniu wszelakich aplikacji z użyciem baz danych można natknąć się na funkcjonalności, które można rozwiązać na wiele sposobów. Z jednej strony można pobrać suche dane i sformatować je w kodzie aplikacji, z drugiej – można wyjąć z bazy danych już gotowe dane. Czasem wydawać się może, że łatwiej jest zastosować pierwszą metodę, ale nic bardziej mylnego.

Bazy danych są specyficznym elementem aplikacji, bardzo istotnym, ale jednak przyczajonym w tle. W związku z tym, że bardzo często korzysta się z frameworków, CMSów i innych gotowych rozwiązań programistycznych, element ten jest pomijany, czy też inaczej: nie jest zgłębiany tak bardzo jak inne elementy backendu. W końcu gdy są gotowe metody by coś zrobić, to po co wyważać otwarte drzwi. Dlatego odnoszę wrażenie, że programiści często są na bakier z, wydawać by się mogło, podstawowymi zagadnieniami bazodanowymi. Sam nie jestem ekspertem, więc co i rusz trafiam na zaskakujące mnie zawołania odkrywające przede mną meandry baz danych.

Ale do rzeczy. Wyobraźmy sobie sytuację, w której porzuca się starą wersję aplikacji, a wprowadza nową, bo oczekiwania co do zmian były tak istotne, że nie było sensu grzebać w poprzedniej wersji, wygodniej było stworzyć po prostu nową. W związku z przenosinami użytkowników z jednej aplikacji do drugiej powstał problem statystyk skuteczności migracji. Załóżmy, że użytkownik aplikacji X otrzymuje wiadomość o możliwości przejścia do aplikacji Y oraz link do mechanizmu migracyjnego. Ten oznacza użytkownika flagą tymczasową (zablokowany), a w momencie wykrycia pełnej rejestracji w aplikacji Y ustawia flagę końcową, świadczącą o sfinalizowaniu migracji (aktywny).

Zasymulujmy to sobie prostą tabelą:

1
2
3
4
5
6
7
8
9
CREATE TABLE `uzytkownicy` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`u_status` ENUM('nieaktywny','aktywny','zablokowany') NOT NULL DEFAULT 'nieaktywny',
`fala` SMALLINT(5) UNSIGNED NULL DEFAULT NULL,
PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=MyISAM
ROW_FORMAT=DEFAULT;

w której pole u_status informuje o statusie migracji, a fala o tym kiedy użytkownik otrzymał link migracyjny. Użytkownicy otrzymują link migracyjny w transzach i chcielibyśmy wiedzieć jaki jest response rate dla poszczególnych fal. Żeby móc takie statystyki sprawdzić musimy najpierw sobie tabelę wypełnić danymi, w tym celu ułatwimy sobie zadanie przygotowując procedurę generującą losowe wpisy:

1
2
3
4
5
6
7
8
9
10
11
12
DELIMITER $$
CREATE PROCEDURE multiInsert()
BEGIN
DECLARE i INT DEFAULT 1;

WHILE (i<=3000) DO
INSERT INTO uzytkownicy(u_status,fala) VALUES('nieaktywny',FLOOR(1 + (RAND() * 9)));
INSERT INTO uzytkownicy(u_status,fala) VALUES('aktywny',FLOOR(1 + (RAND() * 9)));
INSERT INTO uzytkownicy(u_status,fala) VALUES('zablokowany',FLOOR(1 + (RAND() * 9)));
SET i=i+1;
END WHILE;
END$$

Odpalając komendę CALL multiInsert() za jednym zamachem wrzucamy do bazy 6000 losowych wpisów, po 2000 dla każdego statusu. Z kolei fale będą rozłożone nierównomiernie, bo są przypisywane losowo w zakresie 1-9.

Dawniej użyłbym zwykłego zgrupowania wyników po kolumnie fala i wykonał zawołanie dla 3 statusów z osobna, a następnie, już w aplikacji, przeliczyłbym wszystko i wygenerował statystyki. Nie jest to jednak ani wygodne, ani optymalne. Na szczęście trochę poszukiwań przyniosło owoc w postaci bardzo ciekawej składni MySQL, która takie statystyki pozwala „pobrać prosto z bazy”. A więc:

1
2
3
4
5
6
7
8
SELECT CONCAT("Migracja użytkowników, fala ", fala) as "Akcja",
count(*) as "Wielkość wysyłki",
count(case when u_status='zablokowany' then id end) as "Zablokowani",
count(case when u_status='aktywny' then id end) as "Aktywni",
ROUND(count(case when u_status!='nieaktywny' then id end)*100/count(*), 2) as "Response Rate %"
FROM uzytkownicy
where fala is not null
GROUP BY fala

Wynikiem zawołania, w moim przypadku (przypominam, że dane są losowe) było:

AkcjaWielkość wysyłkiZablokowaniAktywniResponse Rate %
Migracja, fala 197831331564.21
Migracja, fala 2102533735867.80
Migracja, fala 3104035634667.50
Migracja, fala 496632132066.36
Migracja, fala 595229032564.60
Migracja, fala 6100433834067.53
Migracja, fala 7100332234466.40
Migracja, fala 8100834833267.46
Migracja, fala 9102437532067.87

Kluczowym elementem zawołania jest tu wykorzystana trzykrotnie składnia case when … then … end. Jest to niejako filtr dla naszych wstępnie zgrupowanych po kolumnie fala wyników. O ile samo zgrupowanie zwróciłoby nam wyniki jak w kolumnie „Wielkość wysyłki„, czyli zwykły count(*), o tyle wykorzystanie powyższej składni pozwala nam dodatkowo przetworzyć zebrane wiersze i zliczyć tylko te, które nas interesują, dla każdej kolumny z osobna. I tak oto w jednym zawołaniu możemy wyświetlić kilka interesujących nas danych. Warto zauważyć, że dla 54000 wpisów zawołanie takie zajmuje jedynie 0,062 sekundy! Aplikacje więc nie powinny ucierpieć na wydajności w przypadku zastosowania tego typu zawołań.

Skomentuj "Przetwarzanie sumarycznych, zgrupowanych wyników bezpośrednio w zawołaniach MySQL":

Musisz się zalogować, aby móc dodać komentarz.