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

Indexação de banco de dados no PostgreSQL


Indexação de banco de dados é o uso de estruturas de dados especiais que visam melhorar o desempenho, obtendo acesso direto às páginas de dados. Um índice de banco de dados funciona como a seção de índice de um livro impresso:olhando na seção de índice, é muito mais rápido identificar a(s) página(s) que contém o termo em que estamos interessados. Podemos facilmente localizar as páginas e acessá-las diretamente . Isso é em vez de escanear as páginas do livro sequencialmente, até encontrarmos o termo que estamos procurando.

Os índices são uma ferramenta essencial nas mãos de um DBA. O uso de índices pode fornecer grandes ganhos de desempenho para uma variedade de domínios de dados. O PostgreSQL é conhecido por sua grande extensibilidade e pela rica coleção de complementos principais e de terceiros, e a indexação não é exceção a essa regra. Os índices do PostgreSQL cobrem um rico espectro de casos, desde os mais simples índices b-tree em tipos escalares até índices geoespaciais GiST até índices fts ou json ou array GIN.

Índices, no entanto, por mais maravilhosos que pareçam (e realmente são!) não vêm de graça. Há uma certa penalidade que acompanha as gravações em uma tabela indexada. Portanto, o DBA, antes de examinar suas opções para criar um índice específico, deve primeiro certificar-se de que o referido índice faz sentido em primeiro lugar, o que significa que os ganhos de sua criação superarão a perda de desempenho nas gravações.

Terminologia de índice básico do PostgreSQL


Antes de descrever os tipos de índices no PostgreSQL e seu uso, vamos dar uma olhada em algumas terminologias que qualquer DBA encontrará mais cedo ou mais tarde ao ler os documentos.
  • Método de acesso ao índice (também chamado de Método de Acesso ):O tipo de índice (B-tree, GiST, GIN, etc)
  • Tipo: o tipo de dados da coluna indexada
  • Operador: uma função entre dois tipos de dados
  • Família de operadores: operador de tipo de dados cruzado, agrupando operadores de tipos com comportamento semelhante
  • Classe de Operador (também mencionado como estratégia de índice ):define os operadores a serem usados ​​pelo índice para uma coluna

No catálogo do sistema PostgreSQL, os métodos de acesso são armazenados em pg_am, classes de operadores em pg_opclass, famílias de operadores em pg_opfamily. As dependências do acima são mostradas no diagrama abaixo:

Tipos de índices no PostgreSQL


O PostgreSQL fornece os seguintes tipos de índice:
  • Árvore B: o índice padrão, aplicável a tipos que podem ser classificados
  • Hash: lida apenas com igualdade
  • GIST: adequado para tipos de dados não escalares (por exemplo, formas geométricas, pés, matrizes)
  • SP-GiST: GIST particionado por espaço, uma evolução do GiST para lidar com estruturas não balanceadas (quadtrees, k-d trees, radix trees)
  • GIN: adequado para tipos complexos (por exemplo, jsonb, fts, arrays)
  • BRIN: um tipo relativamente novo de índice que suporta dados que podem ser classificados armazenando valores mín./máx. em cada bloco

Low, tentaremos sujar as mãos com alguns exemplos do mundo real. Todos os exemplos dados são feitos com PostgreSQL 10.0 (com 10 e 9 clientes psql) no FreeBSD 11.1.

Índices de árvore B


Suponhamos que temos a seguinte tabela:
create table part (
id serial primary key, 
partno varchar(20) NOT NULL UNIQUE, 
partname varchar(80) NOT NULL, 
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
                                  Table "public.part"
   Column       |         Type          |                     Modifiers                     
------------+-----------------------+---------------------------------------------------
 id         | integer                 | not null default nextval('part_id_seq'::regclass)
 partno     | character varying(20)| not null
 partname       | character varying(80)| not null
 partdescr      | text                    |
 machine_id     | integer                 | not null
Indexes:
    "part_pkey" PRIMARY KEY, btree (id)
    "part_partno_key" UNIQUE CONSTRAINT, btree (partno)

Quando definimos essa tabela bastante comum, o PostgreSQL cria dois índices B-tree exclusivos nos bastidores:part_pkey e part_partno_key. Assim, cada restrição única no PostgreSQL é implementada com um ÍNDICE único. Vamos preencher nossa tabela com um milhão de linhas de dados:
testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000

Agora vamos tentar fazer algumas consultas em nossa tabela. Primeiro, dizemos ao cliente psql para relatar os tempos de consulta digitando \timing:
testdb=# select * from part where id=100000;
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,319 ms

Observamos que são necessárias apenas frações de milissegundos para obter nossos resultados. Esperávamos isso, pois para ambas as colunas usadas nas consultas acima, já definimos os índices apropriados. Agora vamos tentar consultar a coluna partname, para a qual não existe índice.
testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 89,173 ms

Aqui vemos claramente que para a coluna não indexada, o desempenho cai significativamente. Agora vamos criar um índice nessa coluna e repetir a consulta:
testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
   id   |   partno   |  partname   | partdescr | machine_id
--------+------------+-------------+-----------+------------
 100000 | PNo:100000 | Part 100000 |           |          0
(1 row)

Time: 0,525 ms

Nosso novo índice part_partname_idx também é um índice de árvore B (o padrão). Primeiro, observamos que a criação do índice na tabela de milhões de linhas levou um tempo significativo, cerca de 16 segundos. Em seguida, observamos que nossa velocidade de consulta foi aumentada de 89 ms para 0,525 ms. Os índices B-tree, além de verificar a igualdade, também podem ajudar em consultas envolvendo outros operadores em tipos ordenados, como <,<=,>=,>. Vamos tentar com <=e>=
testdb=# select count(*) from part where partname>='Part 9999900';
 count
-------
     9
(1 row)

Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
 count  
--------
 999991
(1 row)

Time: 355,618 ms

A primeira consulta é muito mais rápida que a segunda, usando as palavras-chave EXPLAIN (ou EXPLAIN ANALYZE) podemos ver se o índice real é usado ou não:
testdb=# explain select count(*) from part where partname>='Part 9999900';
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 Aggregate  (cost=8.45..8.46 rows=1 width=8)
   ->  Index Only Scan using part_partname_idx on part  (cost=0.42..8.44 rows=1 width=0)
         Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)

Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=14603.22..14603.23 rows=1 width=8)
   ->  Gather  (cost=14603.00..14603.21 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=13603.00..13603.01 rows=1 width=8)
               ->  Parallel Seq Scan on part  (cost=0.00..12561.33 rows=416667 width=0)
                     Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)

Time: 0,461 ms

No primeiro caso, o planejador de consulta escolhe usar o índice part_partname_idx. Também observamos que isso resultará em uma varredura somente de índice, o que significa que não há acesso às tabelas de dados. No segundo caso, o planejador determina que não há sentido em usar o índice, pois os resultados retornados são uma grande parte da tabela, caso em que uma varredura sequencial é considerada mais rápida.

Índices de hash


O uso de índices de hash até e incluindo PgSQL 9.6 foi desencorajado devido a razões relacionadas à falta de escrita WAL. A partir do PgSQL 10.0, esses problemas foram corrigidos, mas os índices de hash ainda faziam pouco sentido de usar. Existem esforços no PgSQL 11 para tornar os índices de hash um método de índice de primeira classe junto com seus irmãos maiores (B-tree, GiST, GIN). Então, com isso em mente, vamos tentar um índice de hash em ação.

Vamos enriquecer nossa tabela de peças com uma nova coluna parttype e preenchê-la com valores de distribuição igual e, em seguida, executar uma consulta que testa parttype igual a 'Steering':
testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as  (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 93,361 ms

Agora criamos um índice de hash para essa nova coluna e tentamos novamente a consulta anterior:
testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
 count
-------
   322
(1 row)

Time: 63,634 ms

Notamos a melhora após usar o índice de hash. Agora vamos comparar o desempenho de um índice de hash em números inteiros com o índice de b-tree equivalente.
testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
   id   |   partno   |  partname   | partdescr | machine_id |  parttype  
--------+------------+-------------+-----------+------------+------------
 500000 | PNo:500000 | Part 500000 |           |     500000 | Suspension
(1 row)

Time: 0,297 ms

Como vemos, com o uso de índices de hash, a velocidade das consultas que verificam a igualdade é muito próxima da velocidade dos índices de árvore B. Diz-se que os índices de hash são marginalmente mais rápidos para igualdade do que as árvores B, na verdade, tivemos que tentar cada consulta duas ou três vezes até que o índice de hash desse um resultado melhor do que o equivalente da árvore b.
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper

Índices GiST


GiST (Generalized Search Tree) é mais do que um único tipo de índice, mas sim uma infraestrutura para construir muitas estratégias de indexação. A distribuição padrão do PostgreSQL fornece suporte para tipos de dados geométricos, tsquery e tsvector. No contrib existem implementações de muitas outras classes de operadores. Ao ler a documentação e o contrib dir, o leitor observará que há uma sobreposição bastante grande entre os casos de uso GiST e GIN:arrays int, pesquisa de texto completo para nomear os casos principais. Nesses casos, o GIN é mais rápido, e a documentação oficial afirma isso explicitamente. No entanto, o GiST oferece amplo suporte a tipos de dados geométricos. Além disso, no momento da redação deste artigo, GiST (e SP-GiST) é o único método significativo que pode ser usado com restrições de exclusão. Veremos um exemplo sobre isso. Suponhamos (permanecendo no campo da engenharia mecânica) que temos a necessidade de definir variações de tipo de máquina para um determinado tipo de máquina, que sejam válidas por um determinado período de tempo; e que para uma variação específica, nenhuma outra variação pode existir para o mesmo tipo de máquina cujo período de tempo se sobrepõe (conflito) com o período de variação específico.
create table machine_type (
	id SERIAL PRIMARY KEY, 
	mtname varchar(50) not null, 
	mtvar varchar(20) not null, 
	start_date date not null, 
	end_date date, 
	CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);

Acima, informamos ao PostgreSQL que para cada nome de tipo de máquina (mtname) pode haver apenas uma variação (mtvar). Start_date denota a data de início do período em que esta variação de tipo de máquina é válida e end_date denota a data de término desse período. Null end_date significa que a variação do tipo de máquina é válida no momento. Agora queremos expressar o requisito não sobreposto com uma restrição. A maneira de fazer isso é com uma restrição de exclusão:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);

A sintaxe EXCLUDE PostgreSQL nos permite especificar muitas colunas de diferentes tipos e com um operador diferente para cada uma. &&é o operador de sobreposição para intervalos de datas e =é o operador de igualdade comum para varchar. Mas contanto que apertemos enter, o PostgreSQL reclama com uma mensagem:
ERROR:  data type character varying has no default operator class for access method "gist"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.

O que está faltando aqui é suporte opclass GiST para varchar. Desde que tenhamos construído e instalado com sucesso a extensão btree_gist, podemos prosseguir com a criação da extensão:
testdb=# create extension btree_gist ;
CREATE EXTENSION

E, em seguida, tentar novamente criar a restrição e testá-la:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR:  conflicting key value violates exclusion constraint "machine_type_per"
DETAIL:  Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).

Índices SP-GiST


SP-GiST, que significa GiST particionado por espaço, como GiST, é uma infraestrutura que permite o desenvolvimento de muitas estratégias diferentes no domínio de estruturas de dados baseadas em disco não balanceadas. A distribuição padrão do PgSQL oferece suporte para pontos bidimensionais, intervalos (qualquer tipo), texto e tipos inet. Assim como o GiST, o SP-GiST pode ser usado em restrições de exclusão, de forma semelhante ao exemplo mostrado no capítulo anterior.

Índices GIN


GIN (Generalized Inverted Index) como GiST e SP-GiST pode fornecer muitas estratégias de indexação. GIN é adequado quando queremos indexar colunas de tipos compostos. A distribuição padrão do PostgreSQL oferece suporte para qualquer tipo de array, jsonb e pesquisa de texto completo (tsvector). No contrib existem implementações de muitas outras classes de operadores. Jsonb, um recurso altamente elogiado do PostgreSQL (e um desenvolvimento relativamente recente (9.4+)) conta com o GIN para suporte ao índice. Outro uso comum do GIN é a indexação para pesquisa de texto completo. A pesquisa de texto completo em PgSQL merece um artigo próprio, então abordaremos aqui apenas a parte de indexação. Primeiro vamos fazer alguma preparação para nossa tabela, dando valores não nulos para a coluna partdescr e atualizando uma única linha com um valor significativo:
testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms

Em seguida, realizamos uma pesquisa de texto na coluna recém-atualizada:
testdb=# select * from part where partdescr @@ 'thermostat';
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 2015,690 ms (00:02,016)

Isso é bem lento, quase 2 segundos para trazer nosso resultado. Agora vamos tentar criar um índice GIN no tipo tsvector e repetir a consulta, usando uma sintaxe amigável ao índice:
testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
   id   |   partno   |  partname   |             partdescr             | machine_id |  parttype  
--------+------------+-------------+-----------------------------------+------------+------------
 500000 | PNo:500000 | Part 500000 | thermostat for the cooling system |     500000 | Suspension
(1 row)

Time: 0,952 ms

E temos uma velocidade de 2.000 vezes. Também podemos notar o tempo relativamente curto que levou para o índice ser criado. Você pode experimentar o uso de GiST em vez de GIN no exemplo acima e medir o desempenho de leituras, gravações e criação de índice para ambos os métodos de acesso.

Índices BRIN


O BRIN (Block Range Index) é a mais nova adição ao conjunto de tipos de índice do PostgreSQL, desde que foi introduzido no PostgreSQL 9.5, tendo apenas alguns anos como recurso central padrão. O BRIN funciona em tabelas muito grandes armazenando informações de resumo para um conjunto de páginas chamado “Intervalo de Blocos”. Os índices BRIN são com perdas (como GiST) e isso requer lógica extra no executor de consultas do PostgreSQL e também a necessidade de manutenção extra. Vamos ver o BRIN em ação:
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
 count
-------
  5001
(1 row)

Time: 5,461 ms

Aqui vemos, em média, uma aceleração de ~ 18 vezes pelo uso do índice BRIN. No entanto, o verdadeiro lar da BRIN está no domínio do big data, por isso esperamos testar essa tecnologia relativamente nova em cenários do mundo real no futuro.