Como acelerar o desempenho da inserção no PostgreSQL

215

Estou testando o desempenho de inserção do Postgres. Eu tenho uma tabela com uma coluna com número como seu tipo de dados. Também existe um índice. Enchi o banco de dados usando esta consulta:

insert into aNumber (id) values (564),(43536),(34560) ...

Eu inseri 4 milhões de linhas muito rapidamente 10.000 por vez com a consulta acima. Depois que o banco de dados atingiu 6 milhões de linhas, o desempenho diminuiu drasticamente para 1 milhão de linhas a cada 15 minutos. Existe algum truque para aumentar o desempenho da inserção? Preciso de um ótimo desempenho de inserção neste projeto.

Usando o Windows 7 Pro em uma máquina com 5 GB de RAM.

Luke101
fonte
5
Vale mencionar sua versão Pg em perguntas também. Nesse caso, não faz muita diferença, mas faz muitas perguntas.
Craig Ringer
1
solte os índices na tabela e os gatilhos, se houver, e execute o script de inserção. Depois de concluir o carregamento em massa, você pode recriar os índices.
Sandeep

Respostas:

481

Consulte preencher um banco de dados no manual do PostgreSQL, o excelente artigo de depesz sobre o tópico e esta questão de SO .

(Note-se que esta resposta é sobre dados em massa de carregamento em um DB existente ou criar um novo. Se você estiver interessado DB restaurar o desempenho com pg_restoreou psqlexecução de pg_dumpsaída, grande parte desta não se aplica, uma vez pg_dumpe pg_restorejá fazer coisas como criar aciona e indexa após concluir uma restauração de esquema + dados) .

Há muito a ser feito. A solução ideal seria importar para uma UNLOGGEDtabela sem índices, depois alterá-la para logado e adicionar os índices. Infelizmente, no PostgreSQL 9.4, não há suporte para alterar tabelas de UNLOGGEDpara logadas. 9.5 adiciona ALTER TABLE ... SET LOGGEDpara permitir que você faça isso.

Se você pode colocar seu banco de dados offline para a importação em massa, use pg_bulkload.

De outra forma:

  • Desativar qualquer gatilho na mesa

  • Solte índices antes de iniciar a importação, recrie-os posteriormente. (Leva muito menos tempo para criar um índice em uma passagem do que para adicionar os mesmos dados progressivamente, e o índice resultante é muito mais compacto).

  • Ao fazer a importação em uma única transação, é seguro eliminar restrições de chave estrangeira, fazer a importação e recriar as restrições antes de confirmar. Não faça isso se a importação estiver dividida em várias transações, pois você poderá introduzir dados inválidos.

  • Se possível, use em COPYvez de INSERTs

  • Se você não pode usar, COPYconsidere o uso de INSERTs com valores múltiplos, se possível. Você parece estar fazendo isso já. Não tente listar também muitos valores em um único VALUESembora; esses valores precisam caber na memória algumas vezes; portanto, mantenha-o em algumas centenas por instrução.

  • Lote suas inserções em transações explícitas, fazendo centenas de milhares ou milhões de inserções por transação. Não há limite prático para o AFAIK, mas o lote permite recuperar um erro marcando o início de cada lote nos dados de entrada. Novamente, você parece estar fazendo isso já.

  • Use synchronous_commit=offe muito commit_delaypara reduzir os custos do fsync (). Isso não ajudará muito se você tiver agrupado seu trabalho em grandes transações, no entanto.

  • INSERTou COPYem paralelo de várias conexões. Quantas depende do subsistema de disco do seu hardware; Como regra geral, você deseja uma conexão por disco rígido físico se estiver usando armazenamento conectado diretamente.

  • Defina um checkpoint_segmentsvalor alto e ative log_checkpoints. Veja os logs do PostgreSQL e verifique se ele não está reclamando sobre os pontos de verificação que ocorrem com muita frequência.

  • Se, e somente se você não se importar em perder todo o cluster do PostgreSQL (seu banco de dados e outros no mesmo cluster), por danos catastróficos se o sistema travar durante a importação, você pode parar a página, definir fsync=off, iniciar a página, fazer a importação, então (vitalmente) pare Pg e ajuste fsync=onnovamente. Consulte configuração WAL . Não faça isso se já houver dados importantes para você em qualquer banco de dados na sua instalação do PostgreSQL. Se você definir, fsync=offtambém poderá definir full_page_writes=off; novamente, lembre-se de ligá-lo novamente após a importação para evitar corrupção de banco de dados e perda de dados. Consulte configurações não duráveis no manual da página.

Você também deve ajustar o seu sistema:

  • Use SSDs de boa qualidade para armazenamento, tanto quanto possível. Bons SSDs com caches de write-back confiáveis ​​e protegidos por energia tornam as taxas de confirmação incrivelmente mais rápidas. Eles são menos benéficos quando você segue os conselhos acima - o que reduz a liberação do disco / número de fsync()s - mas ainda pode ser uma grande ajuda. Não use SSDs baratos sem proteção adequada contra falta de energia, a menos que você não se preocupe em manter seus dados.

  • Se você estiver usando RAID 5 ou RAID 6 para armazenamento conectado diretamente, pare agora. Faça backup dos seus dados, reestruture sua matriz RAID para RAID 10 e tente novamente. O RAID 5/6 é inútil para o desempenho de gravação em massa - embora um bom controlador RAID com um grande cache possa ajudar.

  • Se você tiver a opção de usar um controlador RAID de hardware com um grande cache de write-back com bateria, isso pode realmente melhorar o desempenho de gravação para cargas de trabalho com muitas confirmações. Isso não ajuda muito se você estiver usando a confirmação assíncrona com um commit_delay ou se estiver fazendo menos grandes transações durante o carregamento em massa.

  • Se possível, armazene WAL ( pg_xlog) em um disco / matriz de discos separado. Não faz sentido usar um sistema de arquivos separado no mesmo disco. As pessoas geralmente escolhem usar um par RAID1 para o WAL. Novamente, isso tem mais efeito em sistemas com altas taxas de confirmação e tem pouco efeito se você estiver usando uma tabela não registrada como destino de carregamento de dados.

Você também pode estar interessado em Otimizar o PostgreSQL para testes rápidos .

Craig Ringer
fonte
1
Você concorda que a penalidade de gravação do RAID 5/6 é um pouco atenuada se SSDs de boa qualidade forem usados? Obviamente, ainda há uma penalidade, mas acho que a diferença é muito menos dolorosa do que nos HDDs.
1
Eu não testei isso. Eu diria que é provavelmente menos ruim - os efeitos desagradáveis ​​de amplificação de gravação e (para pequenas gravações) a necessidade de um ciclo de leitura-modificação-gravação ainda existem, mas a penalidade severa por excesso de procura não deve ser um problema.
Craig Ringer
Podemos simplesmente desativar os índices em vez de descartá-los, por exemplo, definindo indisvalid( postgresql.org/docs/8.3/static/catalog-pg-index.html ) como false, carregar os dados e colocar os índices on-line por REINDEX?
Vladislav Rastrusny
1
@CraigRinger Eu testei o RAID-5 vs RAID-10 com SSDs em um Perc H730. O RAID-5 é realmente mais rápido. Também pode ser interessante notar que as inserções / transações em combinação com os bytea grandes parecem ser mais rápidas que a cópia. No geral, bons conselhos.
Atlaste
2
Alguém está vendo grandes melhorias na velocidade UNLOGGED? Um teste rápido mostra algo como uma melhoria de 10 a 20%.
serg
15

O uso COPY table TO ... WITH BINARYque está de acordo com a documentação é " um pouco mais rápido que os formatos de texto e CSV ". Faça isso apenas se você tiver milhões de linhas para inserir e se estiver confortável com dados binários.

Aqui está um exemplo de receita em Python, usando psycopg2 com entrada binária .

Mike T
fonte
1
O modo binário pode economizar muito tempo em algumas entradas, como registros de data e hora, em que a análise não é trivial. Para muitos tipos de dados, ele não oferece muitos benefícios ou pode até ser um pouco mais lento devido ao aumento da largura de banda (por exemplo, números inteiros pequenos). Bom ponto de levantá-lo.
Craig Ringer
11

Além da excelente postagem de Craig Ringer e da depesz, se você deseja acelerar suas inserções através da interface ODBC ( psqlodbc ) usando inserções de instruções preparadas em uma transação, há algumas coisas extras que você precisa fazer para fazê-lo trabalhe rápido:

  1. Defina o nível de reversão em erros como "Transação", especificando Protocol=-1na cadeia de conexão. Por padrão, o psqlodbc usa o nível "Instrução", que cria um SAVEPOINT para cada instrução em vez de uma transação inteira, tornando as inserções mais lentas.
  2. Use instruções preparadas do servidor especificando UseServerSidePrepare=1na cadeia de conexão. Sem essa opção, o cliente envia a instrução de inserção inteira junto com cada linha sendo inserida.
  3. Desative a confirmação automática em cada instrução usando SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF), 0);
  4. Depois que todas as linhas foram inseridas, confirme a transação usando SQLEndTran(SQL_HANDLE_DBC, conn, SQL_COMMIT);. Não há necessidade de abrir explicitamente uma transação.

Infelizmente, o psqlodbc "implementa" SQLBulkOperationsemitindo uma série de instruções de inserção não preparadas, para que, para obter a inserção mais rápida, seja necessário codificar manualmente as etapas acima.

Maxim Egorushkin
fonte
O tamanho do buffer de soquete grande, A8=30000000na cadeia de conexão, também deve ser usado para acelerar as inserções.
Andrus
9

Passei cerca de 6 horas na mesma questão hoje. As inserções vão a uma velocidade 'normal' (menos de 3s por 100K) até 5MI (do total de 30MI) de linhas e, em seguida, o desempenho diminui drasticamente (até 1m por 100K).

Não vou listar todas as coisas que não funcionaram e cortar diretamente a carne.

I deixou cair uma chave primária na tabela de destino (que era um GUID) e meu 30mi ou linhas feliz fluiu ao seu destino a uma velocidade constante de menos de 3 segundos por 100K.

Dennis
fonte
6

Se você inseriu colunas com UUIDs (o que não é exatamente o seu caso) e adiciona à resposta @Dennis (ainda não posso comentar), seja avisado do que usar gen_random_uuid () (requer o módulo PG 9.4 e pgcrypto). muito mais rápido que uuid_generate_v4 ()

=# explain analyze select uuid_generate_v4(),* from generate_series(1,10000);
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=11.674..10304.959 rows=10000 loops=1)
 Planning time: 0.157 ms
 Execution time: 13353.098 ms
(3 filas)

vs


=# explain analyze select gen_random_uuid(),* from generate_series(1,10000);
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=252.274..418.137 rows=10000 loops=1)
 Planning time: 0.064 ms
 Execution time: 503.818 ms
(3 filas)

Além disso, é a maneira oficial sugerida de fazer isso

Nota

Se você precisar apenas de UUIDs gerados aleatoriamente (versão 4), considere usar a função gen_random_uuid () do módulo pgcrypto.

Isso reduziu o tempo de inserção de ~ 2 horas para ~ 10 minutos para 3,7M de linhas.

Francisco Reynoso
fonte
1

Para otimizar o desempenho da inserção, desative o índice, se for uma opção para você. Fora isso, um melhor hardware (disco, memória) também é útil

Icaro
fonte
-1

Também encontrei esse problema de desempenho de inserção. Minha solução é gerar algumas rotinas para concluir o trabalho de inserção. Enquanto isso, SetMaxOpenConnsdeve receber um número adequado, caso contrário, muitos erros de conexão aberta serão alertados.

db, _ := sql.open() 
db.SetMaxOpenConns(SOME CONFIG INTEGER NUMBER) 
var wg sync.WaitGroup
for _, query := range queries {
    wg.Add(1)
    go func(msg string) {
        defer wg.Done()
        _, err := db.Exec(msg)
        if err != nil {
            fmt.Println(err)
        }
    }(query)
}
wg.Wait()

A velocidade de carregamento é muito mais rápida para o meu projeto. Esse trecho de código apenas deu uma idéia de como ele funciona. Os leitores devem poder modificá-lo facilmente.

Patrick
fonte
Bem, você pode dizer isso. Mas reduz o tempo de execução de algumas horas para vários minutos para milhões de linhas no meu caso. :)
Patrick