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

Indeksowanie bazy danych w PostgreSQL

Indeksowanie bazy danych to użycie specjalnych struktur danych, które mają na celu poprawę wydajności poprzez uzyskanie bezpośredniego dostępu do stron danych. Indeks bazy danych działa jak sekcja indeksu książki drukowanej:patrząc do sekcji indeksu, znacznie szybciej można zidentyfikować strony zawierające interesujące nas hasło. Możemy łatwo zlokalizować strony i uzyskać do nich bezpośredni dostęp . To zamiast skanować strony książki sekwencyjnie, aż znajdziemy termin, którego szukamy.

Indeksy są podstawowym narzędziem w rękach administratora baz danych. Korzystanie z indeksów może zapewnić znaczny wzrost wydajności w przypadku różnych domen danych. PostgreSQL jest znany ze swojej ogromnej rozszerzalności i bogatej kolekcji zarówno podstawowych, jak i zewnętrznych dodatków, a indeksowanie nie jest wyjątkiem od tej reguły. Indeksy PostgreSQL obejmują szerokie spektrum przypadków, od najprostszych indeksów b-drzewa na typach skalarnych, przez geoprzestrzenne indeksy GiST, po indeksy GIN fts, json lub tablicowe.

Indeksy jednak, choć wydają się wspaniałe (i rzeczywiście są!), nie są dostępne za darmo. Istnieje pewna kara związana z zapisami na zindeksowanej tabeli. Tak więc DBA, przed zbadaniem swoich opcji tworzenia konkretnego indeksu, powinna najpierw upewnić się, że ten indeks ma sens, co oznacza, że ​​korzyści z jego utworzenia przeważą utratę wydajności podczas zapisów.

Podstawowa terminologia indeksu PostgreSQL

Zanim opiszemy typy indeksów w PostgreSQL i ich zastosowanie, przyjrzyjmy się terminologii, z którą każdy DBA spotka się prędzej czy później podczas czytania dokumentacji.

  • Metoda dostępu do indeksu (nazywana także metodą dostępu ):Typ indeksu (drzewo B, GiST, GIN itp.)
  • Typ: typ danych indeksowanej kolumny
  • Operator: funkcja między dwoma typami danych
  • Rodzina operatora: operator typu danych krzyżowych, grupując operatory typów o podobnym zachowaniu
  • Klasa operatora (wspomniana również jako strategia indeksowania ):definiuje operatory, które mają być używane przez indeks dla kolumny

W katalogu systemowym PostgreSQL metody dostępu są przechowywane w pg_am, klasy operatorów w pg_opclass, rodziny operatorów w pg_opfamily. Zależności powyższych przedstawiono na poniższym schemacie:

Typy indeksów w PostgreSQL

PostgreSQL udostępnia następujące typy indeksów:

  • B-drzewo: indeks domyślny, mający zastosowanie dla typów, które można sortować
  • Hash: obsługuje tylko równość
  • GiST: odpowiedni dla nieskalarnych typów danych (np. kształtów geometrycznych, stóp, tablic)
  • SP-GiST: GIST partycjonowany w przestrzeni, ewolucja GIST do obsługi niezrównoważonych struktur (drzewa czworokątne, drzewa k-d, drzewa radix)
  • GIN: odpowiednie dla złożonych typów (np. jsonb, fts, arrays )
  • BRIN: stosunkowo nowy typ indeksu, który obsługuje dane, które można sortować, przechowując wartości min/maks w każdym bloku

Nisko spróbujemy ubrudzić sobie ręce kilkoma przykładami z prawdziwego świata. Wszystkie podane przykłady zostały wykonane w PostgreSQL 10.0 (zarówno z 10 jak i 9 klientami psql) na FreeBSD 11.1.

Indeksy B-drzewa

Załóżmy, że mamy następującą tabelę:

create table part (
id serial primary key, 
partno varchar(20) NOT NULL UNIQUE, 
partname varchar(80) NOT NULL, 
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
                                  Table "public.part"
   Column       |         Type          |                     Modifiers                     
------------+-----------------------+---------------------------------------------------
 id         | integer                 | not null default nextval('part_id_seq'::regclass)
 partno     | character varying(20)| not null
 partname       | character varying(80)| not null
 partdescr      | text                    |
 machine_id     | integer                 | not null
Indexes:
    "part_pkey" PRIMARY KEY, btree (id)
    "part_partno_key" UNIQUE CONSTRAINT, btree (partno)

Kiedy definiujemy tę dość powszechną tabelę, PostgreSQL tworzy za kulisami dwa unikalne indeksy B-drzewa:part_pkey i part_partno_key. Tak więc każde unikalne ograniczenie w PostgreSQL jest zaimplementowane z unikalnym INDEKSEM. Zapełnijmy naszą tabelę milionem wierszy danych:

testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000

Teraz spróbujmy wykonać kilka zapytań na naszym stole. Najpierw mówimy klientowi psql, aby zgłaszał czasy zapytań, wpisując \timing:

testdb=# select * from part where id=100000;
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,319 ms

Obserwujemy, że otrzymanie naszych wyników zajmuje tylko ułamki milisekundy. Spodziewaliśmy się tego, ponieważ dla obu kolumn użytych w powyższych zapytaniach zdefiniowaliśmy już odpowiednie indeksy. Teraz spróbujmy zapytać o nazwę części kolumny, dla której nie istnieje indeks.

testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 89,173 ms

Tutaj widzimy wyraźnie, że dla kolumny nieindeksowanej wydajność znacznie spada. Teraz utwórzmy indeks dla tej kolumny i powtórz zapytanie:

testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,525 ms

Nasz nowy indeks part_partname_idx jest również indeksem B-drzewa (domyślnie). Najpierw zauważamy, że tworzenie indeksu w tabeli z milionami wierszy zajęło sporo czasu, około 16 sekund. Następnie obserwujemy, że szybkość naszego zapytania wzrosła z 89 ms do 0,525 ms. Indeksy B-drzewa, poza sprawdzaniem równości, mogą również pomóc w zapytaniach dotyczących innych operatorów na uporządkowanych typach, takich jak <,<=,>=,>. Spróbujmy z <=i>=

testdb=# select count(*) from part where partname>='Part 9999900';
 count
-------
     9
(1 row)

Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
 count  
--------
 999991
(1 row)

Time: 355,618 ms

Pierwsze zapytanie jest znacznie szybsze niż drugie, używając słów kluczowych EXPLAIN (lub EXPLAIN ANALYZE) możemy sprawdzić, czy rzeczywisty indeks jest używany, czy nie:

testdb=# explain select count(*) from part where partname>='Part 9999900';
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 Aggregate  (cost=8.45..8.46 rows=1 width=8)
   ->  Index Only Scan using part_partname_idx on part  (cost=0.42..8.44 rows=1 width=0)
         Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)

Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=14603.22..14603.23 rows=1 width=8)
   ->  Gather  (cost=14603.00..14603.21 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=13603.00..13603.01 rows=1 width=8)
               ->  Parallel Seq Scan on part  (cost=0.00..12561.33 rows=416667 width=0)
                     Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)

Time: 0,461 ms

W pierwszym przypadku planista zapytań wybiera indeks part_partname_idx. Zauważamy również, że spowoduje to skanowanie tylko indeksu, co oznacza brak dostępu do tabel danych. W drugim przypadku planista stwierdza, że ​​nie ma sensu używać indeksu, ponieważ zwrócone wyniki stanowią dużą część tabeli, w którym to przypadku skanowanie sekwencyjne jest uważane za szybsze.

Indeksy haszujące

Używanie indeksów mieszających aż do PgSQL 9.6 włącznie było odradzane z powodów związanych z brakiem pisania WAL. Od wersji PgSQL 10.0 te problemy zostały naprawione, ale nadal używanie indeksów haszujących nie miało większego sensu. W PgSQL 11 podejmowane są wysiłki, aby indeksy mieszające stały się metodą indeksowania pierwszej klasy, podobnie jak jej więksi bracia (B-tree, GiST, GIN). Mając to na uwadze, wypróbujmy właściwie indeks skrótu w akcji.

Wzbogacimy naszą tabelę części o nowy typ części kolumny i wypełnimy ją wartościami o równym rozkładzie, a następnie uruchomimy zapytanie testujące typ części równy „Sterowanie”:

testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as  (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 93,361 ms

Teraz tworzymy indeks skrótu dla tej nowej kolumny i ponawiamy poprzednie zapytanie:

testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 63,634 ms

Zauważamy poprawę po zastosowaniu indeksu skrótu. Teraz porównamy wydajność indeksu mieszającego na liczbach całkowitych z równoważnym indeksem b-drzewa.

testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,297 ms

Jak widać, przy użyciu indeksów mieszających szybkość zapytań sprawdzających równość jest bardzo zbliżona do szybkości indeksów B-drzewa. Mówi się, że indeksy haszujące są nieznacznie szybsze dla równości niż B-drzewa, w rzeczywistości musieliśmy próbować każdego zapytania dwa lub trzy razy, aż indeks haszujący da lepszy wynik niż odpowiednik b-drzewa.

Pobierz oficjalny dokument już dziś Zarządzanie i automatyzacja PostgreSQL za pomocą ClusterControlDowiedz się, co musisz wiedzieć, aby wdrażać, monitorować, zarządzać i skalować PostgreSQLPobierz oficjalny dokument

Indeksy GiST

GiST (uogólnione drzewo wyszukiwania) to więcej niż jeden rodzaj indeksu, ale raczej infrastruktura do budowania wielu strategii indeksowania. Domyślna dystrybucja PostgreSQL zapewnia obsługę geometrycznych typów danych, tsquery i tsvector. W contrib są implementacje wielu innych klas operatorów. Czytając dokumentację i katalog contrib, czytelnik zauważy, że przypadki użycia GiST i GIN w dużym stopniu pokrywają się:tablice int, wyszukiwanie pełnotekstowe w celu nazwania głównych przypadków. W takich przypadkach GIN jest szybszy, a oficjalna dokumentacja wyraźnie to stwierdza. Jednak GiST zapewnia obszerną obsługę typów danych geometrycznych. Ponadto, w momencie pisania tego tekstu, GiST (i SP-GiST) jest jedyną sensowną metodą, której można używać z ograniczeniami wykluczenia. Zobaczymy na to przykład. Załóżmy (pozostając w dziedzinie inżynierii mechanicznej), że mamy wymóg zdefiniowania odmian typu maszyn dla danego typu maszyny, które są ważne przez pewien okres czasu; i że dla określonej odmiany nie może istnieć żadna inna odmiana dla tego samego typu maszyny, której okres pokrywa się (konflikt) z określonym okresem odmiany.

create table machine_type (
	id SERIAL PRIMARY KEY, 
	mtname varchar(50) not null, 
	mtvar varchar(20) not null, 
	start_date date not null, 
	end_date date, 
	CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);

Powyżej mówimy PostgreSQL, że dla każdej nazwy typu maszyny (mtname) może istnieć tylko jedna odmiana (mtvar). Data_początkowa oznacza datę początkową okresu, w którym ta odmiana typu maszyny jest ważna, a data_końcowa oznacza datę końcową tego okresu. Null end_date oznacza, że ​​odmiana typu maszyny jest obecnie prawidłowa. Teraz chcemy wyrazić wymóg braku nakładania się za pomocą ograniczenia. Sposobem na to jest ograniczenie wykluczenia:

testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);

Składnia EXCLUDE PostgreSQL pozwala nam określić wiele kolumn różnych typów iz innym operatorem dla każdej z nich. &&to nakładający się operator dla zakresów dat, a =jest powszechnym operatorem równości dla varchar. Ale dopóki naciśniemy Enter, PostgreSQL narzeka z komunikatem:

ERROR:  data type character varying has no default operator class for access method "gist"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.

Brakuje tutaj obsługi opclass GiST dla varchar. Zakładając, że pomyślnie zbudowaliśmy i zainstalowaliśmy rozszerzenie btree_gist, możemy przystąpić do tworzenia rozszerzenia:

testdb=# create extension btree_gist ;
CREATE EXTENSION

Następnie spróbuj ponownie utworzyć ograniczenie i przetestuj je:

testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).

Indeksy SP-GiST

SP-GiST, co oznacza GiST z partycjami przestrzeni, podobnie jak GiST, jest infrastrukturą umożliwiającą rozwój wielu różnych strategii w dziedzinie niezrównoważonych struktur danych opartych na dyskach. Domyślna dystrybucja PgSQL oferuje wsparcie dla punktów dwuwymiarowych, (dowolnego typu) zakresów, typów tekstowych i inet. Podobnie jak GiST, SP-GiST może być używany w ograniczeniach wykluczenia, w sposób podobny do przykładu pokazanego w poprzednim rozdziale.

Indeksy GIN

GIN (Generalized Inverted Index), podobnie jak GiST i SP-GiST, może zapewnić wiele strategii indeksowania. GIN jest odpowiedni, gdy chcemy indeksować kolumny typów złożonych. Domyślna dystrybucja PostgreSQL zapewnia obsługę dowolnego typu tablicy, jsonb i wyszukiwania pełnotekstowego (tsvector). W contrib są implementacje wielu innych klas operatorów. Jsonb, bardzo chwalona funkcja PostgreSQL (i stosunkowo niedawny rozwój (9.4+)) opiera się na GIN do obsługi indeksów. Innym powszechnym zastosowaniem GIN jest indeksowanie do wyszukiwania pełnotekstowego. Wyszukiwanie pełnotekstowe w PgSQL zasługuje na osobny artykuł, więc omówimy tutaj tylko część dotyczącą indeksowania. Najpierw przygotujmy naszą tabelę, podając wartości niezerowe w kolumnie partdescr i aktualizując pojedynczy wiersz o sensowną wartość:

testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms

Następnie przeprowadzamy wyszukiwanie tekstowe w nowo zaktualizowanej kolumnie:

testdb=# select * from part where partdescr @@ 'thermostat';
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 2015,690 ms (00:02,016)

To dość powolne, prawie 2 sekundy na nasz wynik. Teraz spróbujmy utworzyć indeks GIN na typie tsvector i powtórz zapytanie, używając składni przyjaznej dla indeksów:

testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 0,952 ms

I uzyskujemy 2000-krotne przyspieszenie. Możemy również zauważyć stosunkowo krótki czas, jaki zajęło utworzenie indeksu. Możesz eksperymentować z użyciem GiST zamiast GIN w powyższym przykładzie i mierzyć wydajność odczytów, zapisów i tworzenia indeksów dla obu metod dostępu.

Indeksy BRIN

BRIN (Block Range Index) to najnowszy dodatek do zestawu typów indeksów PostgreSQL, ponieważ został wprowadzony w PostgreSQL 9.5, mając zaledwie kilka lat jako standardowa funkcja rdzenia. BRIN działa na bardzo dużych tabelach, przechowując informacje podsumowujące dla zestawu stron o nazwie „Zakres bloków”. Indeksy BRIN są stratne (jak GiST), a to wymaga zarówno dodatkowej logiki w executorze zapytań PostgreSQL, jak i dodatkowej konserwacji. Zobaczmy BRIN w akcji:

testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 5,461 ms

Tutaj widzimy średnio ~18-krotne przyspieszenie dzięki wykorzystaniu indeksu BRIN. Jednak prawdziwym domem BRIN jest domena big data, więc mamy nadzieję przetestować tę stosunkowo nową technologię w rzeczywistych scenariuszach w przyszłości.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Wybieranie danych do tablicy Postgres

  2. Jak zainstalować wiele serwerów PostgreSQL w systemie RedHat Linux?

  3. uzyskać dostęp do aliasów kolumn w klauzuli WHERE w postgresql

  4. Postgres — transpozycja wierszy do kolumn

  5. Analiza porównawcza zarządzanych rozwiązań PostgreSQL w chmurze — Google Cloud:część trzecia