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

O użyteczności wskaźników wyrażeń

Prowadząc szkolenia PostgreSQL, zarówno podstawowe, jak i zaawansowane, często okazuje się, że uczestnicy nie mają pojęcia, jak potężne mogą być indeksy wyrażeń (jeśli w ogóle są ich świadomi). Pozwólcie, że przedstawię krótki przegląd.

Załóżmy więc, że mamy tabelę z różnymi znacznikami czasu (tak, mamy funkcję generate_series, która może generować daty):

CREATE TABLE t AS
SELECT d, repeat(md5(d::text), 10) AS padding
  FROM generate_series(timestamp '1900-01-01',
                       timestamp '2100-01-01',
                       interval '1 day') s(d);
VACUUM ANALYZE t;

Stół zawiera również kolumnę wypełniającą, aby była nieco większa. Zróbmy teraz proste zapytanie zakresowe, wybierając tylko jeden miesiąc z ~200 lat zawartych w tabeli. Jeśli wyjaśnisz zapytanie, zobaczysz coś takiego:

EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01';

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=32 width=332)
   Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
        AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

a na moim laptopie trwa to około 20 ms. Nieźle, biorąc pod uwagę, że musi to przejść przez całą tabelę z ~75 tys. wierszy.

Ale stwórzmy indeks w kolumnie sygnatury czasowej (wszystkie indeksy są tutaj typu domyślnego, tj. btree, chyba że wyraźnie wspomniano):

CREATE INDEX idx_t_d ON t (d);

A teraz spróbujmy ponownie uruchomić zapytanie:

                               QUERY PLAN
------------------------------------------------------------------------
 Index Scan using idx_t_d on t  (cost=0.29..9.97 rows=34 width=332)
   Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
            AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

a to działa w 0,5 ms, czyli mniej więcej 40 razy szybciej. Ale to były oczywiście proste indeksy, tworzone bezpośrednio na kolumnie, a nie indeks wyrażeń. Załóżmy więc, że zamiast tego musimy wybrać dane z każdego pierwszego dnia każdego miesiąca, wykonując zapytanie takie jak to

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

który jednak nie może użyć indeksu, ponieważ musi ocenić wyrażenie w kolumnie, podczas gdy indeks jest zbudowany na samej kolumnie, jak pokazano w WYJAŚNIJ ANALIZĘ:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
   Filter: (date_part('day'::text, d) = '1'::double precision)
   Rows Removed by Filter: 70649
 Planning time: 0.209 ms
 Execution time: 43.018 ms
(5 rows)

Więc nie tylko musi to zrobić sekwencyjne skanowanie, ale także wykonać ocenę, zwiększając czas trwania zapytania do 43 ms.

Baza danych nie może korzystać z indeksu z wielu powodów. Indeksy (przynajmniej indeksy btree) polegają na wysyłaniu zapytań posortowanych danych, dostarczanych przez strukturę podobną do drzewa, i chociaż zapytanie zakresowe może na tym skorzystać, drugie zapytanie (z wywołaniem `extract`) nie.

Uwaga:Inną kwestią jest to, że zbiór operatorów obsługiwanych przez indeksy (tj. które mogą być oceniane bezpośrednio na indeksach) jest bardzo ograniczony. Funkcja „wyodrębnij” nie jest obsługiwana, więc zapytanie nie może obejść problemu z porządkowaniem za pomocą skanowania indeksu bitmap.

Teoretycznie baza danych może próbować przekształcić warunek w warunki zakresu, ale jest to niezwykle trudne i specyficzne dla wyrażenia. W tym przypadku musielibyśmy wygenerować nieskończoną liczbę takich „dziennych” zakresów, ponieważ planista tak naprawdę nie zna znaczników czasu min/maks w tabeli. Więc baza danych nawet nie próbuje.

Ale chociaż baza danych nie wie, jak zmienić warunki, programiści często to robią. Na przykład z warunkami takimi jak

(column + 1) >= 1000

nie jest trudno przepisać to w ten sposób

column >= (1000 - 1)

co działa dobrze z indeksami.

Ale co, jeśli taka transformacja nie jest możliwa, jak na przykład dla przykładowego zapytania

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

W takim przypadku programista musiałby zmierzyć się z tym samym problemem z nieznanymi wartościami min/max dla kolumny d, a nawet wtedy wygenerowałby wiele zakresów.

Cóż, ten wpis na blogu dotyczy indeksów wyrażeń, a do tej pory używaliśmy tylko zwykłych indeksów, zbudowanych bezpośrednio na kolumnie. Stwórzmy więc pierwszy indeks wyrażeń:

CREATE INDEX idx_t_expr ON t ((extract(day FROM d)));
ANALYZE t;

co następnie daje nam ten plan wyjaśnień

                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
   Recheck Cond: (date_part('day'::text, d) = '1'::double precision)
   Heap Blocks: exact=2401
   ->  Bitmap Index Scan on idx_t_expr  (cost=0.00..46.73 rows=2459 width=0)
                                (actual time=1.243..1.243 rows=2401 loops=1)
         Index Cond: (date_part('day'::text, d) = '1'::double precision)
 Planning time: 0.374 ms
 Execution time: 17.136 ms
(7 rows)

Więc chociaż nie daje nam to takiego samego 40-krotnego przyspieszenia, jak indeks w pierwszym przykładzie, można się tego spodziewać, ponieważ to zapytanie zwraca znacznie więcej krotek (2401 vs. 32). Ponadto są one rozrzucone po całej tabeli i nie są tak zlokalizowane, jak w pierwszym przykładzie. Jest to więc niezłe przyspieszenie 2x, aw wielu rzeczywistych przypadkach zobaczysz znacznie większe ulepszenia.

Ale możliwość używania indeksów dla warunków ze złożonymi wyrażeniami nie jest tutaj najciekawszą informacją – to jest poniekąd powód, dla którego ludzie tworzą indeksy wyrażeń. Ale to nie jedyna korzyść.

Jeśli spojrzysz na dwa plany wyjaśnień przedstawione powyżej (bez i z indeksem wyrażeń), możesz zauważyć, że:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
 ...
                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
 ...

Zgadza się – stworzenie indeksu wyrażeń znacznie poprawiło szacunki. Bez indeksu mamy tylko statystyki (MCV + histogram) dla surowych kolumn tabeli, więc baza danych nie wie, jak oszacować wyrażenie

EXTRACT(day FROM d) = 1

Dlatego zamiast tego stosuje domyślne oszacowanie warunków równości, które wynosi 0,5% wszystkich wierszy – ponieważ tabela ma 73050 wierszy, otrzymujemy oszacowanie tylko 365 wierszy. Często zdarza się, że w rzeczywistych aplikacjach występują znacznie gorsze błędy szacowania.

Jednak wraz z indeksem baza zbierała również statystyki dotyczące kolumn indeksu iw tym przypadku kolumna zawiera wyniki wyrażenia. I podczas planowania optymalizator zauważa to i tworzy znacznie lepsze oszacowanie.

To ogromna korzyść, która może pomóc w naprawieniu niektórych przypadków słabych planów zapytań spowodowanych niedokładnymi szacunkami. Jednak większość ludzi nie zdaje sobie sprawy z tego przydatnego narzędzia.

A użyteczność tego narzędzia wzrosła dopiero wraz z wprowadzeniem typu danych JSONB w wersji 9.4, ponieważ jest to jedyny sposób zbierania statystyk dotyczących zawartości dokumentów JSONB.

Podczas indeksowania dokumentów JSONB istnieją dwie podstawowe strategie indeksowania. Możesz utworzyć indeks GIN/GiST na całym dokumencie, np. jak to

CREATE INDEX ON t USING GIN (jsonb_column);

co pozwala na odpytywanie dowolnych ścieżek w kolumnie JSONB, użycie operatora zawierania w celu dopasowania poddokumentów itp. To świetnie, ale nadal masz tylko podstawowe statystyki dotyczące kolumny, które nie są zbyt przydatne jako dokumenty są traktowane jako wartości skalarne (i nikt nie dopasowuje całych dokumentów ani nie używa zakresu dokumentów).

Indeksy wyrażeń, na przykład utworzone w ten sposób:

CREATE INDEX ON t ((jsonb_column->'id'));

będzie przydatny tylko dla konkretnego wyrażenia, tj. ten nowo utworzony indeks będzie przydatny dla

SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

ale nie w przypadku zapytań uzyskujących dostęp do innych kluczy JSON, takich jak na przykład „wartość”

SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';

Nie oznacza to, że indeksy GIN/GiST w całym dokumencie są bezużyteczne, ale musisz wybrać. Albo tworzysz skoncentrowany indeks wyrażeń, przydatny podczas wykonywania zapytań o konkretny klucz i z dodatkową korzyścią statystyk dotyczących wyrażenia. Lub tworzysz indeks GIN/GiST dla całego dokumentu, zdolny do obsługi zapytań o dowolne klucze, ale bez statystyk.

Jednak w tym przypadku możesz mieć ciastko i je zjeść, ponieważ możesz stworzyć oba indeksy jednocześnie, a baza danych wybierze, którego z nich użyć do poszczególnych zapytań. A dzięki indeksom wyrażeń będziesz mieć dokładne statystyki.

Niestety nie możesz zjeść całego ciasta, ponieważ indeksy wyrażeń i indeksy GIN/GiST używają różnych warunków

-- expression (btree)
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

-- GIN/GiST
SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';

więc planista nie może ich używać w tym samym czasie – indeksy wyrażeń do oszacowania i GIN/GiST do wykonania.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Konfiguracja klastra Puma w Heroku

  2. Jak zatrzymać/zabić zapytanie w postgresql?

  3. Jak make_timestamptz() działa w PostgreSQL

  4. Postgres NIE w tablicy

  5. Refaktoryzuj klucz obcy do pól