sql >> Base de Datos >  >> RDS >> PostgreSQL

Cómo aprovechar al máximo sus índices de PostgreSQL

En el mundo de Postgres, los índices son esenciales para navegar de manera eficiente en el almacenamiento de datos de la tabla (también conocido como el "montón"). Postgres no mantiene un agrupamiento para el montón, y la arquitectura MVCC conduce a múltiples versiones de la misma tupla. Crear y mantener índices efectivos y eficientes para admitir aplicaciones es una habilidad esencial.

Siga leyendo para ver algunos consejos sobre cómo optimizar y mejorar el uso de índices en su implementación.

Nota:las consultas que se muestran a continuación se ejecutan en una base de datos de muestra de Pagila sin modificar.

Usar índices de cobertura

Considere una consulta para obtener los correos electrónicos de todos los clientes inactivos. El cliente la tabla tiene un activo columna, y la consulta es directa:

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)

La consulta requiere un escaneo secuencial completo de la tabla de clientes. Vamos a crear un índice en la columna activa:

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)

Esto ayuda, y el escaneo secuencial se ha convertido en un "escaneo de índice". Esto significa que Postgres escaneará el índice "idx_cust1", y luego buscará más en la tabla para leer los otros valores de columna (en este caso, el correo electrónico columna) que necesita la consulta.

PostgreSQL 11 introdujo índices de cobertura. Esta función le permite incluir una o más columnas adicionales en el propio índice, es decir, los valores de estas columnas adicionales se almacenan en el almacenamiento de datos del índice.

Si usáramos esta característica e incluyéramos el valor de email dentro del índice, Postgres no necesitará buscar en el montón de la tabla para obtener el valor de email . Veamos si esto funciona:

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)

El "Escaneo de solo índice" nos dice que la consulta ahora está completamente satisfecha por el propio índice, lo que potencialmente evita toda la E/S del disco para leer el montón de la tabla.

Los índices de cobertura solo están disponibles para los índices B-Tree a partir de ahora. Además, el costo de mantener un índice de cobertura es naturalmente más alto que uno regular.

Usar índices parciales

Los índices parciales solo indexan un subconjunto de las filas de una tabla. Esto mantiene los índices más pequeños en tamaño y más rápidos para escanear.

Supongamos que necesitamos obtener la lista de correos electrónicos de clientes ubicados en California. La consulta es:

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

que tiene un plan de consulta que implica escanear ambas tablas que están unidas:

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)

Veamos qué nos ofrece un índice regular:

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)

El escaneo de dirección se ha reemplazado con un escaneo de índice sobre idx_address1 y un escaneo del montón de direcciones.

Suponiendo que esta es una consulta frecuente y debe optimizarse, podemos usar un índice parcial que solo indexa aquellas filas de direcciones donde el distrito es 'California':

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)

La consulta ahora solo lee el índice idx_address2 y no toca la mesadirección .

Usar índices de valores múltiples

Algunas columnas que necesitan indexación pueden no tener un tipo de datos escalares. Tipos de columna como jsonb , matrices y tsvector tienen valores compuestos o múltiples. Si necesita indexar tales columnas, normalmente también necesita buscar entre los valores individuales de esas columnas.

Tratemos de encontrar todos los títulos de películas que incluyen tomas descartadas detrás de escena. La película la tabla tiene una columna de matriz de texto llamada special_features , que incluye el elemento de matriz de texto Behind The Scenes si una película tiene esa característica. Para encontrar todas esas películas, debemos seleccionar todas las filas que tienen "Detrás de escena" en cualquier de los valores de la matriz special_features :

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

El operador de contención @> comprueba si el lado izquierdo es un superconjunto del lado derecho.

Aquí está el plan de consulta:

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)

lo que requiere un escaneo completo del montón, a un costo de 67.

Veamos si un índice B-Tree regular ayuda:

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)

El índice ni siquiera se considera. El índice B-Tree no tiene idea de que hay elementos individuales en el valor que indexa.

Lo que necesitamos es un índice GIN.

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)

El índice GIN puede admitir la comparación del valor individual con el valor compuesto indexado, lo que da como resultado un plan de consulta con menos de la mitad del costo del original.

Eliminar índices duplicados

Con el tiempo, los índices se acumulan y, a veces, se agrega uno que tiene exactamente la misma definición que otro. Puede usar la vista de catálogo pg_indexes toget las definiciones SQL legibles por humanos de los índices. También puede detectar fácilmente definiciones idénticas:

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

Y aquí está el resultado cuando se ejecuta en la base de datos de stock pagila:

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)

Índices de superconjunto

También es posible que termine con múltiples índices donde uno indexa un superconjunto de columnas que el otro hace. Esto puede o no ser deseable:el superconjunto puede dar como resultado escaneos de solo índice, lo cual es bueno, pero puede ocupar demasiado espacio, o tal vez la consulta que originalmente se pretendía optimizar ya no se usa.

Si desea automatizar la detección de dichos índices, la tabla pg_catalog pg_index es un buen punto de partida.

Índices no utilizados

A medida que evolucionan las aplicaciones que utilizan la base de datos, también lo hacen las consultas que utilizan. Los índices que se agregaron anteriormente ya no pueden ser utilizados por ninguna consulta. Cada vez que se escanea un índice, el administrador de estadísticas lo anota y el recuento acumulativo está disponible en la vista del catálogo del sistema pg_stat_user_indexes como el valor idx_scan . Supervisar este valor durante un período de tiempo (digamos, un mes) da una buena idea de qué índices no se utilizan y se pueden eliminar.

Aquí está la consulta para obtener los recuentos de escaneo actuales para todos los índices en el esquema 'público':

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

con salida como esta:

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)

Reconstruir índices con menos bloqueo

No es raro que los índices deban volver a crearse. Los índices también pueden hincharse, y recrear el índice puede arreglar eso, haciendo que sea más rápido para escanear. Los índices también pueden corromperse. Alterar los parámetros del índice también puede requerir la recreación del índice.

Habilitar creación de índice paralelo

En PostgreSQL 11, la creación del índice B-Tree es simultánea. Puede hacer uso de múltiples trabajadores paralelos para acelerar la creación del índice. Sin embargo, debe asegurarse de que estas entradas de configuración estén configuradas adecuadamente:

SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;

Los valores predeterminados son excesivamente pequeños. Idealmente, estos números deberían aumentar con el número de núcleos de CPU. Consulte los documentos para obtener más información.

Crear índices en segundo plano

También puede crear un índice en segundo plano, utilizando el CONCURRENTLY parámetro de CREATE INDEX comando:

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

Esto es diferente de hacer un índice de creación regular en que no requiere un bloqueo sobre la tabla y, por lo tanto, no bloquea las escrituras. En el lado negativo, lleva más tiempo y recursos completarlo.