Kolejność w zapytaniu z DISTINCT ON

Na problem z nieustawioną kolejnością w zapytaniu zawierającym DISTINCT ON natknąłem się już jakiś czas temu, jednak dopiero teraz zebrałem się, aby go opisać. A warto, bo niby błahy, a jednak ciekawy i sam trochę ignorowałem jego znaczenie.

Otóż korzystając w zapytaniu z DISTINCT ON na zestawie kolumn powinniśmy ustawić kolejność prezentacji wierszy poprzez standardową klauzulę ORDER BY. DISTINCT ON pomija bowiem kolejne wiersze o wartościach, które wystąpiły już wcześniej:

DISTINCT ON ( expression [, …] ) keeps only the first row of each set of rows where the given expressions evaluate to equal. […] Note that the “first row” of each set is unpredictable unless ORDER BY is used to ensure that the desired row appears first. […] The DISTINCT ON expression(s) must match the leftmost ORDER BY expression(s).

http://www.postgresql.org/docs/9.0/static/sql-select.html#SQL-DISTINCT

Bez ustawionej kolejności może być przypadkowa (chodź technicznie to nie do końca prawda) i różna w zależności od sposobu użycia.

Przykład? Opiszę mój przypadek w nieco zmodyfikowanej formie. Miałem zapytanie korzystające z dwóch tabel: pierwsza to tabela z numerami telefonów klientów wraz z etykietami. Jeden klient może posiadać kilka telefonów, nawet z tymi samymi etykietami (ze względu na przypisywanie tych numerów do jego umów, których może być wiele):

id_customer
id_contractnumberlabel
512
600123123
Company Ltd.
513600123123Company Ltd.
514600123123John Poe

Z tabeli pobierane były unikalne numery telefonów dla klienta wraz z pierwszą etykietą – ta bowiem nie była do końca ważna. Korzystałem więc z zapytania:

SELECT DISTINCT ON (id_customer, number)
    id_customer,
    number,
    label
FROM customers

Nakładając na zapytanie ograniczenie na ID żądanego klienta otrzymywałem wiersze z jego telefonami oraz etykietą. Jeżeli do jednego numeru klienta było kilka etykiet, mogła zostać zaprezentowana dowolna z nich, każda bowiem była prawidłowa i w tym zastosowaniu nie miało to znaczenia.

Trzeba było jednak rozszerzyć tabelę o dodatkowe dane (tagi), które nie występowały – jak etykiety – przy każdym wierszu klienta. Jeżeli już jednak istniały, należało je zaprezentować, a jeżeli było ich kilka – najważniejszą z nich według zadanego priorytetu. Pominę przy analizie priorytet, ponieważ nie jest on aż tak istotny. Tabela po zmianach wyglądała więc tak:

id_customer
id_contractnumberlabeltag
512
600123123
Company Ltd.
urgent
513600123123Company Ltd.
514600123123John Poe

Do zapytania dodałem tylko kolumnę z interesującą mnie daną:

SELECT DISTINCT ON (id_customer, number)
    id_customer,
    number,
    label,
    tag
FROM customers

Przy testach wszystko było OK, zawsze zwracane były wiersze z wypełnioną kolumną tag. To nieco uśpiło moją czujność i nie przetestowałem takiej liczb przypadków, abym trafił na pustą wartość mimo istnienia tagu dla numeru.

Sytuacja zmieniła się, gdy zapytanie trafiło do SQL-owego widoku. To zmieniło sposób sortowania wyników, przez co pobierając dane z widoku miałem przypadki zwracania numerów bez tagów mimo, że te ewidentnie istniały. Żeby było zabawniej, wykonanie zapytania bezpośrednio (z pominięciem widoku) działało tak, jak bym chciał.

Wróciłem więc do dokumentacji i stackoverflow. To naprowadziło mnie na podany na początku wpisu fragment dokumentacji. Aby poprawić zapytanie, należało więc użyć ORDER BY:

SELECT DISTINCT ON (id_customer, number)
    id_customer,
    number,
    label,
    tag
FROM customers
ORDER BY tag NULLS LAST

Dzięki temu, wiersze z tagami były zwracane jako pierwsze i ewentualne późniejsze wiersze z pustymi wartościami w tej kolumnie dla tego samego klienta i numeru były pomijane, a nie odwrotnie.

Nie można więc zapominać o klauzuli ORDER BY po kolumnach niewystępujących w DISTINCT ON, bo prędzej czy później się to zemści.

Moje webdevowe lektury

Pracując na co dzień z systemami stworzonymi ładnych parę lat temu, a jednocześnie zbyt złożonymi, aby je co jakiś czas przepisywać w całości, nietrudno wypaść z obiegu. Wprawdzie nasz biznes nie narzeka na brak pomysłów, dzięki czemu co jakiś czas musimy korzystać z czegoś zupełnie nowego (jak opisywane przeze mnie jakiś czas temu websockety), jednak i tak następuje to zbyt rzadko, by nie zardzewieć bez douczania się na własną rękę.

Co czytam?

Książki

Tych nie było wiele, ale gdy już były, to okazywały się strzałem w dziesiątkę. Książki, które opiszę, były kamieniami milowymi w moim rozwoju, a miałem to szczęście, że sięgnąłem po nie w momentach, w których mogłem się mocno zaangażować w ich lekturę.

Pierwsza z nich to: Wzorce projektowe. Rusz głową! (Elisabeth Freeman, Eric Freeman, Bert Bates, Kathy Sierra). Długo myślałem nad zakupem tej książki. Z jednej strony wiedziałem na pewno, że potrzebuję gruntownej wiedzy o wzorcach projektowych. Był to okres, gdy zorientowałem się, że wiele osób w moim otoczeniu sprawnie nimi operuje w rozwiązywaniu realnych problemów i jednocześnie gdy frustrowałem się tym, że moja wiedza w tym temacie jest na tyle ogólnikowa, że nie rozumiem artykułów o wybranych wzorcach na wikipedii. Z drugiej strony, część osób przestrzegało przed tą książką jako przeładowaną obrazkami. Nie pomagał fakt, że była ona wtedy dość droga. Ostatecznie sprezentowałem ją sobie w grudniu 2012 roku i przeczytałem do końca parę miesięcy później. Obrazków faktycznie jest dużo i jest to książka idiotoodporna. Zawdzięczam jej nie tyle dokładne poznanie popularnych wzorców projektowych, ile zmianę sposobu myślenia. Książka przebudowała zupełnie moją ówczesną wiedze o OOP, wskazując wymierne korzyści w codziennych rozwiązaniach. Po jej przeczytaniu widziałem tyle zastosowań wzorców i ogólnie elastycznego OOP, że – jako neofita – chciałem zacząć wszystko na nie przerabiać. Początkowa fascynacja minęła, jednak nowy sposób myślenia został do dziś i jestem w stanie bez problemu wymienić mnóstwo rozwiązań z projektów, nad którymi pracuję, w których architekturze wykorzystałem wzorce i które – mimo niedoskonałości, które widzę po wielu latach – do dziś nie tylko dobrze się trzymają, ale są też dość odporne i łatwe do zmodyfikowania.

Drugą (i ostatnią) książką jest: Antywzorce języka SQL. Jak unikać pułapek podczas programowania baz danych (Bill Karwin). Na książkę tę trafiłem, gdy miałem już względnie duże doświadczenie z bazami danych i pewnie bym ją pominął, gdyby nie autor, którego kojarzyłem… ze stackoverflow. Jego odpowiedzi na tyle zapadły mi w pamięć jako rzeczowe i wyważone, że postanowiłem dać książce szanse. I nie pożałowałem – książka jest dość zwarta i zawiera opisane na przykładach antywzorce wraz z dokładnym wyjaśnieniem, dlaczego nie należy ich stosować i w czym utrudniają później pracę. Po przeczytaniu książki zauważyłem, że sam stosuję wiele z nich, więc dzięki niej mogłem świadomie przestać z nich korzystać na rzecz lepszych rozwiązań, sugerowanych przez człowieka z ogromnym doświadczeniem w tej (i nie tylko zresztą) dziedzinie.

Internet

Nikogo nie powinno zdziwić, że w przypadku webdevu większość materiałów, z których korzystam, znajduje się w internecie na wszelkiego rodzaju stronach czy blogach.

Jako pierwszy wymienię Stack Overflow. Ciężko znaleźć obecnie osobę, która go nie zna. Stack Overflow jest dla mnie najczęściej pierwszym krokiem do rozpoznania problemu. Odpowiedzi pozwalają na szybkie rozeznanie (w razie bardziej złożonych tematów) lub natychmiastowe rozwiązanie (w przypadku tych prostszych) problemu. Obecnie korzystam głównie biernie i raczej nie wspominam dobrze okresu, w którym starałem się odpowiadać na pytania użytkowników. Presja czasu w moim przypadku zabija całą zabawę w odpowiadaniu (trzeba napisać odpowiedź merytorycznie i bardzo szybko, inaczej po opublikowaniu okazuje się, że kilka osób zrobiło już to samo). Tym niemniej, SO stanowi ciągle ogromną bazę wiedzy na dość wysokim poziomie.

Drugim źródłem wiedzy są dla mnie wykopowe tagi #webdev oraz #php. Przeglądam je często w wolnym czasie. Dzięki nim dowiedziałem się o wielu stosowanych obecnie technologiach, których prawdopodobnie szybko zawodowo nie zastosuję, jednak wiedza o ich istnieniu przydaje się czasem w doborze narzędzi do rozwiązania danego problemu. Toczą się tam też dyskusje pozamerytoryczne związane z pracą zawodową w webdevie i pozwala to poznać opinie wielu doświadczonych ludzi. Tagi te przeglądam od 2014 i polecam każdemu śledzenie ich lub przeglądanie przez przynajmniej parę minut dziennie.

Optymalizacja zapytań z GROUP BY w MySQL (i nie tylko)

Przyznam, że do wczoraj sposób działania planera w przypadku zapytań agregujących (z klauzulą GROUP BY) był dla mnie nie do końca jasny.

Akurat trafiłem w mojej aplikacji na zapytanie na tyle proste, a jednocześnie wolne (operujące na dużej tabeli), że łatwo było je analizować. Szukając przyczyny i sposobów optymalizacji, trafiłem na znakomitą odpowiedź na stackoverflow. Pozwolę sobie wkleić jej sedno:

For covering index you add:
1. columns used in where clauses first, then
2. columns used in group by, then
3. columns used in order by, and then
4. columns used in select.

Ze swoim zapytaniem miałem nieco większy problem, ponieważ łączyło ono dwie tabele poprzez JOIN. Jak się okazuje, kolumny z warunków JOIN-owania należy przypisać do pierwszego punktu, co potwierdziło się w moim dzisiejszym pytaniu.

Sprawdziłem działanie tego schematu również na zapytaniach z GROUP BY na dużych tabelach w PostgreSQL i również działało (skutkowało wykonywaniem zapytania w pełni na indeksach), więc można przyjąć, że podobnie działa to dla większości popularnych DBMS.

PostgreSQL – losowe wartości z podzapytania

W PostgreSQL, korzystając z podzapytania używającego funkcji RANDOM() tak, jak np. w MySQL, otrzymamy te same wyniki dla każdego wiersza.

Przykład:

SELECT
    t1.id,
    (SELECT name FROM table2 ORDER BY RANDOM() LIMIT 1) AS random_name
FROM table1 t1

W tym DBMS podzapytanie tego typu wykonuje się raz, więc przy każdym wierszu z tabeli table1 będzie widniała ta sama wartość random_name.

Aby to zmienić, wystarczy sztucznie skorelować podzapytanie z zapytaniem nadrzędnym, np. tak:

SELECT
    t1.id,
    (SELECT name FROM table2 WHERE t1.id = t1.id ORDER BY RANDOM() LIMIT 1) AS random_name
FROM table1 t1

Niby proste, ale jakiś czas temu straciliśmy w pracy na tym trochę czasu (zwłaszcza nie wiedząc, że problem jest związany z podzapytaniami, bo samo zapytanie było bardziej złożone).

SQL savepoint – obsługa złożonych transakcji

Jakiś czas temu natknąłem się w pracy na problem obsługi złożonych transakcji.

Jako, iż SQL (a przynajmniej DBMS, z którego korzystamy) nie obsługuje zagnieżdżonych transakcji, do tej pory – dla wygody – wystarczała nam prosta modyfikacja adaptera bazodanowego, która pozwalała na “zagnieżdżanie transakcji”, a polegała jedynie na zliczaniu żądań rozpoczęcia transakcji i pilnowaniu liczby wywołań commitów/rollbacków na adapterze. W przypadku niezgodności rzeczywista transakcja (otwierana przy pierwszym żądaniu) nie była commitowana, podobnie, gdy wystąpiło przynajmniej jedno wywołanie rollback na adapterze – żadne zmiany nie były stosowane.

W rzadkich sytuacjach nietypowych, gdzie część zapytań miała zostać zastosowana, a część – w razie potrzeby – nie, obsługa odbywała się w kilku niezależnych transakcjach.

Jednak w końcu przyszedł ten moment, gdy rozbicie na wiele transakcji było praktycznie niemożliwe ze względu na złożoność procesu, który trzeba było zmodyfikować.

W skrajnym przypadku mogło to wyglądać tak:

  1. Początek procesu – wykonanie podstawowych zapytań INSERT/UPDATE
  2. Wykonanie serii zapytań INSERT/UPDATE z możliwością warunkowego ich cofnięcia.
  3. Zakończenie procesu – kolejne zapytania INSERT UPDATE.

W pewnych warunkach powinny być więc zastosowane zmiany z punku 1., cofnięte wszystkie z punktu drugiego i zastosowane zapytania z punktu trzeciego.

I tutaj przydatna okazała się klauzula SAVEPOINT, o której istnieniu wcześniej nie wiedziałem, natomiast idealnie wpasowywała się w tego typu sytuacje.

Jej zasada działania jest banalnie prosta. W momencie wywołania:

SAVEPOINT nazwa_savepointa

wewnątrz transakcji tworzony jest punkt, po którym następują kolejne zapytania i do którego można – w razie potrzeby – cofnąć się poprzez

ROLLBACK TO nazwa_savepointa

W ten sposób “zapominamy” o wykonanych po utworzeniu savepointa zapytaniach SQL, natomiast wszystkie zapytania sprzed tego czasu zostaną zastosowane, jeżeli na całej transakcji zostanie wykonany COMMIT.