Acelere a criação do índice parcial do Postgres

8

Eu estou tentando criar índices parciais para uma tabela estática grande (1.2TB) no Postgres 9.4.

Como meus dados são completamente estáticos, posso inserir todos os dados e criar todos os índices.

Nesta tabela de 1,2 TB, tenho uma coluna chamada run_idque divide os dados de maneira limpa. Obtivemos um ótimo desempenho ao criar índices que cobrem uma variedade de run_ids. Aqui está um exemplo:

CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Esses índices parciais nos dão a velocidade de consulta desejada. Infelizmente, a criação de cada índice parcial leva cerca de 70 minutos.

Parece que estamos com CPU limitada ( topestá mostrando 100% para o processo).
Existe algo que eu possa fazer para acelerar a criação de nossos índices parciais?

Especificações do sistema:

  • 18 core Xeon
  • 192GB RAM
  • 12 SSDs em RAID
  • Os autovacuums estão desativados
  • maintenance_work_mem: 64GB (Muito alto?)

Especificações da tabela:

  • Tamanho: 1,26 TB
  • Número de linhas: 10.537 bilhões
  • Tamanho típico do índice: 3,2 GB (existe uma variação de ~ 0,5 GB)

Definição da tabela:

CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))

(Não leia muito os nomes das colunas - eu os ofusquei um pouco.)

Informações de fundo:

  • Temos uma equipe separada no local que consome esses dados, mas na verdade existem apenas um ou dois usuários. (Todos esses dados são gerados por meio de uma simulação.) Os usuários só começam a analisar os dados quando as inserções são concluídas e os índices são completamente construídos. Nossa principal preocupação é reduzir o tempo necessário para gerar dados utilizáveis ​​e, no momento, o gargalo é o tempo de criação do índice.
  • A velocidade da consulta foi completamente adequada ao usar parciais. Na verdade, acho que poderíamos aumentar o número de execuções que cada índice cobre e ainda manter um desempenho de consulta suficientemente bom.
  • Meu palpite é que teremos que particionar a tabela. Estamos tentando esgotar todas as outras opções antes de seguir esse caminho.
queimado
fonte
Essas informações adicionais seriam instrumentais: tipos de dados das colunas envolvidas, consulta típica, cardinalidade (contagem de linhas), quantas diferentes run_id? Distribuído uniformemente? Tamanho do índice resultante no disco? Os dados são estáticos, ok. Mas você é o único usuário?
Erwin Brandstetter
Atualizado com mais informações.
burnsy 27/05
11
" Autovacuums estão desligados " - por quê? Essa é uma péssima ideia. Isso impede que a coleta de estatísticas e, assim, irá produzir planos de consulta maus
a_horse_with_no_name
@a_horse_with_no_name Nós chutar manualmente de uma análise após todos os dados são inseridos
burnsy
Sua situação ainda não está clara para mim. Como são suas consultas? Se sua mesa for completely static, então o que você quer dizer com isso We have a separate team onsite that consumes this data? Você apenas indexa o intervalo run_id >= 266 AND run_id <= 270ou a tabela inteira? Qual é a expectativa de vida de cada índice / quantas consultas o usarão? Para quantos valores diferentes run_id? Soa como ~ 15 milhões. linhas por run_id, o que tornaria cerca de 800 valores diferentes para run_id? Por que obj_type_set, by_s_id, seqnão definido NOT NULL? Qual porcentagem aproximada de valores NULL para cada um?
Erwin Brandstetter

Respostas:

8

Índice BRIN

Disponível desde o Postgres 9.5 e provavelmente exatamente o que você está procurando. Criação de índice muito mais rápida, índice muito menor. Mas as consultas normalmente não são tão rápidas. O manual:

BRIN significa Block Range Index. O BRIN foi projetado para manipular tabelas muito grandes, nas quais determinadas colunas têm alguma correlação natural com sua localização física na tabela. Um intervalo de blocos é um grupo de páginas fisicamente adjacentes na tabela; para cada intervalo de blocos, algumas informações de resumo são armazenadas pelo índice.

Leia mais, há mais.
Depesz fez um teste preliminar.

O ideal para o seu caso: Se você pode escrever linhas agrupadas em run_id, seu índice torna-se muito pequeno e criação muito mais barato.

CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Você pode até indexar a tabela inteira .

Layout da tabela

Seja o que for que você faça, você pode salvar 8 bytes perdidos no preenchimento devido a requisitos de alinhamento por linha, ording colunas como esta:

CREATE TABLE run.perception(
  id               bigint NOT NULL PRIMARY KEY
, run_id           bigint NOT NULL
, frame            bigint NOT NULL
, by_anyone        bigint NOT NULL
, by_me            bigint NOT NULL
, owning_p_id      bigint NOT NULL
, subj_id          bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set     bigint
, by_s_id          integer
, seq              integer
, by               varchar(45) NOT NULL -- or just use type text
);

Torna sua tabela 79 GB menor se nenhuma das colunas tiver valores NULL. Detalhes:

Além disso, você possui apenas três colunas que podem ser NULL. O bitmap NULL ocupa 8 bytes para 9 - 72 colunas. Se apenas uma coluna inteira for NULL, existe uma caixa de canto para um paradoxo de armazenamento: seria mais barato usar um valor fictício: 4 bytes desperdiçados, mas 8 bytes salvos por não precisar de um bitmap NULL para a linha. Mais detalhes aqui:

Índices parciais

Dependendo das suas consultas reais, pode ser mais eficiente ter esses cinco índices parciais, em vez do acima:

CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;

Execute uma transação para cada.

A remoção run_idcomo coluna de índice dessa maneira salva 8 bytes por entrada de índice - 32 em vez de 40 bytes por linha. Cada índice também é mais barato de criar, mas criar cinco em vez de apenas um leva muito mais tempo para uma tabela grande demais para permanecer no cache (como @ Jürgen e @Chris comentaram). Portanto, isso pode ou não ser útil para você.

Particionamento

Com base na herança - a única opção até o Postgres 9.5.
(A nova partição declarativa no Postgres 11 ou, de preferência, 12 é mais inteligente.)

O manual:

Todas as restrições em todos os filhos da tabela pai são examinadas durante a exclusão de restrição; portanto, é provável que um grande número de partições aumente consideravelmente o tempo de planejamento da consulta. Portanto, o particionamento baseado em herança herdada funcionará bem com até cem partições ; não tente usar muitos milhares de partições.

Negrito ênfase minha. Consequentemente, estimando 1000 valores diferentes para run_id, você faria partições com cerca de 10 valores cada.


maintenance_work_mem

Perdi que você já está se ajustando maintenance_work_memna minha primeira leitura. Deixarei citações e conselhos na minha resposta para referência. Por documentação:

maintenance_work_mem (inteiro)

Especifica a quantidade máxima de memória a ser utilizados pelas operações de manutenção, tais como VACUUM, CREATE INDEXe ALTER TABLE ADD FOREIGN KEY. O padrão é 64 megabytes ( 64MB). Como apenas uma dessas operações pode ser executada por vez por uma sessão de banco de dados e uma instalação normalmente não possui muitas delas em execução simultaneamente, é seguro definir esse valor significativamente maior que work_mem. Configurações maiores podem melhorar o desempenho para aspirar e restaurar dumps do banco de dados.

Observe que, quando autovacuumexecutado, até o autovacuum_max_workersmomento em que essa memória pode ser alocada, tenha cuidado para não definir o valor padrão muito alto. Pode ser útil controlar isso separadamente setting autovacuum_work_mem.

Eu definiria apenas o valor necessário - o que depende do tamanho do índice desconhecido (para nós). E apenas localmente para a sessão de execução. Como a citação explica, uma configuração geral muito alta pode passar fome do servidor, caso contrário, o vácuo automático também pode exigir mais RAM. Além disso, não defina muito mais do que o necessário, mesmo na sessão de execução, a RAM livre pode ser bem utilizada no cache de dados.

Pode ficar assim:

BEGIN;

SET LOCAL maintenance_work_mem = 10GB;  -- depends on resulting index size

CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

COMMIT;

Sobre SET LOCAL:

Os efeitos SET LOCALduram apenas até o final da transação atual, confirmada ou não.

Para medir tamanhos de objeto:

O servidor geralmente deve ser configurado razoavelmente, caso contrário, obviamente.

Erwin Brandstetter
fonte
Aposto que o trabalho dele é limitado, pois a tabela é muito maior que a RAM. Ler a tabela com mais frequência piorará o problema, independentemente de haver memória suficiente para classificar cada índice criado na memória ou não.
Jürgen Strobel
Estou com Jurgen neste. Acredito que, devido ao tamanho da tabela, em essência você precise executar uma varredura seqüencial completa na tabela por índice criado. Além disso, não tenho certeza de que você verá tanto desempenho aumentar com a criação de índices parciais separados (tenho 90% de certeza de que não verá nenhum aumento, mas com isso eu poderia estar desligado). A solução para criação de índice envolveria a criação de um índice em todo o intervalo que você deseja consultar como um "índice parcial único" para manter o tempo geral de compilação baixo.
Chris
@ Chris: Eu concordo, 5 índices levarão mais tempo para criar do que apenas um (mesmo que todos juntos sejam menores, criar cada índice é mais barato e as consultas podem ser mais rápidas). Pensando um pouco mais, esse deve ser um caso de uso perfeito para um índice BRIN no Postgres 9.5.
Erwin Brandstetter
3

Talvez isso seja um excesso de engenharia. Você já tentou usar um único índice completo? Índices parciais cobrindo a tabela inteira juntos não fornecem muito ganho, se houver, para pesquisas de índice e, a partir do seu texto, deduzo que você tenha índices para todos os run_ids? Pode haver algumas vantagens em indexar varreduras com índices parciais, ainda assim eu compararia primeiro a solução simples de um índice.

Para cada criação de índice, você precisa de uma verificação completa de E / S na tabela. Portanto, a criação de vários índices parciais requer muito mais IO lendo a tabela do que para um único índice, embora a classificação se espalhe para o disco para o único índice grande. Se você insistir em índices parciais, poderá tentar construir todos (ou vários) índices ao mesmo tempo em paralelo (se a memória permitir).

Para uma estimativa aproximada de maintenance_work_mem necessária para classificar todos os run_ids, que são bigints de 8 bytes, na memória, você precisará de 10,5 * 8 GB + alguma sobrecarga.

Jürgen Strobel
fonte
0

Você também pode criar os índices em outros espaços de tabela que não o padrão. Esses espaços de tabela podem apontar para discos que não são redundantes (apenas recrie os índices se falharem) ou estão em matrizes mais rápidas.

Você também pode considerar particionar a tabela usando os mesmos critérios que seus índices parciais. Isso permitiria a mesma velocidade que o índice durante a consulta, sem realmente criar nenhum índice.

Kirk Roybal
fonte