Índices de capas para GiST

O índice de cobertura não é apenas outro recurso que pode ser útil. Essa coisa é puramente prática. Sem eles, o Index Only Scan pode não dar uma vitória. Embora o índice de cobertura em diferentes situações seja eficaz de diferentes maneiras.

Não se trata realmente de cobrir índices: a rigor, os chamados índices inclusivos apareceram no Postgres. Mas, em ordem: um índice de cobertura é um índice que contém todos os valores de coluna exigidos pela consulta; no entanto, o acesso à própria tabela não é mais necessário. Quase. Você pode ler sobre “quase” e outras nuances em um artigo de Yegor Rogov , incluído em sua série de índices de 10 (!) Partes. E o índice inclusivo é criado especificamente para pesquisar em consultas típicas: os valores dos campos que não podem ser pesquisados ​​são adicionados ao índice de pesquisa, eles são necessários apenas para não consultar a tabela novamente. Esses índices são formados com a palavra-chave INCLUDE.

Anastasia Lubennikova (Postgres Professional) finalizou o método btree para que colunas adicionais pudessem ser incluídas no índice. Esse patch foi incluído no PostgreSQL 11. Mas os patches para os métodos de acesso do GiST / SP-GiST não tiveram tempo de amadurecer antes do lançamento desta versão. No dia 12, o GiST amadureceu.

Um desejo construtivo de ter índices inclusivos para o GiST surgiu há muito tempo: um patch de teste de Andrey Borodin foi oferecido à comunidade em meados de abril de 2018. Ele fez todo o trabalho básico e muito difícil.

No início de agosto de 2019, Alexander Korotkov adicionou melhorias cosméticas e comprometeu o patch.

Para demonstração e algumas pesquisas, geraremos um conjunto de 3 milhões de retângulos. Ao mesmo tempo, algumas palavras sobre o tipo de caixa, pois nem todas as manipulações são intuitivas.

O tipo de caixa - ou seja, o retângulo - está no Postgres há muito tempo, é definido por 2 pontos (o ponto do tipo geométrico) - os vértices opostos do retângulo (ou seja, o retângulo não pode ser oblíquo, desarrumado ao lado). Lemos na documentação : “os valores do tipo box são escritos em uma das seguintes formas:

( ( x1 , y1 ) , ( x2 , y2 ) ) ( x1 , y1 ) , ( x2 , y2 ) x1 , y1 , x2 , y2 

Na prática, você deve escrever, digamos, assim:

 SELECT box('1,2', '3,4'); box ------------- (3,4),(1,2) (1 row) 

Primeiro, o Postgres mostra o vértice superior direito e depois o inferior esquerdo. Se escrevermos assim,

 SELECT box('5,2', '3,4'); box ------------- (5,4),(3,2) (1 row) 

garantiremos que o Postgres não tenha atingido os picos que eles deram a ele. Ele calculou o canto superior direito e o canto inferior esquerdo do canto superior esquerdo e do canto inferior direito. Essa é uma propriedade conveniente quando a localização dos vértices não é conhecida antecipadamente - no caso de geração aleatória, por exemplo. A notação '1,2', '3,4' é equivalente ao ponto (1,2), ponto (3,4). Este formulário às vezes é mais conveniente.


Para empresas: pesquise em 3 milhões de retângulos


 CREATE TABLE boxes(id serial, thebox box, name text); 

Geraremos 3 milhões de retângulos aleatórios. Queremos uma distribuição normal, mas, para não usar a extensão de função de tabela , usamos a abordagem “ruim”: usamos random () - random (), que também fornece uma boa imagem (veja a fig.) Com retângulos, quanto maior, mais, mais próximos eles estão do centro. Seus centros de gravidade também são aleatórios. Tais distribuições são características de alguns tipos de dados reais da cidade. E aqueles que desejam se aprofundar nas leis da estatística ou atualizar memórias podem ler sobre a diferença de variáveis ​​aleatórias, por exemplo, aqui .




 INSERT INTO boxes(thebox, name) SELECT box( point( random()-random(), random()-random() ), point( random()-random(), random()-random() ) ), 'box no.' || x FROM generate_series(1,3000000) AS g(x); 


O tamanho da tabela que mostra \dt+ é 242MB. Agora você pode iniciar a pesquisa.

Estamos procurando sem um índice:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------------- Gather (cost=1000.00..47853.00 rows=3000 width=46) (actual time=0.140..246.998 rows=139189 loops=1) Workers Planned: 2 Workers Launched: 2 -> Parallel Seq Scan on boxes (cost=0.00..46553.00 rows=1250 width=46) (actual time=0.011..106.708 rows=46396 loops=3) Filter: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Rows Removed by Filter: 953604 Planning Time: 0.040 ms Execution Time: 259.262 ms (8 rows) 

Vemos que há uma varredura sequencial Parallel Seq - sequencial (embora paralelizada).

Crie um índice regular e não inclusivo:

 CREATE INDEX ON boxes USING gist(thebox); 

O tamanho do índice boxes_thebox_idx , que mostra \di+ , 262MB. Em resposta à mesma solicitação, obtemos:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Bitmap Heap Scan on boxes (cost=159.66..9033.30 rows=3000 width=46) (actual time=29.101..80.283 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_idx (cost=0.00..158.91 rows=3000 width=0) (actual time=25.029..25.029 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 0.053 ms Execution Time: 86.206 ms (7 rows) 

O tempo de pesquisa foi reduzido em um fator de três e, em vez da verificação Seq paralela, eles receberam uma verificação de índice de bitmap. Não é paralelo, mas funciona mais rápido.

Agora mate o índice antigo e crie um abrangente:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); 

Índice de boxes_thebox_name_idx mais gordo: 356MB. Vamos lá:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN ------------------------------------------------------------------------------ Bitmap Heap Scan on boxes (cost=207.66..9081.30 rows=3000 width=46) (actual time=86.822..152.014 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_name_idx (cost=0.00..206.91 rows=3000 width=0) (actual time=83.044..83.044 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 3.807 ms Execution Time: 157.997 ms (7 rows) 


A digitalização apenas de índice é usada, mas a imagem é triste: o tempo é quase duas vezes maior do que sem ela. Lemos o manual do criador dos índices, na parte I :

‹Os índices do PostgreSQL não contêm informações que permitem avaliar a visibilidade das linhas. Portanto, o método de acesso retorna todas as versões de linhas que se enquadram na condição de pesquisa, independentemente de estarem visíveis para a transação atual ou não. No entanto, se o mecanismo de indexação tivesse que examinar a tabela todas as vezes para determinar a visibilidade, esse método de verificação não seria diferente da verificação de índice comum. O problema é resolvido pelo fato do PostgreSQL suportar o chamado mapa de visibilidade para tabelas, em que o processo a vácuo marca páginas nas quais os dados não foram alterados o tempo suficiente para que todas as transações o vejam, independentemente da hora de início e do nível de isolamento. Se o identificador da linha retornada pelo índice se referir a essa página, a visibilidade não poderá ser verificada. ››

Nós fazemos VACUUM. Repita:

 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Index Only Scan using boxes_thebox_name_idx on boxes (cost=0.41..236.91 rows=3000 width=46) (actual time=0.104..38.651 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Fetches: 0 Planning Time: 0.052 ms Execution Time: 44.337 ms (5 rows) 

Uma questão completamente diferente! O dobro do ganho em comparação com o índice não inclusivo.


Seletividade e ganho


O desempenho de índices inclusivos é altamente dependente da seletividade das condições nas consultas. Para investigar um pouco essa dependência, resolveremos o problema inverso: geraremos um rótulo com um índice do tipo point e procuraremos quantos pontos cairão na caixa especificada. Espalhe os pontos uniformemente ao quadrado.

 CREATE TABLE test_covergist(id serial, tochka point, name text); 

 INSERT INTO test_covergist(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,3000000) AS g(x); 

O tamanho da tabela é 211 MB.

 CREATE INDEX on test_covergist USING gist(tochka); 

Tamanho 213 MB.

Obviamente, levaremos todos os pontos disponíveis para um quadrado:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1087.964..1864.059 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared read=54287 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1084.949..1084.949 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared read=27262 Planning Time: 0.102 ms Execution Time: 2029.501 ms (9 rows) 

Pedimos EXPLAIN para mostrar os buffers. Será útil. Agora, o tempo de execução da solicitação é superior a 2 segundos; pode ser visto que Buffers: leitura compartilhada = 54287. Em outra situação, pudemos ver uma mistura de leitura compartilhada e ocorrência compartilhada - ou seja, alguns buffers são lidos do disco (ou do cache do SO) e outros do cache do buffer. Sabemos o tamanho aproximado da tabela e dos índices, portanto, nos protegeremos configurando buffers compartilhados para que tudo se encaixe - reinicie o Postgres com a opção

 -o "-c shared_buffers=1GB" 

Agora:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=231.032..613.326 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared hit=54248 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=228.068..228.068 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=27223 Planning Time: 0.070 ms Execution Time: 755.915 ms (9 rows) 

Ou seja, a leitura compartilhada se tornou um hit compartilhado e o tempo foi reduzido três vezes.

Outro detalhe importante em EXPLAIN: são devolvidos 3 milhões de pontos e a previsão do número de registros retornados é de 3.000. Spoiler: esse número não será alterado com nenhuma seletividade. O otimizador não sabe como avaliar a cardinalidade ao trabalhar com tipos de caixa ou ponto. E o plano não será alterado: para qualquer tamanho do retângulo, haverá uma verificação de índice de bitmap em test_covergist_tochka_idx.

Aqui estão mais duas medidas com o número de registros emitidos, diferindo por ordens de magnitude:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=27.889..134.054 rows=269882 loops=1) Recheck Cond: ('(300000,300000),(0,0)'::box @> tochka) Heap Blocks: exact=27024 Buffers: shared hit=29534 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=24.847..24.847 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Buffers: shared hit=2510 Planning Time: 0.074 ms Execution Time: 151.269 ms (9 rows) 

Ele retorna 10 vezes menos registros (reais ... linhas = 269882), o tempo diminuiu cerca de 5 vezes.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','30000,30000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1.882..16.095 rows=2780 loops=1) Recheck Cond: ('(30000,30000),(0,0)'::box @> tochka) Heap Blocks: exact=2624 Buffers: shared hit=2655 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1.035..1.035 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Buffers: shared hit=31 Planning Time: 0.154 ms Execution Time: 16.702 ms (9 rows) 

O conteúdo de um quadrado de 30K × 30K (2780) é contado em apenas 16 ms. E quando existem dezenas de registros, eles já são extraídos em frações de um ms e essas medidas não são muito confiáveis.

Por fim, meça o mesmo com o índice inclusivo:

 CREATE INDEX on test_covergist USING gist(tochka) INCLUDE(name); 

Tamanho 316 MB.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.160..568.707 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=40492 Planning Time: 0.090 ms Execution Time: 709.837 ms (6 rows) 

O tempo é quase o mesmo que com um índice convencional, embora a varredura apenas de índice.

Mas:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.083..53.277 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=3735 Planning Time: 0.077 ms Execution Time: 66.162 ms (6 rows) 

E foram 151 ms. E, consequentemente:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.043..0.639 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=52 Planning Time: 0.053 ms Execution Time: 0.791 ms (6 rows) 

Isso já é uma fração de ms para os mesmos registros de 2780 pontos.

Amortecedores como armas


Uma explicação pode ser encontrada e encontrada em uma espingarda que ainda não disparou, mas que estava pendurada na parede: o número de blocos lidos. No caso de um índice inclusivo, apenas blocos do próprio índice são lidos (Heap Fetches: 0). Em três casos, eram os números 40492, 3735 e 52. Mas, ao usar o índice regular, os blocos lidos consistem na soma dos bits lidos no índice Bitmap Heap Scan (54248 com 3 milhões de registros) e nos que precisavam ser lidos do heap (27223) , já que o campo de nome não pode ser extraído de um índice regular. 54248 + 27223 = 81471. O exclusivo era 40492. Para outros dois casos: 29534 + 2510 = 31044 e 2655 + 31 = 2686. No caso de um índice regular, mais blocos são lidos de qualquer maneira, mas com uma melhoria na seletividade, o número de blocos lidos começa a diferir por ordens de magnitude em vez de 2 vezes devido ao fato de que o número de blocos necessários de um heap diminui mais lentamente do que a leitura de blocos de índice.

Total de registros retornados (mil)30002702.7
Blocos de leitura (Normal / Inclusivo)81471/4049231044/37352686/52
Tempo755/710151/6616 / 0,7


Mas talvez o ponto não seja a seletividade, mas simplesmente o tamanho da tabela? Por precaução, repetimos as mesmas etapas, gerando uma tabela com 300 mil e não 3 milhões de registros:

 CREATE TABLE test_covergist_small(id serial, tochka point, name text); INSERT INTO test_covergist_small(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,300000) AS g(x); CREATE INDEX ON test_covergist_small USING gist(tochka); EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist_small WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist_small (cost=14.61..867.19 rows=300 width=31) (actual time=36.115..130.435 rows=300000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=2500 Buffers: shared hit=5225 -> Bitmap Index Scan on test_covergist_small_tochka_idx (cost=0.00..14.53 rows=300 width=0) (actual time=35.894..35.895 rows=300000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=2725 Planning Time: 0.060 ms Execution Time: 158.580 (9 rows) 

Em seguida, repita o mesmo para o índice inclusivo. Aqui estão os resultados:

Total de registros retornados (mil)300270,25
Blocos de leitura (Normal / Inclusivo)5225/37263026/352270/8
Tempo158/17820/130,4 / 0,2


No caso de 100% de cobertura de pontos, a consulta foi um pouco mais lenta que no índice usual. Além disso, como no caso de 3 milhões, tudo se encaixou. Ou seja, a seletividade é importante.

Nossa empresa testou índices inclusivos de GiST em dados reais - um conjunto com vários milhões de retângulos no mapa de Moscou. A conclusão é a mesma: em muitas situações, esses índices aceleram sensivelmente as consultas. Mas o artigo não pode ser ilustrado com fotos e número de testes: esses dados não são de domínio público.

Em vez de uma conclusão


Vamos voltar por um momento para retângulos aleatórios. Vamos tentar fazer o mesmo com o spgist. Você pode se lembrar ou descobrir o que é entender as diferenças entre o SP-GiST e o GiST lendo o artigo Indexes in PostgreSQL - 6 . Crie um índice inclusivo:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); ERROR: access method "spgist" does not support included columns 

Infelizmente, para o SP-GiST, índices inclusivos ainda não foram implementados.
Portanto, há espaço para melhorias!



Source: https://habr.com/ru/post/pt474166/


All Articles