Właściwe zastosowanie indeksów może sprawić, że zapytania będą błyskawiczne.
Indeksy używają wskaźników do szybkiego dostępu do stron danych.
Duże zmiany zaszły w indeksach w PostgreSQL 11, zostało wydanych wiele oczekiwanych poprawek.
Przyjrzyjmy się niektórym wspaniałym funkcjom tej wersji.
Równoległe kompilacje indeksu B-TREE
PostgreSQL 11 wprowadził poprawkę infrastruktury, aby umożliwić równoległe tworzenie indeksów.
Na razie może być używany tylko z indeksem B-Tree.
Budowanie równoległego indeksu B-Tree jest dwa do trzech razy szybsze niż robienie tego samego bez pracy równoległej (lub szeregowego budowania).
W PostgreSQL 11 równoległe tworzenie indeksów jest domyślnie włączone.
Istnieją dwa ważne parametry:
- max_parallel_workers — Ustawia maksymalną liczbę procesów roboczych, które system może obsługiwać dla zapytań równoległych.
- max_parallel_maintenance_workers — Kontroluje maksymalną liczbę procesów roboczych, których można użyć do TWORZENIA INDEKSU.
Sprawdźmy to na przykładzie:
severalnines=# CREATE TABLE test_btree AS SELECT generate_series(1,100000000) AS id;
SELECT 100000000
severalnines=# SET maintenance_work_mem = '1GB';
severalnines=# \timing
severalnines=# CREATE INDEX q ON test_btree (id);
TIME: 25294.185 ms (00:25.294)
Wypróbujmy to z 8-kierunkową pracą równoległą:
severalnines=# SET maintenance_work_mem = '2GB';
severalnines=# SET max_parallel_workers = 16;
severalnines=# SET max_parallel_maintenance_workers = 8;
severalnines=# \timing
severalnines=# CREATE INDEX q1 ON test_btree (id);
TIME: 11001.240 ms (00:11.001)
Widzimy różnicę w wydajności z pracownikiem równoległym, ponad 60% wydajności przy niewielkiej zmianie. Maintenance_work_mem można również zwiększyć, aby uzyskać większą wydajność.
Stół ALTER pomaga również w zwiększeniu liczby pracowników równoległych. Poniższa składnia może być użyta do zwiększenia liczby pracowników równoległych wraz z max_parallel_maintenance_workers. To całkowicie omija model kosztów.
ALTER TABLE test_btree SET (parallel_workers = 24);
Wskazówka:Zresetuj do wartości domyślnych po zakończeniu budowania indeksu, aby zapobiec niepożądanym planom zapytań.
CREATE INDEX z opcją CONCURRENTLY obsługuje budowanie równoległe bez specjalnych ograniczeń, tylko pierwsze skanowanie tabeli jest faktycznie wykonywane równolegle.
Dokładniejsze testy wydajności można znaleźć tutaj.
Dodaj blokowanie predykatów dla indeksów skrótu, Gist i Gin
PostgreSQL 11 jest dostarczany z obsługą blokowania predykatów dla indeksów mieszających, indeksów gin i indeksów gist. Dzięki temu izolacja transakcji SERIALIZABLE będzie znacznie bardziej wydajna dzięki tym indeksom.
Zaleta:blokowanie predykatu może zapewnić lepszą wydajność na możliwym do serializacji poziomie izolacji, zmniejszając liczbę fałszywych alarmów, co prowadzi do niepotrzebnych niepowodzeń serializacji.
W PostgreSQL 10 zakresem blokady jest relacja, ale w PostgreSQL 11 blokadą jest tylko strona.
Przetestujmy to.
severalnines=# CREATE TABLE sv_predicate_lock1(c1 INT, c2 VARCHAR(10)) ;
CREATE TABLE
severalnines=# CREATE INDEX idx1_sv_predicate_lock1 ON sv_predicate_lock1 USING 'hash(c1) ;
CREATE INDEX
severalnines=# INSERT INTO sv_predicate_lock1 VALUES (generate_series(1, 100000), 'puja') ;
INSERT 0 100000
severalnines=# BEGIN ISOLATION LEVEL SERIALIZABLE ;
BEGIN
severalnines=# SELECT * FROM sv_predicate_lock1 WHERE c1=10000 FOR UPDATE ;
c1 | c2
-------+-------
10000 | puja
(1 row)
Jak widać poniżej, blokada znajduje się na poziomie strony, a nie relacji. W PostgreSQL 10 był na poziomie relacji, więc jest to DUŻA ZWYCIĘSTWO dla jednoczesnych transakcji w PostgreSQL 11.
severalnines=# SELECT locktype, relation::regclass, mode FROM pg_locks ;
locktype | relation | mode
---------------+-------------------------+-----------------
relation | pg_locks | AccessShareLock
relation | idx1_sv_predicate_lock1 | AccessShareLock
relation | sv_predicate_lock1 | RowShareLock
virtualxid | | ExclusiveLock
transactionid | | ExclusiveLock
page | idx1_sv_predicate_lock1 | SIReadLock
tuple | sv_predicate_lock1 | SIReadLock
(7 rows)
Wskazówka:skanowanie sekwencyjne zawsze będzie wymagało blokady predykatu na poziomie relacji. Może to spowodować zwiększoną liczbę niepowodzeń serializacji. Pomocne może być zachęcanie do korzystania ze skanowania indeksu poprzez zmniejszenie random_page_cost i/lub zwiększenie cpu_tuple_cost.
Zezwalaj na GORĄCE aktualizacje niektórych indeksów wyrażeń
Funkcja Heap Only Tuple (HOT) eliminuje zbędne wpisy indeksu i umożliwia ponowne wykorzystanie miejsca zajmowanego przez DELETEd lub przestarzałe UPDATEd krotki bez wykonywania próżni w całej tabeli. Zmniejsza rozmiar indeksu, unikając tworzenia wpisów indeksu o identycznych kluczach.
Jeśli wartość wyrażenia indeksu pozostaje niezmieniona po UPDATE, zezwól na GORĄCE aktualizacje tam, gdzie wcześniej PostgreSQL ich nie zezwalał, co daje znaczny wzrost wydajności w tych przypadkach.
Jest to szczególnie przydatne w przypadku indeksów, takich jak JSON->>pole, w którym zmienia się wartość JSON, ale wartość indeksowana nie.
Ta funkcja została wycofana w wersji 11.1 ze względu na pogorszenie wydajności (tylko AT Free BSD według Simona), więcej szczegółów / testu porównawczego można znaleźć tutaj. Powinno to zostać naprawione w przyszłej wersji.
Zezwalaj na skanowanie całych stron indeksu z haszowaniem
Indeks mieszający:Planer zapytań rozważy użycie indeksu mieszającego za każdym razem, gdy indeksowana kolumna jest zaangażowana w porównanie przy użyciu operatora =. Nie był również bezpieczny w przypadku awarii (nie był zalogowany do WAL), więc musi zostać odbudowany po awarii bazy danych, a zmiany w hashu nie zostały zapisane przez replikację strumieniową.
W PostgreSQL 10 indeks skrótu był rejestrowany przez WAL, co oznacza, że jest bezpieczny dla CRASH i może być replikowany. Indeksy haszujące zajmują znacznie mniej miejsca w porównaniu do B-Tree, dzięki czemu mogą lepiej zmieścić się w pamięci.
W PostgreSQL 11 indeksy Btree mają optymalizację zwaną „próżnią pojedynczej strony”, która oportunistycznie usuwa martwe wskaźniki indeksu ze stron indeksu, zapobiegając ogromnemu rozrostowi indeksu, który w innym przypadku miałby miejsce. Ta sama logika została przeniesiona do indeksów skrótu. Przyspiesza recykling przestrzeni, zmniejszając wzdęcia.
STATYSTYKI indeksu funkcji
Możliwe jest teraz określenie wartości STATISTICS dla kolumny indeksu funkcji. Jest bardzo cenny dla wydajności specjalistycznej aplikacji. Możemy teraz zbierać statystyki w kolumnach z wyrażeniami, które pomogą planistom w podjęciu dokładniejszej decyzji.
severalnines=# CREATE INDEX idx1_stats ON stat ((s1 + s2)) ;
CREATE INDEX
severalnines=# ALTER INDEX idx1_stats ALTER COLUMN 1 SET STATISTICS 1000 ;
ALTER INDEX
severalnines=# \d+ idx1_stats
Index "public.idx1_stats"
Column | Type | Definition | Storage | Stats target
--------+---------+------------+---------+--------------
expr | numeric | (c1 + c2) | main | 1000
btree, for table "public.stat1"
sprawdzam
Dodano nowy moduł Contrib amcheck. Można sprawdzić tylko indeksy B-Tree.
Przetestujmy to!
severalnines=# CREATE EXTENSION amcheck ;
CREATE EXTENSION
severalnines=# SELECT bt_index_check('idx1_stats') ;
ERROR: invalid page in block 0 of relation base/16385/16580
severalnines=#CREATE INDEX idx1_hash_data1 ON data1 USING hash (c1) ;
CREATE INDEX
severalnines=# SELECT bt_index_check('idx1_hash_data1') ;
ERROR: only B-Tree indexes are supported as targets for verification
DETAIL: Relation "idx1_hash_data1" is not a B-Tree index.
Możliwy lokalny indeks partycjonowany
Przed PostgreSQL11 nie było możliwe utworzenie indeksu dla tabeli podrzędnej lub tabeli partycjonowanej.
W PostgreSQL 11, gdy CREATE INDEX jest uruchamiane na partycjonowanej tabeli / tabeli nadrzędnej, tworzy wpisy katalogu dla indeksu w partycjonowanej tabeli i kaskadowo tworzy rzeczywiste indeksy na istniejących partycjach. Utworzy je również w przyszłych partycjach.
Spróbujmy utworzyć tabelę nadrzędną i podzielić ją na partycje:
severalnines=# create table test_part ( a int, list varchar(5) ) partition by list (list);
CREATE TABLE
severalnines=# create table part_1 partition of test_part for values in ('India');
CREATE TABLE
severalnines=# create table part_2 partition of test_part for values in ('USA');
CREATE TABLE
severalnines=#
severalnines=# \d+ test_part
Table "public.test_part"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
a | integer | | | | plain | |
list | character varying(5) | | | | extended | |
Partition key: LIST (list)
Partitions: part_1 FOR VALUES IN ('India'),
part_2 FOR VALUES IN ('USA')
Spróbujmy utworzyć indeks w tabeli nadrzędnej:
severalnines=# create index i_test on test_part (a);
CREATE INDEX
severalnines=# \d part_2
Table "public.part_2"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('USA')
Indexes:
"part_2_a_idx" btree (a)
severalnines=# \d part_1
Table "public.part_1"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('India')
Indexes:
"part_1_a_idx" btree (a)
Indeks jest kaskadowany do wszystkich partycji w PostgreSQL 11, co jest naprawdę fajną funkcją.
Covering Index (w tym KLAUZULA dla indeksów)
Można określić klauzulę INCLUDE, aby dodać kolumny do indeksu. Jest to skuteczne w przypadku dodawania kolumn, które nie są związane z ograniczeniem przez unikalność unikatowego indeksu. Kolumny INCLUDE istnieją wyłącznie po to, aby umożliwić większej liczbie zapytań korzystanie ze skanowania tylko z indeksem. Na razie tylko indeksy B-drzewa obsługują klauzulę INCLUDE.
Sprawdźmy zachowanie bez INCLUDE. Nie użyje tylko skanowania indeksu, jeśli w polu SELECT pojawią się dodatkowe kolumny. Można to osiągnąć za pomocą klauzuli INCLUDE.
severalnines=# CREATE TABLE no_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO no_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX old_unique_idx ON no_include(a, b);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
EXPLAIN ANALYZE SELECT a, b FROM no_include WHERE a < 1000; - It will do index only scan
EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It will not do index only scan as we have extra column in select.
severalnines=# CREATE INDEX old_idx ON no_include (a, b, c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It did index only scan as index on all three columns.
QUERY PLAN
-------------------------------------------------
Index Only Scan using old_idx on no_include
(cost=0.42..14.92 rows=371 width=12)
(actual time=0.086..0.291 rows=334 loops=1)
Index Cond: (a < 1000)
Heap Fetches: 0
Planning Time: 2.108 ms
Execution Time: 0.396 ms
(5 rows)
Spróbujmy z klauzulą include. W poniższym przykładzie UNIQUE CONSTRAINT jest tworzone w kolumnach aib, ale indeks zawiera kolumnę c.
severalnines=# CREATE TABLE with_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO with_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM with_include WHERE a < 10000;
QUERY PLAN
-----------------------------------------------------
Index Only Scan using new_unique_idx on with_include
(cost=0.42..116.06 rows=3408 width=12)
(actual time=0.085..2.348 rows=3334 loops=1)
Index Cond: (a < 10000)
Heap Fetches: 0
Planning Time: 1.851 ms
Execution Time: 2.840 ms
(5 rows)
Kolumny z głównej listy kolumn nie mogą się pokrywać z tymi z listy uwzględnianych
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (a);
ERROR: 42P17: included columns must not intersect with key columns
LOCATION: DefineIndex, indexcmds.c:373
Kolumna używana z wyrażeniem na liście głównej działa:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(round(a), b) INCLUDE (a);
CREATE INDEX
Wyrażenia nie mogą być używane na liście dołączania, ponieważ nie mogą być używane w skanowaniu tylko indeksu:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(a, b) INCLUDE (round(c));
ERROR: 0A000: expressions are not supported in included columns
LOCATION: ComputeIndexAttrs, indexcmds.c:1446
Wniosek
Nowe funkcje PostgreSQL z pewnością poprawią żywotność administratorów baz danych, więc ma on stać się silną alternatywą w otwartej bazie danych. Rozumiem, że kilka funkcji indeksów jest obecnie ograniczonych do B-Tree, nadal jest to świetny początek ery wykonywania równoległych dla PostgreSQL i zmierza do ładnego narzędzia do dokładnego przyjrzenia się. Dzięki!