Coś ostatnie kilka miesięcy za bardzo skupiłam się na MySQL. Pora to zmienić i napisać coś o PostgreSQL. Chciała bym się z Wami podzielić informacją na temat zapytań ze słowem kluczowym WITH czyli zapytań CTEs (Common Table Expressions, link do dokumentacji znajdziecie tutaj).
Zobaczmy jakiś przykład, aby zobrazować sobie problem, z którym mamy się zmierzyć. W tabeli table1 przechowuje informację o numerach telefonów, ich czasu pojawienia się i zweryfikowania, do jakiego areacodu, operatora i jakiej grupy należą. Ja na to miast potrzebuje wyciągnąć informację o 10 numerach telefonu, które zostały zweryfikowane najwcześniej, ale dla areacodów zweryfikowanych najpóźniej. Może mały realny przypadek, no ale chyba mi wybaczycie :-P
test=# WITH last10_areacode AS ( select max(verified_at), areacode from table1 group by areacode order by 1 DESC limit 10), top_mobile_number AS ( select min(verified_at) as min_verified_at, mobile_number from table1 where areacode in (select areacode from last10_areacode) group by mobile_number order by 1 ASC limit 10) select template_id as tid,template_info_id as tinfoid,mobile_number,appeared_at,verified_at,external_operator_id,areacode from table1 where verified_at IN (select min_verified_at from top_mobile_number) limit 10; tid | tinfoid | mobile_number | appeared_at | verified_at | operator | areacode -----+---------+---------------+---------------------+---------------------+----------+---------- 239 | 10 | 18017255282 | 2007-07-13 19:55:09 | 2007-07-13 19:55:09 | 31002 | 801 91 | 10 | 18012057665 | 2007-04-06 02:20:27 | 2007-04-06 02:20:27 | 31005 | 801 91 | 10 | 17087032052 | 2007-04-06 18:16:04 | 2007-04-06 18:16:04 | 31002 | 708 91 | 10 | 17086465706 | 2007-04-04 02:13:34 | 2007-04-04 02:13:34 | 31005 | 708 91 | 10 | 17082885755 | 2007-04-11 16:18:08 | 2007-04-11 16:18:08 | 31002 | 708 91 | 10 | 12144035942 | 2007-07-14 18:30:24 | 2007-07-14 18:30:24 | 31005 | 214 138 | 10 | 19373612169 | 2007-04-16 16:23:25 | 2007-04-16 16:23:25 | 31005 | 937 138 | 10 | 14059244193 | 2007-04-18 02:13:48 | 2007-04-18 02:13:48 | 31005 | 405 138 | 10 | 16193475127 | 2007-04-23 19:31:51 | 2007-04-23 19:31:51 | 31005 | 619 91 | 10 | 12147144210 | 2007-07-24 23:59:08 | 2007-07-24 23:59:08 | 31002 | 214 (10 rows)Dzięki słowu WITH tworzymy zapytanie pomocnicze, którego wynik jest zapisywany do tabeli tymczasowej, istniejącej tylko i wyłącznie na czas działania całego zapytania. Jeśli dobrze zauważyliście, to już wiecie, że wynik zapytania last10_areacode został użyty do wygenerowania wyniku dla tabeli top_mobile_number, a ten wynik dla głównego zapytania. Idźmy jednak dalej.
Zapytania RECURSIVE (ale tak na prawdę iteracyjne!)
Razem ze słowem WITH możemy użyć RECURSIVE. Dzięki temu będziemy mieli możliwość odwołania się do własnego wyniku.
Aby wytłumaczyć o co mi chodzi, stworzę prostą tabelę, zawierającą informację o kategoriach. Kategoria może być główną (kolumna parent jest pusta) lub podkategorią. Ilość poziomów podkategorii jest dowolna.
test=# create table category (id serial, name varchar(255), parent integer, created_at timestamp); CREATE TABLE test=# \d category Table "public.category" Column | Type | Modifiers ------------+-----------------------------+------------------------------------------------------- id | integer | not null default nextval('category_id_seq'::regclass) name | character varying(255) | parent | integer | created_at | timestamp without time zone | test=# select * from category; id | name | parent | created_at ----+----------------+--------+---------------------------- 1 | top category 1 | | 2015-04-14 18:31:32.312868 2 | top category 2 | | 2015-04-14 18:31:37.280778 3 | top category 3 | | 2015-04-14 18:31:40.952949 4 | top category 4 | | 2015-04-14 18:31:45.809776 5 | top category 5 | | 2015-04-14 18:31:49.808637 6 | category 6 | 2 | 2015-04-14 18:32:30.0089 7 | category 7 | 2 | 2015-04-14 18:32:39.504864 8 | category 8 | 2 | 2015-04-14 18:32:43.632998 9 | category 9 | 4 | 2015-04-14 18:32:59.016916 10 | category 10 | 6 | 2015-04-14 18:33:36.224982 11 | category 11 | 7 | 2015-04-14 18:34:05.088904 12 | category 12 | 11 | 2015-04-14 18:35:00.69685 (12 rows)
Aby wybrać rekordy, które dotyczą tylko i wyłącznie kategorii głównej o id = 2 i jej dzieci, stworzyłam następujące zapytanie:
test=# WITH RECURSIVE get_parents_category(id, name, parent) AS ( SELECT id, name, parent FROM category WHERE parent = 2 OR id = 2 UNION SELECT c.id, c.name, c.parent FROM get_parents_category gpc, category c WHERE gpc.id = c.parent) SELECT * FROM get_parents_category; id | name | parent ----+----------------+-------- 2 | top category 2 | 6 | category 6 | 2 7 | category 7 | 2 8 | category 8 | 2 10 | category 10 | 6 11 | category 11 | 7 12 | category 12 | 11 (7 rows)
Aby wytłumaczyć co się dzieję w tej konstrukcji zapytania, po patrzcie na poniższy psełdo kod:
WITH RECURSIVE RECURSIVE_QUERY_NAME(ARG1, ARG2) AS ( START_QUERY UNION LOOP_QUERY ) MAIN_QUERY;
Aby zwrócić do MAIN_QUERY (to wywołanie RECURSIVE_QUERY_NAME) wynik działania, PostgreSQL wykonuje następujące kroki:
- Wywołanie zapytanie START_QUERY, a wynik zapisywany jest w tabeli pracującej (working table) i do zapytania LOOP_QUERY.
- Dopóki w tabeli pracującej (working table) są jakieś rekordy, powtarzane są czynności:
- Wywołanie zapytania LOOP_QUERY. Wynik jest zapisywany i w zależności od typu złączenia UNION [ALL], rekordy zduplikowane są usuwane lub nie. Wszystkie wiersze zostają wstawione jako wynik działania LOOP_QUERY oraz tymczasowej tabeli intermediate table.
- Zamiana zawartości tabeli pracującej z zawartością tabeli intermediate, a następnie opróżnienie tabeli intermediate.
Modyfikacja danych
PostgreSQL daje nam także możliwość modyfikacji danych (INSERT, UPDATE lub DELETE) przy użyciu słowa kluczowego WITH.
Nie ma możliwości użycia polecenia modyfikującego w zapytaniu RECURSIVE, ale możemy zwrócić rekordy poszukiwane do tej operacji.
Spróbuję usunąć wszystkie wiersze z tabeli category, które dotyczą głównej kategorii o id=2. Zacznijmy od sprawdzenia zawartości całej tabeli:
test=# select * from category; id | name | parent | created_at ----+----------------+--------+---------------------------- 1 | top category 1 | | 2015-04-14 18:31:32.312868 2 | top category 2 | | 2015-04-14 18:31:37.280778 3 | top category 3 | | 2015-04-14 18:31:40.952949 4 | top category 4 | | 2015-04-14 18:31:45.809776 5 | top category 5 | | 2015-04-14 18:31:49.808637 6 | category 6 | 2 | 2015-04-14 18:32:30.0089 7 | category 7 | 2 | 2015-04-14 18:32:39.504864 8 | category 8 | 2 | 2015-04-14 18:32:43.632998 9 | category 9 | 4 | 2015-04-14 18:32:59.016916 10 | category 10 | 6 | 2015-04-14 18:33:36.224982 11 | category 11 | 7 | 2015-04-14 18:34:05.088904 12 | category 12 | 11 | 2015-04-14 18:35:00.69685 (12 rows) test=# WITH RECURSIVE delete_old_category(id) AS ( SELECT id FROM category WHERE parent = 2 OR id = 2 UNION SELECT c.id FROM delete_old_category foo, category c WHERE foo.id = c.parent) DELETE FROM category WHERE id IN (SELECT id FROM delete_old_category); DELETE 7 test=# select * from category; id | name | parent | created_at ----+----------------+--------+---------------------------- 1 | top category 1 | | 2015-04-14 18:31:32.312868 3 | top category 3 | | 2015-04-14 18:31:40.952949 4 | top category 4 | | 2015-04-14 18:31:45.809776 5 | top category 5 | | 2015-04-14 18:31:49.808637 9 | category 9 | 4 | 2015-04-14 18:32:59.016916 (5 rows)Zapytanie usuwające stare kategorie jest bardzo podobne do powyższego SELECTa, bo wyciągnięte dane z zapytania RECURSIVE, są użyte w zapytaniu głównym do usunięcia interesujących nas rekordów po kluczu głównym.
Czasami jednak przydało by się zapisać gdzieś usunięte rekordy do jakiś logów (w końcu może jednak w przyszłości się okazać, że były to dość istotne informacje). W takim przypadku PostgreSQL także przyjdzie nam z pomocą w jednym zapytaniu.
test=# select * from table1 where areacode = '654'; template_id | template_info_id | mobile_number | appeared_at | verified_at | alias_name | created_at | updated_at | fake_id | external_operator_id | areacode -------------+------------------+---------------+---------------------+---------------------+--------------+---------------------+---------------------+---------+----------------------+---------- 4019 | 10 | 16549684775 | 2010-03-11 07:23:53 | 2010-03-11 07:23:53 | OTHERS | 2011-03-01 20:16:32 | 2011-03-01 20:16:32 | 1500323 | 31002 | 654 3687 | 10 | 16543245678 | 2010-04-05 20:25:08 | 2010-04-05 20:25:08 | AIRBENDERVID | 2011-03-01 21:15:02 | 2011-03-01 21:15:02 | 2140545 | 0 | 654 2977 | 10 | 16546547654 | 2009-10-12 00:06:20 | 2009-10-12 00:06:20 | PARAVID | 2011-03-01 19:17:56 | 2011-03-01 19:17:56 | 2396877 | 0 | 654 (3 rows) test=# WITH move_rows AS ( DELETE FROM table1 where areacode = '654' RETURNING * ) INSERT INTO table1_log SELECT * FROM move_rows; INSERT 0 3 test=# select * from table1_log; template_id | template_info_id | mobile_number | appeared_at | verified_at | alias_name | created_at | updated_at | fake_id | external_operator_id | areacode -------------+------------------+---------------+---------------------+---------------------+--------------+---------------------+---------------------+---------+----------------------+---------- 4019 | 10 | 16549684775 | 2010-03-11 07:23:53 | 2010-03-11 07:23:53 | OTHERS | 2011-03-01 20:16:32 | 2011-03-01 20:16:32 | 1500323 | 31002 | 654 3687 | 10 | 16543245678 | 2010-04-05 20:25:08 | 2010-04-05 20:25:08 | AIRBENDERVID | 2011-03-01 21:15:02 | 2011-03-01 21:15:02 | 2140545 | 0 | 654 2977 | 10 | 16546547654 | 2009-10-12 00:06:20 | 2009-10-12 00:06:20 | PARAVID | 2011-03-01 19:17:56 | 2011-03-01 19:17:56 | 2396877 | 0 | 654 (3 rows)
W zapytaniu ze słowem kluczowym WITH usuwane są rekordy z tabeli table1, a przy pomocy słowa RETURNING, są zwracane do zapytania głównego. Tam są wstawiane do tabeli table1_log.
Szkoda, że MySQL nie wspiera tego typu zapytań. Przydałyby mi się w pracy :-P