Dev & EngARTIGO

Domain, enum, check ou tabela de referência: como escolher o guardião certo da integridade no PostgreSQL

Um post no Planet PostgreSQL mostra por que uma simples coluna de região expôs o limite de confiar só na aplicação para garantir integridade de dados no PostgreSQL.

Domain, enum, check ou tabela de referência: como escolher o guardião certo da integridade no PostgreSQL
Domain, enum, check ou tabela de referência: como escolher o guardião certo da integridade no PostgreSQL

Um post publicado no Planet PostgreSQL↳PostgreSQL11 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 →, assinado no blog My DBA Notebook, usa a adição de uma coluna de região para mostrar por que regra de negócio só na aplicação é promessa que alguém, cedo ou tarde, vai quebrar.

Um colega da pessoa responsável pelo blog My DBA Notebook abriu um PR simples: o negócio precisava de uma coluna de região numa tabela existente, e ele adicionou uma coluna text solta. Código limpo↳Clean code4 conteúdosMelhores práticas de segurança com o GitHub CopilotDevSecOps · dez 2025Estruturando Arquitetura API .Net Core 2.2Dev (Back & Front) · ago 2019Padronização no código e na equipeDev (Back & Front) · jun 2019Ver tudo em Dev (Back & Front) →, PR pequeno, aprovação em cinco minutos. A aprovação não veio. Em post publicado no Planet PostgreSQL, quem mantém o blog conta por quê e usa o caso para discutir, com profundidade incomum, as quatro formas reais de impor integridade de dados no PostgreSQL.

A justificativa do colega foi a de sempre: "queremos garantir a integridade dentro do código". O post é direto ao recusar esse argumento, e o motivo é técnico, não ideológico: a regra da aplicação vale só enquanto só aquela aplicação grava na tabela. No dia em que outro serviço, um script de carga em lote ou uma sessão psql qualquer também escrever ali, a regra desaparece. O dado passa a ser exatamente o que alguém digitou.

As quatro opções, lado a lado

O post detalha quatro caminhos para impor um conjunto fechado (ou semi-fechado) de valores numa coluna, e o trade-off real de cada um não é óbvio à primeira vista.

OpçãoPonto fortePonto fraco
domainRegra centralizada, reaproveitável em várias tabelasdrop domain ... cascade apaga a coluna (e os dados) em todo lugar que usa o domínio
enumTipagem forte, compacta, ordenávelDá para adicionar valor, mas nunca remover um valor do tipo
text + checkSimples, fácil de entender, fácil de evoluirExige cuidado ao aplicar em tabela grande, sob risco de travar o banco
Tabela de referênciaRigor relacional total, aceita metadados extrasExige join, índice na coluna que referencia e disciplina de manutenção

Um domain concentra a regra num único lugar, reutilizável em qualquer tabela que o declare, algo como create domain region_name as text check (value in ('europe', 'apac', 'americas')). O problema é o comando de remoção: um drop domain ... cascade carrega junto todas as colunas que dependem dele, inclusive os dados. Numa tabela de 24 milhões de linhas, como a do caso relatado, esse é o tipo de gatilho que ninguém quer perto de produção.

O enum nativo do PostgreSQL (create type region as enum (...)) dá tipagem forte e armazenamento compacto. A armadilha está no ciclo de vida: dá para adicionar valor ao enum, e desde o PostgreSQL 10 até dá para renomear um valor existente, mas remover um valor do tipo não é opção. Para um conjunto que só cresce (ou que nunca muda), tudo bem. Para regiões de negócio, que ganham e perdem entradas com o tempo, é a opção errada.

A tabela de referência, com id inteiro como chave primária e code com unique como rótulo legível, é a resposta "correta" sempre que o domínio tem atributos próprios ou muda com frequência. Ela também rende junção mais rápida, porque comparar inteiros custa menos que comparar texto. O custo é a máquina extra: join, lookup antes de inserir, e um índice na coluna que referencia, que o PostgreSQL não cria sozinho, sob pena de cada delete ou update na tabela-pai forçar um scan completo da tabela-filha para checar linhas órfãs.

Por que o time escolheu check constraint

A equipe por trás do blog optou pela coluna text com check constraint. A razão não é performance nem elegância: é a combinação de integridade real com a menor curva de surpresa para o time e a maior facilidade de evolução. Um check se adiciona, se troca e se remove sem reescrever a tabela, sem aposentar valor nenhum e sem exigir que todo desenvolvedor entenda um conceito de catálogo pouco usado no dia a dia.

Queremos impor a integridade dos dados dentro do código.

We want to enforce data integrity inside the code instead.colega, citado em My DBA Notebook

A resposta a essa frase, no post, resume o argumento inteiro: aplicação não é guardiã final de integridade, o banco é.

O padrão seguro em tabela grande

A parte mais útil do post, para quem opera PostgreSQL em produção, é o roteiro de duas situações distintas numa tabela com 24 milhões de linhas: a primeira adição da coluna, e a mudança futura dos valores aceitos.

Para a primeira adição, duas perguntas importam. A primeira é se adicionar a coluna com valor padrão reescreve a tabela inteira: desde o PostgreSQL 11, não reescreve, desde que o default seja uma constante, o que é resolvido no nível do catálogo. A segunda pergunta, mais sutil, é sobre validação: o PostgreSQL não é esperto o suficiente para perceber que uma coluna nova, com default constante que já satisfaz o check, dispensa varredura.

Essa otimização está registrada desde 2016 na lista pgsql-hackers e ainda consta na To-Do list do projeto sem implementação.

Podemos testar a restrição CHECK apenas uma vez, contra o valor que todas as linhas novas vão ter.

we can test the CHECK constraint just once against the value that all new rows will haveRobert Haas, em discussão na lista pgsql-hackers

O caminho documentado e seguro, então, é adicionar a restrição como not valid na mesma transação que cria a coluna, o que não reescreve nem varre nada:

sql
alter table my_table
 add column region text not null default 'europe',
 add constraint region_is_known
 check (region in ('europe', 'apac', 'americas'))
 not valid;

Depois, roda-se a validação separadamente, com validate constraint region_is_known. Esse comando varre a tabela inteira, mas segura apenas um lock share update exclusive, que bloqueia outro DDL e o vacuum, sem travar leituras nem escritas. Como todas as linhas antigas já carregam o mesmo valor padrão 'europe', a varredura não encontra nada fora da regra, e dá para confirmar o resultado consultando pg_constraint.convalidated.

A segunda situação, que o post já antecipa como a próxima tarefa do desenvolvedor, é quando o negócio pede uma região nova, a exemplo de um 'middle-east', numa tabela que já tem 24 milhões de linhas reais. Aqui o roteiro vira duas transações separadas, porque um alter table add constraint direto nessa escala exigiria um lock access exclusive, bloqueando leitura e escrita enquanto varre tudo.

A primeira transação troca a restrição antiga pela nova, também como not valid, sob um lock_timeout curto para desistir em vez de travar o banco esperando uma fila de locks:

sql
BEGIN;
set local lock_timeout = '2s';
alter table my_table drop constraint region_is_known;
alter table my_table add constraint region_is_known
 check (region in ('europe', 'apac', 'americas', 'middle-east'))
 not valid;
COMMIT;

A segunda transação roda validate constraint em horário escolhido, sob o lock mais leve já mencionado. Como a mudança é puramente aditiva, nenhuma linha antiga deixa de satisfazer a regra, e a validação só confirma o óbvio. O post é enfático num ponto: pular essa segunda etapa deixa uma restrição que parece proteger a tabela mas ignora silenciosamente as linhas antigas, e se um dia a mudança for remover um valor em vez de adicionar, é preciso migrar as linhas antes, porque a validação vai falhar nas que ainda carregam o valor retirado.

O que muda para quem opera banco no Brasil

O recorte prático para quem lida com schema em produção é claro: nenhuma das quatro opções é universalmente certa, e a escolha depende da volatilidade do conjunto de valores e do tamanho da tabela. Vale registrar como regra de bolso, a partir do próprio post: enum serve a conjunto verdadeiramente fixo para sempre; domain serve quando a regra precisa se repetir em várias tabelas e a equipe aceita o risco do cascade; tabela de referência serve quando o domínio carrega atributos próprios; check serve ao caso comum, de conjunto que muda com o tempo numa única coluna.

O ponto mais amplo do post é o de defesa em profundidade: validação na aplicação pega erro de digitação na interface, mas só a restrição no banco protege contra todo cliente, todo script e toda importação que não passa por aquela tela.

Nenhuma camada isolada é suficiente, nem mesmo a do próprio banco assumida sem checagem: o post encerra reconhecendo que a própria autoria também presumiu, errado, que o PostgreSQL otimizaria a validação sozinho, e só descobriu o contrário ao testar. Para qualquer equipe que mexe em schema de tabela grande, essa é a lição que vale mais que a escolha entre domain, enum e check: teste o plano, não confie na suposição, nem mesmo na sua.

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

Roberto DinizColunista

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.

Mais de Roberto Diniz
Ver perfil →
Leia também