Temos uma tabela de 2,2 GB no Postgres com 7.801.611 linhas. Estamos adicionando uma coluna uuid / guid a ela e estou me perguntando qual é a melhor maneira de preencher essa coluna (pois queremos adicionar uma NOT NULL
restrição a ela).
Se eu entendi o Postgres corretamente, uma atualização é tecnicamente uma exclusão e inserção, portanto isso basicamente está reconstruindo toda a tabela de 2,2 gb. Também temos um escravo em funcionamento, para que não queremos que isso fique para trás.
Existe alguma maneira melhor do que escrever um script que o preencha lentamente ao longo do tempo?
postgresql
storage
ddl
Collin Peters
fonte
fonte
ALTER TABLE .. ADD COLUMN ...
parte ou deve ser respondida também?Respostas:
Depende muito dos detalhes de suas necessidades.
Se você tiver espaço livre suficiente (pelo menos 110% de
pg_size_pretty((pg_total_relation_size(tbl))
) no disco e puder permitir um bloqueio de compartilhamento por algum tempo e um bloqueio exclusivo por um período muito curto , crie uma nova tabela incluindo auuid
coluna usandoCREATE TABLE AS
. Por quê?O código abaixo usa uma função do
uuid-oss
módulo adicional .Bloqueie a tabela contra alterações simultâneas no
SHARE
modo (ainda permitindo leituras simultâneas). Tentativas de gravar na tabela aguardarão e eventualmente falharão. Ver abaixo.Copie a tabela inteira enquanto preenche a nova coluna rapidamente - possivelmente solicitando linhas de maneira favorável enquanto estiver nela.
Se você deseja reordenar linhas, defina o valor
work_mem
mais alto possível (apenas para a sua sessão, e não globalmente).Em seguida, adicione restrições, chaves estrangeiras, índices, gatilhos etc. à nova tabela. Ao atualizar grandes partes de uma tabela, é muito mais rápido criar índices do zero do que adicionar linhas iterativamente.
Quando a nova tabela estiver pronta, descarte a antiga e renomeie a nova para substituí-la. Somente esta última etapa adquire um bloqueio exclusivo na tabela antiga para o restante da transação - que deve ser muito curta agora.
Também requer que você exclua qualquer objeto, dependendo do tipo de tabela (visualizações, funções usando o tipo de tabela na assinatura, ...) e os recrie posteriormente.
Faça tudo em uma transação para evitar estados incompletos.
Isso deve ser mais rápido. Qualquer outro método de atualização em vigor também deve reescrever a tabela inteira, apenas de uma maneira mais cara. Você só seguiria essa rota se não tiver espaço livre suficiente no disco ou não puder bloquear a tabela inteira ou gerar erros para tentativas simultâneas de gravação.
O que acontece com gravações simultâneas?
Outras transações (em outras sessões) que tentam
INSERT
/UPDATE
/DELETE
na mesma tabela após a transação terSHARE
bloqueado o bloqueio aguardarão até que o bloqueio seja liberado ou o tempo limite comece, o que ocorrer primeiro. Eles falharão de qualquer maneira, uma vez que a tabela na qual eles estavam tentando gravar foi excluída.A nova tabela possui um novo OID da tabela, mas as transações simultâneas já resolveram o nome da tabela para o OID da tabela anterior . Quando a trava é finalmente liberada, eles tentam trancar a mesa antes de escrever nela e descobrem que ela se foi. O Postgres responderá:
Onde
123456
está o OID da tabela antiga. Você precisa capturar essa exceção e tentar novamente as consultas no código do seu aplicativo para evitá-lo.Se você não pode permitir que isso aconteça, você deve manter sua tabela original.
Duas alternativas mantendo a tabela existente
Atualização no local (possivelmente executando a atualização em pequenos segmentos por vez) antes de adicionar a
NOT NULL
restrição. Adicionar uma nova coluna com valores NULL e semNOT NULL
restrição é barato.Desde o Postgres 9.2, você também pode criar uma
CHECK
restrição comNOT VALID
:Isso permite que você atualize linhas ponto a ponto - em várias transações separadas . Isso evita manter os bloqueios de linha por muito tempo e também permite a reutilização de linhas mortas. (Você precisará executar
VACUUM
manualmente se não houver tempo suficiente para o vácuo automático entrar.) Finalmente, adicione aNOT NULL
restrição e remova aNOT VALID CHECK
restrição:Resposta relacionada discutindo
NOT VALID
em mais detalhes:Prepare o novo estado em uma tabela temporária ,
TRUNCATE
o original e refil a partir da tabela temporária. Tudo em uma transação . Você ainda precisaSHARE
bloquear antes de preparar a nova tabela para evitar a perda de gravações simultâneas.Detalhes nestas respostas relacionadas ao SO:
fonte
LOCK
até oDROP
. Eu só podia expressar palpites selvagens e inúteis. Quanto ao 2., considere o adendo à minha resposta.Não tenho uma resposta "melhor", mas tenho uma resposta "menos ruim" que pode permitir que você faça as coisas razoavelmente rápido.
Minha tabela tinha linhas de 2MM e o desempenho da atualização foi ruim quando tentei adicionar uma coluna de registro de data e hora secundária que padronizou a primeira.
Depois de 40 minutos de espera, tentei fazer isso em um pequeno lote para ter uma idéia de quanto tempo isso levaria - a previsão era de 8 horas.
A resposta aceita é definitivamente melhor - mas esta tabela é muito usada no meu banco de dados. Existem algumas dezenas de mesas que FKEY nele; Eu queria evitar trocar CHAVES ESTRANGEIRAS em tantas tabelas. E depois há pontos de vista.
Um pouco de pesquisa de documentos, estudos de caso e StackOverflow, e eu tive o "A-Ha!" momento. O dreno não estava no UPDATE principal, mas em todas as operações do INDEX. Minha tabela tinha 12 índices - alguns para restrições exclusivas, outros para acelerar o planejador de consultas e alguns para pesquisa de texto completo.
Cada linha que foi ATUALIZADA não estava apenas trabalhando em um DELETE / INSERT, mas também na sobrecarga de alterar cada índice e verificar restrições.
Minha solução foi eliminar todos os índices e restrições, atualizar a tabela e adicionar todos os índices / restrições novamente.
Demorou cerca de 3 minutos para escrever uma transação SQL que fez o seguinte:
O script levou 7 minutos para ser executado.
A resposta aceita é definitivamente melhor e mais adequada ... e praticamente elimina a necessidade de tempo de inatividade. No entanto, no meu caso, seria necessário muito mais trabalho de "Desenvolvedor" para usar essa solução e tínhamos uma janela de 30 minutos de tempo de inatividade programada em que ela poderia ser realizada. Nossa solução abordou isso em 10.
fonte