PostgreSQL: Criando e Alterando Tabelas no PostgreSQL

Última atualização: 2026-08-26

Uma tabela é o objeto mais central em um banco de dados—é a "planilha" que armazena seus dados, onde cada linha é um registro e cada coluna é um campo.

1. O Que Você Vai Aprender


2. Uma História Real de uma Equipe de E-Commerce

(1) A Dor: Como Projetar Três Tabelas Centrais

A equipe de Alice recebeu um novo requisito: criar 3 tabelas centrais para o sistema de e-commerce—users (usuários), products (produtos) e orders (pedidos). Os requisitos são:

Alice estava insegura: onde colocar as restrições? Como escolher os tipos de dados? Como configurar exclusões em cascata de chaves estrangeiras?

(2) A Solução: CREATE TABLE + Restrições

O PostgreSQL permite declarar todas as restrições no momento da criação da tabela, para que o banco de dados garanta a qualidade dos seus dados:

SQL
-- Criar tabela de usuários com unicidade de email e timestamp automático
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    password_hash CHAR(60) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Criar tabela de produtos com verificação de preço > 0
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) CHECK (price > 0),
    stock INTEGER DEFAULT 0 CHECK (stock >= 0),
    attributes JSONB DEFAULT '{}'::jsonb
);

-- Criar tabela de pedidos com chaves estrangeiras e verificação de status
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    product_id INTEGER REFERENCES products(id),
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    status VARCHAR(20) DEFAULT 'pending'
        CHECK (status IN ('pending', 'paid', 'shipped', 'delivered', 'cancelled')),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

(3) O Retorno


3. Sintaxe do CREATE TABLE

(1) Estrutura Completa da Sintaxe

SQL
CREATE TABLE [IF NOT EXISTS] nome_tabela (
    nome_coluna tipo_dado [restricao_coluna ...],
    ...
    [, restricao_tabela ...]
);

(2) Restrições no Nível da Coluna

Restrição Sintaxe Descrição
PRIMARY KEY nome_coluna tipo PRIMARY KEY Chave primária (única + não nula)
NOT NULL nome_coluna tipo NOT NULL Não permite NULL
UNIQUE nome_coluna tipo UNIQUE Valores devem ser únicos
DEFAULT nome_coluna tipo DEFAULT valor Valor padrão
CHECK nome_coluna tipo CHECK (condicao) Verificação de condição
REFERENCES nome_coluna tipo REFERENCES tabela(col) Referência de chave estrangeira

(3) Restrições no Nível da Tabela

SQL
-- Chave primária composta
CREATE TABLE order_items (
    order_id INTEGER,
    product_id INTEGER,
    quantity INTEGER,
    PRIMARY KEY (order_id, product_id)
);

-- Chave estrangeira nomeada com cascata
CREATE TABLE comments (
    id SERIAL PRIMARY KEY,
    user_id INTEGER,
    content TEXT,
    CONSTRAINT fk_user
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE SET NULL
);

4. Tipos de Dados em Resumo

O PostgreSQL suporta mais de 40 tipos de dados; os mais comuns estão abaixo:

Categoria Tipo Descrição Exemplo
Inteiro SMALLINT 2 bytes, -32768 ~ 32767 idade, pequenas quantidades
INTEGER (INT) 4 bytes, -2,1 bilhões ~ 2,1 bilhões inteiro comum (padrão recomendado)
BIGINT 8 bytes, ±9,2 trilhões IDs grandes, valores (em centavos)
Autoincremento SERIAL INTEGER com autoincremento ID de chave primária (recomendado)
BIGSERIAL BIGINT com autoincremento chave primária de tabela grande
IDENTITY Autoincremento padrão SQL (PG 10+) recomendado para novos projetos
Ponto flutuante REAL 4 bytes, precisão de 6 dígitos computação científica
DOUBLE PRECISION 8 bytes, precisão de 15 dígitos computação científica
Ponto fixo DECIMAL(p,s) Decimal exato preço, valor (recomendado)
NUMERIC(p,s) Igual a DECIMAL mesmo que acima
String VARCHAR(n) Comprimento variável, com limite superior nome, email
TEXT Comprimento variável, sem limite superior descrição, conteúdo
CHAR(n) Comprimento fixo valor hash, código
Booleano BOOLEAN true / false / nulo flags
Data DATE data aniversário
TIMESTAMP data+hora (sem fuso horário) hora local
TIMESTAMPTZ data+hora+fuso horário apps globais (recomendado)
INTERVAL intervalo de tempo cálculo de duração
UUID UUID UUID de 128 bits ID distribuído
JSON JSON JSON texto boa compatibilidade
JSONB JSON binário (recomendado) consultas indexadas rápidas
💡 Dica: No PostgreSQL, VARCHAR e TEXT não têm diferença de desempenho (diferente do MySQL). Na maioria dos casos, VARCHAR (com limite de comprimento) ou TEXT (sem limite) é recomendado.


5. Restrições em Detalhes

(1) PRIMARY KEY

100%
graph TB
    PK[PRIMARY KEY] --> U[UNIQUE<br/>Sem valores duplicados]
    PK --> NN[NOT NULL<br/>Não pode ser vazio]
    PK --> IDX[Índice Automático<br/>Índice B-Tree criado automaticamente]

▶ Exemplo: Chave Primária Simples e Composta

SQL
-- Chave primária de coluna única (mais comum)
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

-- Chave primária composta (para tabelas de junção)
CREATE TABLE product_tags (
    product_id INTEGER REFERENCES products(id),
    tag_id INTEGER REFERENCES tags(id),
    PRIMARY KEY (product_id, tag_id)
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

(2) FOREIGN KEY

Ação em cascata Comportamento ON DELETE Comportamento ON UPDATE
CASCADE Exclusão em cascata (excluir usuário → excluir seus pedidos) Atualização em cascata
SET NULL Definir como NULL Definir como NULL
SET DEFAULT Definir como valor padrão Definir como valor padrão
RESTRICT Rejeitar exclusão (comportamento padrão) Rejeitar atualização
NO ACTION Igual a RESTRICT (padrão SQL) Igual a RESTRICT

▶ Exemplo: Chave Estrangeira e Ações em Cascata

SQL
-- Itens do pedido: excluir pedido -> excluir todos os seus itens (CASCADE)
-- Produto: excluir produto -> definir product_id como NULL (SET NULL)
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
    product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
    quantity INTEGER NOT NULL DEFAULT 1
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

(3) Restrição CHECK

▶ Exemplo: Restrição CHECK Protegendo a Qualidade dos Dados

SQL
-- Preço deve ser positivo, desconto não pode exceder o preço
CREATE TABLE promotions (
    id SERIAL PRIMARY KEY,
    product_id INTEGER REFERENCES products(id),
    discount_price DECIMAL(10, 2) CHECK (discount_price > 0),
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    -- CHECK do PG pode referenciar outras colunas na mesma linha!
    CHECK (end_date > start_date)
    -- Nota: CHECK não pode referenciar uma subconsulta entre tabelas; validação entre tabelas precisa de um trigger
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

6. ALTER TABLE

(1) Operações Comuns de Modificação

Operação Sintaxe Descrição
Adicionar coluna ALTER TABLE t ADD COLUMN col tipo Adiciona uma coluna
Remover coluna ALTER TABLE t DROP COLUMN col Remove uma coluna
Renomear coluna ALTER TABLE t RENAME COLUMN antigo TO novo Renomeia uma coluna
Alterar tipo ALTER TABLE t ALTER COLUMN col TYPE novo_tipo Altera o tipo de dado
Definir padrão ALTER TABLE t ALTER COLUMN col SET DEFAULT val Define valor padrão
Remover padrão ALTER TABLE t ALTER COLUMN col DROP DEFAULT Remove valor padrão
Definir NOT NULL ALTER TABLE t ALTER COLUMN col SET NOT NULL Torna não nulo
Remover NOT NULL ALTER TABLE t ALTER COLUMN col DROP NOT NULL Permite nulo
Adicionar restrição ALTER TABLE t ADD CONSTRAINT nome CHECK (...) Adiciona CHECK
Renomear tabela ALTER TABLE nome_antigo RENAME TO nome_novo Renomeia tabela

▶ Exemplo: Modificar Estrutura da Tabela

SQL
-- Adicionar uma nova coluna
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Adicionar com valor padrão
ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT true;

-- Alterar tipo da coluna (requer USING para tipos incompatíveis)
ALTER TABLE users ALTER COLUMN phone TYPE TEXT;

-- Renomear coluna
ALTER TABLE users RENAME COLUMN phone TO phone_number;

-- Definir valor padrão
ALTER TABLE users ALTER COLUMN is_active SET DEFAULT true;

-- Adicionar restrição CHECK
ALTER TABLE products ADD CONSTRAINT price_reasonable
    CHECK (price < 100000);

-- Remover uma coluna
ALTER TABLE users DROP COLUMN IF EXISTS phone_number;

-- Renomear tabela
ALTER TABLE users RENAME TO customers;

Output:

TEXT 📖 Somente leitura
-- instrução SQL executada com sucesso
⚠️ Nota: Alterar o tipo de uma coluna pode exigir reescrever toda a tabela (ex.: VARCHAR(50)→VARCHAR(100) não exige, mas INTEGER→TEXT exige). Em tabelas grandes, use ALTER TABLE ... ALTER COLUMN ... TYPE ... USING ... e certifique-se de que a cláusula USING converta corretamente.


7. DROP TABLE

▶ Exemplo: Excluir uma Tabela

SQL
-- Exclusão segura (sem erro se a tabela não existir)
DROP TABLE IF EXISTS test_table;

-- Exclusão em cascata (também remove objetos dependentes como views)
DROP TABLE IF EXISTS users CASCADE;

Output:

TEXT 📖 Somente leitura
-- instrução SQL executada com sucesso
Opção Descrição
IF EXISTS Sem erro se a tabela não existir
CASCADE Também remove objetos que dependem desta tabela (views, referências de chave estrangeira, etc.)
RESTRICT Rejeita a exclusão quando há objetos dependentes (comportamento padrão)
🔥 Erro comum: DROP TABLE é irreversível! Todos os dados são permanentemente perdidos. Sempre faça pg_dump de backup primeiro em produção.


8. Inspecionando a Estrutura da Tabela com psql

Comando Função Exemplo
\dt Listar todas as tabelas \dt
\dt+ Listar tabelas (com informações de tamanho) \dt+
\d nomedatabela Mostrar estrutura da tabela \d users
\d+ nomedatabela Mostrar estrutura da tabela (detalhado) \d+ users

▶ Exemplo: Inspecionar Estrutura da Tabela

BASH
# Listar todas as tabelas no banco de dados atual
\dt

# Mostrar estrutura da tabela users
\d users

Output:

TEXT 📖 Somente leitura
                                        Table "public.users"
   Column    |          Type          | Collation | Nullable |              Default
-------------+------------------------+-----------+----------+-----------------------------------
 id          | integer                |           | not null | nextval('users_id_seq'::regclass)
 email       | character varying(255) |           | not null |
 name        | character varying(100) |           | not null |
 password_hash | character(60)        |           | not null |
 created_at  | timestamp with time zone |         |          | now()
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "users_email_key" UNIQUE CONSTRAINT, btree (email)

9. Exemplo Completo: Três Tabelas Centrais do E-Commerce

SQL
-- ============================================
-- Exemplo completo: tabelas centrais do e-commerce
-- Usuários, Produtos, Pedidos com restrições adequadas
-- ============================================

-- 1. Tabela de usuários
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    password_hash CHAR(60) NOT NULL,
    role VARCHAR(20) DEFAULT 'customer'
        CHECK (role IN ('customer', 'admin', 'manager')),
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 2. Tabela de produtos
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL CHECK (price > 0),
    stock INTEGER DEFAULT 0 CHECK (stock >= 0),
    category VARCHAR(50),
    attributes JSONB DEFAULT '{}'::jsonb,
    is_available BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 3. Tabela de pedidos com chaves estrangeiras
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    status VARCHAR(20) DEFAULT 'pending'
        CHECK (status IN ('pending', 'paid', 'shipped', 'delivered', 'cancelled')),
    total_amount DECIMAL(12, 2) DEFAULT 0 CHECK (total_amount >= 0),
    shipping_address TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 4. Tabela de itens do pedido (tabela de junção)
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(10, 2) NOT NULL CHECK (unit_price > 0),
    subtotal DECIMAL(12, 2) GENERATED ALWAYS AS (quantity * unit_price) STORED
);

-- 5. Inserir dados de exemplo
INSERT INTO users (email, name, password_hash) VALUES
    ('alice@example.com', 'Alice', '$2a$12$dummyhashforalice1234567890abcdefghijklmnopqr'),
    ('bob@example.com', 'Bob', '$2a$12$dummyhashforbob1234567890abcdefghijklmnopqrstuv');

INSERT INTO products (name, price, stock, category, attributes) VALUES
    ('Running Shoes', 89.99, 150, 'Footwear', '{"color": "red", "size": 42}'::jsonb),
    ('Laptop Backpack', 49.99, 300, 'Accessories', '{"color": "black", "material": "nylon"}'::jsonb);

-- 6. Verificar estrutura da tabela
\d users
\d products
\d orders
\d order_items

❓ Perguntas Frequentes

P: Qual a diferença entre SERIAL e IDENTITY, e qual devo usar? R: SERIAL é sintaxe proprietária do PG que internamente cria uma sequence e define um valor padrão. IDENTITY é sintaxe padrão SQL (PG 10+), com comportamento mais rigoroso (você não pode inserir manualmente um valor a menos que use OVERRIDING SYSTEM VALUE). Para novos projetos, IDENTITY é recomendado.

P: VARCHAR ou TEXT—qual escolher? R: No PG, VARCHAR e TEXT têm desempenho idêntico. VARCHAR(n) tem limite de comprimento, TEXT não tem. Sugestão: use VARCHAR quando houver um limite claro de comprimento (ex.: email VARCHAR(255)), e TEXT quando não houver limite (ex.: corpo de artigo). Não use VARCHAR sem comprimento—isso equivale a TEXT.

P: TIMESTAMP ou TIMESTAMPTZ? R: Sempre use TIMESTAMPTZ (com fuso horário) é recomendado. Ele armazena hora UTC e converte automaticamente para o fuso horário do cliente na exibição. TIMESTAMP não tem fuso horário e causa confusão em aplicações com múltiplos fusos.

P: A exclusão em cascata CASCADE de chave estrangeira é segura? R: CASCADE é conveniente em desenvolvimento (excluir usuário → excluir pedidos → excluir itens do pedido, cascata de três níveis), mas tenha cuidado em produção—excluir acidentalmente um usuário pode excluir grandes quantidades de dados. Sugestão: use RESTRICT para tabelas de negócio críticas, CASCADE para tabelas de log/temporárias.

P: ALTER TABLE em uma tabela grande bloqueia a tabela? R: Operações simples (ADD COLUMN com valor padrão, aumentar comprimento de VARCHAR) não bloqueiam a tabela. Mas alterar tipo de dado, adicionar restrição NOT NULL, etc., exigem varredura completa da tabela e bloquearão a tabela. Para tabelas grandes, use CREATE INDEX CONCURRENTLY (abordado em uma lição posterior) ou execute durante uma janela de manutenção.

P: Qual o número máximo de colunas em uma tabela? R: Uma tabela PG pode ter até 1600 colunas (na prática, frequentemente menos devido ao limite de 8KB de tamanho de linha). Mas mais de 50 colunas geralmente é um problema de design—considere dividir a tabela ou usar JSONB para armazenar campos esparsos.


📖 Resumo


📝 Exercícios

  1. Básico (★): Crie uma tabela categories (id, name, description, created_at), onde name deve ser NOT NULL e UNIQUE. Após inserir 3 linhas de teste, use \d categories para inspecionar a estrutura da tabela.

  2. Intermediário (★★): Com base nas três tabelas de e-commerce criadas nesta lição, use ALTER TABLE para adicionar uma coluna last_login_at TIMESTAMPTZ e uma coluna login_count INTEGER DEFAULT 0 à tabela users. Em seguida, adicione uma restrição CHECK garantindo que login_count não possa ser negativo.

  3. Desafio (★★★): Crie uma tabela product_reviews com: id (chave primária), product_id (chave estrangeira referenciando products), user_id (chave estrangeira referenciando users), rating (1–5, restrição CHECK), title, content, created_at. Requisitos: quando um usuário for excluído, mantenha a avaliação mas defina user_id como NULL; quando um produto for excluído, exclua em cascata todas as suas avaliações.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%