DataARTIGO

O insert que falha logo depois de uma carga bem-sucedida no PostgreSQL

Sequences dessincronizadas fazem um bulk load 'verde' quebrar no primeiro insert real. Entenda o mecanismo e como realinhar todo o schema de uma vez.

0
O insert que falha logo depois de uma carga bem-sucedida no PostgreSQL
Imagem gerada por IA

Há uma classe de falha no PostgreSQLPostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data que engana até quem tem experiência: a carga termina sem reclamar, as contagens de linha batem com o arquivo de fixtures, todas as foreign keys resolvem, o CI fica verde, e então a aplicação tenta inserir uma única linha própria e o banco recusa com duplicate key value violates unique constraint. Nada está corrompido, nada precisa ser restaurado. O que existe é uma sequence fora de sincronia com a tabela que ela alimenta, e o artigo de Mikhail Shytsko, fundador do Seedfast, publicado no Planet PostgreSQL, disseca o mecanismo com exemplos rodados contra o PostgreSQL 18.6.

O ponto central é conceitual antes de ser operacional: ids explícitos e ids gerados vêm de lugares diferentes, e carregar os primeiros não move os segundos. É um caso clássico de que o problema não está na performance nem no plano de execução, e sim no desenho do fluxo de carga.

Por que a sequence sai de sincronia

Uma coluna bigserial é, na prática, um bigint carregando um default de nextval(''). Quando o INSERT fornece o próprio valor de id, esse default simplesmente nunca é avaliado, e a sequence permanece parada enquanto a tabela se enche ao redor dela. O exemplo da fonte é direto:

sql
CREATE TABLE users (id bigserial PRIMARY KEY, email text NOT NULL UNIQUE);

INSERT INTO users (id, email)
VALUES (1, 'a@example.com'), (2, 'b@example.com'), (3, 'c@example.com');

SELECT last_value, is_called FROM users_id_seq;
-- last_value = 1, is_called = f

Três linhas na tabela e a sequence ainda reporta o estado de criação: last_value igual a 1 com is_called em false, o que em conjunto significa que o valor 1 ainda não foi entregue. O próximo insert que deixa o Postgres escolher o id pede 1, e 1 já pertence à linha carregada segundos antes. Daí o erro tardio, que é justamente o que confunde: a semente rodou, o CI passou, e a falha ficou esperando a primeira escrita que um humano ou um teste efetivamente realiza.

Colunas identity não resolvem isso

Desde o PostgreSQL 10 a grafia aderente ao padrão SQL é a coluna identity, e equipes que migraram de serial às vezes assumem que o problema migrou junto. Não migrou. Com GENERATED BY DEFAULT AS IDENTITY, escrever ids explícitos passa sem reclamar e a colisão chega igual no insert seguinte.

A variante GENERATED ALWAYS é mais rígida, e nesse caso a rigidez ajuda: ela recusa a carga de imediato e imprime a saída embaixo, HINT: Use OVERRIDING SYSTEM VALUE to override. O detalhe importante é que aceitar a dica não corrige nada. O OVERRIDING SYSTEM VALUE apenas suspende a checagem que bloqueia a gravação do seu valor e não tem absolutamente nada a dizer sobre a sequence, que continua estacionada em 1 enquanto as linhas entram. A rigidez transforma uma armadilha silenciosa em reclamação imediata, mas quem contorna a reclamação herda exatamente a mesma sequence velha.

O conserto de uma linha, e dois jeitos de errar

A correção aponta a sequence para o maior valor que a tabela contém:

sql
SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT max(id) FROM users));

O próximo insert retorna 4 e o incidente acaba. Mas há dois erros que a maioria das receitas na internet comete.

Não hardcode o nome da sequence. Quase toda versão desse snippet escreve users_id_seq diretamente, o que é correto até alguém renomear a tabela. Sequences não seguem o rename:

sql
ALTER TABLE users RENAME TO members;
SELECT pg_get_serial_sequence('members', 'id');
-- public.users_id_seq

A tabela agora é members, mas a sequence continua sendo users_id_seq. Um script que monta o nome por concatenação de string passa a mirar uma sequence que nada tem a ver com a tabela que ele pensa estar consertando. A função pg_get_serial_sequence() consulta o catálogo em vez de adivinhar, e responde também para colunas identity, apesar do "serial" no nome.

Cuidado com a tabela vazia. Sobre zero linhas, max(id) é NULL, e como setval é estrita, passar NULL faz a chamada retornar sem tocar em nada. Isso é inofensivo numa sequence recém-criada, mas silenciosamente errado numa que uma execução anterior já avançou, situação típica de suítes que fazem TRUNCATE entre rodadas. A forma que sobrevive aos dois casos carrega seu próprio argumento is_called:

sql
SELECT setval(pg_get_serial_sequence('empty2', 'id'), coalesce(max(id), 1), max(id) IS NOT NULL)
FROM empty2;

O terceiro argumento é a raiz de um mistério comum. Se você já se perguntou por que um reset deixou a numeração começando em 2 em vez de 1, é a flag is_called. O experimento de duas linhas deixa claro:

sql
SELECT setval('flagcheck_id_seq', 10);        -- próximo insert recebe 11
SELECT setval('flagcheck_id_seq', 10, false);  -- próximo insert recebe 10

Resetando identity nos próprios termos

Colunas identity têm sintaxe nativa que nunca toca no nome da sequence:

sql
ALTER TABLE t_ident ALTER COLUMN id RESTART WITH 3;

É mais legível e é checado em tempo de parse, mas cobre apenas identity: apontá-lo para uma coluna serial gera erro explícito. A forma com setval funciona nos dois tipos, o que vale algo para um script que precisa consertar um schema inteiro sem ramificar. E quando a tabela é descartável em vez de semeada, TRUNCATE t_ident RESTART IDENTITY esvazia e rebobina a sequence para 1 num único comando, valendo para serial e identity igualmente. Vale reforçar: um TRUNCATE simples remove as linhas e deixa a sequence exatamente onde estava.

Realinhando um schema inteiro após a carga

Consertar uma tabela na mão serve para um incidente. Depois de um bulk load num schema de tamanho razoável, o desejável é encontrar e mover toda sequence afetada sem nomear nenhuma, e o catálogo tem informação suficiente para isso. Shytsko propõe um bloco DO que percorre information_schema.columns, resolve a sequence de cada coluna via pg_get_serial_sequence() e executa o setval idempotente com o coalesce e a flag corretos. A saída de exemplo, com NOTICE: realigned public.users_id_seq for members.id, flagra a armadilha do rename em ação: a sequence por trás de members.id ainda se chama users_id_seq. A recomendação é rodar isso uma vez ao fim da carga, em vez de espalhar chamadas de setval pelo arquivo de fixtures, onde apodrecem toda vez que uma tabela é adicionada.

Ou pare de escrever ids explícitos

O autor é honesto ao chamar tudo isso de "reparo de uma ferida autoinfligida". Os ids aparecem na carga porque um arquivo de fixtures quer que o usuário 1 seja Alice para as asserções se apoiarem nele. Essa conveniência é o que coloca sequence e tabela em trilhos separados. As saídas de verdade são duas: deixar o banco atribuir os ids e capturá-los com RETURNING ou uma CTE, de modo que nenhum id literal apareça no arquivo, ou gerar os dados em vez de escrevê-los à mão. A segunda é a rota do Seedfast, produto do próprio autor, o que convém ter em mente ao ler as recomendações. Nenhuma das duas, porém, ajuda com o dump restaurado hoje de manhã; para esse cenário, o bloco DO de realinhamento continua sendo a ferramenta certa.

O valor prático para quem administra bancos em produção é reconhecer o padrão cedo: duplicate key logo após uma carga limpa raramente é corrupção e quase nunca exige restore. É sequence dessincronizada, e a correção é barata desde que se resista a hardcodar o nome da sequence e se cuide do caso da tabela vazia.

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados do iMasters, um agente de inteligência artificial com revisão editorial humana. Publicado sob revisão editorial de Rafael Chinaglia - iMasters. Saiba como produzimos no expediente.

Roberto DinizEspecialista virtual

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Ver perfil

Comentários

0/1200

Ninguém comentou ainda. Começa a conversa?