Przeprowadzam aktualizację, w której wymagam dokładnej równości tstzrange
zmiennej. ~ 1M wierszy jest modyfikowanych, a zapytanie zajmuje ~ 13 minut. Wynik EXPLAIN ANALYZE
moż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_range
oczekuje 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_stats
widoku w kolumnie n_distinct
wynosi 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_range
jest 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_range
powyż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).
Ogólne wyniki były znacznie szybsze (kilkakrotnie). Zgaduję, że dzieje się tak z powodu usunięcia indeksów, zgodnie z odpowiedzią Erwina .
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.
(a = b)
do dwóch „zawiera” warunki:(a @> b AND b @> a)
? Czy plan się zmienia?(lower(t_range),upper(t_range))
ponieważ sprawdzasz równość.Odpowiedzi:
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_timing
i uruchamiając zapytaniaEXPLAIN (ANALYZE, BUFFERS)
Różne plany przedstawiają wiersze do aktualizacji w różnych zamówieniach. Jeden jest w
trip_id
porzą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_id
do 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.
źródło
track_io_timing
i (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.Nie jestem do końca pewien, dlaczego selektywność predykatu równości jest tak radykalnie zawyżona na podstawie indeksu GiST w
tstzrange
kolumnie. Choć samo w sobie jest to interesujące, wydaje się, że nie ma znaczenia dla konkretnego przypadku.Ponieważ
UPDATE
modyfikujesz 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ówUPDATE
.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 poUPDATE
. To nadal byłoby szybsze niż utrzymanie indeksu podczas dużegoUPDATE
.W przypadku
UPDATE
jednej trzeciej wszystkich wierszy prawdopodobnie opłaci się również usunięcie wszystkich innych indeksów - i ponowne utworzenie ich poUPDATE
. 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żtstzrange
nie 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:
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: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
:- ale to wyrażenie nie jest,(cast(t_range AS text))
IMMUTABLE
ponieważ tekstowa reprezentacjatimestamptz
wartości zależy od bieżącegotimezone
ustawienia. 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_traveled
może już mieć taką samą wartość jaktt.shape_dist_traveled
dla kilku zaktualizowanych wierszy (i nie polegasz na żadnych efektach ubocznychUPDATE
podobnych wyzwalaczy ...), możesz przyspieszyć zapytanie, wykluczając puste aktualizacje:Oczywiście obowiązują wszystkie ogólne porady dotyczące optymalizacji wydajności. Postgres Wiki to dobry punkt wyjścia.
VACUUM FULL
byłoby dla ciebie trucizną, ponieważ niektóre martwe krotki (lub miejsca zarezerwowane przezFILLFACTOR
) są korzystne dlaUPDATE
wydajnoś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:
źródło