PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Como faço grandes atualizações sem bloqueio no PostgreSQL?

Coluna/Linha


... Não preciso que a integridade transacional seja mantida em toda a operação, pois sei que a coluna que estou alterando não será gravada ou lida durante a atualização.

Qualquer UPDATE no modelo MVCC do PostgreSQL escreve uma nova versão de toda a linha . Se as transações simultâneas mudarem qualquer coluna da mesma linha, surgem problemas de simultaneidade demorados. Detalhes no manual. Conhecendo a mesma coluna não será afetado por transações simultâneas evita algumas possíveis complicações, mas não outras.

Índice


Para evitar ser desviado para uma discussão fora do tópico, vamos supor que todos os valores de status para as 35 milhões de colunas estejam atualmente configurados para o mesmo valor (não nulo), tornando um índice inútil.

Ao atualizar a tabela inteira (ou partes principais dele) Postgres nunca usa um índice . Uma varredura sequencial é mais rápida quando todas ou a maioria das linhas precisam ser lidas. Pelo contrário:a manutenção do índice significa custo adicional para o UPDATE .

Desempenho


Por exemplo, digamos que eu tenha uma tabela chamada "pedidos" com 35 milhões de linhas e queira fazer isso:
UPDATE orders SET status = null;

Eu entendo que você está buscando uma solução mais geral (veja abaixo). Mas para abordar a questão real perguntado:Isso pode ser resolvido em uma questão de milissegundos , independentemente do tamanho da tabela:
ALTER TABLE orders DROP column status
                 , ADD  column status text;

O manual (até Postgres 10):

Quando uma coluna é adicionada com ADD COLUMN , todas as linhas existentes na tabela são inicializadas com o valor padrão da coluna (NULL se não houver DEFAULT cláusula é especificada). Se não houver DEFAULT cláusula, esta é apenas uma mudança de metadados [...]

O manual (desde o Postgres 11):

Quando uma coluna é adicionada com ADD COLUMN e um DEFAULT não volátil for especificado, o padrão é avaliado no momento da instrução e o resultado armazenado nos metadados da tabela. Esse valor será usado para a coluna de todas as linhas existentes. Se não houver DEFAULT for especificado, NULL será usado. Em nenhum dos casos é necessária uma reescrita da tabela.

Adicionando uma coluna com um DEFAULT volátil ou alterar o tipo de uma coluna existente exigirá que toda a tabela e seus índices sejam reescritos. [...]

E:

A DROP COLUMN form não remove fisicamente a coluna, mas simplesmente a torna invisível para operações SQL. As operações subseqüentes de inserção e atualização na tabela armazenarão um valor nulo para a coluna. Assim, eliminar uma coluna é rápido, mas não reduzirá imediatamente o tamanho em disco da tabela, pois o espaço ocupado pela coluna descartada não é recuperado. O espaço será recuperado ao longo do tempo à medida que as linhas existentes forem atualizadas.

Certifique-se de não ter objetos dependendo da coluna (restrições de chave estrangeira, índices, visualizações, ...). Você precisaria descartar/recriá-los. Tirando isso, pequenas operações na tabela de catálogo do sistema pg_attribute faça o trabalho. Requer um bloqueio exclusivo na mesa, o que pode ser um problema para cargas simultâneas pesadas. (Como Buurman enfatiza em seu comentário.) Tirando isso, a operação é uma questão de milissegundos.

Se você tiver um padrão de coluna que deseja manter, adicione-o novamente em um comando separado . Fazer isso no mesmo comando aplica-o a todas as linhas imediatamente. Ver:
  • Adicionar nova coluna sem bloqueio de tabela?

Para realmente aplicar o padrão, considere fazê-lo em lotes:
  • O PostgreSQL otimiza a adição de colunas com DEFAULTs não NULL?

Solução geral


dblink foi mencionado em outra resposta. Ele permite o acesso a bancos de dados Postgres "remotos" em conexões separadas implícitas. O banco de dados "remoto" pode ser o atual, conseguindo assim "transações autônomas" :o que a função escreve no banco de dados "remoto" é confirmado e não pode ser revertido.

Isso permite executar uma única função que atualiza uma tabela grande em partes menores e cada parte é confirmada separadamente. Evita a sobrecarga de transações para um número muito grande de linhas e, mais importante, libera bloqueios após cada parte. Isso permite que as operações simultâneas prossigam sem muito atraso e torna os deadlocks menos prováveis.

Se você não tiver acesso simultâneo, isso dificilmente será útil - exceto para evitar ROLLBACK após uma exceção. Considere também SAVEPOINT para esse caso.

Isenção de responsabilidade


Em primeiro lugar, muitas pequenas transações são realmente mais caras. Isso só faz sentido para tabelas grandes . O ponto ideal depende de muitos fatores.

Se você não tiver certeza do que está fazendo:uma única transação é o método seguro . Para que isso funcione corretamente, as operações simultâneas na mesa precisam funcionar. Por exemplo:escritas simultâneas pode mover uma linha para uma partição que supostamente já está processada. Ou leituras simultâneas podem ver estados intermediários inconsistentes. Você foi avisado.

Instruções passo a passo


O módulo adicional dblink precisa ser instalado primeiro:
  • Como usar (instalar) o dblink no PostgreSQL?

A configuração da conexão com o dblink depende muito da configuração do cluster de banco de dados e das políticas de segurança em vigor. Pode ser complicado. Resposta posterior relacionada com mais como se conectar com dblink :
  • Inserções persistentes em uma UDF mesmo que a função seja abortada

Crie um FOREIGN SERVER e um USER MAPPING conforme instruído lá para simplificar e agilizar a conexão (a menos que você já tenha uma).
Assumindo uma serial PRIMARY KEY com ou sem algumas lacunas.
CREATE OR REPLACE FUNCTION f_update_in_steps()
  RETURNS void AS
$func$
DECLARE
   _step int;   -- size of step
   _cur  int;   -- current ID (starting with minimum)
   _max  int;   -- maximum ID
BEGIN
   SELECT INTO _cur, _max  min(order_id), max(order_id) FROM orders;
                                        -- 100 slices (steps) hard coded
   _step := ((_max - _cur) / 100) + 1;  -- rounded, possibly a bit too small
                                        -- +1 to avoid endless loop for 0
   PERFORM dblink_connect('myserver');  -- your foreign server as instructed above

   FOR i IN 0..200 LOOP                 -- 200 >> 100 to make sure we exceed _max
      PERFORM dblink_exec(
       $$UPDATE public.orders
         SET    status = 'foo'
         WHERE  order_id >= $$ || _cur || $$
         AND    order_id <  $$ || _cur + _step || $$
         AND    status IS DISTINCT FROM 'foo'$$);  -- avoid empty update

      _cur := _cur + _step;

      EXIT WHEN _cur > _max;            -- stop when done (never loop till 200)
   END LOOP;

   PERFORM dblink_disconnect();
END
$func$  LANGUAGE plpgsql;

Ligar:
SELECT f_update_in_steps();

Você pode parametrizar qualquer parte de acordo com sua necessidade:o nome da tabela, nome da coluna, valor, ... apenas certifique-se de higienizar os identificadores para evitar injeção de SQL:
  • Nome da tabela como parâmetro de função do PostgreSQL

Evite UPDATEs vazios:
  • Como posso (ou posso) SELECT DISTINCT em várias colunas?