Imported from artubss/SKILLS-CLAUDE-CODE (
skills/skills/database/postgresql/SKILL.md). Install upstream withnpx skills add artubss/SKILLS-CLAUDE-CODE --skill postgresql. Copyright stays with the author.
Design de Tabelas PostgreSQL
Use esta skill quando
- Projetar um schema para PostgreSQL
- Selecionar tipos de dados e constraints
- Planejar indexes, partições ou políticas RLS
- Revisar tabelas para escala e manutenibilidade
Não use esta skill quando
- Você está direcionando um banco de dados não-PostgreSQL
- Você precisa apenas de ajuste de query sem mudanças de schema
- Você requer um guia de modelagem agnóstico a BD
Instruções
- Capture entidades, padrões de acesso e metas de escala (linhas, QPS, retenção).
- Escolha tipos de dados e constraints que reforçam invariantes.
- Adicione indexes para caminhos reais de query e valide com
EXPLAIN. - Planeje particionamento ou RLS onde exigido por escala ou controle de acesso.
- Revise o impacto de migração e aplique mudanças com segurança.
Segurança
- Evite DDL destrutivo em produção sem backups e um plano de rollback.
- Use migrations e validação em staging antes de aplicar mudanças de schema.
Regras Principais
- Defina uma PRIMARY KEY para tabelas de referência (usuários, pedidos, etc.). Nem sempre necessário para dados de série temporal/eventos/logs. Quando usado, prefira
BIGINT GENERATED ALWAYS AS IDENTITY; useUUIDapenas quando unicidade global/opacidade é necessária. - Normalize primeiro (até 3NF) para eliminar redundância de dados e anomalias de atualização; denormalize apenas para leituras de alto ROI medidas onde desempenho de join é comprovadamente problemático. Denormalização prematura cria carga de manutenção.
- Adicione NOT NULL em todo lugar semanticamente necessário; use DEFAULTs para valores comuns.
- Crie indexes para caminhos de acesso que você realmente consulta: PK/unique (automático), colunas FK (manual!), filtros/ordenações frequentes e chaves de join.
- Prefira TIMESTAMPTZ para tempo de evento; NUMERIC para moeda; TEXT para strings; BIGINT para valores inteiros, DOUBLE PRECISION para floats (ou
NUMERICpara aritmética decimal exata).
"Gotchas" do PostgreSQL
- Identificadores: sem aspas → minúsculas. Evite nomes aspados/com maiúsculas mistas. Convenção: use
snake_casepara nomes de tabelas/colunas. - Unique + NULLs: UNIQUE permite múltiplos NULLs. Use
UNIQUE (...) NULLS NOT DISTINCT(PG15+) para restringir a um NULL. - Indexes em FK: PostgreSQL não faz auto-index em colunas FK. Adicione-os.
- Sem coercões silenciosas: extrapolação de comprimento/precisão gera erro (sem truncamento). Exemplo: inserir 999 em
NUMERIC(2,0)falha com erro, diferente de alguns bancos que silenciosamente truncam ou arredondam. - Sequences/identity têm lacunas (normal; não "corrija"). Rollbacks, crashes e transações concorrentes criam lacunas em sequências de ID (1, 2, 5, 6...). Este é comportamento esperado—não tente tornar IDs consecutivos.
- Armazenamento em heap: sem PK clusterizado por padrão (diferente de SQL Server/MySQL InnoDB);
CLUSTERé reorganização única, não mantida em inserts subsequentes. Ordem de linha em disco é ordem de inserção a menos que explicitamente clusterizado. - MVCC: updates/deletes deixam tuplas mortas; vacuum as manipula—projete para evitar churn de linhas largas em hot spots.
Tipos de Dados
- IDs:
BIGINT GENERATED ALWAYS AS IDENTITYpreferido (GENERATED BY DEFAULTtambém aceitável);UUIDquando mesclando/federando/usado em sistema distribuído ou para IDs opacos. Gere comuuidv7()(preferido se usar PG18+) ougen_random_uuid()(se usar versão mais antiga de PG). - Inteiros: prefira
BIGINTa menos que espaço em disco seja crítico;INTEGERpara ranges menores; eviteSMALLINTa menos que restringido. - Floats: prefira
DOUBLE PRECISIONsobreREALa menos que espaço em disco seja crítico. UseNUMERICpara aritmética decimal exata. - Strings: prefira
TEXT; se limites de comprimento forem necessários, useCHECK (LENGTH(col) <= n)em vez deVARCHAR(n); eviteCHAR(n). UseBYTEApara dados binários. Strings grandes/binários (>2KB limiar padrão) automaticamente armazenados em TOAST com compressão. Armazenamento TOAST:PLAIN(sem TOAST),EXTENDED(comprime + fora-de-linha),EXTERNAL(fora-de-linha, sem compressão),MAIN(comprime, mantém em linha se possível). PadrãoEXTENDEDgeralmente ótimo. Controle comALTER TABLE tbl ALTER COLUMN col SET STORAGE strategyeALTER TABLE tbl SET (toast_tuple_target = 4096)para limiar. Case-insensitive: para tratamento de locale/acento use collações não-determinísticas; para ASCII simples use expression indexes emLOWER(col)(preferido a menos que coluna precise PK/FK/UNIQUE case-insensitive) ouCITEXT. - Moeda:
NUMERIC(p,s)(nunca float). - Tempo:
TIMESTAMPTZpara timestamps;DATEpara apenas data;INTERVALpara durações. EviteTIMESTAMP(sem timezone). Usenow()para hora de início de transação,clock_timestamp()para hora de parede atual. - Booleanos:
BOOLEANcom constraintNOT NULLa menos que valores tri-estado sejam necessários. - Enums:
CREATE TYPE ... AS ENUMpara conjuntos pequenos e estáveis (ex: estados dos EUA, dias da semana). Para valores orientados por lógica de negócio e em evolução (ex: statuses de pedido) → use TEXT (ou INT) + CHECK ou tabela de lookup. - Arrays:
TEXT[],INTEGER[], etc. Use para listas ordenadas onde você consulta elementos. Index com GIN para containment (@>,<@) e overlap (&&) queries. Acesso:arr[1](1-indexado),arr[1:3](slicing). Bom para tags, categorias; evite para relações—use tabelas de junção. Sintaxe literal:'{val1,val2}'ouARRAY[val1,val2]. - Tipos de range:
daterange,numrange,tstzrangepara intervalos. Suportam overlap (&&), containment (@>), operadores. Index com GiST. Bom para agendamento, versionamento, ranges numéricos. Escolha um esquema de bounds e use consistentemente; prefira[)(inclusivo/exclusivo) por padrão. - Tipos de rede:
INETpara endereços IP,CIDRpara ranges de rede,MACADDRpara endereços MAC. Suportam operadores de rede (<<,>>,&&). - Tipos geométricos:
POINT,LINE,POLYGON,CIRCLEpara dados espaciais 2D. Index com GiST. Considere PostGIS para recursos espaciais avançados. - Text search:
TSVECTORpara documentos de busca full-text,TSQUERYpara queries de busca. Indextsvectorcom GIN. Sempre especifique idioma:to_tsvector('english', col)eto_tsquery('english', 'query'). Nunca use versões com um único argumento. Isto se aplica tanto a expressões de index quanto queries. - Tipos de domínio:
CREATE DOMAIN email AS TEXT CHECK (VALUE ~ '^[^@]+@[^@]+$')para tipos customizados reutilizáveis com validação. Reforça constraints entre tabelas. - Tipos compostos:
CREATE TYPE address AS (street TEXT, city TEXT, zip TEXT)para dados estruturados dentro de colunas. Acesso com sintaxe(col).field. - JSONB: preferido sobre JSON; index com GIN. Use apenas para attrs opcionais/semi-estruturados. APENAS use JSON se a ordenação original do conteúdo DEVE ser preservada.
- Tipos de vetor: tipo
vectordepgvectorpara busca de similaridade de vetor para embeddings.
Não use os seguintes tipos de dados
- NÃO use
timestamp(sem time zone); USEtimestamptzem vez disso. - NÃO use
char(n)ouvarchar(n); USEtextem vez disso. - NÃO use tipo
money; USEnumericem vez disso. - NÃO use tipo
timetz; USEtimestamptzem vez disso. - NÃO use
timestamptz(0)ou qualquer outra especificação de precisão; USEtimestamptzem vez disso. - NÃO use tipo
serial; USEgenerated always as identityem vez disso.
Tipos de Tabela
- Regular: padrão; totalmente durável, logged.
- TEMPORARY: escopo de sessão, auto-dropped, não logged. Mais rápido para work scratch.
- UNLOGGED: persistente mas não crash-safe. Escritas mais rápidas; bom para caches/staging.
Row-Level Security
Habilite com ALTER TABLE tbl ENABLE ROW LEVEL SECURITY. Crie políticas: CREATE POLICY user_access ON orders FOR SELECT TO app_users USING (user_id = current_user_id()). Controle de acesso baseado em usuário embutido no nível de linha.
Constraints
- PK: UNIQUE implícito + NOT NULL; cria um index B-tree.
- FK: especifique ação
ON DELETE/UPDATE(CASCADE,RESTRICT,SET NULL,SET DEFAULT). Adicione index explícito na coluna referenciadora—acelera joins e previne problemas de locking em deletes/updates do pai. UseDEFERRABLE INITIALLY DEFERREDpara dependências FK circulares verificadas no final da transação. - UNIQUE: cria um index B-tree; permite múltiplos NULLs a menos que
NULLS NOT DISTINCT(PG15+). Comportamento padrão:(1, NULL)e(1, NULL)são permitidos. ComNULLS NOT DISTINCT: apenas um(1, NULL)permitido. PrefiraNULLS NOT DISTINCTa menos que você especificamente precise de NULLs duplicados. - CHECK: constraints locais de linha; valores NULL passam no check (lógica tri-valorada). Exemplo:
CHECK (price > 0)permite preços NULL. Combine comNOT NULLpara reforçar:price NUMERIC NOT NULL CHECK (price > 0). - EXCLUDE: previne valores sobrepostos usando operadores.
EXCLUDE USING gist (room_id WITH =, booking_period WITH &&)previne double-booking de salas. Requer tipo de index apropriado (geralmente GiST).
Indexação
- B-tree: padrão para queries de igualdade/range (
=,<,>,BETWEEN,ORDER BY) - Compostos: ordem importa—index é usado se igualdade no prefixo esquerdo (
WHERE a = ? AND b > ?usa index em(a,b), masWHERE b = ?não). Coloque colunas mais seletivas/frequentemente filtradas primeiro. - Covering:
CREATE INDEX ON tbl (id) INCLUDE (name, email)- inclui colunas não-chave para index-only scans sem visitar tabela. - Parcial: para hot subsets (
WHERE status = 'active'→CREATE INDEX ON tbl (user_id) WHERE status = 'active'). Qualquer query comstatus = 'active'pode usar este index. - Expression: para chaves de busca computadas (
CREATE INDEX ON tbl (LOWER(email))). Expression deve corresponder exatamente em cláusula WHERE:WHERE LOWER(email) = 'user@example.com'. - GIN: containment/existência JSONB, arrays (
@>,?), busca full-text (@@) - GiST: ranges, geometria, constraints de exclusão
- BRIN: dados muito grandes, naturalmente ordenados (série temporal)—overhead mínimo de armazenamento. Efetivo quando ordem de linha em disco correlaciona com coluna indexada (ordem de inserção ou após
CLUSTER).
Particionamento
- Use para tabelas muito grandes (>100M linhas) onde queries consistentemente filtram na chave de partição (geralmente tempo/data).
- Uso alternativo: use para tabelas onde tarefas de manutenção de dados ditam ex: dados podados ou substituídos em bulk periodicamente
- RANGE: comum para série temporal (
PARTITION BY RANGE (created_at)). Crie partições:CREATE TABLE logs_2024_01 PARTITION OF logs FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'). TimescaleDB automatiza particionamento baseado em tempo ou ID com políticas de retenção e compressão. - LIST: para valores discretos (
PARTITION BY LIST (region)). Exemplo:FOR VALUES IN ('us-east', 'us-west'). - HASH: para distribuição uniforme quando nenhuma chave natural (
PARTITION BY HASH (user_id)). Cria N partições com módulo. - Constraint exclusion: requer constraints
CHECKem partições para planner de query podar. Auto-criado para particionamento declarativo (PG10+). - Prefira particionamento declarativo ou hypertables. NÃO use herança de tabela.
- Limitações: sem constraints UNIQUE globais—inclua chave de partição em PK/UNIQUE. FKs de tabelas particionadas não suportadas; use triggers.
Considerações Especiais
Tabelas Update-Heavy
- Separe colunas hot/cold—coloque colunas frequentemente atualizadas em tabela separada para minimizar bloat.
- Use
fillfactor=90para deixar espaço para HOT updates que evitam manutenção de index. - Evite atualizar colunas indexadas—previne HOT updates benéficos.
- Particione por padrões de atualização—separe linhas frequentemente atualizadas em partição diferente de dados estáveis.
Workloads Insert-Heavy
- Minimize indexes—crie apenas o que você consulta; cada index desacelera inserts.
- Use
COPYouINSERTmulti-linha em vez de inserts de linha única. - Tabelas UNLOGGED para dados de staging reconstruíveis—escritas muito mais rápidas.
- Adie criação de index para bulk loads—drop index, carregue dados, recrie indexes.
- Particione por tempo/hash para distribuir carga. TimescaleDB automatiza particionamento e compressão de dados insert-heavy.
- Use chave natural para primary key tal como (timestamp, device_id) se reforçar unicidade global é importante muitas tabelas insert-heavy não precisam de primary key.
- Se você precisa de chave substituta, Prefira
BIGINT GENERATED ALWAYS AS IDENTITYsobreUUID.
Design Amigável a Upsert
- Requer index UNIQUE nas colunas de conflito target—
ON CONFLICT (col1, col2)precisa de index unique exato (indexes parciais não funcionam). - Use
EXCLUDED.columnpara referenciar valores que seriam inseridos; atualize apenas colunas que realmente mudaram para reduzir overhead de escrita. DO NOTHINGmais rápido queDO UPDATEquando nenhuma atualização real é necessária.
Evolução Segura de Schema
- DDL Transacional: a maioria das operações DDL podem rodar em transações e ser rolled back—
BEGIN; ALTER TABLE...; ROLLBACK;para teste seguro. - Criação de index concorrente:
CREATE INDEX CONCURRENTLYevita bloquear escritas mas não pode rodar em transações. - Defaults voláteis causam rewrites: adicionar colunas
NOT NULLcom defaults voláteis (ex:now(),gen_random_uuid()) reescreve tabela inteira. Defaults não-voláteis são rápidos. - Drop constraints antes de colunas:
ALTER TABLE DROP CONSTRAINTentãoDROP COLUMNpara evitar problemas de dependência. - Mudanças de assinatura de função:
CREATE OR REPLACEcom argumentos diferentes cria overloads, não replacements. DROP versão antiga se nenhum overload desejado.
Generated Columns
... GENERATED ALWAYS AS (<expr>) STOREDpara campos computados, indexáveis. PG18+ adiciona colunasVIRTUAL(computadas em leitura, não armazenadas).
Extensions
pgcrypto:crypt()para hashing de senha.uuid-ossp: funções UUID alternativas; prefirapgcryptopara novos projetos.pg_trgm: busca de texto fuzzy com operador%, funçãosimilarity(). Index com GIN para aceleração deLIKE '%pattern%'.citext: tipo de texto case-insensitive. Prefira expression indexes emLOWER(col)a menos que você precise de constraints case-insensitive.btree_gin/btree_gist: habilite indexes de tipos misto (ex: index GIN em colunas JSONB e texto).hstore: pares chave-valor; geralmente supersedido por JSONB mas útil para mapeamentos simples de string.timescaledb: essencial para série temporal—particionamento automatizado, retenção, compressão, aggregates contínuos.postgis: suporte geoespacial compreensivo além de tipos geométricos básicos—essencial para aplicações baseadas em localização.pgvector: busca de similaridade de vetor para embeddings.pgaudit: audit logging para toda atividade de banco de dados.
Orientação JSONB
- Prefira
JSONBcom index GIN. - Padrão:
CREATE INDEX ON tbl USING GIN (jsonb_col);→ acelera:- Containment
jsonb_col @> '{"k":"v"}' - Existência de chave
jsonb_col ? 'k', qualquer/todas as chaves?\|,?& - Path containment em docs aninhados
- Disjunção
jsonb_col @> ANY(ARRAY['{"status":"active"}', '{"status":"pending"}'])
- Containment
- Workloads pesados
@>: considere opclassjsonb_path_opspara indexes menores/mais rápidos apenas containment:CREATE INDEX ON tbl USING GIN (jsonb_col jsonb_path_ops);- Trade-off: perde suporte para queries de existência de chave (
?,?|,?&)—apenas suporta containment (@>)
- Igualdade/range em campo scalar específico: extraia e index com B-tree (coluna gerada ou expression):
ALTER TABLE tbl ADD COLUMN price INT GENERATED ALWAYS AS ((jsonb_col->>'price')::INT) STORED;CREATE INDEX ON tbl (price);- Prefira queries como
WHERE price BETWEEN 100 AND 500(usa B-tree) sobreWHERE (jsonb_col->>'price')::INT BETWEEN 100 AND 500sem index.
- Arrays dentro de JSONB: use GIN +
@>para containment (ex: tags). Considerejsonb_path_opsse apenas fazer containment. - Mantenha relações core em tabelas; use JSONB para attrs opcionais/variáveis.
- Use constraints para limitar valores JSONB permitidos em coluna ex:
config JSONB NOT NULL CHECK(jsonb_typeof(config) = 'object')
Exemplos
Usuários
CREATE TABLE users (
user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX ON users (LOWER(email));
CREATE INDEX ON users (created_at);
Pedidos
CREATE TABLE orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id),
status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','PAID','CANCELED')),
total NUMERIC(10,2) NOT NULL CHECK (total > 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON orders (user_id);
CREATE INDEX ON orders (created_at);
JSONB
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY REFERENCES users(user_id),
attrs JSONB NOT NULL DEFAULT '{}',
theme TEXT GENERATED ALWAYS AS (attrs->>'theme') STORED
);
CREATE INDEX profiles_attrs_gin ON profiles USING GIN (attrs);