PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Holen Sie das Beste aus Ihren PostgreSQL-Indizes heraus

In der Postgres-Welt sind Indizes unerlässlich, um effizient durch den Tabellendatenspeicher (auch bekannt als „Heap“) zu navigieren. Postgres verwaltet kein Clustering für den Heap, und die MVCC-Architektur führt dazu, dass mehrere Versionen desselben Tupels herumliegen. Das Erstellen und Pflegen effektiver und effizienter Indizes zur Unterstützung von Anwendungen ist eine grundlegende Fähigkeit.

Lesen Sie weiter, um einige Tipps zur Optimierung und Verbesserung der Verwendung von Indizes in Ihrer Bereitstellung zu erhalten.

Hinweis:Die unten gezeigten Abfragen werden auf einer unmodifizierten Beispieldatenbank von Pagil ausgeführt.

Abdeckende Indizes verwenden

Betrachten Sie eine Abfrage, um die E-Mails aller inaktiven Kunden abzurufen. Der Kunde Tabelle hat eine aktive Spalte, und die Abfrage ist unkompliziert:

pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                        QUERY PLAN
-----------------------------------------------------------
 Seq Scan on customer  (cost=0.00..16.49 rows=15 width=32)
   Filter: (active = 0)
(2 rows)

Die Abfrage erfordert einen vollständigen sequenziellen Scan der Kundentabelle. Lassen Sie uns einen Index für die aktive Spalte erstellen:

pagila=# CREATE INDEX idx_cust1 ON customer(active);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                                 QUERY PLAN
-----------------------------------------------------------------------------
 Index Scan using idx_cust1 on customer  (cost=0.28..12.29 rows=15 width=32)
   Index Cond: (active = 0)
(2 rows)

Das hilft, und aus dem sequentiellen Scan ist ein „Index-Scan“ geworden. Dies bedeutet, dass Postgres den Index „idx_cust1“ scannt und dann den Tabellenspeicher weiter durchsucht, um die anderen Spaltenwerte zu lesen (in diesem Fall die E-Mail Spalte), die die Abfrage benötigt.

PostgreSQL 11 führte abdeckende Indizes ein. Mit dieser Funktion können Sie eine oder mehrere zusätzliche Spalten in den Index selbst aufnehmen – das heißt, die Werte dieser zusätzlichen Spalten werden im Indexdatenspeicher gespeichert.

Wenn wir diese Funktion verwenden und den Wert von email in den Index aufnehmen, muss Postgres nicht in den Heap der Tabelle schauen, um den Wert vonemail abzurufen . Mal sehen, ob das funktioniert:

pagila=# CREATE INDEX idx_cust2 ON customer(active) INCLUDE (email);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Only Scan using idx_cust2 on customer  (cost=0.28..12.29 rows=15 width=32)
   Index Cond: (active = 0)
(2 rows)

Der „Index Only Scan“ teilt uns mit, dass die Abfrage jetzt vollständig vom Index selbst erfüllt wird, wodurch möglicherweise die gesamte Festplatten-E/A zum Lesen des Heaps der Tabelle vermieden wird.

Überdeckende Indizes sind ab sofort nur für B-Tree-Indizes verfügbar. Außerdem sind die Kosten für die Aufrechterhaltung eines abdeckenden Indexes natürlich höher als bei einem regulären.

Teilindizes verwenden

Teilindizes indizieren nur eine Teilmenge der Zeilen in einer Tabelle. Dadurch bleiben die Indizes kleiner und können schneller durchsucht werden.

Angenommen, wir benötigen die Liste der E-Mail-Adressen von Kunden in Kalifornien. Die Abfrage lautet:

SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';

die einen Abfrageplan hat, der das Scannen beider verknüpfter Tabellen beinhaltet:

pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                              QUERY PLAN
----------------------------------------------------------------------
 Hash Join  (cost=15.65..32.22 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=15.54..15.54 rows=9 width=4)
         ->  Seq Scan on address a  (cost=0.00..15.54 rows=9 width=4)
               Filter: (district = 'California'::text)
(6 rows)

Mal sehen, was uns ein regulärer Index bringt:

pagila=# CREATE INDEX idx_address1 ON address(district);
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                                      QUERY PLAN
---------------------------------------------------------------------------------------
 Hash Join  (cost=12.98..29.55 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=12.87..12.87 rows=9 width=4)
         ->  Bitmap Heap Scan on address a  (cost=4.34..12.87 rows=9 width=4)
               Recheck Cond: (district = 'California'::text)
               ->  Bitmap Index Scan on idx_address1  (cost=0.00..4.34 rows=9 width=0)
                     Index Cond: (district = 'California'::text)
(8 rows)

Der Scan von Adresse wurde durch einen Index-Scan über idx_address1 ersetzt , und ein Scan des Heaps der Adresse.

Unter der Annahme, dass dies eine häufige Abfrage ist und optimiert werden muss, können wir einen Teilindex verwenden, der nur die Adresszeilen indiziert, in denen der Bezirk „Kalifornien“ ist:

pagila=# CREATE INDEX idx_address2 ON address(address_id) WHERE district='California';
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 Hash Join  (cost=12.38..28.96 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=12.27..12.27 rows=9 width=4)
         ->  Index Only Scan using idx_address2 on address a  (cost=0.14..12.27 rows=9 width=4)
(5 rows)

Die Abfrage liest jetzt nur noch den Index idx_address2 und berührt nicht die TischAdresse .

Mehrwertige Indizes verwenden

Einige Spalten, die indiziert werden müssen, haben möglicherweise keinen skalaren Datentyp. Spaltentypen wie jsonb , Arrays und tsvector haben zusammengesetzte oder mehrere Werte. Wenn Sie solche Spalten indizieren müssen, müssen Sie in der Regel auch die einzelnen Werte in diesen Spalten durchsuchen.

Versuchen wir, alle Filmtitel zu finden, die Outtakes hinter den Kulissen enthalten. Der Film Tabelle hat eine Text-Array-Spalte namens special_features , das das Text-Array-Element Behind The Scenes enthält wenn ein Film diese Funktion hat. Um alle diese Filme zu finden, müssen wir alle Zeilen auswählen, die „Hinter den Kulissen“ in irgendeinem enthalten der Werte des Arrays special_features :

SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';

Der Containment-Operator @> prüft, ob die linke Seite eine Obermenge der rechten Seite ist.

Hier ist der Abfrageplan:

pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                           QUERY PLAN
-----------------------------------------------------------------
 Seq Scan on film  (cost=0.00..67.50 rows=5 width=15)
   Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)

was einen vollständigen Scan des Heaps zu einem Preis von 67.

erfordert

Mal sehen, ob ein normaler B-Tree-Index hilft:

pagila=# CREATE INDEX idx_film1 ON film(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                           QUERY PLAN
-----------------------------------------------------------------
 Seq Scan on film  (cost=0.00..67.50 rows=5 width=15)
   Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)

Der Index wird nicht einmal berücksichtigt. Der B-Tree-Index hat keine Ahnung, dass der indexierte Wert einzelne Elemente enthält.

Was wir brauchen, ist ein GIN-Index.

pagila=# CREATE INDEX idx_film2 ON film USING GIN(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                                QUERY PLAN
---------------------------------------------------------------------------
 Bitmap Heap Scan on film  (cost=8.04..23.58 rows=5 width=15)
   Recheck Cond: (special_features @> '{"Behind The Scenes"}'::text[])
   ->  Bitmap Index Scan on idx_film2  (cost=0.00..8.04 rows=5 width=0)
         Index Cond: (special_features @> '{"Behind The Scenes"}'::text[])
(4 rows)

Der GIN-Index kann den Abgleich des einzelnen Werts mit dem indizierten zusammengesetzten Wert unterstützen, was zu einem Abfrageplan führt, der weniger als die Hälfte der Kosten des Originals kostet.

Doppelte Indizes beseitigen

Im Laufe der Zeit sammeln sich Indizes an, und manchmal wird einer hinzugefügt, der genau dieselbe Definition wie ein anderer hat. Sie können die Katalogansicht pg_indexes verwenden um die für Menschen lesbaren SQL-Definitionen von Indizes zu erhalten. Sie können auch leicht identische Definitionen erkennen:

  SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
    FROM pg_indexes
GROUP BY defn
  HAVING count(*) > 1;

Und hier ist das Ergebnis, wenn es auf der Stock-Pagila-Datenbank ausgeführt wird:

pagila=#   SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
pagila-#     FROM pg_indexes
pagila-# GROUP BY defn
pagila-#   HAVING count(*) > 1;
                                indexes                                 |                                defn
------------------------------------------------------------------------+------------------------------------------------------------------
 {payment_p2017_01_customer_id_idx,idx_fk_payment_p2017_01_customer_id} | CREATE INDEX  ON public.payment_p2017_01 USING btree (customer_id
 {payment_p2017_02_customer_id_idx,idx_fk_payment_p2017_02_customer_id} | CREATE INDEX  ON public.payment_p2017_02 USING btree (customer_id
 {payment_p2017_03_customer_id_idx,idx_fk_payment_p2017_03_customer_id} | CREATE INDEX  ON public.payment_p2017_03 USING btree (customer_id
 {idx_fk_payment_p2017_04_customer_id,payment_p2017_04_customer_id_idx} | CREATE INDEX  ON public.payment_p2017_04 USING btree (customer_id
 {payment_p2017_05_customer_id_idx,idx_fk_payment_p2017_05_customer_id} | CREATE INDEX  ON public.payment_p2017_05 USING btree (customer_id
 {idx_fk_payment_p2017_06_customer_id,payment_p2017_06_customer_id_idx} | CREATE INDEX  ON public.payment_p2017_06 USING btree (customer_id
(6 rows)

Superset-Indizes

Es ist auch möglich, dass Sie am Ende mehrere Indizes haben, bei denen einer eine Obermenge von Spalten indiziert, die der andere tut. Dies kann wünschenswert sein oder auch nicht – die Obermenge kann zu Index-Only-Scans führen, was eine gute Sache ist, aber möglicherweise zu viel Platz in Anspruch nimmt, oder vielleicht wird die Abfrage, die ursprünglich optimiert werden sollte, nicht mehr verwendet.

Wenn Sie die Erkennung solcher Indizes automatisieren möchten, ist die pg_catalog-Tabellepg_index ein guter Ausgangspunkt.

Nicht verwendete Indizes

Mit der Weiterentwicklung der Anwendungen, die die Datenbank verwenden, entwickeln sich auch die Abfragen, die sie verwenden. Zuvor hinzugefügte Indizes können von keiner Abfrage mehr verwendet werden. Jedes Mal, wenn ein Index gescannt wird, wird dies vom Statistikmanager vermerkt und die kumulierte Anzahl ist in der Systemkatalogansicht pg_stat_user_indexes verfügbar als Wert idx_scan . Wenn Sie diesen Wert über einen bestimmten Zeitraum (z. B. einen Monat) überwachen, erhalten Sie eine gute Vorstellung davon, welche Indizes nicht verwendet werden und entfernt werden können.

Hier ist die Abfrage, um die aktuellen Scan-Zähler für alle Indizes im „öffentlichen“ Schema zu erhalten:

SELECT relname, indexrelname, idx_scan
FROM   pg_catalog.pg_stat_user_indexes
WHERE  schemaname = 'public';

mit Ausgabe wie dieser:

pagila=# SELECT relname, indexrelname, idx_scan
pagila-# FROM   pg_catalog.pg_stat_user_indexes
pagila-# WHERE  schemaname = 'public'
pagila-# LIMIT  10;
    relname    |    indexrelname    | idx_scan
---------------+--------------------+----------
 customer      | customer_pkey      |    32093
 actor         | actor_pkey         |     5462
 address       | address_pkey       |      660
 category      | category_pkey      |     1000
 city          | city_pkey          |      609
 country       | country_pkey       |      604
 film_actor    | film_actor_pkey    |        0
 film_category | film_category_pkey |        0
 film          | film_pkey          |    11043
 inventory     | inventory_pkey     |    16048
(10 rows)

Indizes mit weniger Sperren neu erstellen

Es ist nicht ungewöhnlich, dass Indizes neu erstellt werden müssen. Indizes können auch aufgebläht werden, und die Neuerstellung des Index kann das beheben, was dazu führt, dass er schneller zu scannen ist. Indizes können auch beschädigt werden. Das Ändern von Indexparametern kann auch eine Neuerstellung des Index erfordern.

Paralell-Indexerstellung aktivieren

In PostgreSQL 11 erfolgt die B-Tree-Indexerstellung gleichzeitig. Es kann mehrere parallele Worker verwenden, um die Indexerstellung zu beschleunigen. Allerdings müssen Sie sicherstellen, dass diese Konfigurationseinträge passend gesetzt sind:

SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;

Die Standardwerte sind unangemessen klein. Idealerweise sollten diese Zahlen mit der Anzahl der CPU-Kerne steigen. Weitere Informationen finden Sie in der Dokumentation.

Indizes im Hintergrund erstellen

Sie können einen Index auch im Hintergrund erstellen, indem Sie GLEICHZEITIG verwenden Parameter des CREATE INDEX Befehl:

pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX

Dies unterscheidet sich von einem normalen Create-Index dadurch, dass es keine Sperre für die Tabelle erfordert und daher keine Schreibvorgänge sperrt. Auf der anderen Seite erfordert die Fertigstellung mehr Zeit und Ressourcen.