O Blueprint de Esquemas e Restrições
Um esquema do PostgreSQL é mais do que uma lista de tabelas e colunas.
É um conjunto de promessas sobre quais dados são permitidos existir, e restrições são o mecanismo que mantém essas promessas verdadeiras, mesmo quando o código da aplicação tem bugs.
Esta página constrói o modelo mental por trás do design de esquemas e restrições antes de você tocar nas páginas de "como fazer" desta seção sobre chaves, colunas geradas, exclusões lógicas (soft deletes) ou transações DDL.
Entender este modelo é importante porque toda decisão posterior, desde a escolha de uma estratégia de chave primária até a escolha de uma restrição CHECK em vez de uma verificação na aplicação, deriva da mesma pergunta subjacente: onde a fonte da verdade para a correção deve residir?
Resumo
- Um esquema define a forma dos seus dados, e restrições definem quais instâncias dessa forma são válidas.
- Por que Importa: Bugs, condições de corrida e migrações incompletas eventualmente tentarão escrever dados inválidos, e somente o banco de dados pode garantir que eles nunca se fixem.
- Conceitos-Chave: normalização, chave primária, chave estrangeira, restrição
CHECK, restriçãoUNIQUE, DDL. - Quando Usar: Toda tabela que você cria precisa desse pensamento, mas é mais importante para dados financeiros, sistemas multi-inquilino (multi-tenant) e qualquer coisa com relacionamentos entre tabelas.
- Limitações / Trocas: Restrições estritas adicionam sobrecarga no tempo de escrita e podem tornar algumas migrações mais difíceis de aplicar sem tempo de inatividade.
- Tópicos Relacionados: chaves primárias e estrangeiras, colunas geradas e de identidade, transações DDL, exclusões lógicas (soft deletes).
Fundamentos
Um esquema no sentido de design é o conjunto de tabelas, colunas, tipos e relacionamentos que modelam seu domínio de negócio.
Isso é distinto de um "esquema" do PostgreSQL, que é um objeto de namespace, embora os dois compartilhem um nome por razões históricas.
O design de um esquema começa com a normalização, a prática de dividir dados em tabelas de forma que cada fato seja armazenado em exatamente um lugar.
Uma divisão normalizada de pedidos e itens_pedido, por exemplo, evita repetir o endereço de entrega de um cliente em cada item do pedido.
A normalização reduz anomalias de atualização, onde mudar um fato requer tocar em muitas linhas, mas também significa mais junções (joins) no tempo de consulta.
A maioria dos esquemas de produção fica em algum lugar entre totalmente normalizado e deliberadamente desnormalizado, e esse ponto de chegada é uma decisão de design, não um acidente.
Restrições são as regras que tornam um esquema mais do que uma sugestão.
Uma restrição NOT NULL diz que um valor é sempre exigido, uma restrição CHECK diz que um valor deve satisfazer uma expressão booleana, e uma restrição UNIQUE diz que nenhuma linha pode compartilhar um valor.
Uma CHAVE PRIMÁRIA combina NOT NULL e UNIQUE em uma única coluna ou conjunto de colunas que identifica uma linha.
Uma CHAVE ESTRANGEIRA liga uma coluna em uma tabela a uma chave em outra, de modo que uma referência nunca possa apontar para uma linha que não existe.
Pense em um esquema sem restrições como um armário de arquivos sem etiquetas: qualquer um pode colocar qualquer coisa em qualquer lugar. Pense em restrições como as etiquetas que tornam a recuperação e a confiança possíveis.
A ilustração mais simples é uma única tabela com uma chave primária e uma restrição CHECK trabalhando juntas.
CREATE TABLE app.order_lines (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL,
qty int NOT NULL CHECK (qty > 0)
);Esta única instrução codifica três promessas separadas: cada linha tem uma identidade estável, cada item pertence a um pedido, e nenhum item pode jamais reivindicar uma quantidade não positiva.
Mecânicas e Interações
Restrições não são documentação passiva; elas são aplicadas em cada escrita pelo executor antes que uma transação seja permitida a confirmar.
Quando você insere ou atualiza uma linha, o PostgreSQL verifica imediatamente as restrições NOT NULL e CHECK como parte dessa instrução.
Restrições UNIQUE e PRIMARY KEY são suportadas por um índice, então a unicidade é imposta consultando o novo valor nesse índice antes que a linha seja permitida a ser gravada.
Restrições de CHAVE ESTRANGEIRA são verificadas por um gatilho (trigger) do sistema que consulta a tabela referenciada, e é por isso que uma chave pai sem índice pode tornar as verificações de chave estrangeira lentas.
Por padrão, a maioria das restrições é verificada no final de cada instrução, mas restrições DEFERRABLE podem ser adiadas para o final da transação.
Esse adiamento é importante quando você precisa inserir linhas que se referenciam, já que uma verificação de chave estrangeira estritamente imediata rejeitaria a primeira linha antes que a segunda existisse.
DDL, as instruções que criam ou alteram esses objetos, tem suas próprias mecânicas que valem a pena entender cedo.
No PostgreSQL, a maioria das instruções DDL são transacionais, o que significa que um CREATE TABLE ou ALTER TABLE dentro de um bloco BEGIN/COMMIT pode ser revertido como qualquer outra mudança.
Isso é incomum em comparação com muitos outros bancos de dados relacionais, e é uma vantagem genuína quando uma migração de múltiplos passos precisa ser "tudo ou nada".
Adicionar uma restrição a uma tabela existente, no entanto, não é gratuito no momento em que você a adiciona.
ALTER TABLE ... ADD CONSTRAINT tipicamente toma um bloqueio ACCESS EXCLUSIVE enquanto valida cada linha existente contra a nova regra.
Em uma tabela pequena, essa validação é instantânea, mas em uma tabela grande e movimentada, ela pode bloquear leituras e escritas durante a varredura.
O PostgreSQL oferece um padrão de dois passos para este exato problema: adicione a restrição como NOT VALID primeiro, o que bloqueia apenas novas escritas, e então execute VALIDATE CONSTRAINT separadamente para verificar as linhas existentes com um bloqueio muito mais leve.
Colunas geradas e de identidade estendem este mesmo modelo de aplicação para valores computados.
Uma coluna de identidade garante uma chave substituta gerada pelo servidor e tolerante a lacunas, sem que a aplicação precise escolher um valor, enquanto uma coluna gerada deriva seu valor de outras colunas e não pode ser escrita diretamente.
Ambas removem uma categoria inteira de bugs onde dois caminhos de código discordam sobre como computar a mesma coisa.
Considerações Avançadas e Aplicações
Em escala, as questões interessantes de esquema passam de "quais restrições eu preciso" para "como eu as altero sem interrupção".
Adicionar uma restrição NOT NULL a uma tabela grande costumava exigir uma reescrita completa da tabela em versões mais antigas do PostgreSQL, mas o PostgreSQL moderno pode validá-la como uma verificação rápida contra uma restrição CHECK existente. Assim, o preenchimento e a restrição são frequentemente melhor feitos como passos separados e sequenciados.
Esquemas multi-inquilino levantam uma questão relacionada sobre onde a isolação do inquilino reside, seja como uma coluna tenant_id com segurança em nível de linha, uma chave estrangeira imposta por restrição, ou esquemas completamente separados por inquilino.
Restrições de exclusão, construídas na mesma infraestrutura que índices únicos, mas generalizadas para qualquer operador, resolvem problemas que restrições únicas não conseguem, como prevenir intervalos de datas sobrepostos para o mesmo recurso.
Domínios oferecem outra camada de reutilização, permitindo que você defina um tipo nomeado com uma restrição CHECK embutida uma vez e o aplique a muitas colunas de forma consistente, embora sejam menos flexíveis do que restrições em nível de tabela quando uma regra precisa referenciar múltiplas colunas.
A questão arquitetônica mais profunda é onde a lógica de validação deve residir, e essa questão não tem uma resposta universalmente correta, apenas trocas.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Restrições de banco de dados (CHECK, UNIQUE, FK) | Imposta para todo escritor, incluindo scripts e outros serviços | Custo de migração e validação em tabelas grandes | Invariantes que nunca devem ser violados, independentemente do chamador |
| Validação na camada de aplicação | Rápida iteração, mensagens de erro ricas, fácil de testar isoladamente | Contornada por SQL direto, jobs em lote ou um segundo serviço | Modelagem de entrada voltada para o usuário e regras de negócio que mudam frequentemente |
| Gatilhos de banco de dados | Podem impor regras entre linhas e entre tabelas que as restrições não conseguem expressar | Mais difíceis de raciocinar, podem esconder lógica de leitores do esquema | Invariantes raros muito complexos para uma restrição declarativa |
| Validação em nível de ORM | Conveniente, co-localizada com o código do modelo | Tão forte quanto o único caminho de código que usa o ORM | Uma segunda linha de defesa sobreposta a restrições reais |
O padrão que se sustenta melhor em produção é o de camadas: validação de aplicação para experiência do usuário, restrições de banco de dados para o invariante que nunca deve quebrar, e gatilhos reservados para o caso raro em que nenhum dos dois consegue expressar claramente.
A evolução do esquema também tem uma dimensão de observabilidade, já que uma violação de restrição em produção é um sinal, não apenas um erro, e rastrear quais restrições rejeitam mais linhas ao longo do tempo frequentemente aponta para uma incompatibilidade entre o que a aplicação assume e o que realmente acontece a montante.
Equívocos Comuns
- "A validação da aplicação é suficiente se o código for cuidadoso." Todo escritor adicional, script, job ETL ou serviço futuro que toque na tabela contorna completamente o código da aplicação, então apenas uma restrição de banco de dados é garantida para se aplicar em todos os lugares.
- "Normalização é sempre o padrão correto." Normalização pesada pode transformar leituras simples em junções caras de múltiplas vias, e uma coluna deliberadamente desnormalizada é às vezes a troca certa para um caminho de leitura quente.
- "DDL é instantâneo, então posso executar migrações a qualquer momento." Muitas operações DDL tomam bloqueios fortes que impedem leituras e escritas concorrentes, e o padrão seguro é frequentemente dividir uma mudança em um passo com bloqueio leve seguido por um passo de validação.
- "Chaves estrangeiras sempre tornam as escritas lentas demais para valer a pena." O custo real é geralmente um índice ausente na coluna de referência, não a chave estrangeira em si, e uma chave estrangeira indexada adiciona apenas uma sobrecarga pequena e previsível.
- "NULL significa a mesma coisa que uma string vazia ou zero." NULL representa desconhecido ou não aplicável, e tratá-lo como equivalente a um valor padrão causa bugs silenciosos em comparações e agregações.
FAQs
Qual é a diferença entre um "esquema" do PostgreSQL e o design de esquema de banco de dados?
- Um esquema do PostgreSQL (como em
CREATE SCHEMA app) é um namespace que agrupa tabelas, views e funções. - Design de esquema como disciplina refere-se a como você modela tabelas, chaves e restrições para o seu domínio.
- Os dois conceitos compartilham um nome, mas operam em níveis completamente diferentes.
Toda tabela deve ter uma chave primária substituta?
- Uma chave substituta, como uma coluna de identidade, é geralmente o padrão mais seguro para estabilidade de junções.
- Uma chave natural ainda merece uma restrição
UNIQUE, mesmo quando uma chave substituta existe. - Chaves naturais compostas funcionam bem para tabelas puramente de referência ou de consulta que raramente são referenciadas por muitas tabelas filhas.
Por que eu usaria uma restrição CHECK em vez de validar no código da aplicação?
Uma restrição CHECK é imposta para todo escritor naquela tabela, incluindo scripts, migrações e outros serviços, enquanto a validação da aplicação protege apenas o único caminho de código que a executa.
Adicionar uma chave estrangeira sempre bloqueia a tabela por muito tempo?
- O bloqueio em si é breve, mas validar as linhas existentes contra a nova chave pode levar tempo proporcional ao tamanho da tabela.
- Indexar a coluna de referência antecipadamente mantém essa validação rápida.
- O padrão
NOT VALIDmaisVALIDATE CONSTRAINTminimiza o bloqueio em tabelas grandes e movimentadas.
Qual é a diferença prática entre UNIQUE e PRIMARY KEY?
Uma PRIMARY KEY é uma restrição UNIQUE combinada com NOT NULL, e uma tabela pode ter apenas uma chave primária, mas muitas restrições únicas separadas.
Quando devo usar uma restrição EXCLUSION em vez de UNIQUE?
Uma restrição de exclusão generaliza a unicidade para qualquer operador, o que a torna a ferramenta certa quando você precisa prevenir intervalos sobrepostos, como duas reservas para o mesmo recurso compartilhando qualquer janela de tempo sobreposta.
Colunas geradas são mais lentas para escrever do que colunas regulares?
- Uma coluna gerada adiciona um pequeno custo de computação em cada inserção ou atualização que toca em suas entradas.
- Esse custo é quase sempre menor do que os bugs causados por dois caminhos de código computando o mesmo valor derivado de forma diferente.
- Colunas geradas armazenadas também adicionam uma pequena quantidade de armazenamento extra em comparação com computar o valor no tempo de leitura.
DDL do PostgreSQL é realmente transacional?
A maioria das instruções DDL, incluindo CREATE TABLE e ALTER TABLE, participa totalmente em transações e é revertida de forma limpa se a transação circundante for abortada, o que é uma vantagem genuína sobre bancos de dados onde DDL é commit automático.
Como adiciono um constraint NOT NULL a uma tabela enorme sem downtime?
- Adicione primeiro uma restrição
CHECKcomoNOT VALIDque espelhe a regraNOT NULL. - Valide-a separadamente com
VALIDATE CONSTRAINT, que toma um bloqueio muito mais leve. - O PostgreSQL moderno pode então derivar a garantia
NOT NULLdessa restriçãoCHECKvalidada sem uma segunda varredura completa da tabela.
Qual é a diferença entre um domínio e uma restrição CHECK em nível de tabela?
Um domínio empacota um tipo base com uma restrição embutida para que possa ser reutilizado consistentemente em muitas colunas e tabelas, enquanto uma restrição CHECK em nível de tabela se aplica apenas àquela única tabela e pode referenciar múltiplas colunas de uma vez.
Por que a normalização importa se o armazenamento é barato?
A normalização não é primariamente sobre custo de armazenamento; é sobre evitar anomalias de atualização onde o mesmo fato armazenado em múltiplos lugares pode ficar dessincronizado.
Exclusões lógicas (soft deletes) devem ser impostas com uma restrição?
Exclusões lógicas geralmente dependem de uma coluna deleted_at anulável combinada com um índice parcial ou restrição única parcial, de modo que as regras de unicidade se apliquem apenas às linhas ainda ativas.
Qual é o maior erro de design de esquema que as equipes cometem no início?
Tratar restrições como documentação opcional em vez do mecanismo de imposição real é o erro mais comum, pois deixa a correção dependente de todo escritor futuro se comportar perfeitamente.
Relacionados
- Noções Básicas de Design de Esquema - exemplos práticos de chaves e convenções de nomenclatura
- Chaves Primárias e Estrangeiras - integridade referencial e comportamento ON DELETE na prática
- Restrições CHECK e UNIQUE - regras de validação em nível de linha e em nível de conjunto
- Colunas Geradas e de Identidade - valores computados e gerados pelo servidor
- Transações DDL - como as mudanças de esquema participam de transações e bloqueios
- Exclusões Lógicas vs. Exclusões Físicas - estratégia de exclusão e suas implicações em restrições
Versões da Stack: Esta página foi escrita para PostgreSQL 18.4 (versão principal estável 18, linha de manutenção 17 também suportada).