it-swarm.dev

Działanie indeksów w PostgreSQL

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

   Friends ( user_id1 ,user_id2) 

user_id1 i user_id2 to klucze obce do tabeli user

  1. Czy to są 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 utworzę 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?

80
codecool

Oto wyniki zapytania tabeli w druga kolumna 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)
);

Ograniczenie UNIQUE skutecznie implementuje unikalny indeks. 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 w adratt_uni indeks i przed testem uruchomiłem:

CLUSTER adratt;
ANALYZE adratt;

Skanowanie sekwencyjne w poszukiwaniu zapytań na (adr_id, att_id) są 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

Wynik analizy 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 x szybsze niż bez indeksu.
Po wyłączeniu skanowania indeksów planista zapytań wybiera skanowanie sterty map bitowych, które wykonuje prawie tak samo szybko. Dopiero po wyłączeniu tego wraca do skanowania sekwencyjnego.

83

do 1) Tak i nie.

W przypadku zapytania wykorzystującego 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 w jednej kolumnie, np. where user_id1 = 1 Ma to znaczenie, ponieważ zwykle do porównania przez optymalizator można użyć tylko kolumn „wiodących”. Zatem where user_id1 = 1 Będzie mógł korzystać z indeksu (identyfikator_użytkownika1, identyfikator_użytkownika2), ale nie będzie mógł indeksować (identyfikator_użytkownika2, identyfikator_użytkownika1) dla wszystkich przypadków.

Po zabawie z tym (po tym, jak Erwin tak uprzejmie pokazał nam konfigurację, w której 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 (user_id1, user_id2) (którego możesz się sam dowiedzieć bardzo łatwo, po prostu tworzenie takiego klucza podstawowego)

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

Dodatkowo Jaki indeks do utworzenia? przez depesz wykonuje dobrą robotę wyjaśniając kolejność w kolumnach indeksu i innych tematach związanych z indeksem.

30

Ad 1)
Istnieją ograniczenia w PostgreSQL jak to opisuje @a_horse_w_na_nazwie . Do wersja 8. indeksy wielokolumnowe mogły być używane tylko w przypadku zapytań dotyczących wiodących kolumn. Zostało to poprawione w wersji 8.1. aktualny podręcznik dla Postgres 1 (zaktualizowany) wyjaśnia:

Wielokolumnowy indeks drzewa B 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 i b = 42 w górę do ostatniego wpisu za pomocą a = 5. Wpisy indeksu za pomocą c> = 77 zostanie pominięty, ale nadal będzie musiał zostać zeskanowany. Z tego indeksu można zasadniczo korzystać w przypadku zapytań, które mają ograniczenia na b i/lub c bez ograniczeń na a - ale cały indeks musiałby zostać przeskanowany, więc w większości przypadków planista wolałby sekwencyjne skanowanie tabeli niż użycie indeksu.

Podkreśl moje. Mogę to potwierdzić z doświadczenia.
Zobacz także dodany przypadek testowy moja późniejsza odpowiedź tutaj .

12

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żdy krotek w zestawie wyników musi zostać odwiedzony, aby sprawdzić widoczność. Być może myślisz o Oracle.

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

Prawdziwe indeksy obejmujące (aktualizacja) są wprowadzane za pomocą klauzuli INCLUDE w 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, uruchomiłem również testy z tabelą dwóch liczb całkowitych i nic 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 to z pewnością nie jedyny czynnik tutaj .

Podsumowując główne punkty:

  • Indeksy wielokolumnowe mogą być używane z zapytaniami dotyczącymi kolumn nieprzednich, 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ą zawarte w indeksie (indeks obejmujący), a wszystkie zaangażowane wiersze (na blok) są widoczne dla wszystkich transakcji, możesz uzyskać „skanowanie tylko indeksu” na stronie 9.2 lub nowszej.

12
  1. Czy to są 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ń o formie select * from foo where baz=?

Erwin wykazał , że takie indeksy mogą rzeczywiście przyspieszyć zapytanie, ale ten efekt jest ograniczony i nie ma takiej samej kolejności, jak zwykle oczekuje się, że indeks poprawi wyszukiwanie - polega na tym, że „pełna 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 niepowodujących, ale na jeden z dwóch drugorzędnych i względnie drobnych sposobów, a nie w dramatyczny sposób, jakiego zwykle oczekuje się od indeksu z uwagi na jego strukturę 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 wynosi obejmujący , więc w ogóle nie ma wyszukiwania tabel Ups, 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, uderzenie 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 - uderzenie 74 bufory ponownie):

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

Tak więc dostęp przez indeks jest dwa razy szybszy w tym przypadku: 30 buforów - co pod względem indeksowania jest „nieco szybsze” !, a YMMV w zależności od względny rozmiar tabeli i indeksu, wraz z liczbą przefiltrowanych wierszy i charakterystyką grupowania danych w tabeli

Natomiast zapytania w kolumnie wiodącej wykorzystują strukturę btree indeksu - w tym przypadku naciśnięcie 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