SQLprogramowanieStarszy inżynier baz danych

Na jakim dokładnym progu w fazie optymalizacji zapytań planista **PostgreSQL** decyduje, że węzeł **Gather Merge** jest lepszy niż węzeł **Gather** do zbiegania wyników równolegle, i jaka konkretna cecha podstawowych węzłów skanowania dyktuje ten wybór?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie.

Wprowadzenie możliwości zrównoleglej obsługi zapytań w PostgreSQL 9.6 przyniosło węzeł Gather, który łączy wyniki z procesów roboczych tła do procesu głównego. Jednak standardowy węzeł Gather niszczy dowolne porządkowanie krotek wygenerowane przez równoległych pracowników, co wymusza kosztowny finalny krok Sort, aby przywrócić sekwencję. Aby wyeliminować tę redundancję podczas przetwarzania wewnętrznie uporządkowanych strumieni danych, w wersji 10 wprowadzono węzeł Gather Merge, który wykonuje k-kierunkowe łączenie posortowanych danych z pracowników, omijając konieczność materializacji i sortowania po stronie lidera.

Planista decyduje się na wprowadzenie Gather Merge wyłącznie wtedy, gdy równoległy subplan gwarantuje wynik uporządkowany zgodnie z wymaganą właściwością, zazwyczaj generowaną przez Index Scans lub Merge Joins, które zachowują sekwencję krotek. Jeśli subplan traci porządek przez operacje takie jak Hash Joins lub nieuporządkowane agregacje, Gather Merge staje się niekwalifikowany, zmuszając optymalizator do wyboru między Gather, a następnie kosztownym Sort, lub całkowitym porzuceniem równoległości, aby utrzymać porządek z użyciem pojedynczego procesu.

Gdy subplan gwarantuje uporządkowane wyjście, Gather Merge pozwala liderowi na wykonanie strumieniowego łączenia przy użyciu minimalnych buforów pamięci, zamiast materializować i sortować wszystkie krotki. Strategia pamięci zmienia się z jednego dużego przydziału na sortowanie w liderze do mniejszych utrzymywanych przez pracowników posortowanych przebiegów, znacznie redukując ryzyko wyczerpania work_mem i przelania na dysk podczas wielkoskalowych zamówień uporządkowanych.

Sytuacja z życia

Nasz zespół zarządzał platformą analityki szeregów czasowych przechowującą pomiary z czujników w tabeli PostgreSQL podzielonej według godzin, zawierającej ponad 2 miliardy wierszy. Krytyczna tablica rozdzielcza wymagała wyświetlenia 1000 najnowszych pomiarów we wszystkich partycjach, posortowanych według timestamp w kolejności malejącej, z budżetem opóźnienia poniżej 500 milisekund. Początkowy plan zapytania jednowątkowego nie spełniał tych wymagań, tworząc wąskie gardło w doświadczeniu użytkownika w czasie szczytowym obciążenia analitycznego.

Jednowątkowe skanowanie indeksu: Początkowo rozważaliśmy wykorzystanie odwrotnego Index Scan w każdej partycji, a następnie węzła Limit wykonywanego sekwencyjnie. To podejście oferowało prostotę implementacji i deterministyczne porządkowanie bez złożonej koordynacji równoległej. Jednak nie udało mu się nasycić pasma I/O naszej macierzy pamięci NVMe i regularnie przekraczało 2 sekundy podczas szczytowego obciążenia, co czyniło je nieakceptowalnym dla aktualizacji w czasie rzeczywistym tablicy rozdzielczej.

Równoległe skanowanie sekwencyjne z zebraniem i sortowaniem: Drugie podejście polegało na włączeniu max_parallel_workers_per_gather i użyciu Parallel Seq Scan z standardowym węzłem Gather, zbierającym wszystkie wiersze do lidera dla finalnego Sort i Limit. To wykorzystywało równoległość CPU i znacznie poprawiło przepustowość skanowania. Niemniej jednak spowodowało to, że lider musiał przydzielić ponad 4 GB work_mem do sortowania milionów wierszy, co często wywoływało przelania na dysk i błędy OutOfMemory na naszym ograniczonym węźle lidera, co osłabiało stabilność systemu.

Równoległe skanowanie indeksu z zebraniem i scaleniem: Ostatecznie wybraliśmy plan, w którym pracownicy wykonywali Parallel Index Scans w malejącej kolejności czasowej, zasilając węzeł Gather Merge. Pracownicy skanowali strony liści indeksu w wymaganej sekwencji, przesyłając posortowane krotki do lidera, który wykonywał lekkie k-kierunkowe łączenie w celu wydobycia 1000 najlepszych wierszy. Ta architektura wyeliminowała potrzebę finalnego sortowania w liderze, drastycznie redukując presję na pamięć przy zachowaniu efektywności strumienia.

Wybraliśmy podejście Gather Merge, ponieważ unikalnie zaspokajało zarówno wymagania dotyczące opóźnienia, jak i pamięci, wykorzystując istniejącą strukturę indeksu, zamiast walczyć z nią operacjami opartymi na haszach. To rozwiązanie zmniejszyło pamięć lidera do poniżej 64 MB dla buforów scalania i osiągnęło stałe czasy odpowiedzi poniżej 300 ms. System teraz obsługuje szczytowe obciążenia bez wyczerpania pamięci, potwierdzając wybór architektoniczny w celu zachowania porządku poprzez równoległe wykonanie.

Co często umyka kandydatom

Dlaczego umieszczanie Hash Aggregate poniżej węzła Gather Merge powoduje, że planista PostgreSQL albo odrzuca plan, albo wprowadza jawny krok Sort, a jak to się różni od zachowania GroupAggregate?

Hash Aggregate buduje nieuporządkowaną tablicę haszującą do grupowania krotek, co z natury niszczy dowolną sekwencję wejściową wygenerowaną przez podstawowe skanowania. Ponieważ Gather Merge wymaga ściśle uporządkowanych strumieni wejściowych od wszystkich równoległych pracowników, aby wykonać swoje strumieniowe k-kierunkowe łączenie, nieuporządkowane wyjście z bloków agregacji blokuje jego bezpośrednie użycie. Odwrotnie, GroupAggregate może działać na wstępnie posortowanych wejściach i zachować porządek krotek, gdy klucze GROUP BY pasują do porządku sortowania, co czyni go kompatybilnym z Gather Merge bez konieczności wstępnego sortowania.

Jak koszt parallel_tuple_cost wpływa na próg, przy którym planista przełącza się z planu Gather na plan Gather Merge przy szacowaniu kosztów łączenia posortowanych strumieni z ośmiu równoległych pracowników?

parallel_tuple_cost dodaje narzut CPU na każdy wiersz przy przenoszeniu wierszy między równoległymi pracownikami a procesem lidera. Dla Gather Merge ten koszt jest nieco wyższy niż dla standardowego węzła Gather z powodu dodatkowej logiki porównawczej potrzebnej do utrzymania kopca scalającego. Gdy oszacowany zbiór wyników jest mały, planista może preferować węzeł Gather w połączeniu z tanim Sort w liderze, zamiast Gather Merge, ponieważ łączny narzut ośmiu strumieni scalających może przewyższyć koszt sortowania małej partii krotek centralnie.

Jaka konkretna ograniczenie pojawia się podczas korzystania z DECLARE CURSOR z opcją SCROLL nad planem zapytania, który zawiera węzeł Gather Merge, i dlaczego wykonawca może cicho materializować cały zbiór wyników mimo strumieniowego charakteru scalania?

SCROLL kursory wymagają możliwości przesuwania się wstecz przez zbiór wyników, co konieczne jest do materializacji wierszy w work_mem lub przelaniu na dysk, aby wspierać odwrócone pobieranie. Chociaż Gather Merge produkuje efektywne, strumieniowe i uporządkowane wyjście, opcja SCROLL zmusza wykonawcę do wstawienia węzła Materialize powyżej Gather Merge, aby zbuforować wiersze do potencjalnego odwrotnego przeprowadzania. Taka materializacja pochłania pamięć proporcjonalnie do rozmiaru zbioru wyników, co skutecznie niweczy korzyści z efektywności pamięci strategii strumieniowego scalania i potencjalnie powoduje przelania na dysk, które byłyby unikane, wybierając Gather Merge początkowo.