Por que o Postgres UPDATE demorou 39 horas?

17

Eu tenho uma tabela do Postgres com ~ 2.1 milhões de linhas. Eu executei a atualização abaixo:

WITH stops AS (
    SELECT id,
           rank() OVER (ORDER BY offense_timestamp,
                     defendant_dl,
                     offense_street_number,
                     offense_street_name) AS stop
    FROM   consistent.master
    WHERE  citing_jurisdiction=1
)

UPDATE consistent.master
SET arrest_id=stops.stop
FROM stops
WHERE master.id = stops.id;

Essa consulta levou 39 horas para ser executada. Estou executando isso em um processador de laptop i7 Q720 de 4 núcleos (físico), muita RAM, nada mais executando a grande maioria das vezes. Sem restrições de espaço no disco rígido. A mesa havia sido recentemente aspirada, analisada e reindexada.

Durante todo o tempo em que a consulta estava em execução, pelo menos após a WITHconclusão inicial , o uso da CPU era geralmente baixo e o HDD estava em uso 100%. O disco rígido estava sendo usado com tanta força que qualquer outro aplicativo rodava consideravelmente mais devagar que o normal.

A configuração de energia do laptop estava em alto desempenho (Windows 7 x64).

Aqui está o EXPLAIN:

Update on master  (cost=822243.22..1021456.89 rows=2060910 width=312)
  CTE stops
    ->  WindowAgg  (cost=529826.95..581349.70 rows=2060910 width=33)
          ->  Sort  (cost=529826.95..534979.23 rows=2060910 width=33)
                Sort Key: consistent.master.offense_timestamp, consistent.master.defendant_dl, consistent.master.offense_street_number, consistent.master.offense_street_name
                ->  Seq Scan on master  (cost=0.00..144630.06 rows=2060910 width=33)
                      Filter: (citing_jurisdiction = 1)
  ->  Hash Join  (cost=240893.51..440107.19 rows=2060910 width=312)
        Hash Cond: (stops.id = consistent.master.id)
        ->  CTE Scan on stops  (cost=0.00..41218.20 rows=2060910 width=48)
        ->  Hash  (cost=139413.45..139413.45 rows=2086645 width=268)
              ->  Seq Scan on master  (cost=0.00..139413.45 rows=2086645 width=268)

citing_jurisdiction=1exclui apenas algumas dezenas de milhares de linhas. Mesmo com essa WHEREcláusula, ainda estou operando em mais de 2 milhões de linhas.

O disco rígido é criptografado com o TrueCrypt 7.1a. Que atrasa as coisas um pouco, mas não o suficiente para causar uma consulta para tomar que muitas horas.

A WITHpeça leva apenas cerca de 3 minutos para ser executada.

O arrest_idcampo não tinha índice para chave estrangeira. Existem 8 índices e 2 chaves estrangeiras nesta tabela. Todos os outros campos da consulta são indexados.

O arrest_idcampo não tinha restrições, exceto NOT NULL.

A tabela possui 32 colunas no total.

arrest_idé do tipo variando de caracteres (20) . Percebo que rank()produz um valor numérico, mas tenho que usar caracteres variáveis ​​(20) porque tenho outras linhas em citing_jurisdiction<>1que esses dados não são numéricos para esse campo.

O arrest_idcampo estava em branco para todas as linhas com citing_jurisdiction=1.

Este é um laptop pessoal sofisticado (de 1 ano atrás). Eu sou o único usuário. Nenhuma outra consulta ou operação estava em execução. O bloqueio parece improvável.

Não há gatilhos em nenhum lugar nesta tabela ou em qualquer outro lugar no banco de dados.

Outras operações nesse banco de dados nunca levam uma quantidade abundante de tempo. Com a indexação adequada, as SELECTconsultas geralmente são bastante rápidas.

Aren Cambre
fonte
Aqueles Seq Scansão um pouco assustador ...
rogerdpack

Respostas:

18

Aconteceu recentemente algo semelhante com uma tabela de 3,5 milhões de linhas. Minha atualização nunca terminaria. Depois de muitas experiências e frustrações, finalmente encontrei o culpado. Acabou sendo os índices na tabela que estão sendo atualizados.

A solução foi eliminar todos os índices da tabela que está sendo atualizada antes de executar a instrução update. Depois disso, a atualização terminou em alguns minutos. Depois que a atualização foi concluída, recriei os índices e voltei aos negócios. Provavelmente, isso não o ajudará neste momento, mas pode alguém procurar respostas.

Eu manteria os índices na tabela da qual você está puxando os dados. Esse não precisará continuar atualizando nenhum índice e deve ajudar a encontrar os dados que você deseja atualizar. Funcionou bem em um laptop lento.

JC Avena
fonte
3
Estou mudando a melhor resposta para você. Desde que publiquei isso, encontrei outras situações em que os índices são o problema, mesmo se a coluna que está sendo atualizada já tiver um valor e não tiver um índice (!). Parece que o Postgres tem um problema com a maneira como gerencia índices em outras colunas. Não há razão para esses outros índices aumentarem o tempo de consulta de uma atualização quando a única alteração em uma tabela é atualizar uma coluna não indexada e você não está aumentando o espaço alocado para nenhuma linha dessa coluna.
Aren Cambre
11
Obrigado! Espero que ajude os outros. Isso teria me poupado horas de dores de cabeça por algo aparentemente muito simples.
JC Avena
5
@ArenCambre - existe um motivo: o PostgreSQL copia a linha inteira para um local diferente e marca a versão antiga como excluída. É assim que o PostgreSQL implementa o MVCC (Multi-Version Concurrency Control).
Piotr Findeisen
Minha pergunta é ... por que é o culpado? Veja também stackoverflow.com/a/35660593/32453
rogerdpack
15

O seu maior problema é fazer grandes quantidades de trabalho com muita gravação e muita procura em um disco rígido de laptop. Isso nunca será rápido, não importa o que você faça, especialmente se for o tipo de unidade mais lenta de 5400 RPM enviada em muitos laptops.

TrueCrypt torna as coisas mais lentas do que "um pouco" para gravações. As leituras serão razoavelmente rápidas, mas as gravações fazem o RAID 5 parecer mais rápido. A execução de um banco de dados em um volume TrueCrypt será uma tortura para gravações, especialmente gravações aleatórias.

Nesse caso, acho que você estaria perdendo seu tempo tentando otimizar a consulta. Você está reescrevendo a maioria das linhas de qualquer maneira, e será lento com sua situação de gravação horrível. O que eu recomendaria é:

BEGIN;
SELECT ... INTO TEMPORARY TABLE master_tmp ;
TRUNCATE TABLE consistent.master;
-- Now DROP all constraints on consistent.master, then:
INSERT INTO consistent.master SELECT * FROM master_tmp;
-- ... and re-create any constraints.

Eu suspeito que será mais rápido do que apenas remover e recriar as restrições sozinho, porque um UPDATE terá padrões de gravação bastante aleatórios que matarão seu armazenamento. Duas inserções em massa, uma em uma tabela não registrada e outra em uma tabela registrada no WAL sem restrições, provavelmente serão mais rápidas.

Se você possui backups absolutamente atualizados e não se importa de restaurar seu banco de dados a partir de backups, também pode reiniciar o PostgreSQL com o fsync=offparâmetro e full_page_writes=off temporariamente para esta operação em massa. Qualquer problema inesperado, como perda de energia ou falha no sistema operacional, deixará seu banco de dados irrecuperável fsync=off.

O POSTGreSQL equivalente a "sem registro" é usar tabelas não registradas. Essas tabelas não registradas são truncadas se o banco de dados for encerrado de maneira suja enquanto estiver sujo. O uso de tabelas não registradas reduzirá pela metade a carga de gravação e reduzirá o número de pesquisas, para que elas sejam MUITO mais rápidas.

Como no Oracle, pode ser uma boa idéia descartar um índice e depois recriá-lo após uma grande atualização em lote. O planejador do PostgreSQL não pode descobrir que uma grande atualização está ocorrendo, pausar as atualizações do índice e reconstruir o índice no final; mesmo que pudesse, seria muito difícil descobrir em que ponto isso valia a pena, especialmente com antecedência.

Craig Ringer
fonte
Essa resposta é evidente na grande quantidade de gravações e no terrível desempenho da criptografia, além da lenta movimentação do laptop. Eu também observaria que a presença de 8 índices produz muitas gravações extras e anula a aplicabilidade das atualizações de linhas em bloco HOT , portanto, a remoção de índices e o uso de um fator de preenchimento mais baixo na tabela podem impedir uma grande migração de linhas
dbenhur
11
Bom apelo para aumentar as chances de HOTs com um fator de preenchimento - embora com o TrueCrypt forçando os ciclos de leitura e reescrita de blocos em blocos enormes, não tenho certeza se isso ajudará muito; a migração de linha pode até ser mais rápida, porque o crescimento da tabela está fazendo pelo menos blocos de gravações lineares.
Craig Ringer
2,5 anos depois, estou fazendo algo semelhante, mas em uma tabela maior. Apenas para ter certeza, é uma boa ideia eliminar todos os índices, mesmo que a única coluna que estou atualizando não esteja indexada?
Aren Cambre
11
@ArenCambre Nesse caso ... bem, é complicado. Se a maioria de suas atualizações estiver qualificada HOT, é melhor deixar os índices no lugar. Caso contrário, é provável que você queira largar e recriar. A coluna não está indexada, mas para poder fazer uma atualização HOT também é preciso haver espaço livre na mesma página, portanto, depende um pouco da quantidade de espaço morto existente na tabela. Se for principalmente de gravação, eu diria que abandone todos os índices. Se houver lotes atualizados, pode haver buracos e você pode ficar bem. Ferramentas como pageinspecte pg_freespacemappodem ajudar a determinar isso.
Craig Ringer
Obrigado. Nesse caso, é uma coluna booleana que já tinha uma entrada em todas as linhas. Eu estava mudando a entrada em algumas linhas. Acabei de confirmar: a atualização levou apenas 2 horas após a queda de todos os índices. Antes, tive que interromper a atualização após 18 horas, porque estava demorando muito. Isso apesar do fato de que a coluna que estava sendo atualizada definitivamente não foi indexada.
Aren Cambre
2

Alguém dará uma resposta melhor para o Postgres, mas aqui estão algumas observações da perspectiva do Oracle que podem ser aplicadas (e os comentários são muito longos para o campo de comentário).

Minha primeira preocupação seria tentar atualizar 2 milhões de linhas em uma transação. No Oracle, você escreveria uma imagem anterior de cada bloco sendo atualizado para que outra sessão ainda tenha uma leitura consistente sem ler seus blocos modificados e você poderá reverter. Essa é uma longa reversão sendo construída. Geralmente, é melhor você fazer as transações em pequenos pedaços. Diga 1.000 registros por vez.

Se você tiver índices na tabela e a tabela for considerada fora de operação durante a manutenção, é melhor remover os índices antes de uma grande operação e recriá-los novamente depois. Mais barato, tentando constantemente manter os índices a cada registro atualizado.

O Oracle permite dicas "sem registro" nas instruções para interromper o diário. Ele acelera bastante as instruções, mas deixa seu banco de dados em uma situação "irrecuperável". Então, você deseja fazer backup antes e fazer backup novamente imediatamente depois. Não sei se o Postgres tem opções semelhantes.

Glenn
fonte
O PostgreSQL não tem problemas com uma longa reversão, não existe. O ROLBACK é muito rápido no PostgreSQL, não importa o tamanho da sua transação. Oracle! = PostgreSQL
Frank Heikens 28/03
@FrankHeikens Obrigado, isso é interessante. Vou ter que ler como funciona o diário no Postgres. Para que todo o conceito de transação funcione, de alguma forma duas versões diferentes dos dados precisam ser mantidas durante uma transação, a imagem anterior e a imagem posterior, e esse é o mecanismo ao qual estou me referindo. De uma forma ou de outra, acho que existe um limite além do qual os recursos para manter a transação serão muito caros.
Glenn
2
O @Glenn postgres mantém as versões de uma linha na própria tabela - veja aqui para obter uma explicação. O compromisso é que você tenha tuplas "mortas" por aí, que são limpas de forma assíncrona com o que é chamado de "vácuo" no postgres (o Oracle não precisa de vácuo porque nunca possui linhas "mortas" na própria tabela)
Jack Douglas
Você é bem-vindo, e bastante tardiamente: bem vindo ao site :-)
Jack Douglas
@Glenn O documento canônico para o controle de concorrência de versão de linha do PostgreSQL é postgresql.org/docs/current/static/mvcc-intro.html e vale a pena ler. Veja também wiki.postgresql.org/wiki/MVCC . Observe que o MVCC com linhas mortas e VACUUMé apenas metade da resposta; O PostgreSQL também usa o chamado "write ahead log" (efetivamente um diário) para fornecer confirmações atômicas e proteger contra gravações parciais, etc. Veja postgresql.org/docs/current/static/wal-intro.html
Craig Ringer