Muitos ALTER TABLE
comandos do PostgreSQL, como adicionar uma nova coluna com um valor padrão , têm otimizações inteligentes nas versões mais recentes do PostgreSQL que permitem que eles sejam executados basicamente instantaneamente, mesmo em tabelas grandes, uma vez que o Postgres adquiriu brevemente um bloqueio na tabela .
Infelizmente, essa advertência final é importante. Um comando como este da postagem do blog vinculada
ALTER TABLE users ADD COLUMN credits bigint NOT NULL DEFAULT 0;
ainda precisa esperar por um bloqueio exclusivo na users
tabela antes de poder ser executado, mesmo que seja executado instantaneamente assim que o bloqueio for adquirido. Pior, enquanto espera por esse bloqueio, ele bloqueia todas as gravações e leituras que envolvem a tabela.
Alguns passos simples para reproduzir isso (testado no Postgres 13.3):
Em um
psql
shell, crie uma tabela, inicie uma transação, faça uma leitura da tabela e não confirme:CREATE TABLE users (id SERIAL, name TEXT); INSERT INTO users (name) VALUES ('bob'), ('fred'); START TRANSACTION; SELECT * FROM users WHERE id = 1;
Deixe o primeiro shell aberto, abra um segundo e tente alterar a tabela:
ALTER TABLE users ADD COLUMN credits bigint NOT NULL DEFAULT 0;
Observe que esta consulta trava, esperando que a transação no primeiro shell seja confirmada.
Abra um terceiro terminal e tente executar
SELECT * FROM users WHERE id = 2;
Observe que isso também trava; agora está bloqueado aguardando
ALTER TABLE
a conclusão do comando, que por sua vez é bloqueado aguardando a conclusão da primeira transação.
Parece que a maioria ou todos os ALTER TABLE
comandos se comportam assim. Mesmo que a operação em si seja muito rápida ou possa ser executada sem manter um bloqueio para toda a operação, ALTER TABLE
ainda precisa adquirir brevemente um bloqueio exclusivo na tabela antes de iniciar seu trabalho e, enquanto espera por esse bloqueio, todas as outras instruções que tocar na mesa - até lê! - estão bloqueados.
Desnecessário dizer que esse comportamento é bastante problemático se você deseja fazer alterações em uma tabela que ocasionalmente está envolvida em transações de longa duração. Se a ALTER TABLE
instrução for bloqueada por uma transação de longa duração que esteja mantendo qualquer tipo de bloqueio envolvendo a tabela no momento em que a ALTER TABLE
instrução for executada, todas as interações com essa tabela serão bloqueadas até o final de qualquer transação aleatória de longa duração. , e qualquer coisa que dependa dessa tabela provavelmente passa por um tempo de inatividade.
Existe uma solução canônica para este problema?
Uma solução grosseira que tentei é usar um script wrapper que tenta repetidamente executar a ALTER TABLE
instrução por meio de uma conexão com lock_timeout
um valor pequeno (por exemplo, 5 segundos). Se ALTER TABLE
falhar devido ao tempo limite de bloqueio, a transação é abortada e o script detecta o erro, aguarda um ou dois minutos e tenta todo o processo novamente. Isso evita o tempo de inatividade total, mas ainda tem implicações de desempenho, pois cada tentativa fracassada de executar a ALTER TABLE
instrução ainda bloqueia as consultas por alguns segundos.
O que eu realmente gostaria de fazer é, de alguma forma, dizer ao Postgres que quero que a ALTER TABLE
instrução aguarde um momento em que possa adquirir o bloqueio na tabela sem bloquear outras consultas nesse meio tempo. (Não me importo se isso significa esperar horas até finalmente chegar a um momento em que nenhuma outra consulta esteja tocando a mesa; se evitar bloquear outras consultas, é uma troca absolutamente aceitável.) Existe alguma maneira de fazer isso - talvez algum encantamento que posso incluir na ALTER TABLE
declaração ou algum parâmetro de configuração que posso definir para alterar esse comportamento?
Infelizmente, não há uma ótima alternativa para apenas tentar novamente em um loop. Mas talvez você possa tornar a repetição mais inteligente. Quando preciso fazer isso e posso estar em um bloco de transação, pego o bloqueio explicitamente e uso a opção NOWAIT .
Você pode definir o valor do tempo limite para ser menor (muito menor) do que alguns segundos. Ou você pode usar NOWAIT, que deve ser a mesma coisa que definir lock_timeout para seu valor mais baixo possível, mas redefinindo automaticamente assim que o bloqueio for adquirido (relevante no caso de transações com várias instruções).
Sim, ter algumas opções melhores aqui seria bom. Pode ser controverso descobrir exatamente como isso seria, no entanto. Talvez algo como o bloqueio de baixa prioridade do MySQL, que se mantém na fila de espera, mas permite que outros garçons saltem sobre ele se o outro garçom puder obter imediatamente o bloqueio no modo desejado.