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
- CREATE TABLE para criar uma tabela (definições de coluna, restrições, valores padrão)
- Um tour rápido pelos tipos de dados comuns do PostgreSQL
- Restrições PRIMARY KEY / FOREIGN KEY / UNIQUE / NOT NULL / CHECK
- ALTER TABLE para modificar a estrutura da tabela
- DROP TABLE para excluir uma tabela
\ddo psql para inspecionar a estrutura da tabela
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:
- Tabela de usuários: email único, senha não nula, hora de registro gravada automaticamente
- Tabela de produtos: preço deve ser maior que 0, estoque não pode ser negativo
- Tabela de pedidos: vinculada a usuário e produto, status limitado a um conjunto fixo de valores
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:
-- 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
- Qualidade dos dados garantida no nível do banco de dados (impossível inserir um produto com preço < 0)
- Exclusão em cascata de chave estrangeira (excluir um usuário exclui automaticamente todos os seus pedidos)
- Valores padrão reduzem o código na camada de aplicação (
created_atpreenchido automaticamente)
3. Sintaxe do CREATE TABLE
(1) Estrutura Completa da Sintaxe
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
-- 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 |
5. Restrições em Detalhes
(1) PRIMARY KEY
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
-- 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:
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
-- 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:
CREATE TABLE
(3) Restrição CHECK
▶ Exemplo: Restrição CHECK Protegendo a Qualidade dos Dados
-- 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:
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
-- 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:
-- instrução SQL executada com sucesso
ALTER TABLE ... ALTER COLUMN ... TYPE ... USING ... e certifique-se de que a cláusula USING converta corretamente.
7. DROP TABLE
▶ Exemplo: Excluir uma Tabela
-- 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:
-- 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) |
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
# Listar todas as tabelas no banco de dados atual
\dt
# Mostrar estrutura da tabela users
\d users
Output:
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
-- ============================================
-- 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
- CREATE TABLE define a estrutura da tabela; definições de coluna incluem tipo de dado + restrição + valor padrão
- Escolha de tipo de dado: INTEGER/SERIAL para inteiros, DECIMAL para dinheiro, TIMESTAMPTZ para tempo, JSONB para estruturas flexíveis
- Cinco restrições principais: PRIMARY KEY / FOREIGN KEY / UNIQUE / NOT NULL / CHECK
- Cascatas de chave estrangeira: CASCADE (cascata) / SET NULL / RESTRICT (padrão)—escolha conforme necessidade de negócio
- ALTER TABLE pode adicionar/remover/renomear colunas e modificar tipos e restrições
- DROP TABLE é irreversível; IF EXISTS previne erros; CASCADE remove objetos dependentes
\ddo psql mostra estrutura da tabela,\dtlista todas as tabelas
📝 Exercícios
-
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 categoriespara inspecionar a estrutura da tabela. -
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 TIMESTAMPTZe uma colunalogin_count INTEGER DEFAULT 0à tabelausers. Em seguida, adicione uma restrição CHECK garantindo quelogin_countnão possa ser negativo. -
Desafio (★★★): Crie uma tabela
product_reviewscom: 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.