System partycjonowania w PostgreSQL został po raz pierwszy dodany w PostgreSQL 8.1 przez założyciela 2ndQuadrant Simon Riggs . Opierał się na dziedziczeniu relacji i wykorzystywał nowatorską technikę wykluczania tabel ze skanowania przez zapytanie, zwaną „wykluczeniem ograniczeń”. Chociaż w tamtym czasie był to ogromny krok naprzód, obecnie jest postrzegany jako nieporęczny w użyciu, a także powolny, a zatem wymaga wymiany.
W wersji 10 został zastąpiony dzięki heroicznym wysiłkomAmit Langote z nowoczesnym „deklaratywnym partycjonowaniem”. Ta nowa technologia oznaczała, że nie musisz już ręcznie pisać kodu, aby kierować krotki do ich właściwych partycji, i nie musisz już ręcznie deklarować poprawnych ograniczeń dla każdej partycji:system robił to automatycznie za Ciebie.
Niestety, w PostgreSQL 10 to prawie wszystko, co zrobił. Ze względu na samą złożoność i ograniczenia czasowe w implementacji PostgreSQL 10 brakowało wielu rzeczy. Robert Haas wygłosił referat na ten temat w warszawskim PGConf.EU.
Wiele osób pracowało nad poprawą sytuacji dla PostgreSQL 11; oto moja próba przeliczenia. Podzieliłem je na trzy obszary:
- Nowe funkcje partycjonowania
- Lepsza obsługa DDL dla tabel partycjonowanych
- Optymalizacja wydajności.
Nowe funkcje partycjonowania
W PostgreSQL 10 Twoje partycjonowane tabele mogą być w RANGE i LISTA tryby. Są to potężne narzędzia, na których można oprzeć wiele rzeczywistych baz danych, ale w przypadku wielu innych projektów potrzebujesz nowego trybu dodanego w PostgreSQL 11:HASH partycjonowanie . Wielu klientów tego potrzebuje, a Amul Sul ciężko pracował, aby było to możliwe. Oto prosty przykład:
CREATE TABLE clients ( client_id INTEGER, name TEXT ) PARTITION BY HASH (client_id); CREATE TABLE clients_0 PARTITION OF clients FOR VALUES WITH (MODULUS 3, REMAINDER 0); CREATE TABLE clients_1 PARTITION OF clients FOR VALUES WITH (MODULUS 3, REMAINDER 1); CREATE TABLE clients_2 PARTITION OF clients FOR VALUES WITH (MODULUS 3, REMAINDER 2);
Użycie tej samej wartości modułu dla wszystkich partycji nie jest obowiązkowe; pozwala to na późniejsze utworzenie większej liczby partycji i redystrybucję wierszy po jednej partycji naraz, jeśli to konieczne.
Kolejna bardzo przydatna funkcja, napisana przez Amita Khandekara to możliwość zezwolenia na AKTUALIZACJĘ przenosić wiersze z jednej partycji na drugą — to znaczy, jeśli nastąpi zmiana wartości kolumny partycjonującej, wiersz zostanie automatycznie przeniesiony do właściwej partycji. Wcześniej ta operacja powodowała błąd.
Kolejna nowa funkcja, napisana przez Amit Langote i swoje naprawdę , czy to WSTAWIĆ DO AKTUALIZACJI KONFLIKTÓW można zastosować do tabel partycjonowanych . Wcześniej to polecenie kończyło się niepowodzeniem, jeśli kierowało się do tabeli partycjonowanej. Możesz sprawić, by działało, wiedząc dokładnie, na której partycji znajdzie się wiersz, ale nie jest to zbyt wygodne. Nie będę omawiał szczegółów tego polecenia, ale jeśli kiedykolwiek chciałeś mieć UPSERT w Postgresie to jest to. Jedynym zastrzeżeniem jest to, że AKTUALIZACJA akcja może nie przenieść wiersza do innej partycji.
Na koniec kolejna urocza nowa funkcja w PostgreSQL 11, tym razem autorstwa Jevan Ladhe, Beena Emerson, Ashutosh Bapat, Rahila Syed, iRobert Haas to obsługa domyślnej partycji w tabeli partycjonowanej , czyli partycja, która otrzymuje wszystkie wiersze, które nie mieszczą się w żadnej ze zwykłych partycji. Jednak, chociaż fajna na papierze, ta funkcja nie jest zbyt wygodna w ustawieniach produkcyjnych, ponieważ niektóre operacje wymagają mocniejszego blokowania z domyślnymi partycjami niż bez. Przykład:utworzenie nowej partycji wymaga przeskanowania partycji domyślnej w celu ustalenia, czy żadne istniejące wiersze nie pasują do granic nowej partycji. Być może w przyszłości te wymagania dotyczące blokady zostaną obniżone, ale w międzyczasie sugeruję, aby ich nie używać.
Lepsza obsługa DDL
W PostgreSQL 10 niektóre DDL odmawiały pracy po zastosowaniu do tabeli partycjonowanej i wymagały przetwarzania każdej partycji osobno. W PostgreSQL 11 naprawiliśmy kilka z tych ograniczeń, zgodnie z wcześniejszymi zapowiedziami Simona Riggsa. Po pierwsze, możesz teraz użyć UTWÓRZ INDEKS na partycjonowanym stole , funkcja napisana przez Ciebie naprawdę. To może być postrzegane jako kwestia zmniejszenia nudy:zamiast powtarzać polecenie dla każdej partycji (i upewniając się, że nigdy nie zapomnisz o każdej nowej partycji), możesz to zrobić tylko raz dla macierzystej tabeli partycjonowanej i zostanie ona automatycznie zastosowana do wszystkich partycji, istniejących i przyszłych.
Jedną fajną rzeczą, o której należy pamiętać, jest dopasowanie istniejących indeksów w partycjach. Jak wiesz, tworzenie indeksu jest blokującą propozycją, więc im mniej czasu to zajmie, tym lepiej. Napisałem tę funkcję, aby istniejące indeksy na partycji były porównywane z indeksami tworzonymi, a jeśli istnieją dopasowania, nie jest konieczne skanowanie partycji w celu utworzenia nowych indeksów:zostaną użyte istniejące indeksy.
Razem z tym, również przez swoje, możesz także tworzyć UNIKALNE ograniczenia, a także PRIMARY KEY ograniczenia . Dwa zastrzeżenia:po pierwsze, klucz partycji musi być częścią klucza podstawowego. Pozwala to na przeprowadzanie unikalnych kontroli lokalnie na partycję, unikając indeksów globalnych. Po drugie, nie jest jeszcze możliwe posiadanie kluczy obcych, które odwołują się do tych kluczy podstawowych. Pracuję nad tym dla PostgreSQL 12.
Inną rzeczą, którą możesz zrobić (dzięki tej samej osobie), jest utworzenie DLA KAŻDEGO RZĘDU wyzwalacze na partycjonowanej tabeli i mieć zastosowanie do wszystkich partycji (istniejących i przyszłych). Jako efekt uboczny możesz odroczyć unikalny ograniczenia dotyczące tabel partycjonowanych. Jedno zastrzeżenie:tylko PO wyzwalacze są dozwolone, dopóki nie dowiemy się, jak radzić sobie z PRZED wyzwalacze, które przenoszą wiersze do innej partycji.
Wreszcie, tabela podzielona na partycje może mieć KLUCZ OBCY ograniczenia . Jest to bardzo przydatne do dzielenia dużych tabel faktów przy jednoczesnym unikaniu zwisających odniesień, których wszyscy nienawidzą. Mój kolega Gabriele Bartolini złapał mnie za kolana, kiedy dowiedział się, że to napisałem i popełniłem, krzycząc, że to zmieniła grę i jak mogłem być tak niewrażliwy, żeby go o tym nie informować. Ja po prostu kontynuuję hakowanie kodu dla zabawy.
Praca wydajnościowa
Wcześniej wstępne przetwarzanie zapytań w celu sprawdzenia, których partycji nie skanować (wykluczenie ograniczeń) było raczej uproszczone i powolne. Zostało to ulepszone dzięki godnej podziwu pracy zespołowej wykonanej przez Amita Langote, Davida Rowleya, Beenę Emerson, Dilipa Kumara, aby najpierw wprowadzić „szybsze przycinanie”, a następnie oparte na nim „przycinanie w czasie wykonywania”. Wynik jest znacznie mocniejszy i szybszy (David Rowley opisałem to już w poprzednim artykule). Po tych wszystkich wysiłkach przycinanie partycji jest stosowane w trzech punktach życia zapytania:
- W czasie planowania zapytań
- Po odebraniu parametrów zapytania
- W każdym punkcie, w którym jeden węzeł zapytania przekazuje wartości jako parametry do innego węzła.
Jest to niezwykłe ulepszenie w stosunku do oryginalnego systemu, które można było zastosować tylko w czasie planowania zapytań i wierzę, że zadowoli wielu.
Możesz zobaczyć tę funkcję w działaniu, porównując dane wyjściowe EXPLAIN dla zapytania przed i po wyłączeniu enable_partition_pruning opcja. Jako bardzo uproszczony przykład porównaj ten plan bez przycinania:
SET enable_partition_pruning TO off; EXPLAIN (ANALYZE, COSTS off) SELECT * FROM clientes WHERE cliente_id = 1234;
QUERY PLAN ------------------------------------------------------------------------- Append (actual time=6.658..10.549 rows=1 loops=1) -> Seq Scan on clientes_1 (actual time=4.724..4.724 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 24978 -> Seq Scan on clientes_00 (actual time=1.914..1.914 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12644 -> Seq Scan on clientes_2 (actual time=0.017..1.021 rows=1 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12570 -> Seq Scan on clientes_3 (actual time=0.746..0.746 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12448 -> Seq Scan on clientes_01 (actual time=0.648..0.648 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12482 -> Seq Scan on clientes_4 (actual time=0.774..0.774 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12400 -> Seq Scan on clientes_5 (actual time=0.717..0.717 rows=0 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12477 Planning Time: 0.375 ms Execution Time: 10.603 ms
z tym wyprodukowanym z przycinaniem:
EXPLAIN (ANALYZE, COSTS off) SELECT * FROM clientes WHERE cliente_id = 1234;
QUERY PLAN ---------------------------------------------------------------------- Append (actual time=0.054..2.787 rows=1 loops=1) -> Seq Scan on clientes_2 (actual time=0.052..2.785 rows=1 loops=1) Filter: (cliente_id = 1234) Rows Removed by Filter: 12570 Planning Time: 0.292 ms Execution Time: 2.822 ms
Jestem pewien, że uznasz to za przekonujące. Możesz zobaczyć mnóstwo bardziej wyrafinowanych przykładów, przeglądając plik oczekiwanych testów regresji.
Kolejnym punktem było wprowadzenie złączeń partycjonowanych przez Ashutosha Bapata . Pomysł polega na tym, że jeśli masz dwie partycjonowane tabele i są one podzielone na partycje w identyczny sposób, to po połączeniu można połączyć każdą partycję z jednej strony z odpowiadającą jej partycją z drugiej strony; jest to znacznie lepsze niż łączenie każdej partycji z boku z każdą partycją po drugiej stronie. Fakt, że schematy partycji muszą być dokładnie dopasowane może wydawać się, że nie będzie to miało większego zastosowania w świecie rzeczywistym, ale w rzeczywistości jest wiele sytuacji, w których ma to zastosowanie. Przykład:tabela zamówień i odpowiadająca jej tabela orders_items. Na szczęście jest już dużo pracy nad złagodzeniem tego ograniczenia.
Ostatnią rzeczą, o której chcę wspomnieć, są agregaty podzielone na partycje, autorstwa Jevan Chalke, Ashutosh Bapat, iRobert Haas . Ta optymalizacja oznacza, że agregacja zawierająca klucze partycji w GROUP BY Klauzula może być wykonana przez agregację wierszy każdej partycji oddzielnie, co jest znacznie szybsze.
Myśli zamykające
Po znaczących zmianach w tym cyklu, PostgreSQL ma znacznie bardziej fascynującą historię partycjonowania. Chociaż wciąż trzeba wprowadzić wiele ulepszeń, zwłaszcza w celu poprawy wydajności i współbieżności różnych operacji obejmujących partycjonowane tabele, jesteśmy teraz w punkcie, w którym partycjonowanie deklaratywne stało się bardzo cennym narzędziem do obsługi wielu przypadków użycia. W 2ndQuadrant będziemy nadal wnosić kod, aby ulepszyć PostgreSQL w tym obszarze i innych, tak jak robiliśmy to dla każdego wydania od wersji 8.0.