PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

O korzyściach z posortowanych ścieżek

Miałem przyjemność uczestniczyć w PGDay UK w zeszłym tygodniu – bardzo fajne wydarzenie, mam nadzieję, że będę miał okazję wrócić w przyszłym roku. Było wiele ciekawych prelekcji, ale ta, która szczególnie przykuła moją uwagę, to Performace dla zapytań z grupowaniem Aleksieja Basztanowa.

Wygłosiłem w przeszłości sporo podobnych rozmów nastawionych na wydajność, więc wiem, jak trudno jest przedstawić wyniki benchmarków w zrozumiały i interesujący sposób, a Alexey wykonał całkiem niezłą robotę. Więc jeśli masz do czynienia z agregacją danych (tj. BI, analityką lub podobnymi obciążeniami), polecam przejrzenie slajdów, a jeśli masz szansę wziąć udział w wystąpieniu na innej konferencji, gorąco to polecam.

Ale jest jeden punkt, w którym nie zgadzam się z przemówieniem. W wielu miejscach rozmowa sugerowała, że ​​generalnie powinieneś preferować HashAggregate, ponieważ sortowanie jest powolne.

Uważam to za nieco mylące, ponieważ alternatywą dla HashAggregate jest GroupAggregate, a nie Sort. Tak więc zalecenie zakłada, że ​​każda grupa GroupAggregate ma zagnieżdżone sortowanie, ale to nie do końca prawda. GroupAggregate wymaga posortowanych danych wejściowych, a jawne sortowanie nie jest jedynym sposobem na to – mamy również węzły IndexScan i IndexOnlyScan, które eliminują koszty sortowania i zachowują inne korzyści związane z posortowanymi ścieżkami (zwłaszcza IndexOnlyScan).

Pozwolę sobie zademonstrować, jak działa (IndexOnlyScan+GroupAggregate) w porównaniu z HashAggregate i (Sort+GroupAggregate) – skrypt, którego użyłem do pomiarów, jest tutaj. Buduje cztery proste tabele, każda z 100 milionami wierszy i różną liczbą grup w kolumnie „branch_id” (określającą rozmiar tablicy haszującej). Najmniejszy ma 10 tys. grup

-- table with 10k groups
create table t_10000   (branch_id bigint, amount numeric);
insert into t_10000 select mod(i, 10000), random()
 from generate_series(1,100000000) s(i);

a trzy dodatkowe stoły mają grupy 100k, 1M i 5M. Uruchommy to proste zapytanie agregujące dane:

SELECT branch_id, SUM(amount) FROM t_10000 GROUP BY 1

a następnie przekonaj bazę danych do korzystania z trzech różnych planów:

1) HashAggregat

SET enable_sort = off;
SET enable_hashagg = on;

EXPLAIN SELECT branch_id, SUM(amount) FROM t_10000 GROUP BY 1;
                                 QUERY PLAN
----------------------------------------------------------------------------
 HashAggregate  (cost=2136943.00..2137067.99 rows=9999 width=40)
   Group Key: branch_id
   ->  Seq Scan on t_10000  (cost=0.00..1636943.00 rows=100000000 width=19)
(3 rows)

2) GroupAggregate (z sortowaniem)

SET enable_sort = on;
SET enable_hashagg = off;

EXPLAIN SELECT branch_id, SUM(amount) FROM t_10000 GROUP BY 1;
                                 QUERY PLAN
-------------------------------------------------------------------------------
 GroupAggregate  (cost=16975438.38..17725563.37 rows=9999 width=40)
   Group Key: branch_id
   ->  Sort  (cost=16975438.38..17225438.38 rows=100000000 width=19)
         Sort Key: branch_id
         ->  Seq Scan on t_10000  (cost=0.00..1636943.00 rows=100000000 ...)
(5 rows)

3) GroupAggregate (z IndexOnlyScan)

SET enable_sort = on;
SET enable_hashagg = off;

CREATE INDEX ON t_10000 (branch_id, amount);

EXPLAIN SELECT branch_id, SUM(amount) FROM t_10000 GROUP BY 1;
                          QUERY PLAN
--------------------------------------------------------------------------
 GroupAggregate  (cost=0.57..3983129.56 rows=9999 width=40)
   Group Key: branch_id
   ->  Index Only Scan using t_10000_branch_id_amount_idx on t_10000
                         (cost=0.57..3483004.57 rows=100000000 width=19)
(3 rows)

Wyniki

Po zmierzeniu czasów dla każdego planu na wszystkich stołach wyniki wyglądają tak:

W przypadku małych tablic mieszających (pasujących do pamięci podręcznej L3, która w tym przypadku wynosi 16 MB), ścieżka HashAggregate jest wyraźnie szybsza niż obie posortowane ścieżki. Ale wkrótce GroupAgg+IndexOnlyScan będzie równie szybki, a nawet szybszy – wynika to z wydajności pamięci podręcznej, głównej zalety GroupAggregate. Podczas gdy HashAggregate musi jednocześnie przechowywać w pamięci całą tabelę skrótów, GroupAggregate musi przechowywać tylko ostatnią grupę. A im mniej pamięci używasz, tym bardziej prawdopodobne jest, że zmieści się ją w pamięci podręcznej L3, która jest mniej więcej o rząd wielkości szybsza w porównaniu ze zwykłą pamięcią RAM (w przypadku pamięci podręcznych L1/L2 różnica jest jeszcze większa).

Tak więc, mimo że IndexOnlyScan wiąże się ze znacznym obciążeniem (w przypadku 10 tys. jest o około 20% wolniejszy niż ścieżka HashAggregate), w miarę wzrostu tabeli haszowania współczynnik trafień w pamięci podręcznej L3 szybko spada, a różnica ostatecznie sprawia, że ​​GroupAggregate jest szybszy. I ostatecznie nawet GroupAggregate+Sort staje się na równi ze ścieżką HashAggregate.

Możesz argumentować, że twoje dane mają zazwyczaj dość małą liczbę grup, a zatem tablica mieszająca zawsze zmieści się w pamięci podręcznej L3. Należy jednak wziąć pod uwagę, że pamięć podręczna L3 jest współdzielona przez wszystkie procesy działające na procesorze, a także przez wszystkie części planu zapytań. Więc chociaż obecnie mamy ~20 MB pamięci podręcznej L3 na gniazdo, twoje zapytanie otrzyma tylko część tego, a ten bit będzie współdzielony przez wszystkie węzły w twoim (prawdopodobnie dość złożonym) zapytaniu.

Aktualizacja 2016/07/26 :Jak wskazano w komentarzach Petera Geoghegana, sposób generowania danych prawdopodobnie prowadzi do korelacji – nie wartości (czy raczej skrótów wartości), ale alokacji pamięci. Powtórzyłem zapytania z odpowiednio dobranymi danymi, czyli robiąc

insert into t_10000 select (10000*random())::bigint, random()
 from generate_series(1,100000000) s(i);

zamiast

insert into t_10000 select mod(i, 10000), random()
 from generate_series(1,100000000) s(i);

a wyniki wyglądają tak:

Porównując to z poprzednim wykresem, myślę, że jest całkiem jasne, że wyniki są jeszcze bardziej na korzyść posortowanych ścieżek, szczególnie dla zestawu danych z grupami 5M. Zestaw danych 5M pokazuje również, że GroupAgg z jawnym sortowaniem może być szybszy niż HashAgg.

Podsumowanie

Chociaż HashAggregate jest prawdopodobnie szybszy niż GroupAggregate z jawnym sortowaniem (choć waham się, czy tak jest zawsze), użycie GroupAggregate z IndexOnlyScan szybciej może z łatwością sprawić, że będzie on znacznie szybszy niż HashAggregate.

Oczywiście nie możesz bezpośrednio wybrać dokładnego planu – planista powinien to zrobić za Ciebie. Ale wpływasz na proces selekcji, (a) tworząc indeksy i (b) ustawiając work_mem . Dlatego czasami obniżamy wartość work_mem (i maintenance_work_mem ) wartości zapewniają lepszą wydajność.

Dodatkowe indeksy nie są jednak darmowe – kosztują zarówno czas procesora (przy wstawianiu nowych danych), jak i miejsce na dysku. W przypadku IndexOnlyScans wymagania dotyczące miejsca na dysku mogą być dość znaczące, ponieważ indeks musi zawierać wszystkie kolumny, do których odwołuje się zapytanie, a zwykły IndexScan nie zapewni takiej samej wydajności, ponieważ generuje wiele losowych operacji we/wy na tabeli (eliminując wszystkie potencjalne zyski).

Kolejną fajną cechą jest stabilność wydajności – zauważ, jak szanse na czasy HashAggregate zależą od liczby grup, podczas gdy ścieżki GroupAggregate działają w większości tak samo.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Samodzielne dostarczanie kont użytkowników w PostgreSQL za pośrednictwem nieuprzywilejowanego dostępu anonimowego

  2. Reprezentowanie dat, godzin i interwałów w PostgreSQL

  3. PostgreSQL działa wolno na dużym stole z tablicami i mnóstwem aktualizacji

  4. Postgres ręcznie zmienia sekwencję

  5. Generowanie szeregów czasowych między dwiema datami w PostgreSQL