A aplicação correta de índices pode tornar as consultas extremamente rápidas.
Índices usam ponteiros para acessar páginas de dados de forma rápida.
Grandes mudanças aconteceram nos índices no PostgreSQL 11, muitos patches muito aguardados foram lançados.
Vamos dar uma olhada em alguns dos grandes recursos desta versão.
Construções paralelas do índice B-TREE
O PostgreSQL 11 introduziu um patch de infraestrutura para permitir a criação paralela de índices.
Ele só pode ser usado com o índice B-Tree por enquanto.
Construir um índice B-Tree paralelo é duas a três vezes mais rápido do que fazer a mesma coisa sem trabalho paralelo (ou construção serial).
No PostgreSQL 11, a criação de índice paralelo está ativada por padrão.
Existem dois parâmetros importantes:
- max_parallel_workers - Define o número máximo de trabalhadores que o sistema pode suportar para consultas paralelas.
- max_parallel_maintenance_workers - Controla o número máximo de processos de trabalho que podem ser usados para CRIAR ÍNDICE.
Vamos verificar com um exemplo:
severalnines=# CREATE TABLE test_btree AS SELECT generate_series(1,100000000) AS id;
SELECT 100000000
severalnines=# SET maintenance_work_mem = '1GB';
severalnines=# \timing
severalnines=# CREATE INDEX q ON test_btree (id);
TIME: 25294.185 ms (00:25.294)
Vamos tentar com o trabalho paralelo de 8 vias:
severalnines=# SET maintenance_work_mem = '2GB';
severalnines=# SET max_parallel_workers = 16;
severalnines=# SET max_parallel_maintenance_workers = 8;
severalnines=# \timing
severalnines=# CREATE INDEX q1 ON test_btree (id);
TIME: 11001.240 ms (00:11.001)
Podemos ver a diferença de desempenho com o trabalhador paralelo, mais de 60% de desempenho com apenas uma pequena mudança. O Maintenance_work_mem também pode ser aumentado para obter mais desempenho.
A tabela ALTER também ajuda a aumentar os trabalhadores paralelos. A sintaxe abaixo pode ser usada para aumentar os trabalhadores paralelos junto com max_parallel_maintenance_workers. Isso ignora completamente o modelo de custo.
ALTER TABLE test_btree SET (parallel_workers = 24);
Dica:RESET para o padrão assim que a construção do índice for concluída para evitar um plano de consulta adverso.
CREATE INDEX com a opção CONCURRENTLY suporta compilações paralelas sem restrições especiais, apenas a primeira varredura de tabela é realmente executada em paralelo.
Testes de desempenho mais profundos podem ser encontrados aqui.
Adicionar bloqueio de predicado para índices de hash, gist e gin
O PostgreSQL 11 é fornecido com suporte de bloqueio de predicado para índices hash, índices gin e índices gist. Isso tornará o isolamento de transações SERIALIZABLE muito mais eficiente com esses índices.
Benefício:o bloqueio de predicado pode fornecer melhor desempenho no nível de isolamento serializável, reduzindo o número de falsos positivos que levam a falhas de serialização desnecessárias.
No PostgreSQL 10, o intervalo de bloqueio é a relação, mas no PostgreSQL 11, o bloqueio é apenas a página.
Vamos testá-lo.
severalnines=# CREATE TABLE sv_predicate_lock1(c1 INT, c2 VARCHAR(10)) ;
CREATE TABLE
severalnines=# CREATE INDEX idx1_sv_predicate_lock1 ON sv_predicate_lock1 USING 'hash(c1) ;
CREATE INDEX
severalnines=# INSERT INTO sv_predicate_lock1 VALUES (generate_series(1, 100000), 'puja') ;
INSERT 0 100000
severalnines=# BEGIN ISOLATION LEVEL SERIALIZABLE ;
BEGIN
severalnines=# SELECT * FROM sv_predicate_lock1 WHERE c1=10000 FOR UPDATE ;
c1 | c2
-------+-------
10000 | puja
(1 row)
Como podemos ver abaixo, o bloqueio está no nível da página em vez da relação. No PostgreSQL 10 era no nível de relação, então é um GRANDE GANHO para transações simultâneas no PostgreSQL 11.
severalnines=# SELECT locktype, relation::regclass, mode FROM pg_locks ;
locktype | relation | mode
---------------+-------------------------+-----------------
relation | pg_locks | AccessShareLock
relation | idx1_sv_predicate_lock1 | AccessShareLock
relation | sv_predicate_lock1 | RowShareLock
virtualxid | | ExclusiveLock
transactionid | | ExclusiveLock
page | idx1_sv_predicate_lock1 | SIReadLock
tuple | sv_predicate_lock1 | SIReadLock
(7 rows)
Dica:Uma varredura sequencial sempre precisará de um bloqueio de predicado em nível de relação. Isso pode resultar em um aumento da taxa de falhas de serialização. Pode ser útil incentivar o uso de verificações de índice reduzindo random_page_cost e/ou aumentando cpu_tuple_cost.
Permitir atualizações HOT para alguns índices de expressão
O recurso Heap Only Tuple (HOT) elimina entradas de índice redundantes e permite a reutilização do espaço ocupado por tuplas DELETEd ou UPDATEd obsoletas sem executar um vácuo em toda a tabela. Ele reduz o tamanho do índice evitando a criação de entradas de índice com chaves idênticas.
Se o valor de uma expressão de índice permanecer inalterado após UPDATE, permita atualizações HOT onde anteriormente o PostgreSQL não permitia, dando um aumento significativo de desempenho nesses casos.
Isso é especialmente útil para índices como JSON->>campo onde o valor JSON muda, mas o valor indexado não.
Este recurso foi revertido em 11.1 devido à degradação do desempenho (AT Free BSD apenas conforme Simon), mais detalhes / benchmark podem ser encontrados aqui. Isso deve ser corrigido na versão futura.
Permitir que páginas inteiras de índice de hash sejam verificadas
Índice de hash:o planejador de consulta considerará o uso de um índice de hash sempre que uma coluna indexada estiver envolvida em uma comparação usando o operador =. Também não era seguro contra falhas (não registrado no WAL), portanto, precisa ser reconstruído após falhas no banco de dados e as alterações no hash não foram gravadas por meio de replicação de streaming.
No PostgreSQL 10, o índice de hash foi registrado no WAL, ou seja, é seguro contra CRASH e pode ser replicado. Os índices de hash usam muito menos espaço em comparação com o B-Tree para que possam caber melhor na memória.
No PostgreSQL 11, os índices Btree têm uma otimização chamada "single page vácuo", que remove de forma oportuna os ponteiros de índice mortos das páginas de índice, evitando uma enorme quantidade de inchaço de índice, que de outra forma ocorreria. A mesma lógica foi portada para índices Hash. Acelera a reciclagem do espaço, reduzindo o inchaço.
ESTATÍSTICAS do Índice de Função
Agora é possível especificar um valor STATISTICS para uma coluna de índice de função. É altamente valioso para a eficiência de um aplicativo especializado. Agora podemos coletar estatísticas em colunas de expressão, que ajudarão o planejador a tomar uma decisão mais precisa.
severalnines=# CREATE INDEX idx1_stats ON stat ((s1 + s2)) ;
CREATE INDEX
severalnines=# ALTER INDEX idx1_stats ALTER COLUMN 1 SET STATISTICS 1000 ;
ALTER INDEX
severalnines=# \d+ idx1_stats
Index "public.idx1_stats"
Column | Type | Definition | Storage | Stats target
--------+---------+------------+---------+--------------
expr | numeric | (c1 + c2) | main | 1000
btree, for table "public.stat1"
amcheck
Um novo amcheck do módulo Contrib foi adicionado. Apenas índices B-Tree podem ser verificados.
Vamos testar!
severalnines=# CREATE EXTENSION amcheck ;
CREATE EXTENSION
severalnines=# SELECT bt_index_check('idx1_stats') ;
ERROR: invalid page in block 0 of relation base/16385/16580
severalnines=#CREATE INDEX idx1_hash_data1 ON data1 USING hash (c1) ;
CREATE INDEX
severalnines=# SELECT bt_index_check('idx1_hash_data1') ;
ERROR: only B-Tree indexes are supported as targets for verification
DETAIL: Relation "idx1_hash_data1" is not a B-Tree index.
Índice local particionado possível
Antes do PostgreSQL11, não era possível criar um índice em uma tabela filha ou em uma tabela particionada.
No PostgreSQL 11, quando CREATE INDEX é executado em uma tabela particionada/tabela pai, ele cria entradas de catálogo para um índice na tabela particionada e em cascata para criar índices reais nas partições existentes. Ele irá criá-los em partições futuras também.
Vamos tentar criar uma tabela pai e uma partição dela:
severalnines=# create table test_part ( a int, list varchar(5) ) partition by list (list);
CREATE TABLE
severalnines=# create table part_1 partition of test_part for values in ('India');
CREATE TABLE
severalnines=# create table part_2 partition of test_part for values in ('USA');
CREATE TABLE
severalnines=#
severalnines=# \d+ test_part
Table "public.test_part"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
a | integer | | | | plain | |
list | character varying(5) | | | | extended | |
Partition key: LIST (list)
Partitions: part_1 FOR VALUES IN ('India'),
part_2 FOR VALUES IN ('USA')
Vamos tentar criar um índice na tabela pai:
severalnines=# create index i_test on test_part (a);
CREATE INDEX
severalnines=# \d part_2
Table "public.part_2"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('USA')
Indexes:
"part_2_a_idx" btree (a)
severalnines=# \d part_1
Table "public.part_1"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('India')
Indexes:
"part_1_a_idx" btree (a)
O índice é distribuído em cascata para todas as partições do PostgreSQL 11, o que é um recurso muito legal.
Índice de cobertura (incluir CLÁUSULA para índices)
Uma cláusula INCLUDE para adicionar colunas ao índice pode ser especificada. Isso é eficaz ao adicionar colunas que não estão relacionadas a uma restrição exclusiva de um índice exclusivo. As colunas INCLUDE existem apenas para permitir que mais consultas se beneficiem de verificações somente de índice. Apenas índices de árvore B suportam a cláusula INCLUDE por enquanto.
Vamos verificar o comportamento sem INCLUDE. Ele não usará apenas a varredura de índice se colunas adicionais aparecerem no SELECT. Isso pode ser feito usando a cláusula INCLUDE.
severalnines=# CREATE TABLE no_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO no_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX old_unique_idx ON no_include(a, b);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
EXPLAIN ANALYZE SELECT a, b FROM no_include WHERE a < 1000; - It will do index only scan
EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It will not do index only scan as we have extra column in select.
severalnines=# CREATE INDEX old_idx ON no_include (a, b, c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It did index only scan as index on all three columns.
QUERY PLAN
-------------------------------------------------
Index Only Scan using old_idx on no_include
(cost=0.42..14.92 rows=371 width=12)
(actual time=0.086..0.291 rows=334 loops=1)
Index Cond: (a < 1000)
Heap Fetches: 0
Planning Time: 2.108 ms
Execution Time: 0.396 ms
(5 rows)
Vamos tentar com a cláusula include. No exemplo abaixo, a UNIQUE CONSTRAINT é criada nas colunas aeb, mas o índice inclui uma coluna c.
severalnines=# CREATE TABLE with_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO with_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM with_include WHERE a < 10000;
QUERY PLAN
-----------------------------------------------------
Index Only Scan using new_unique_idx on with_include
(cost=0.42..116.06 rows=3408 width=12)
(actual time=0.085..2.348 rows=3334 loops=1)
Index Cond: (a < 10000)
Heap Fetches: 0
Planning Time: 1.851 ms
Execution Time: 2.840 ms
(5 rows)
Não pode haver sobreposição entre as colunas da lista de colunas principal e as da lista de inclusão
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (a);
ERROR: 42P17: included columns must not intersect with key columns
LOCATION: DefineIndex, indexcmds.c:373
Uma coluna usada com uma expressão na lista principal funciona:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(round(a), b) INCLUDE (a);
CREATE INDEX
As expressões não podem ser usadas em uma lista de inclusão porque não podem ser usadas em uma varredura somente de índice:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(a, b) INCLUDE (round(c));
ERROR: 0A000: expressions are not supported in included columns
LOCATION: ComputeIndexAttrs, indexcmds.c:1446
Conclusão
Os novos recursos do PostgreSQL certamente melhorarão a vida dos DBAs, por isso está se tornando uma forte opção alternativa em DB de código aberto. Eu entendo que alguns recursos de índices estão atualmente limitados ao B-Tree, ainda é um ótimo começo da era de execução paralela para o PostgreSQL e indo para uma boa ferramenta para olhar de perto. Obrigado!