Trabalho de índices no PostgreSQL

73

Eu tenho algumas perguntas sobre o trabalho de índices no PostgreSQL. Eu tenho uma Friendstabela com o seguinte índice:

   Friends ( user_id1 ,user_id2) 

user_id1e user_id2são chaves estrangeiras para a usertabela

  1. São equivalentes? Se não, então por quê?

    Index(user_id1,user_id2) and Index(user_id2,user_id1)
  2. Se eu criar a Chave Primária (user_id1, user_id2), ele criará automaticamente índices para ela e

    Se os índices na primeira pergunta não forem equivalentes, qual índice será criado no comando acima da chave primária?

codecool
fonte

Respostas:

77

Aqui estão os resultados da consulta de uma tabela na segunda coluna de um índice de várias colunas .
Os efeitos são fáceis de reproduzir para qualquer pessoa. Apenas tente em casa.

Eu testei com o PostgreSQL 9.0.5 no Debian usando uma tabela de tamanho médio de um banco de dados da vida real com 23322 linhas. Ele implementa o relacionamento n: m entre as tabelas adr(endereço) e att(atributo), mas isso não é relevante aqui. Esquema simplificado:

CREATE TABLE adratt (
  adratt_id serial PRIMARY KEY
, adr_id    integer NOT NULL
, att_id    integer NOT NULL
, log_up    timestamp(0) NOT NULL DEFAULT (now())::timestamp(0)
, CONSTRAINT adratt_uni UNIQUE (adr_id, att_id)
);

A UNIQUErestrição efetivamente implementa um índice exclusivo. Repeti o teste com um índice simples para ter certeza e obtive resultados idênticos aos esperados.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

A tabela está agrupada no adratt_uniíndice e antes do teste eu executei:

CLUSTER adratt;
ANALYZE adratt;

As verificações sequenciais para consultas (adr_id, att_id)são o mais rápido possível. O índice de várias colunas ainda será usado para uma condição de consulta apenas na segunda coluna do índice.

Fiz as consultas algumas vezes para preencher o cache e selecionei a melhor dentre dez execuções para obter resultados comparáveis.

1. Consulta usando as duas colunas

SELECT *
FROM   adratt
WHERE  att_id = 90
AND    adr_id = 10;

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
(1 row)

Saída de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..3.48 rows=1 width=20) (actual time=0.022..0.025 rows=1 loops=1)
  Index Cond: ((adr_id = 10) AND (att_id = 90))
Total runtime: 0.067 ms

2. Consulta usando a primeira coluna

SELECT * FROM adratt WHERE adr_id = 10

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       126 |     10 |     10 | 2008-07-29 09:35:54
       125 |     10 |     13 | 2008-07-29 09:35:54
      4711 |     10 |     21 | 2008-07-29 09:35:54
     29322 |     10 |     22 | 2011-06-06 15:50:38
     29321 |     10 |     30 | 2011-06-06 15:47:17
       124 |     10 |     62 | 2008-07-29 09:35:54
     21913 |     10 |     78 | 2008-07-29 09:35:54
       123 |     10 |     90 | 2008-07-29 09:35:54
     28352 |     10 |    106 | 2010-11-22 12:37:50
(9 rows)

Saída de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..8.23 rows=9 width=20) (actual time=0.007..0.023 rows=9 loops=1)
  Index Cond: (adr_id = 10)
Total runtime: 0.058 ms

3. Consulta usando a segunda coluna

SELECT * FROM adratt WHERE att_id = 90

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
       180 |     39 |     90 | 2008-08-29 15:46:07
...
(83 rows)

Saída de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..818.51 rows=83 width=20) (actual time=0.014..0.694 rows=83 loops=1)
  Index Cond: (att_id = 90)
Total runtime: 0.849 ms

4. Desative o indexscan e o bitmapscan

SET enable_indexscan = off;
SELECT * FROM adratt WHERE att_id = 90

Saída de EXPLAIN ANALYZE:

Bitmap Heap Scan on adratt  (cost=779.94..854.74 rows=83 width=20) (actual time=0.558..0.743 rows=83 loops=1)
  Recheck Cond: (att_id = 90)
  ->  Bitmap Index Scan on adratt_uni  (cost=0.00..779.86 rows=83 width=0) (actual time=0.544..0.544 rows=83 loops=1)
        Index Cond: (att_id = 90)
Total runtime: 0.894 ms
SET enable_bitmapscan = off
SELECT * FROM adratt WHERE att_id = 90

Saída de EXPLAIN ANALYZE:

Seq Scan on adratt  (cost=0.00..1323.10 rows=83 width=20) (actual time=0.009..2.429 rows=83 loops=1)
  Filter: (att_id = 90)
Total runtime: 2.680 ms

Conclusão

Como esperado, o índice de várias colunas é usado para uma consulta somente na segunda coluna.
Como esperado, é menos eficaz, mas a consulta ainda é 3x mais rápida do que sem o índice.
Depois de desativar as varreduras de índice, o planejador de consultas escolhe uma varredura de heap de bitmap, que executa quase tão rapidamente. Somente depois de desativar isso, ele também volta a uma varredura seqüencial.

Erwin Brandstetter
fonte
agrupamento vai fazer a diferença se o número de partidas no índice é alto o suficiente (ver aqui para a prova - observe as duplas corridas para obter o cache de dados)
Jack Douglas
11
@ JackDouglas: Eu pensei mais nisso. O agrupamento pode ajudar em geral, porque é efetivamente também a vacuum fulle a reindex. Outros, que ele vai ajudar varreduras de índice na primeira ou ambas as colunas principais muito , mas machucar consultas na segunda coluna. Em uma tabela recém-agrupada em cluster, as linhas com o mesmo valor na segunda coluna são espalhadas, para que um máximo de blocos precise ser lido.
Erwin Brandstetter
28

re 1) Sim e não.

Para uma consulta que usa as duas colunas, por exemplo where (user_id1, user_id2) = (1,2), não importa qual índice é criado.

Para uma consulta que tem uma condição em apenas uma das colunas, por exemplo where user_id1 = 1, isso importa, porque geralmente apenas as colunas "principais" podem ser usadas para uma comparação pelo otimizador. Portanto, where user_id1 = 1seria capaz de usar o índice (user_id1, user_id2), mas não seria capaz de um índice (user_id2, user_id1) para todos os casos.

Depois de brincar com isso (depois que Erwin gentilmente nos mostrou uma configuração onde funciona), parece que isso depende muito da distribuição de dados da segunda coluna, embora ainda não tenha descoberto qual situação permite que o otimizador use colunas à direita para uma condição WHERE.

Oracle 11 que também pode (às vezes) usar colunas que não estão no início da definição do índice.

re 2) Sim, ele criará um índice

Citação do manual

Adicionar uma chave primária criará automaticamente um índice btree exclusivo na coluna ou grupo de colunas usadas na chave primária.

re 2a) Primary Key (user_id1,user_id2)criará um índice em (user_id1, user_id2) (que você pode descobrir por si mesmo com muita facilidade, basta criar uma chave primária)

Eu recomendo que você leia o capítulo sobre índices no manual , que basicamente responde a todas as perguntas acima.

Além disso, qual índice criar? by depesz faz um bom trabalho ao explicar a ordem nas colunas do índice e outros tópicos relacionados ao índice.

um cavalo sem nome
fonte
11

Anúncio 1)
Existem limitações no PostgreSQL como @a_horse_with_no_name descreve . Até a versão 8.0, os índices de várias colunas só podiam ser usados ​​para consultas nas colunas principais. Isso foi aprimorado na versão 8.1. O manual atual do Postgres 10 (atualizado) explica:

Um índice de árvore B de várias colunas pode ser usado com condições de consulta que envolvem qualquer subconjunto das colunas do índice, mas o índice é mais eficiente quando há restrições nas colunas principais (mais à esquerda). A regra exata é que restrições de igualdade nas colunas iniciais, além de quaisquer restrições de desigualdade na primeira coluna que não tenham uma restrição de igualdade, serão usadas para limitar a parte do índice que é varrida. As restrições nas colunas à direita dessas colunas são verificadas no índice; portanto, elas salvam as visitas na tabela corretamente, mas não reduzem a parte do índice que deve ser varrida. Por exemplo, dado um índice (a, b, c)e uma condição de consulta WHERE a = 5 AND b >= 42 AND c < 77, o índice precisaria ser verificado na primeira entrada com a= 5 eb= 42 até a última entrada com a= 5. As entradas de índice com c> = 77 seriam ignoradas, mas ainda teriam que ser verificadas. Esse índice poderia, em princípio, ser usado para consultas com restrições be / ou csem restrições a- mas o índice inteiro teria que ser verificado, portanto, na maioria dos casos, o planejador preferiria uma varredura seqüencial de tabela usando o índice.

Ênfase minha. Eu posso confirmar isso por experiência própria.
Veja também o caso de teste adicionado à minha resposta posterior aqui .

Erwin Brandstetter
fonte
11

Isso é uma resposta à resposta de Jack , um comentário não faria.

Não havia índices de cobertura no PostgreSQL antes da versão 9.2. Devido ao modelo MVCC, todas as tuplas no conjunto de resultados devem ser visitadas para verificar a visibilidade. Você pode estar pensando no Oracle.

Os desenvolvedores do PostgreSQL falam sobre "verificações apenas de índice" . De fato, o recurso foi lançado com o Postgres 9.2. Leia a mensagem de confirmação .
Depesz escreveu uma postagem no blog muito informativa .

Índices de cobertura verdadeiros (atualização) são introduzidos com a INCLUDEcláusula no Postgres 11. Relacionados:

Isso também é um pouco errado:

ele se baseia no fato de que uma 'verificação completa' de um índice geralmente é mais rápida que uma 'verificação completa' da tabela indexada devido às colunas extras na tabela que não aparecem no índice.

Conforme relatado nos comentários da minha outra resposta, também executei testes com uma tabela de dois números inteiros e nada mais. O índice mantém as mesmas colunas da tabela. O tamanho de um índice btree é cerca de 2/3 do da tabela. Não é o suficiente para explicar uma aceleração do fator 3. Fiz mais testes, com base na sua configuração, simplificados para duas colunas e com 100000 linhas. Na minha instalação do PostgreSQL 9.0, os resultados foram consistentes.

Se a tabela tiver colunas adicionais, a aceleração do índice se tornará mais substancial, mas esse certamente não é o único fator aqui .

Para resumir os principais pontos:

  • Os índices de várias colunas podem ser usados ​​com consultas em colunas não principais, mas a aceleração é apenas em torno do fator 3 para critérios seletivos (pequena porcentagem de linhas no resultado). Maior para tuplas maiores, menor para partes maiores da tabela no conjunto de resultados.

  • Crie um índice adicional nessas colunas se o desempenho for importante.

  • Se todas as colunas envolvidas forem incluídas em um índice (índice de cobertura) e todas as linhas envolvidas (por bloco) estiverem visíveis para todas as transações, você poderá obter uma "varredura apenas de índice" na página 9.2 ou posterior.

Erwin Brandstetter
fonte
7
  1. São equivalentes? Se não, então por quê?

    Índice (user_id1, user_id2) e Índice (user_id2, user_id1)

Eles não são equivalentes e, de um modo geral, o índice (bar, baz) não será eficiente para consultas no formulário select * from foo where baz=?

Erwin demonstrou que esses índices podem realmente acelerar uma consulta, mas esse efeito é limitado e não da mesma ordem em que você geralmente espera que um índice melhore uma pesquisa - ele se baseia no fato de que uma 'varredura completa' de um índice geralmente é mais rápido que uma 'verificação completa' da tabela indexada devido às colunas extras na tabela que não aparecem no índice.

Resumo: os índices podem ajudar as consultas, mesmo em colunas não principais, mas de uma das duas maneiras secundárias e relativamente menores e não da maneira dramática que você normalmente espera que um índice ajude devido à sua estrutura btree

As duas maneiras pelas quais o índice pode ajudar são se uma varredura completa do índice for significativamente mais barata que uma varredura completa da tabela e: 1. as pesquisas da tabela forem baratas (porque existem poucas ou estão agrupadas), ou 2. o índice está cobrindo para que não haja pesquisas de tabela em todos os aspectos, consulte os comentários de Erwins aqui

testbed:

create table foo(bar integer not null, baz integer not null, qux text not null);

insert into foo(bar, baz, qux)
select random()*100, random()*100, 'some random text '||g from generate_series(1,10000) g;

consulta 1 (sem índice, atingindo 74 buffers ):

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=181.41..181.42 rows=1 width=32) (actual time=3.301..3.302 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..181.30 rows=43 width=32) (actual time=0.043..3.228 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.335 ms

consulta 2 (com índice - o otimizador ignora o índice - atingindo 74 buffers novamente):

create index bar_baz on foo(bar, baz);

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=199.12..199.13 rows=1 width=32) (actual time=3.277..3.277 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..199.00 rows=50 width=32) (actual time=0.043..3.210 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.311 ms

consulta 2 (com índice - e enganamos o otimizador para usá-lo):

explain (buffers, analyze, verbose) select max(qux) from foo where bar>-1000 and baz=0;
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=115.56..115.57 rows=1 width=32) (actual time=1.495..1.495 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=36 read=30
   ->  Bitmap Heap Scan on stack.foo  (cost=73.59..115.52 rows=17 width=32) (actual time=1.370..1.428 rows=52 loops=1)
         Output: bar, baz, qux
         Recheck Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
         Buffers: shared hit=36 read=30
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..73.58 rows=17 width=0) (actual time=1.356..1.356 rows=52 loops=1)
               Index Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
               Buffers: shared read=30
 Total runtime: 1.535 ms

Portanto, o acesso via índice é duas vezes mais rápido, atingindo 30 buffers - que em termos de indexação são 'um pouco mais rápidos'! E YMMV, dependendo do tamanho relativo da tabela e do índice, juntamente com o número de linhas filtradas e características de cluster dos dados na tabela

Por outro lado, as consultas na coluna principal fazem uso da estrutura btree do índice - nesse caso, atingindo 2 buffers :

explain (buffers, analyze, verbose) select max(qux) from foo where bar=0;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=75.70..75.71 rows=1 width=32) (actual time=0.172..0.173 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=38
   ->  Bitmap Heap Scan on stack.foo  (cost=4.64..75.57 rows=50 width=32) (actual time=0.036..0.097 rows=59 loops=1)
         Output: bar, baz, qux
         Recheck Cond: (foo.bar = 0)
         Buffers: shared hit=38
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..4.63 rows=50 width=0) (actual time=0.024..0.024 rows=59 loops=1)
               Index Cond: (foo.bar = 0)
               Buffers: shared hit=2
 Total runtime: 0.209 ms
Jack Douglas
fonte