Jak radzić sobie ze złym planem zapytań spowodowanym dokładną równością typu zakresu?

28

Przeprowadzam aktualizację, w której wymagam dokładnej równości tstzrangezmiennej. ~ 1M wierszy jest modyfikowanych, a zapytanie zajmuje ~ 13 minut. Wynik EXPLAIN ANALYZEmożna zobaczyć tutaj , a rzeczywiste wyniki są bardzo różne od tych oszacowanych przez narzędzie do planowania zapytań. Problem polega na tym, że podczas skanowania indeksu t_rangeoczekuje się zwrócenia jednego wiersza.

Wydaje się to być związane z faktem, że statystyki dotyczące typów zakresów są przechowywane inaczej niż statystyki innych typów. Patrząc w pg_statswidoku w kolumnie n_distinctwynosi 1, a pozostałe obszary (na przykład most_common_vals, most_common_freqs) są puste.

Jednak gdzieś muszą być przechowywane statystyki t_range. Niezwykle podobna aktualizacja, w której używam „wewnątrz” na t_range zamiast dokładnej równości, zajmuje około 4 minut i używa zasadniczo innego planu zapytań (patrz tutaj ). Drugi plan zapytań ma dla mnie sens, ponieważ zostanie użyty każdy wiersz w tabeli tymczasowej i znaczna część tabeli historii. Co ważniejsze, narzędzie do planowania zapytań przewiduje w przybliżeniu prawidłową liczbę wierszy dla włączonego filtra t_range.

Dystrybucja t_rangejest nieco niezwykła. Używam tej tabeli do przechowywania stanu historycznego innej tabeli, a zmiany w drugiej tabeli pojawiają się naraz w dużych zrzutach, więc nie ma wielu różnych wartości t_range. Oto liczby odpowiadające każdej z unikalnych wartości t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

Liczby dla różnych t_rangepowyżej są kompletne, więc liczność wynosi ~ 3 mln (z których ~ 1 mln będzie miało wpływ jedno z zapytań o aktualizację).

Dlaczego zapytanie 1 działa znacznie słabiej niż zapytanie 2? W moim przypadku zapytanie 2 jest dobrym zamiennikiem, ale jeśli naprawdę wymagana była równość dokładnych zakresów, jak mogę zmusić Postgres do korzystania z inteligentniejszego planu zapytań?

Definicja tabeli z indeksami (usuwanie niepotrzebnych kolumn):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Zapytanie 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Zapytanie 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Aktualizacje Q1 999753 wierszy i aktualizacje Q2 999753 + 36791 = 1036544 (tj. Tabela temp jest taka, że ​​każdy wiersz spełniający warunek zakresu czasu jest aktualizowany).

Próbowałem tego zapytania w odpowiedzi na komentarz @ ypercube :

Zapytanie 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

Plan zapytań i wyniki (patrz tutaj ) były pośrednie między dwoma poprzednimi przypadkami (~ 6 minut).

EDYCJA 2016/02/05

Po 1,5 roku nie mam już dostępu do danych, stworzyłem tabelę testową o tej samej strukturze (bez indeksów) i podobnej liczności. Odpowiedź jjanesa sugerowała, że ​​przyczyną może być uporządkowanie tabeli tymczasowej użytej do aktualizacji. Nie byłem w stanie przetestować tej hipotezy bezpośrednio, ponieważ nie mam dostępu do track_io_timing(za pomocą Amazon RDS).

  1. Ogólne wyniki były znacznie szybsze (kilkakrotnie). Zgaduję, że dzieje się tak z powodu usunięcia indeksów, zgodnie z odpowiedzią Erwina .

  2. W tym przypadku testowym kwerendy 1 i 2 zajmowały zasadniczo tyle samo czasu, ponieważ oba używały łączenia scalającego. Oznacza to, że nie byłem w stanie uruchomić niczego, co spowodowało, że Postgres wybrał łączenie mieszające, więc nie mam pojęcia, dlaczego Postgres wybrał słabo działające łączenie mieszające.

abeboparebop
źródło
1
Co jeśli warunek równości konwertowane (a = b)do dwóch „zawiera” warunki: (a @> b AND b @> a)? Czy plan się zmienia?
ypercubeᵀᴹ
@ypercube: plan zmienia się znacznie, choć nadal nie jest całkiem optymalny - patrz moja edycja # 2.
abeboparebop
1
Innym pomysłem byłoby dodanie regularnego indeksu btree, (lower(t_range),upper(t_range))ponieważ sprawdzasz równość.
ypercubeᵀᴹ

Odpowiedzi:

9

Największa różnica w czasie w twoich planach wykonania jest w górnym węźle, w samej aktualizacji. Sugeruje to, że większość czasu spędzasz na we / wy podczas aktualizacji. Możesz to sprawdzić, włączając track_io_timingi uruchamiając zapytaniaEXPLAIN (ANALYZE, BUFFERS)

Różne plany przedstawiają wiersze do aktualizacji w różnych zamówieniach. Jeden jest w trip_idporządku, a drugi w dowolnej kolejności, w jakiej znajdują się fizycznie w tabeli temp.

Wydaje się, że aktualizowana tabela ma korelację fizyczną z kolumną trip_id, a aktualizacja wierszy w tej kolejności prowadzi do wydajnych wzorców We / Wy z odczytami z wyprzedzeniem / sekwencjami. Podczas gdy fizyczna kolejność tabeli temp wydaje się prowadzić do wielu losowych odczytów.

Jeśli możesz dodać znak order by trip_iddo instrukcji, która utworzyła tabelę tymczasową, może to rozwiązać problem.

PostgreSQL nie bierze pod uwagę efektów zamówienia IO podczas planowania operacji UPDATE. (W przeciwieństwie do operacji WYBIERZ, gdzie bierze je pod uwagę). Gdyby PostgreSQL był bardziej sprytny, albo uświadomiłby sobie, że jeden plan generuje bardziej wydajną kolejność, albo wtrąciłby jawny węzeł sortowania między aktualizacją a jego węzłem potomnym, aby aktualizacja otrzymywała wiersze w kolejności ctid.

Masz rację, że PostgreSQL wykonuje słabą pracę, oceniając selektywność połączeń równości na zakresach. Jest to jednak tylko stycznie związane z podstawowym problemem. Bardziej wydajne zapytanie dotyczące wybranej części aktualizacji może przypadkowo wprowadzić wiersze do właściwej aktualizacji w lepszej kolejności, ale jeśli tak, to w większości zależy od szczęścia.

jjanes
źródło
Niestety nie jestem w stanie zmodyfikować track_io_timingi (ponieważ minęło półtora roku!) Nie mam już dostępu do oryginalnych danych. Jednak przetestowałem twoją teorię, tworząc tabele o tym samym schemacie i podobnym rozmiarze (miliony wierszy) i uruchamiając dwie różne aktualizacje - jedną, w której tabela aktualizacji temp została posortowana jak tabela oryginalna, a druga, w której została posortowana quasi-losowo. Niestety dwie aktualizacje zajmują mniej więcej tyle samo czasu, co oznacza, że ​​kolejność tabeli aktualizacji nie wpływa na to zapytanie.
abeboparebop
7

Nie jestem do końca pewien, dlaczego selektywność predykatu równości jest tak radykalnie zawyżona na podstawie indeksu GiST w tstzrangekolumnie. Choć samo w sobie jest to interesujące, wydaje się, że nie ma znaczenia dla konkretnego przypadku.

Ponieważ UPDATEmodyfikujesz jedną trzecią (!) Wszystkich istniejących wierszy 3M, indeks w ogóle nie pomoże . Wręcz przeciwnie, stopniowe aktualizowanie indeksu oprócz tabeli spowoduje znaczne zwiększenie kosztów UPDATE.

Po prostu zachowaj swoje proste Zapytanie 1 . Prostym, radykalnym rozwiązaniem jest upuszczenie indeksu przed UPDATE. Jeśli potrzebujesz go do innych celów, utwórz go ponownie po UPDATE. To nadal byłoby szybsze niż utrzymanie indeksu podczas dużego UPDATE.

W przypadku UPDATEjednej trzeciej wszystkich wierszy prawdopodobnie opłaci się również usunięcie wszystkich innych indeksów - i ponowne utworzenie ich po UPDATE. Jedyny minus: potrzebujesz dodatkowych uprawnień i wyłącznego zamka na stole (tylko przez krótką chwilę, jeśli używasz)CREATE INDEX CONCURRENTLY ).

Pomysł @ ypercube, aby użyć btree zamiast indeksu GiST, wydaje się zasadniczo dobry. Ale nie dla jednej trzeciej wszystkich wierszy (gdzie na początku żaden indeks nie jest dobry), i nie tylko (lower(t_range),upper(t_range)), ponieważ tstzrangenie jest to dyskretny typ zakresu.

Większość dyskretnych typów zakresów ma postać kanoniczną, co upraszcza pojęcie „równości”: definiują ją dolna i górna granica wartości w postaci kanonicznej. Dokumentacja:

Dyskretny typ zakresu powinien mieć funkcję kanonizacji, która jest świadoma pożądanego rozmiaru kroku dla typu elementu. Funkcja kanonizacji ma za zadanie przekształcanie równoważnych wartości typu zakresu, aby miały identyczne reprezentacje, w szczególności konsekwentnie włączające lub wyłączające granice. Jeśli funkcja kanonizacji nie jest określona, ​​wówczas zakresy o innym formatowaniu będą zawsze traktowane jako nierówne, nawet jeśli mogą reprezentować ten sam zestaw wartości w rzeczywistości.

Wbudowany rodzaju zakres int4range, int8rangei daterangecałkowitego zużycia postaci kanonicznej, który zawiera dolną granicę i górną granicę obejmuje; to jest [),. Typy zakresów zdefiniowane przez użytkownika mogą jednak korzystać z innych konwencji.

Nie dzieje się tak w przypadku tstzrange, gdy włączenie równości górnej i dolnej granicy należy wziąć pod uwagę. Ewentualny indeks btree musiałby być włączony:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

Zapytania musiałyby używać tych samych wyrażeń w WHERE klauzuli.

Można pokusić się o zindeksowanie całej wartości oddanej do text: (cast(t_range AS text))- ale to wyrażenie nie jest, IMMUTABLEponieważ tekstowa reprezentacja timestamptzwartości zależy od bieżącego timezoneustawienia. Będziesz musiał postawić dodatkowe kroki wIMMUTABLE funkcji otoki, która tworzy postać kanoniczną, i utworzyć indeks funkcjonalny na tym ...

Dodatkowe środki / alternatywne pomysły

Jeśli shape_dist_traveledmoże już mieć taką samą wartość jak tt.shape_dist_traveleddla kilku zaktualizowanych wierszy (i nie polegasz na żadnych efektach ubocznych UPDATEpodobnych wyzwalaczy ...), możesz przyspieszyć zapytanie, wykluczając puste aktualizacje:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Oczywiście obowiązują wszystkie ogólne porady dotyczące optymalizacji wydajności. Postgres Wiki to dobry punkt wyjścia.

VACUUM FULLbyłoby dla ciebie trucizną, ponieważ niektóre martwe krotki (lub miejsca zarezerwowane przez FILLFACTOR) są korzystne dla UPDATEwydajności.

Przy tak wielu zaktualizowanych wierszach i jeśli możesz sobie na to pozwolić (brak równoczesnego dostępu lub innych zależności), może być nawet szybsze napisanie zupełnie nowej tabeli zamiast aktualizacji w miejscu. Instrukcje w tej pokrewnej odpowiedzi:

Erwin Brandstetter
źródło