← Stack web // stack web · back end · banco de dados

PostgreSQL: tipos e restrições que o banco cobra

Todo sistema tem uma regra que ninguém pode furar. Preço não é negativo, cliente não fica sem nome, dois pedidos não usam o mesmo número. O PostgreSQL deixa você escrever essa regra dentro da tabela, e daí em diante ele mesmo recusa o que a quebra.

O que o PostgreSQL resolve

Um sistema quase nunca tem um caminho só até o banco. Tem o formulário do site, tem a importação da planilha, tem o script que alguém rodou às onze da noite pra corrigir uma coisa, tem a API que o parceiro chama. Se a regra de "preço maior que zero" mora no código do formulário, ela vale para o formulário e para mais nada.

O PostgreSQL guarda os dados num servidor que fica ligado esperando conexão, atende várias ao mesmo tempo e, principalmente, cobra as regras que você declarou na tabela em todos esses caminhos. O manual chama isso de restrição, e diz que quando alguém tenta gravar um valor que violaria uma restrição, um erro é levantado. O dado ruim não entra, não importa por onde ele tentou.

Os conceitos que você precisa antes da primeira linha

  • Servidor, banco e psql. O servidor fica rodando em segundo plano e é dono dos arquivos. O psql é o cliente de terminal, a janela onde você digita. Um servidor guarda vários bancos, e você conecta em um por vez.
  • Tipo de coluna. Cada coluna declara o que aceita: integer pra número inteiro, text pra texto, numeric pra valor com casa decimal, date pra data. O tipo já é a primeira barreira, porque texto não entra em coluna de número.
  • NOT NULL. Diz que a coluna nunca pode ficar vazia. Sem isso, o banco aceita uma linha sem nome, e o vazio vai reaparecer meses depois no relatório de alguém.
  • CHECK. A restrição em que você escreve a condição você mesmo, em SQL. CHECK (preco > 0) é uma frase que o banco passa a conferir em toda linha nova. Dá pra batizar a restrição com CONSTRAINT, e o nome que você deu volta na mensagem de erro.
  • PRIMARY KEY e UNIQUE. UNIQUE proíbe valor repetido na coluna. PRIMARY KEY é a combinação de UNIQUE com NOT NULL, e marca a coluna que identifica cada linha.

O banco por dentro: o relacional com regras e tipos ricos

A teoria de base é a mesma do relacional da página do MySQL: tabela, linha, coluna com tipo, chave primária e estrangeira, e os quatro verbos do SQL. O que muda no PostgreSQL é o quanto você consegue declarar de regra e de estrutura pro próprio banco, em vez de vigiar na aplicação.

  • Constraints como regras de negócio: além de NOT NULL e UNIQUE, o CHECK escreve a regra na coluna: saldo NUMERIC CHECK (saldo >= 0). Qualquer aplicação que tentar violar leva erro, inclusive a que ainda não foi escrita.
  • Tipos além do básico: TEXT sem limite, NUMERIC pra dinheiro, TIMESTAMPTZ pra tempo com fuso, ARRAY de qualquer tipo e o JSONB, que guarda documento consultável dentro de uma coluna, o recurso que aproxima o Postgres do MongoDB sem sair do relacional.
  • Transação é o contrato de tudo-ou-nada: entre BEGIN e COMMIT, ou todas as sentenças valem, ou nenhuma vale. Transferência que debita de um lado e credita do outro só existe com isso.
CREATE TABLE contas (
  id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  dono  TEXT NOT NULL,
  saldo NUMERIC(10,2) NOT NULL CHECK (saldo >= 0)
);

BEGIN;                                       -- transação
UPDATE contas SET saldo = saldo - 50 WHERE id = 1;
UPDATE contas SET saldo = saldo + 50 WHERE id = 2;
COMMIT;                                      -- os dois juntos, ou nada

Se o CHECK do saldo reprovar o débito, o COMMIT não acontece e o crédito é desfeito junto. É essa garantia que faz banco sério de dinheiro rodar em cima de um relacional.

Hello world: uma regra que o banco cobra de você

O roteiro abaixo cria uma tabela com três restrições, grava uma linha certa e tenta gravar duas erradas de propósito. As duas erradas são o ponto da página.

  1. Instale o PostgreSQL. A página oficial de download é postgresql.org/download.

    • Ubuntu ou Debian: sudo apt install postgresql, que instala a versão empacotada pela distribuição. Pra escolher a versão, a própria página ensina a ligar o repositório oficial e instalar postgresql-18.
    • macOS: a página lista o Postgres.app, o instalador interativo da EDB e o Homebrew, que usa o formato postgresql@NN com o número da versão maior.
    • Windows: instalador interativo da EDB, que traz servidor, pgAdmin e o atalho SQL Shell (psql) no menu Iniciar.
  2. Abra o psql. No Linux, o pacote cria um usuário de sistema chamado postgres, e é por ele que você entra na primeira vez:

    sudo -u postgres psql

    No Windows, abra o SQL Shell (psql) e aceite os valores sugeridos até a senha. Deu certo quando aparecem duas linhas parecidas com estas, seguidas do prompt:

    psql (18.6)
    Type "help" for help.
    
    postgres=#
  3. Crie o banco e conecte nele.

    CREATE DATABASE loja;
    \c loja

    O que estava escrito antes do =# vira loja. Esse pedaço do prompt é o nome do banco em que você está, e conferir ele evita a metade dos sustos que vêm depois.

  4. Crie a tabela com as restrições. Repare que a regra do preço tem nome próprio.

    CREATE TABLE produto (
        produto_no integer PRIMARY KEY,
        nome text NOT NULL,
        preco numeric CONSTRAINT preco_positivo CHECK (preco > 0)
    );

    A resposta é CREATE TABLE. Agora veja o que o banco entendeu:

    \d produto
  5. Grave uma linha válida.

    INSERT INTO produto VALUES (1, 'teclado', 120.00);

    A resposta é INSERT 0 1. O zero é herança antiga e não quer dizer nada. O 1 é a contagem de linhas gravadas.

  6. Tente gravar preço negativo. Este comando tem que falhar.

    INSERT INTO produto VALUES (2, 'monitor', -50);
  7. Tente gravar sem nome. Este também.

    INSERT INTO produto VALUES (3, NULL, 10);
  8. Veja o que sobrou na tabela.

    SELECT * FROM produto;
  9. Saia do psql.

    \q
A prova de que funcionou. A prova desta página são as duas recusas. No passo 6, o psql responde com um bloco que começa em ERROR, cita a tabela produto e o nome que você deu à regra, e ainda mostra a linha recusada:
ERROR:  new row for relation "produto" violates check constraint "preco_positivo"
DETAIL:  Failing row contains (2, monitor, -50).
No passo 7 vem outro ERROR, agora dizendo que o valor nulo na coluna nome viola a restrição not-null. E o SELECT do passo 8 desenha a tabela com uma linha só, a do teclado. Os produtos 2 e 3 não existem, não entraram torto, não ficaram pela metade. Foi o banco que barrou, sem uma linha de código de aplicação no meio. Se os dois INSERT tiverem passado, você criou a tabela sem as restrições: rode \d produto e confira se o CHECK e o not null aparecem ali.

Uma coisa pra experimentar antes de sair

Rode o INSERT do teclado uma segunda vez, igualzinho. Ele falha também, e por um motivo diferente dos outros dois: o produto_no 1 já existe, e PRIMARY KEY proíbe repetir. São três restrições diferentes, três erros diferentes, e cada família de erro tem um código próprio no manual.

O que o PostgreSQL não é

PostgreSQL não é um depósito passivo, um lugar onde o programa despeja o que quiser e depois vai lá buscar. Essa é a imagem que quase todo mundo carrega no começo, e ela leva direto pro hábito de validar tudo na aplicação e declarar a tabela com todas as colunas soltas, aceitando qualquer coisa.

O problema aparece quando o segundo caminho até o banco existe. A rotina de importação não passa pelo formulário. O script de correção não passa pela API. O sistema novo que a empresa comprou também escreve na mesma tabela. Cada caminho desses teria que reimplementar a mesma regra, e um vai esquecer. A restrição declarada na tabela vale para todos de uma vez, inclusive para o desenvolvedor apressado com o terminal aberto.

Validar no formulário continua valendo, porque a pessoa merece a mensagem em português antes de apertar o botão. A restrição do banco é a rede embaixo do trapézio, o que segura quando a validação de cima falhou ou nem foi chamada.

Fontes

O hello world desta página foi executado em 15/08/2026, numa máquina Linux com o PostgreSQL 18.6 compilado do tarball oficial de código-fonte e servindo numa porta de teste. O psql abriu com psql (18.6), Type "help" for help. e o prompt postgres=#, igual ao passo 2. O CREATE DATABASE loja respondeu CREATE DATABASE, o \c loja respondeu You are now connected to database "loja" as user "postgres". e o CREATE TABLE respondeu CREATE TABLE. O \d produto mostrou not null em produto_no e nome, o índice "produto_pkey" PRIMARY KEY, btree (produto_no) e a linha "preco_positivo" CHECK (preco > 0::numeric). O INSERT do teclado devolveu INSERT 0 1. As duas recusas saíram com o texto exato da caixa acima, primeiro ERROR: new row for relation "produto" violates check constraint "preco_positivo" com DETAIL: Failing row contains (2, monitor, -50)., depois ERROR: null value in column "nome" of relation "produto" violates not-null constraint. O SELECT trouxe uma linha só, 1 | teclado | 120.00. Repetir o INSERT do teclado deu ERROR: duplicate key value violates unique constraint "produto_pkey" com DETAIL: Key (produto_no)=(1) already exists.. Com \set VERBOSITY verbose, os três erros mostraram os códigos 23514, 23502 e 23505, na ordem em que a página cita. O passo 1 e a linha sudo -u postgres psql do passo 2 ficaram de fora do teste, porque a máquina usada não tem sudo. Ali o servidor subiu com pg_ctl e o cliente entrou com psql -U postgres.

Para continuar

Se você veio direto pra cá, vale ver MySQL depois, que resolve o mesmo problema com outra sintaxe e é o que você mais encontra em hospedagem barata e em projeto antigo. Do outro lado da comparação está o MongoDB, que guarda documento em vez de linha de tabela e trata a forma do dado de um jeito bem diferente do que você acabou de fazer aqui. Os três estão no mapa da stack web, junto com as linguagens que vão conversar com eles.

O curso desta trilha está em preparação. A lista paga menos.

Os tutoriais daqui continuam gratuitos. O que está a caminho é um curso pago que transforma esse mapa em formação guiada, na ordem certa e com projeto. Deixe seu e-mail: você recebe os estudos novos gratuitos e, no lançamento, o desconto de quem entrou na lista primeiro.