Działanie indeksów w PostgreSQL

73

Mam kilka pytań dotyczących działania indeksów w PostgreSQL. Mam Friendstabelę z następującym indeksem:

   Friends ( user_id1 ,user_id2) 

user_id1i user_id2są kluczami obcymi do usertabeli

  1. Czy są one równoważne? Jeśli nie to dlaczego?

    Index(user_id1,user_id2) and Index(user_id2,user_id1)
  2. Jeśli utworzę klucz podstawowy (identyfikator_użytkownika1, identyfikator_użytkownika2), czy automatycznie tworzy dla niego indeksy i

    Jeśli indeksy w pierwszym pytaniu nie są równoważne, to który indeks jest tworzony na podstawie polecenia klucza podstawowego?

codecool
źródło

Odpowiedzi:

77

Oto wyniki zapytania do tabeli w drugiej kolumnie indeksu wielokolumnowego .
Efekty są łatwe do odtworzenia dla każdego. Po prostu spróbuj w domu.

Testowałem z PostgreSQL 9.0.5 na Debianie, używając średniej wielkości tabeli bazy danych z 23322 wierszami. Implementuje relację n: m między tabelami adr(adres) i att(atrybut), ale tutaj nie ma to znaczenia. Uproszczony schemat:

CREATE TABLE adratt (
  adratt_id serial PRIMARY KEY
, adr_id    integer NOT NULL
, att_id    integer NOT NULL
, log_up    timestamp(0) NOT NULL DEFAULT (now())::timestamp(0)
, CONSTRAINT adratt_uni UNIQUE (adr_id, att_id)
);

UNIQUEOgraniczenie skutecznie realizuje unikalną indeksu. Dla pewności powtórzyłem test z prostym indeksem i uzyskałem identyczne wyniki, jak oczekiwano.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

Tabela jest skupiona na adratt_uniindeksie, a przed testem uruchomiłem:

CLUSTER adratt;
ANALYZE adratt;

Sekwencyjne skanowanie zapytań (adr_id, att_id)jest tak szybkie, jak to tylko możliwe. Indeks wielokolumnowy będzie nadal używany jako warunek zapytania w samej drugiej kolumnie indeksu.

Uruchomiłem zapytania kilka razy, aby zapełnić pamięć podręczną i wybrałem najlepszy z dziesięciu przebiegów, aby uzyskać porównywalne wyniki.

1. Zapytanie za pomocą obu kolumn

SELECT *
FROM   adratt
WHERE  att_id = 90
AND    adr_id = 10;

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
(1 row)

Wyjście EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..3.48 rows=1 width=20) (actual time=0.022..0.025 rows=1 loops=1)
  Index Cond: ((adr_id = 10) AND (att_id = 90))
Total runtime: 0.067 ms

2. Zapytanie za pomocą pierwszej kolumny

SELECT * FROM adratt WHERE adr_id = 10

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       126 |     10 |     10 | 2008-07-29 09:35:54
       125 |     10 |     13 | 2008-07-29 09:35:54
      4711 |     10 |     21 | 2008-07-29 09:35:54
     29322 |     10 |     22 | 2011-06-06 15:50:38
     29321 |     10 |     30 | 2011-06-06 15:47:17
       124 |     10 |     62 | 2008-07-29 09:35:54
     21913 |     10 |     78 | 2008-07-29 09:35:54
       123 |     10 |     90 | 2008-07-29 09:35:54
     28352 |     10 |    106 | 2010-11-22 12:37:50
(9 rows)

Wyjście EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..8.23 rows=9 width=20) (actual time=0.007..0.023 rows=9 loops=1)
  Index Cond: (adr_id = 10)
Total runtime: 0.058 ms

3. Zapytanie za pomocą drugiej kolumny

SELECT * FROM adratt WHERE att_id = 90

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
       180 |     39 |     90 | 2008-08-29 15:46:07
...
(83 rows)

Wyjście EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..818.51 rows=83 width=20) (actual time=0.014..0.694 rows=83 loops=1)
  Index Cond: (att_id = 90)
Total runtime: 0.849 ms

4. Wyłącz indeksowanie i skanowanie bitmaps

SET enable_indexscan = off;
SELECT * FROM adratt WHERE att_id = 90

Dane wyjściowe EXPLAIN ANALYZE:

Bitmap Heap Scan on adratt  (cost=779.94..854.74 rows=83 width=20) (actual time=0.558..0.743 rows=83 loops=1)
  Recheck Cond: (att_id = 90)
  ->  Bitmap Index Scan on adratt_uni  (cost=0.00..779.86 rows=83 width=0) (actual time=0.544..0.544 rows=83 loops=1)
        Index Cond: (att_id = 90)
Total runtime: 0.894 ms
SET enable_bitmapscan = off
SELECT * FROM adratt WHERE att_id = 90

Wyjście EXPLAIN ANALYZE:

Seq Scan on adratt  (cost=0.00..1323.10 rows=83 width=20) (actual time=0.009..2.429 rows=83 loops=1)
  Filter: (att_id = 90)
Total runtime: 2.680 ms

Wniosek

Zgodnie z oczekiwaniami indeks wielokolumnowy służy do zapytania tylko w drugiej kolumnie.
Zgodnie z oczekiwaniami jest mniej skuteczny, ale zapytanie jest nadal 3 razy szybsze niż bez indeksu.
Po wyłączeniu skanowania indeksów narzędzie do planowania zapytań wybiera skanowanie sterty map bitowych, które wykonuje prawie tak samo szybko. Dopiero po wyłączeniu to wraca do skanowania sekwencyjnego.

Erwin Brandstetter
źródło
klastrów będzie mieć znaczenie, jeżeli liczba meczów w indeksie jest wystarczająco wysokie (patrz tutaj dla dowodu - należy zwrócić uwagę na podwójne przejazdy, aby uzyskać buforowane dane)
Jack Douglas
1
@JackDouglas: Zastanowiłem się nad tym. Klastrów może pomóc ogólnie, ponieważ jest skuteczny także vacuum fulla reindex. Inne niż to pomoże skanowania indeksu na pierwszej lub obu czołowych kolumnach dużo , ale boli zapytań na drugiej kolumnie. W świeżo klastrowanej tabeli wiersze o tej samej wartości w drugiej kolumnie są rozłożone, tak że trzeba będzie odczytać maksymalnie bloki.
Erwin Brandstetter,
28

re 1) Tak i nie.

W przypadku zapytania, które wykorzystuje obie kolumny, np where (user_id1, user_id2) = (1,2). Nie ma znaczenia, który indeks jest tworzony.

W przypadku zapytania, które ma warunek tylko na jednej kolumnie, np where user_id1 = 1. Ma to znaczenie, ponieważ zwykle do porównania przez optymalizator można użyć tylko „wiodących” kolumn. where user_id1 = 1Byłby więc w stanie używać indeksu (identyfikator_użytkownika1, identyfikator_użytkownika2), ale nie byłby w stanie indeksować (identyfikator_użytkownika2, identyfikator_użytkownika1) we wszystkich przypadkach.

Po zabawie z tym (po tym, jak Erwin tak uprzejmie pokazał nam konfigurację, w której to działa), wydaje się, że zależy to w dużej mierze od dystrybucji danych w drugiej kolumnie, chociaż jeszcze nie dowiedziałem się, która sytuacja umożliwia optymalizatorowi użycie końcowych kolumn na warunek GDZIE.

Oracle 11, który może również (czasami) używać kolumn, które nie znajdują się na początku definicji indeksu.

re 2) Tak, utworzy indeks

Cytat z instrukcji

Dodanie klucza podstawowego spowoduje automatyczne utworzenie unikalnego indeksu btree w kolumnie lub grupie kolumn używanych w kluczu podstawowym.

re 2a) Primary Key (user_id1,user_id2)utworzy indeks na (identyfikator_użytkownika1, identyfikator_użytkownika2) (którego można bardzo łatwo dowiedzieć się, po prostu tworząc taki klucz podstawowy)

Gorąco polecam przeczytanie rozdziału o indeksach w podręczniku , ponieważ w zasadzie odpowiada on na wszystkie powyższe pytania.

Dodatkowo, jaki indeks utworzyć? autor: depesz wykonuje dobrą robotę wyjaśniając porządek w kolumnach indeksu i innych tematach związanych z indeksem.

koń bez imienia
źródło
11

Ad 1)
Istnieją ograniczenia w PostgreSQL, takie jak @a_horse_with_no_name . Do wersji 8.0 indeksy wielokolumnowe mogły być używane tylko do zapytań dotyczących wiodących kolumn. Zostało to poprawione w wersji 8.1. Aktualna instrukcja dla PostgreSQL 10 (aktualizacja) wyjaśnia:

Wielokolumnowy indeks B-drzewa może być używany z warunkami zapytania obejmującymi dowolny podzbiór kolumn indeksu, ale indeks jest najbardziej wydajny, gdy istnieją ograniczenia na wiodących (skrajnie lewych) kolumnach. Dokładna zasada jest taka, że ​​ograniczenia równości w wiodących kolumnach oraz wszelkie ograniczenia nierówności w pierwszej kolumnie, które nie mają ograniczenia równości, zostaną wykorzystane do ograniczenia części skanowanego indeksu. Ograniczenia dotyczące kolumn po prawej stronie tych kolumn są sprawdzane w indeksie, dzięki czemu zapisują właściwe wizyty w tabeli, ale nie zmniejszają części indeksu, którą należy przeskanować. Na przykład, biorąc pod uwagę indeks (a, b, c)i warunek zapytania WHERE a = 5 AND b >= 42 AND c < 77, indeks musiałby zostać zeskanowany od pierwszego wpisu za pomocą a= 5 ib= 42 w górę do ostatniego wpisu za pomocą a= 5. Wpisy indeksu za pomocą c> = 77 zostaną pominięte, ale nadal będą musiały zostać zeskanowane. Zasadniczo ten indeks może być używany do zapytań, które mają ograniczenia bi / lub cbez ograniczeń a- ale cały indeks musiałby zostać przeskanowany, więc w większości przypadków planista wolałby sekwencyjne skanowanie tabeli niż korzystanie z indeksu.

Podkreśl moje. Mogę to potwierdzić z doświadczenia.
Zobacz także przypadek testowy, który dodał moją późniejszą odpowiedź tutaj .

Erwin Brandstetter
źródło
11

To jest odpowiedź na odpowiedź Jacka , komentarz nie zrobiłby tego.

Przed wersją 9.2 nie było indeksów pokrywających w PostgreSQL . Ze względu na model MVCC każdą krotkę w zestawie wyników należy odwiedzić, aby sprawdzić widoczność. Być może myślisz o Oracle.

Programiści PostgreSQL mówią o „skanach indeksowych” . W rzeczywistości funkcja ta została wydana wraz z Postgres 9.2. Przeczytaj komunikat zatwierdzenia .
Depesz napisał bardzo pouczający post na blogu .

Prawdziwe indeksy pokrycia (aktualizacja) zostały wprowadzone wraz z INCLUDEklauzulą ​​dotyczącą Postgres 11. Powiązane:

To też jest trochę nieprzyjemne:

opiera się na fakcie, że „pełne skanowanie” indeksu jest często szybsze niż „pełne skanowanie” indeksowanej tabeli ze względu na dodatkowe kolumny w tabeli, które nie pojawiają się w indeksie.

Jak podano w komentarzach do mojej drugiej odpowiedzi, przeprowadziłem również testy z tabelą dwóch liczb całkowitych i niczym więcej. Indeks zawiera te same kolumny co tabela. Rozmiar indeksu btree wynosi około 2/3 wielkości tabeli. Za mało, aby wyjaśnić przyspieszenie czynnika 3. Przeprowadziłem więcej testów, w oparciu o twoją konfigurację, uproszczonych do dwóch kolumn i 100000 wierszy. W mojej instalacji PostgreSQL 9.0 wyniki były spójne.

Jeśli tabela ma dodatkowe kolumny, przyspieszenie z indeksem staje się bardziej znaczące, ale z pewnością nie jest to jedyny czynnik .

Podsumowując główne punkty:

  • Indeksy wielokolumnowe mogą być używane z zapytaniami dotyczącymi nieprzedstawiających się kolumn, ale przyspieszenie wynosi tylko około 3 dla kryteriów selektywnych (mały procent wierszy w wyniku). Wyższy dla większych krotek, niższy dla większych części tabeli w zestawie wyników.

  • Utwórz dodatkowy indeks w tych kolumnach, jeśli wydajność jest ważna.

  • Jeśli wszystkie zaangażowane kolumny są uwzględnione w indeksie (indeks obejmujący), a wszystkie zaangażowane wiersze (na blok) są widoczne dla wszystkich transakcji, można uzyskać „skanowanie tylko indeksu” na stronie 9.2 lub nowszej.

Erwin Brandstetter
źródło
7
  1. Czy są one równoważne? Jeśli nie to dlaczego?

    Indeks (identyfikator_użytkownika1, identyfikator_użytkownika2) i indeks (identyfikator_użytkownika2, identyfikator_użytkownika1)

Nie są one równoważne i ogólnie mówiąc indeks (bar, baz) nie będzie skuteczny w przypadku zapytań formularza select * from foo where baz=?

Erwin wykazał, że takie indeksy rzeczywiście mogą przyspieszyć zapytanie, ale ten efekt jest ograniczony i nie ma takiej samej kolejności, jak zwykle oczekuje się, że indeks poprawi wyszukiwanie - opiera się na fakcie, że „pełne skanowanie” indeksu jest często szybsze niż „pełne skanowanie” indeksowanej tabeli ze względu na dodatkowe kolumny w tabeli, które nie pojawiają się w indeksie.

Podsumowanie: indeksy mogą pomagać w zapytaniach nawet w kolumnach nie prowadzących, ale na jeden z dwóch drugorzędnych i względnie drobnych sposobów, a nie w dramatyczny sposób, zwykle oczekuje się, że indeks pomoże z powodu jego struktury btree

nb dwa sposoby, w jakie indeks może pomóc, jeśli pełne skanowanie indeksu jest znacznie tańsze niż pełne skanowanie tabeli i: 1. wyszukiwanie tabel jest tanie (ponieważ jest ich niewiele lub są one zgrupowane), lub 2. Indeks obejmuje, więc nie ma wyszukiwania tabel we wszystkich przypadkach , patrz komentarze Erwins tutaj

testbed:

create table foo(bar integer not null, baz integer not null, qux text not null);

insert into foo(bar, baz, qux)
select random()*100, random()*100, 'some random text '||g from generate_series(1,10000) g;

zapytanie 1 (bez indeksu, trafienie w 74 bufory ):

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=181.41..181.42 rows=1 width=32) (actual time=3.301..3.302 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..181.30 rows=43 width=32) (actual time=0.043..3.228 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.335 ms

zapytanie 2 (z indeksem - optymalizator ignoruje indeks - ponownie uderza 74 bufory ):

create index bar_baz on foo(bar, baz);

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=199.12..199.13 rows=1 width=32) (actual time=3.277..3.277 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..199.00 rows=50 width=32) (actual time=0.043..3.210 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.311 ms

zapytanie 2 (z indeksem - i oszukujemy optymalizatora, aby go użył):

explain (buffers, analyze, verbose) select max(qux) from foo where bar>-1000 and baz=0;
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=115.56..115.57 rows=1 width=32) (actual time=1.495..1.495 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=36 read=30
   ->  Bitmap Heap Scan on stack.foo  (cost=73.59..115.52 rows=17 width=32) (actual time=1.370..1.428 rows=52 loops=1)
         Output: bar, baz, qux
         Recheck Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
         Buffers: shared hit=36 read=30
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..73.58 rows=17 width=0) (actual time=1.356..1.356 rows=52 loops=1)
               Index Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
               Buffers: shared read=30
 Total runtime: 1.535 ms

Zatem dostęp za pośrednictwem indeksu jest w tym przypadku dwukrotnie szybszy niż w przypadku 30 buforów - co pod względem indeksowania jest „nieco szybsze” !, i YMMV w zależności od względnego rozmiaru tabeli i indeksu, wraz z liczbą przefiltrowanych wierszy i cech klastrowania danych w tabeli

Natomiast zapytania w kolumnie wiodącej wykorzystują strukturę btree indeksu - w tym przypadku uderzając w 2 bufory :

explain (buffers, analyze, verbose) select max(qux) from foo where bar=0;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=75.70..75.71 rows=1 width=32) (actual time=0.172..0.173 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=38
   ->  Bitmap Heap Scan on stack.foo  (cost=4.64..75.57 rows=50 width=32) (actual time=0.036..0.097 rows=59 loops=1)
         Output: bar, baz, qux
         Recheck Cond: (foo.bar = 0)
         Buffers: shared hit=38
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..4.63 rows=50 width=0) (actual time=0.024..0.024 rows=59 loops=1)
               Index Cond: (foo.bar = 0)
               Buffers: shared hit=2
 Total runtime: 0.209 ms
Jack Douglas
źródło