Qual índice usar com muitos valores duplicados?

14

Vamos fazer algumas suposições:

Eu tenho uma tabela que se parece com isso:

 a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22

Fatos sobre o meu conjunto:

  • O tamanho da tabela inteira é ~ 10 10 linhas.

  • Eu tenho ~ 100k linhas com valor ana coluna a, semelhante para outros valores (por exemplo c).

  • Isso significa ~ 100k valores distintos na coluna 'a'.

  • A maioria das minhas consultas lerá todos ou a maioria dos valores para um determinado valor em a, por exemplo select sum(b) from t where a = 'c'.

  • A tabela é escrita de tal maneira que os valores consecutivos são fisicamente próximos (ou é escrito em ordem ou assumimos que CLUSTERfoi usado nessa tabela e coluna a).

  • A tabela raramente é atualizada, se é que alguma vez foi atualizada, estamos preocupados apenas com a velocidade de leitura.

  • A tabela é relativamente estreita (digamos, ~ 25 bytes por tupla, + 23 bytes de sobrecarga).

Agora, a pergunta é: que tipo de índice devo usar? Meu entendimento é:

  • BTree Meu problema aqui é que o índice BTree será enorme, pois, tanto quanto eu sei, ele armazenará valores duplicados (é necessário, pois não pode assumir que a tabela esteja fisicamente classificada). Se o BTree for enorme, acabo tendo que ler o índice e as partes da tabela para as quais o índice aponta. (Podemos usar fillfactor = 100para diminuir um pouco o tamanho do índice.)

  • BRIN Meu entendimento é que eu posso ter um pequeno índice aqui à custa da leitura de páginas inúteis. Usar um pequeno pages_per_rangesignifica que o índice é maior (o que é um problema com o BRIN, pois preciso ler todo o índice), e ter um grande pages_per_rangesignifica que vou ler muitas páginas inúteis. Existe uma fórmula mágica para encontrar um bom valor pages_per_rangeque leve em consideração essas compensações?

  • GIN / GiST Não tenho certeza de que sejam relevantes aqui, pois são usados ​​principalmente para pesquisa de texto completo, mas também ouvi dizer que eles são bons em lidar com chaves duplicadas. Um índice GINou GiSTajudaria aqui?

Outra pergunta é: o Postgres usará o fato de uma tabela ser CLUSTEReditada (supondo que não haja atualizações) no planejador de consultas (por exemplo, pesquisando binário as páginas de início / fim relevantes)? Um pouco relacionado, posso apenas armazenar todas as minhas colunas em um BTree e soltar a tabela completamente (ou obter algo equivalente, acredito que esses são índices agrupados no SQL server)? Existe algum índice híbrido BTree / BRIN que ajudaria aqui?

Prefiro evitar o uso de matrizes para armazenar meus valores, pois minha consulta será menos legível dessa maneira (eu entendo que isso reduziria o custo dos 23 bytes por sobrecarga de tupla, reduzindo o número de tuplas).

foo
fonte
"usado principalmente para pesquisa de texto completo" O GiST é amplamente utilizado pelo PostGIS.
Jpmc26 11/03/19

Respostas:

15

BTree

Meu problema aqui é que o índice BTree será enorme, já que, por padrão, ele armazenará valores duplicados (também, pois não pode assumir que a tabela esteja fisicamente classificada). Se o BTree é enorme, acabo tendo que ler o índice e as partes da tabela que o índice aponta também ...

Não necessariamente - ter um índice de btree 'coberto' será o tempo de leitura mais rápido e, se é tudo o que você deseja (por exemplo, se você puder pagar pelo armazenamento extra), é a sua melhor aposta.

BRIN

Meu entendimento é que eu posso ter um pequeno índice aqui às custas da leitura de páginas inúteis. Usar um pequeno pages_per_rangesignifica que o índice é maior (o que é um problema com o BRIN, pois preciso ler todo o índice), e ter um grande pages_per_rangesignifica que vou ler muitas páginas inúteis.

Se você não puder pagar a sobrecarga de armazenamento de um índice btree de cobertura, o BRIN é ideal para você, porque você já possui um cluster (isso é crucial para o BRIN ser útil). Os índices BRIN são pequenos , portanto é provável que todas as páginas estejam na memória se você escolher um valor adequado de pages_per_range.

Existe uma fórmula mágica para encontrar um bom valor de pages_per_range que leve em consideração essas compensações?

Nenhuma fórmula mágica, mas comece com pages_per_range um pouco menos do que o tamanho médio (em páginas) ocupado pelo avalor médio . Você provavelmente está tentando minimizar: (número de páginas BRIN digitalizadas) + (número de páginas heap digitalizadas) para uma consulta típica. Procure Heap Blocks: lossy=nno plano de execução pages_per_range=1e compare com outros valores para pages_per_range- ou seja, veja quantos blocos de heap desnecessários estão sendo varridos.

GIN / GiST

Não tenho certeza de que são relevantes aqui, pois são usados ​​principalmente para pesquisa de texto completo, mas também ouvi dizer que eles são bons em lidar com chaves duplicadas. O GIN/ GiSTindex ajudaria aqui?

Vale a pena considerar o GIN, mas provavelmente não o GiST - no entanto, se o agrupamento natural realmente for bom, o BRIN provavelmente será uma aposta melhor.

Aqui está uma amostra de comparação entre os diferentes tipos de índice para dados fictícios, um pouco como o seu:

tabela e índices:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

tamanhos de relação:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
nome | tamanho | páginas | linhas / página
: ----------------- | : ------ | ----: | --------:
foo | 149 MB | 19118 135
foo_btree_covering | 56 MB | 7132 364
foo_btree | 56 MB | 7132 364
foo_gin | 2928 kB | 366 7103
foo_brin_2 | 264 kB | 33 78787
foo_brin_4 | 136 kB | 17 152941

cobrindo btree:

explain analyze select sum(b) from foo where a='a';
| PLANO DE CONSULTA |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------------------- |
| Agregado (custo = 3282,57..3282,58 linhas = 1 largura = 8) (tempo real = 45,942..45,942 linhas = 1 loops = 1) |
| -> Somente digitalização de índice usando foo_btree_covering em foo (custo = 0,43..3017,80 linhas = 105907 largura = 4) (tempo real = 0,038..27,286 linhas = 100000 loops = 1) |
| Índice Cond: (a = 'a' :: texto) |
| Buscas de pilha: 0 |
| Tempo de planejamento: 0,099 ms |
| Tempo de execução: 45,968 ms |

btree simples:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
| PLANO DE CONSULTA |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Agregado (custo = 4064.57..4064.58 linhas = 1 largura = 8) (tempo real = 54.242..54.242 linhas = 1 loops = 1) |
| -> Varredura de índice usando foo_btree on foo (custo = 0,43..3799,80 linhas = 105907 largura = 4) (tempo real = 0,037..33,084 linhas = 100000 loops = 1) |
| Índice Cond: (a = 'a' :: texto) |
| Tempo de planejamento: 0,135 ms |
| Tempo de execução: 54,280 ms |

BRIN pages_per_range = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
| PLANO DE CONSULTA |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Agregado (custo = 21595,38..21595,39 linhas = 1 largura = 8) (tempo real = 52.455..52.455 linhas = 1 loops = 1) |
| -> Varredura de Heap de Bitmap no foo (custo = 888.78..21330.61 linhas = 105907 largura = 4) (tempo real = 2.738..31.967 linhas = 100000 loops = 1) |
| Verifique novamente Cond: (a = 'a' :: text) |
| Linhas removidas pela verificação do índice: 96 |
| Blocos de heap: com perdas = 736 |
| -> Varredura de índice de bitmap em foo_brin_4 (custo = 0,00..862,30 linhas = 105907 largura = 0) (tempo real = 2,720..2,720 linhas = 7360 loops = 1) |
| Índice Cond: (a = 'a' :: texto) |
| Tempo de planejamento: 0,101 ms |
| Tempo de execução: 52,501 ms |

BRIN pages_per_range = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
| PLANO DE CONSULTA |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Agregado (custo = 21659,38..21659,39 linhas = 1 largura = 8) (tempo real = 53,971..53,971 linhas = 1 loops = 1) |
| -> Varredura de Heap de Bitmap no foo (custo = 952.78..21394.61 linhas = 105907 largura = 4) (tempo real = 5.286..33.492 linhas = 100000 loops = 1) |
| Verifique novamente Cond: (a = 'a' :: text) |
| Linhas removidas pela verificação do índice: 96 |
| Blocos de heap: com perdas = 736 |
| -> Varredura de índice de bitmap em foo_brin_2 (custo = 0,00..926,30 linhas = 105907 largura = 0) (tempo real = 5,275..5,275 linhas = 7360 loops = 1) |
| Índice Cond: (a = 'a' :: texto) |
| Tempo de planejamento: 0,095 ms |
| Tempo de execução: 54,016 ms |

GIN:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
| PLANO DE CONSULTA |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------ |
| Agregado (custo = 21687.38..21687.39 linhas = 1 largura = 8) (tempo real = 55.331..55.331 linhas = 1 loops = 1) |
| -> Varredura de Heap de Bitmap no foo (custo = 980.78..21422.61 linhas = 105907 largura = 4) (tempo real = 12.377..33.956 linhas = 100000 loops = 1) |
| Verifique novamente Cond: (a = 'a' :: text) |
| Blocos de heap: exato = 736 |
| -> Varredura de índice de bitmap em foo_gin (custo = 0,00..954,30 linhas = 105907 largura = 0) (tempo real = 12,271..12,271 linhas = 100000 loops = 1) |
| Índice Cond: (a = 'a' :: texto) |
| Tempo de planejamento: 0,118 ms |
| Tempo de execução: 55,366 ms |

dbfiddle aqui

Jack diz que tenta topanswers.xyz
fonte
Portanto, um índice de cobertura ignoraria a leitura da tabela completamente à custa do espaço em disco? Parece uma boa troca. Acho que queremos dizer a mesma coisa para o índice BRIN por 'ler o índice inteiro' (me corrija se estiver errado); eu quis dizer escanear todo o índice BRIN, que acho que é o que está acontecendo em dbfiddle.uk/… , não?
foo
@foo sobre o "(tem também, já que não pode assumir que a tabela está fisicamente classificada)." A ordem física (cluster ou não) da tabela é irrelevante. O índice tem os valores na ordem correta. Porém, os índices da árvore B do Postgres precisam armazenar todos os valores (e sim, várias vezes). É assim que eles são projetados. Armazenar cada valor distinto apenas uma vez seria um bom recurso / melhoria. Você pode sugerir aos desenvolvedores do Postgres (e até ajudar a implementá-lo.) Jack deve comentar, acho que a implementação do b-trees da Oracle faz isso.
ypercubeᵀᴹ
1
@foo - você está completamente correto, uma varredura de um índice BRIN sempre varre todo o índice ( pgcon.org/2016/schedule/attachments/… , 2º último slide) - embora isso não seja mostrado no plano de explicação no violino , é isso?
Jack diz que tente topanswers.xyz
2
@ ypercubeᵀᴹ você pode usar o COMPRESS no Oracle que armazena cada prefixo distinto uma vez por bloco.
Jack diz que tente topanswers.xyz
@JackDouglas Eu li Bitmap Index Scancomo o significado 'leia todo o índice brin', mas talvez seja a leitura errada. O Oracle COMPRESSparece algo que seria útil aqui, pois reduziria o tamanho da árvore B, mas estou preso com a página!
foo
6

Além de btree e brin, que parecem as opções mais sensatas, algumas outras opções exóticas que podem valer a pena investigar - elas podem ser úteis ou não no seu caso:

  • INCLUDEíndices . Eles estarão - esperançosamente - na próxima versão principal (10) do Postgres, em algum momento por volta de setembro de 2017. Um índice em (a) INCLUDE (b)tem a mesma estrutura de um índice, (a)mas inclui nas páginas da folha todos os valores de b(mas não ordenado). O que significa que você não pode usá-lo, por exemplo, para SELECT * FROM t WHERE a = 'a' AND b = 2 ;. O índice pode ser usado, mas enquanto um (a,b)índice localizar as linhas correspondentes com uma única pesquisa, o índice de inclusão precisará passar pelos valores (possivelmente 100 K, como no seu caso) que correspondem a = 'a'e verificam os bvalores.
    Por outro lado, o índice é um pouco menos amplo que o (a,b)índice e você não precisa do pedido bpara a sua consulta calcular SUM(b). Você também pode ter, por exemplo(a) INCLUDE (b,c,d) que pode ser usado para consultas semelhantes às suas, agregadas nas três colunas.

  • Índices filtrados (parciais) . Uma sugestão que pode parecer um pouco louca * no início:

    CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;

    Um índice para cada avalor. No seu caso, cerca de 100K índices. Embora isso pareça muito, considere que cada índice será muito pequeno, tanto em tamanho (número de linhas) quanto em largura (pois armazenará apenas bvalores). Em todos os outros aspectos, ele (os 100K índices juntos) atuará como um índice de árvore b (a,b)enquanto estiver usando o espaço de um (b)índice.
    A desvantagem é que você terá que criar e mantê-los, sempre que um novo valor afor adicionado à tabela. Como sua tabela é bastante estável, sem muitas (ou nenhuma) inserções / atualizações, isso não parece ser um problema.

  • Tabelas de resumo. Como a tabela é bastante estável, você sempre pode criar e preencher uma tabela de resumo com os agregados mais comuns necessários ( sum(b), sum(c), sum(d), avg(b), count(distinct b)etc). Será pequeno (apenas 100 mil linhas) e precisará ser preenchido apenas uma vez e atualizado somente quando as linhas forem inseridas / atualizadas / excluídas na tabela principal.

*: ideia copiada desta empresa que executa 10 milhões de índices em seu sistema de produção: The Heap: executando 10 milhões de índices Postgresql em produção (e contando) .

ypercubeᵀᴹ
fonte
1 é interessante, mas como você aponta, a página 10 ainda não foi divulgada. 2 faz som louco (ou pelo menos contra a 'sabedoria comum'), eu vou ter uma leitura, uma vez que você apontar que poderia trabalhar com a minha quase não escreve workflow. 3. não funcionaria para mim, usei SUMcomo exemplo, mas, na prática, minhas consultas não podem ser pré-computadas (elas são mais parecidas select ... from t where a = '?' and ??com as ??que seriam outras condições definidas pelo usuário.
foo
1
Bem, nós não podemos ajudar se nós não sabemos o que ??é;)
ypercubeᵀᴹ
Você mencionou índices filtrados. Que tal particionar a tabela?
Jpmc26 11/03/19
@ jpmc26 engraçado, eu estava pensando em adicionar na resposta que a sugestão de índices filtrados é de certa forma uma forma de particionamento. O particionamento também pode ser útil aqui, mas não tenho certeza. Isso resultaria em muitos pequenos índices / tabelas.
ypercubeᵀᴹ
2
Espero que a cobertura parcial dos índices btree seja o rei do desempenho aqui, já que os dados quase nunca são atualizados. Mesmo que isso signifique 100 mil índices. O tamanho total do índice é menor (exceto um índice BRIN, mas o Postgres precisa ler e filtrar páginas de heap adicionalmente). A geração de índice pode ser automatizada com SQL dinâmico. Exemplo de DOdeclaração nesta resposta relacionada .
Erwin Brandstetter