PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Sobre a utilidade dos índices de expressão


Ao ministrar treinamentos do PostgreSQL, tanto em tópicos básicos quanto avançados, muitas vezes descubro que os participantes têm muito pouca ideia de quão poderosos podem ser os índices de expressão (se eles estão cientes deles). Então deixe-me dar-lhe uma breve visão geral.

Então, digamos que temos uma tabela, com um intervalo de timestamps (sim, temos a função generate_series que pode gerar datas):
CREATE TABLE t AS
SELECT d, repeat(md5(d::text), 10) AS padding
  FROM generate_series(timestamp '1900-01-01',
                       timestamp '2100-01-01',
                       interval '1 day') s(d);
VACUUM ANALYZE t;

A mesa também inclui uma coluna de preenchimento, para torná-la um pouco maior. Agora, vamos fazer uma consulta de intervalo simples, selecionando apenas um mês dos ~200 anos incluídos na tabela. Se você explicar na consulta, verá algo assim:
EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01';

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=32 width=332)
   Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
        AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

e no meu laptop, isso é executado em ~ 20ms. Nada mal, considerando que isso precisa percorrer toda a tabela com ~ 75k linhas.

Mas vamos criar um índice na coluna timestamp (todos os índices aqui são do tipo padrão, ou seja, btree, a menos que mencionado explicitamente):
CREATE INDEX idx_t_d ON t (d);

E agora vamos tentar executar a consulta novamente:
                               QUERY PLAN
------------------------------------------------------------------------
 Index Scan using idx_t_d on t  (cost=0.29..9.97 rows=34 width=332)
   Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
            AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

e isso é executado em 0,5 ms, aproximadamente 40 vezes mais rápido. Mas é claro que isso era um índice simples, criado diretamente na coluna, não um índice de expressão. Então, vamos supor que precisamos selecionar dados de cada primeiro dia de cada mês, fazendo uma consulta como esta
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

que, no entanto, não pode usar o índice, pois precisa avaliar uma expressão na coluna enquanto o índice é construído na própria coluna, conforme mostrado em EXPLAIN ANALYZE:
                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
   Filter: (date_part('day'::text, d) = '1'::double precision)
   Rows Removed by Filter: 70649
 Planning time: 0.209 ms
 Execution time: 43.018 ms
(5 rows)

Então não só isso tem que fazer uma varredura seqüencial, mas também tem que fazer a avaliação, aumentando a duração da consulta para 43ms.

O banco de dados não pode usar o índice por vários motivos. Índices (pelo menos índices btree) dependem da consulta de dados classificados, fornecidos pela estrutura em forma de árvore, e enquanto a consulta de intervalo pode se beneficiar disso, a segunda consulta (com chamada `extract`) não pode.

Nota:Outro problema é que o conjunto de operadores suportados por índices (ou seja, que podem ser avaliados diretamente em índices) é muito limitado. E a função "extrair" não é suportada, portanto, a consulta não pode contornar o problema de ordenação usando uma varredura de índice de bitmap.

Em teoria, o banco de dados pode tentar transformar a condição em condições de intervalo, mas isso é extremamente difícil e específico para expressão. Nesse caso, teríamos que gerar um número infinito desses intervalos “por dia”, porque o planejador não conhece realmente os timestamps min/max na tabela. Portanto, o banco de dados nem tenta.

Mas enquanto o banco de dados não sabe como transformar as condições, os desenvolvedores geralmente sabem. Por exemplo, com condições como
(column + 1) >= 1000

não é difícil reescrever assim
column >= (1000 - 1)

que funciona muito bem com os índices.

Mas e se tal transformação não for possível, como por exemplo para a consulta de exemplo
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

Nesse caso, o desenvolvedor teria que enfrentar o mesmo problema com min/max desconhecido para a coluna d e, mesmo assim, geraria muitos intervalos.

Bem, este post do blog é sobre índices de expressão, e até agora usamos apenas índices regulares, construídos diretamente na coluna. Então, vamos criar o primeiro índice de expressão:
CREATE INDEX idx_t_expr ON t ((extract(day FROM d)));
ANALYZE t;

que então nos dá este plano de explicação
                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
   Recheck Cond: (date_part('day'::text, d) = '1'::double precision)
   Heap Blocks: exact=2401
   ->  Bitmap Index Scan on idx_t_expr  (cost=0.00..46.73 rows=2459 width=0)
                                (actual time=1.243..1.243 rows=2401 loops=1)
         Index Cond: (date_part('day'::text, d) = '1'::double precision)
 Planning time: 0.374 ms
 Execution time: 17.136 ms
(7 rows)

Portanto, embora isso não nos dê a mesma aceleração de 40x que o índice no primeiro exemplo, isso é meio esperado, pois essa consulta retorna muito mais tuplas (2401 vs. 32). Além disso, eles estão espalhados por toda a tabela e não tão localizados como no primeiro exemplo. Portanto, é uma boa aceleração de 2x e, em muitos casos do mundo real, você verá melhorias muito maiores.

Mas a capacidade de usar índices para condições com expressões complexas não é a informação mais interessante aqui – essa é a razão pela qual as pessoas criam índices de expressão. Mas esse não é o único benefício.

Se você observar os dois planos de explicação apresentados acima (sem e com o índice de expressão), poderá notar isso:
                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
 ...
                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
 ...

Certo – a criação do índice de expressão melhorou significativamente as estimativas. Sem o índice só temos estatísticas (MCV + histograma) para as colunas da tabela bruta, então o banco de dados não sabe estimar a expressão
EXTRACT(day FROM d) = 1

Então, em vez disso, aplica uma estimativa padrão para condições de igualdade, que é 0,5% de todas as linhas – como a tabela tem 73.050 linhas, acabamos com uma estimativa de apenas 365 linhas. É comum ver erros de estimativa muito piores em aplicativos do mundo real.

Com o índice, no entanto, o banco de dados também coletou estatísticas nas colunas do índice e, nesse caso, a coluna contém os resultados da expressão. E durante o planejamento, o otimizador percebe isso e produz uma estimativa muito melhor.

Esse é um grande benefício e pode ajudar a corrigir alguns casos de planos de consulta ruins causados ​​por estimativas imprecisas. No entanto, a maioria das pessoas desconhece essa ferramenta útil.

E a utilidade dessa ferramenta só aumentou com a introdução do tipo de dados JSONB na versão 9.4, porque é a única maneira de coletar estatísticas sobre o conteúdo dos documentos JSONB.

Ao indexar documentos JSONB, existem duas estratégias básicas de indexação. Você pode criar um índice GIN/GiST em todo o documento, por exemplo. assim
CREATE INDEX ON t USING GIN (jsonb_column);

que permite consultar caminhos arbitrários na coluna JSONB, usar o operador de contenção para combinar subdocumentos, etc. Isso é ótimo, mas você ainda tem apenas as estatísticas básicas por coluna, que
não são muito úteis como os documentos são tratados como valores escalares (e ninguém corresponde a documentos inteiros ou usa um intervalo de documentos).

Índices de expressão, por exemplo, criados assim:
CREATE INDEX ON t ((jsonb_column->'id'));

será útil apenas para a expressão específica, ou seja, este índice recém-criado será útil para
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

mas não para consultas que acessam outras chaves JSON, como 'valor', por exemplo
SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';

Isso não quer dizer que os índices GIN/GiST em todo o documento sejam inúteis, mas você tem que escolher. Ou você cria um índice de expressão focado, útil ao consultar uma chave específica e com o benefício adicional de estatísticas sobre a expressão. Ou você cria um índice GIN/GiST em todo o documento, capaz de lidar com consultas em chaves arbitrárias, mas sem as estatísticas.

No entanto, você pode comer um bolo e comê-lo também, neste caso, porque você pode criar os dois índices ao mesmo tempo, e o banco de dados escolherá qual deles usar para consultas individuais. E você terá estatísticas precisas, graças aos índices de expressão.

Infelizmente, você não pode comer o bolo inteiro, porque os índices de expressão e os índices GIN/GiST usam condições diferentes
-- expression (btree)
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

-- GIN/GiST
SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';

para que o planejador não possa usá-los ao mesmo tempo – índices de expressão para estimativa e GIN/GiST para execução.