No mundo Postgres, os índices são essenciais para navegar com eficiência no armazenamento de dados de tabela (também conhecido como “heap”). O Postgres não mantém um clustering para o heap, e a arquitetura MVCC leva a várias versões da mesma tupla. Criar e manter índices eficazes e eficientes para dar suporte a aplicativos é uma habilidade essencial.
Continue lendo para conferir algumas dicas sobre como otimizar e melhorar o uso de índices em sua implantação.
Observação:as consultas mostradas abaixo são executadas em um banco de dados de amostra não modificado.
Usar índices de cobertura
Considere uma consulta para buscar os e-mails de todos os clientes inativos. O cliente tabela tem um ativo coluna, e a consulta é direta:
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)
A consulta pede uma varredura sequencial completa da tabela de clientes. Vamos criar um índice na coluna ativa:
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)
Isso ajuda, e a varredura sequencial se tornou uma “varredura de índice”. Isso significa que o Postgres verificará o índice “idx_cust1” e, em seguida, fará uma pesquisa adicional na pilha da tabela para ler os outros valores da coluna (neste caso, o email coluna) que a consulta precisa.
O PostgreSQL 11 introduziu a cobertura de índices. Esse recurso permite incluir uma ou mais colunas adicionais no próprio índice – ou seja, os valores dessas colunas extras são armazenados no armazenamento de dados do índice.
Se usarmos esse recurso e incluirmos o valor de email dentro do índice, o Postgres não precisará examinar o heap da tabela para obter o valor deemail . Vamos ver se isso 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)
O “Index Only Scan” nos diz que a consulta agora está completamente satisfeita pelo próprio índice, evitando potencialmente toda a E/S de disco para ler o heap da tabela.
Índices de cobertura estão disponíveis apenas para índices B-Tree a partir de agora. Além disso, o custo de manutenção de um índice de cobertura é naturalmente maior do que um índice regular.
Usar índices parciais
Índices parciais indexam apenas um subconjunto das linhas em uma tabela. Isso mantém os índices menores em tamanho e mais rápido para varrer.
Suponha que precisamos obter a lista de e-mails de clientes localizados na Califórnia. A consulta é:
SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';
que tem um plano de consulta que envolve a verificação de ambas as tabelas que são 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)
Vamos ver o que um índice regular nos traz:
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)
A verificação do endereço foi substituído por uma verificação de índice em idx_address1 , e uma varredura do heap do endereço.
Assumindo que esta é uma consulta frequente e precisa ser otimizada, podemos usar o índice parcial que indexa apenas as linhas de endereço onde o distrito é ‘Califórnia’:
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)
A consulta agora lê apenas o índice idx_address2 e não toca na mesaendereço .
Usar índices de vários valores
Algumas colunas que precisam de indexação podem não ter um tipo de dados escalar. Tipos de coluna como jsonb , matrizes e tsvector têm valores compostos ou múltiplos. Se você precisar indexar essas colunas, geralmente é necessário pesquisar os valores individuais nessas colunas também.
Vamos tentar encontrar todos os títulos de filmes que incluem cenas de bastidores. Ofilme tabela tem uma coluna de matriz de texto chamada special_features , que inclui o elemento de matriz de texto Behind The Scenes se um filme tem esse recurso. Para encontrar todos esses filmes, precisamos selecionar todas as linhas que tenham “Behind The Scenes” emqualquer dos valores do array special_features :
SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';
O operador de contenção @> verifica se o lado esquerdo é um superconjunto do lado direito.
Aqui está o plano 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)
que exige uma varredura completa do heap, a um custo de 67.
Vamos ver se um índice B-Tree regular ajuda:
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)
O índice nem é considerado. O índice B-Tree não faz ideia de que existem elementos individuais no valor que ele indexou.
O que precisamos é de um í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)
O índice GIN é capaz de suportar a correspondência do valor individual com o valor composto indexado, resultando em um plano de consulta com menos da metade do custo do original.
Eliminar índices duplicados
Com o tempo, os índices se acumulam e, às vezes, é adicionado um que tem exatamente a mesma definição de outro. Você pode usar a visualização de catálogo
pg_indexes
para obter as definições de índices SQL legíveis por humanos. Você também pode detectar facilmente definições idênticas: SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
FROM pg_indexes
GROUP BY defn
HAVING count(*) > 1;
E aqui está o resultado quando executado no banco de dados do 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
Também é possível que você acabe com vários índices em que um indexa um superconjunto de colunas que o outro faz. Isso pode ou não ser desejável – o superconjunto pode resultar em varreduras somente de índice, o que é uma coisa boa, mas pode ocupar muito espaço, ou talvez a consulta que foi originalmente planejada para otimizar não seja mais usada.
Se você deseja automatizar a detecção de tais índices, o pg_catalog tablepg_index é um bom ponto de partida.
Índices não usados
À medida que os aplicativos que usam o banco de dados evoluem, também evoluem as consultas que eles usam. Índices que foram adicionados anteriormente não podem mais ser usados por nenhuma consulta. Toda vez que um índice é verificado, ele é observado pelo gerenciador de estatísticas e a contagem acumulativa está disponível na visualização do catálogo do sistema
pg_stat_user_indexes
como o valor idx_scan
. Monitorar esse valor por um período de tempo (digamos, um mês) dá uma boa ideia de quais índices não são usados e podem ser removidos. Aqui está a consulta para obter as contagens de varredura atuais para todos os índices no esquema 'público':
SELECT relname, indexrelname, idx_scan
FROM pg_catalog.pg_stat_user_indexes
WHERE schemaname = 'public';
com saída assim:
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 com menos bloqueio
Não é incomum que os índices precisem ser recriados. Os índices também podem ficar inchados, e a recriação do índice pode corrigir isso, fazendo com que ele se torne mais rápido na verificação. Os índices também podem ficar corrompidos. A alteração dos parâmetros do índice também pode exigir a recriação do índice.
Ativar criação de índice paralelo
No PostgreSQL 11, a criação do índice B-Tree é simultânea. Ele pode usar vários trabalhadores paralelos para acelerar a criação do índice. No entanto, você precisa ter certeza de que essas entradas de configuração estão definidas adequadamente:
SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;
Os valores padrão são excessivamente pequenos. Idealmente, esses números devem aumentar com o número de núcleos de CPU. Consulte os documentos para obter mais informações.
Criar índices em segundo plano
Você também pode criar um índice em segundo plano, usando o CONCORRENTEMENTE parâmetro do CREATE INDEX comando:
pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX
Isso é diferente de fazer um índice de criação regular, pois não requer um bloqueio sobre a tabela e, portanto, não bloqueia gravações. No lado negativo, leva mais tempo e recursos para ser concluído.