Blogi deweloperów PLD/FreeBSD
14 Feb 2007 - 22:08 in by TomaszTrojanowski
W bólach implementacji swoich builderów natknąłem się na ciekawą własność NULLa w MySQL. Nie wiem czy pojawia sie ona również innych SQLach.

Najprościej bedzie na przykladzie. Załózmy, że mamy dwie tabele "process" i "task". Tabela "process" zawiera listę procesów wykonywanych przez pewien abstrakcyjny procesor, na każdy proces składa się pewna ilość zadań, przy czym każde zadanie może należeć tylko do jednego procesu; czyli prosta relacja jeden do wielu. Dodatkowo każde zadanie posiada właściwość "status" określająca stan tego zadania. Dla ustalenia uwagi niech pole "status" może przyjmować trzy wartości: "none", "running" lub "completed". Definicja tych tabel może wyglądac tak:

CREATE TABLE process (
    id INT UNSIGNED PRIMARY KEY NOT NULL auto_increment,
    name VARCHAR);
CREATE TABLE task (
    id INT UNSIGNED PRIMARY KEY NOT NULL auto_increment,
    id_process INT UNSIGNED NOT NULL,
    status ENUM('none', 'running', 'completed') NOT NULL);

Zapytanie które zwróci nam identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu jest oczywiście trywialne i wyglada tak:

SELECT id_process, COUNT(*) FROM task GROUP BY id_process;

Problemem natomiast jest zapytanie zwracajace identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu, ale takich które nie zostały jeszcze zakończone, czyli mają status różny od 'completed'. Proste:

SELECT id_process, COUNT(status != 'completed') FROM task GROUP BY id_process;
nie wystarcza ponieważ COUNT zlicza zgrupowane wartości bez wzgledu na ich wartość. W tym przypadku zarówno TRUE jak i FALSE jest traktowane jako element to zliczenia. Zatem powyższe zapytanie zwraca to samo co poprzednie, czyli identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu.

Możnaby oczywiście uzyć funkcji SUM i IF w następujący sposób:

SELECT id_process, SUM(IF(status != 'completed), 1, 0) FROM task GROUP BY id_process;
Ale to nie dość, że niezgodne z zasadami sztuki, to jeszcze mało czytelne i pewnie mało wydajne.

Aby w prosty sposób uzyskać oczekiwany wynik wystarczy wykorzystać to że wartości NULLniepoliczalne dla funkcji COUNT. Jeżeli zgrupujemy rekordy które w zadanym polu maja cztery wartości z czego dwie są równe NULL to funkcja COUNT na tym polu zwróci nam 2, a nie 4.

Wystarczy zatem doprowadzić ostatnie zapytanie wykorzystujące funkcję COUNT do takiej postaci, w której wartość argumentu jest policzalna gdy pole "status" jest inne niż 'completed', a niepoliczalna (czyli równa NULL) w przeciwnym wypadku. Wyrażenie

status != 'completed' OR NULL
spełnia te założenia ponieważ przyjmuje jedną z dwóch wartości: TRUE dla pola "status" różnego od 'completed', albo NULL w przeciwnym wypadku.

Ostatecznie zapytanie zwracajace identyfikator procesu wraz z ilością zadań przyporzadkowanych do tego procesu, takich które nie zostały jeszcze zakończone wygląda tak:

SELECT id_process, COUNT(status != 'completed' OR NULL) FROM task GROUP BY id_process;

Śliczne, prawda?


6 Comments

1Marcin Gajda replied 2 weeks, 1 day after the incident

Tak…

Śliczne? Prymitywne, hack taki, że aż żołądek boli. Wygląda na to, że nie doszedłeś jeszcze w nauce SQL do klauzuli WHERE…

SELECT id_process, COUNT(*) FROM task WHERE status <> 'completed' GROUP BY id_process;

Mógłbyś też pomyśleć o przesiadce na bazę danych (np. PostgreSQL), a nie korzystać z żałosnej imitacji. … reply

2TomaszTrojanowski replied 2 weeks, 1 day after the incident

Fakt, błedem było nie napisanie bezpośrednio do czego mi to posłuży. Więc naprawiam ten błąd, bo widzę czytanie ze zrozumieniem to rzadka sztuka.

Używając tej swojej klauzuli WHERE wylistuj wszystkie takie procesy, dla których wszystkie zadania (z tabeli "task") mają status 'completed'. Bo ja to zrobię tak:

SELECT id_process, COUNT(status != 'completed' OR NULL) AS count FROM task GROUP BY id_process HAVING count = 0;

Tylko żeby Cię żołądek nie rozbolał, jak będziesz się gimnastykował. … reply

3MarcinGajda replied 2 weeks, 2 days after the incident

Ależ...

SELECT id_process FROM task WHERE status = 'completed' GROUP BY id_process;

Coś mi się wydaje, że owracasz jednak kota ogonem, zupełnie niepotrzebnie. Przeczytaj o WHERE, zanim zaczniej kombinować po górkę.

Dobrze byłoby, gdybyś też obejrzał plan dla Twojego i mojego zapytania. Może wtedy być zrozumiał, dlaczego Twoje podejście jest co najmniej niewłaściwe. … reply

3.1TomaszTrojanowski, 1 hour, 3 minutes later 1 hour, 3 minutes later
Przecież SELECT id_process FROM task WHERE status = 'completed' GROUP BY id_process; zwróci liste procesów które mają przynajmniej jedno zadanie zakończone, a nie liste procesów, w których wszystkie zadania zostały zakończone. Nie widzisz tego?

Cały problem leży w tym, żeby uzyskać listę procesów, w których wszystkie zadania się zakończyły, użycie WHERE w tym przypadku nic nie da, bo przed wykonaniem funkcji COUNT ograniczy zbiór poczatkowy do zadań które się nie zakończyły (w przypadku WHERE status <> 'completed') albo do zadań które się zakończyły w przypadku WHERE status = 'completed'). Czyli używając WHERE status = 'completed' nie wiemy czy są w tak ograniczonej tabeli rekordy z status <> 'completed'

Na przykładzie będzie prościej. Przyjmijmy, że w tabela "task" wygląda tak (pominąłem pierwsze pole bo jest nieistotne w tym wypadki):

id_process status
1 none
1 running
2 completed
2 completed
3 completed
3 running
Moje
SELECT id_process, COUNT(status != 'completed' OR NULL) AS count FROM task
GROUP BY id_process HAVING count = 0;
zwróci:
id_process count
2 0
Twoje
SELECT id_process, COUNT(*) AS count FROM task WHERE status = 'completed' GROUP BY id_process;
zwróci
id_process count
2 2
3 1
Oczywiscie count jest nieistotne, ale te zapytania zwracaja zupełnie różną liste procesów.

Rozumiesz juz o co chodzi czy mam tłumaczyć dalej. … reply

3.1.1MarcinGajda, 39 minutes later: Wszystkie... 39 minutes later: Wszystkie...
No tak - mają być wszystkie. Wobec tego biję się w piersi, rzeczywiście moje zapytanie nie daje poprawnego wyniku.

Mimo wszystko napisałbym to inaczej:

SELECT id_process
  FROM task
  WHERE status = 'completed'
  GROUP BY id_process
EXCEPT
SELECT id_process
  FROM task
  WHERE status <> 'completed'
  GROUP BY id_process

To zapytanie wciąż będzie szybsze w planie wykonania od Twojego. … reply

3.1.1.1TomaszTrojanowski, 11 hours, 4 minutes later 11 hours, 4 minutes later
Nie jestem do końca przekonany, czy będzie szybsze. Nawet jeżeli tak, to najprawdopodobniej
SELECT DISTINCT id_process AS p1 FROM task
  WHERE status = 'completed' AND
  NOT EXISTS (SELECT * FROM task WHERE status <> 'completed'  AND id_process = p1);
będzie jeszcze szybsze. Ale to nie o to chodzi. … reply

Leave a Reply

You may have to login or register to comment if you haven't already.

About

Tomasz Trojanowski mieszka w Łańcucie, wraz z żoną Iwoną i synami Jakubem i Arkadiuszem. Pracuje jako administrator serwerów internetowych. W wolnych chwilach zajmuje się projektem PLD/FreeBSD.

r1 – 14 Feb 2007 – 22:35:21 – Main.TomaszTrojanowski
Copyright © 1999-2008. Zawartość tego serwisu jest własnością osób współpracujących. Wyślij pomysły, pytania dotyczące serwisu.