Boas PráticasMay 26, 20269 min de leitura

Guia de Design de Schema PostgreSQL: Boas Práticas e Ferramentas

PostgreSQL oferece um sistema de tipos mais rico e restrições mais poderosas do que a maioria dos bancos de dados relacionais. Este guia cobre decisões de design de schema específicas do PostgreSQL — tipos de dados, índices, restrições, particionamento e schemas — com conselhos práticos para sistemas em produção.

PostgreSQL é o banco de dados relacional open-source mais rico em recursos disponível hoje. Seu sistema de tipos, capacidades de indexação e sistema de restrições vão muito além do que a maioria dos bancos oferece — mas somente se você souber como usá-los. Decisões de design de schema que funcionam bem no MySQL ou SQLite podem ser subótimas no PostgreSQL, e vice-versa.

Este guia cobre as escolhas de design de schema específicas do PostgreSQL que mais importam em produção: quais tipos de dados usar, como indexar efetivamente, como estruturar schemas com namespaces e como ferramentas como ER Flow podem agilizar o processo de design.

Escolhendo os Tipos Numéricos Certos

PostgreSQL oferece múltiplos tipos de inteiro. Use `SMALLINT` (2 bytes, até ~32.000) para pequenas tabelas de lookup como códigos de status. Use `INTEGER` (4 bytes, até ~2,1 bilhões) para a maioria dos IDs e contagens. Use `BIGINT` (8 bytes) para tabelas que excederão 2 bilhões de linhas, IDs voltados ao usuário que você não quer esgotar e qualquer contador que cresce sem limite.

Para chaves primárias auto-incrementadas, prefira `BIGSERIAL` em vez de SERIAL para qualquer tabela que possa escalar. A diferença de performance é negligível, mas esgotar os IDs SERIAL em produção é um incidente doloroso. Para sistemas distribuídos, considere gen_random_uuid() (v4) ou o emergente uuid_generate_v7() (ordenado por tempo, disponível via extensão) para UUIDs que ordenam corretamente em índices clusterizados.

text vs. varchar: A Resposta do PostgreSQL

Na maioria dos bancos de dados, VARCHAR(n) é mais eficiente do que TEXT porque é armazenado de forma diferente. No PostgreSQL, `TEXT` e `VARCHAR` são armazenados de forma idêntica — não há diferença de performance. A restrição VARCHAR(n) simplesmente adiciona uma verificação de comprimento. A comunidade PostgreSQL geralmente recomenda usar TEXT para strings de comprimento variável e adicionar uma restrição CHECK (length(column) <= n) apenas quando você realmente precisa impor um limite. Isso mantém os schemas mais simples sem sacrificar nada.

Usando jsonb para Dados Semi-Estruturados

O tipo `jsonb` do PostgreSQL armazena JSON em formato binário que suporta indexação e consultas eficientes. É um dos recursos mais poderosos do PostgreSQL. Use jsonb para colunas onde a estrutura varia entre linhas — blobs de metadados, objetos de configuração, payloads de API de terceiros e atributos extensíveis. Evite usá-lo como substituto de colunas devidamente normalizadas: se você consulta data->'email' em uma cláusula WHERE em cada requisição, essa coluna deveria ser uma coluna TEXT adequada com um índice, não um campo JSON.

Quando você usa jsonb, crie um índice GIN (CREATE INDEX idx ON table USING gin(column)) para habilitar consultas de contenção eficientes (@>) e verificações de existência de chave (?). Índices GIN em jsonb são um dos grandes diferenciadores do PostgreSQL em relação a outros bancos de dados.

Índices: B-tree, GIN e GiST

PostgreSQL suporta múltiplos tipos de índice. B-tree (o padrão) trata consultas de igualdade e intervalo: =, <, >, BETWEEN, LIKE 'prefixo%'. É o que você usa para chaves estrangeiras, colunas de status e intervalos de datas — a grande maioria dos índices.

GIN (Generalized Inverted Index) é otimizado para valores compostos — arrays, jsonb e busca de texto completo (tsvector). Use-o quando um único valor de coluna contém múltiplos elementos pesquisáveis. GiST (Generalized Search Tree) trata dados geométricos, busca de texto completo e tipos de intervalo (daterange, tsrange). Para a maioria do trabalho de design de schema você usará índices B-tree e recorrerá ao GIN ao lidar com jsonb ou busca de texto completo.

Índices Parciais para Padrões Comuns

Índices parciais são um recurso subutilizado do PostgreSQL. Um índice parcial só indexa linhas que correspondem a uma condição WHERE. Para tabelas com soft-delete, um índice parcial em deleted_at IS NULL significa que o índice contém apenas registros ativos — é menor e mais rápido do que um índice completo. Para consultas filtradas por status (WHERE status = 'pending'), um índice parcial em status com uma condição WHERE status = 'pending' é muito mais eficiente do que um índice completo na coluna status. ``sql CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'pending';

Restrições Além de Chaves Estrangeiras

PostgreSQL suporta restrições expressivas que vão além de NOT NULL e FOREIGN KEY. Restrições `CHECK` impõem regras de domínio: CHECK (price > 0), CHECK (end_date > start_date), CHECK (status IN ('active', 'inactive', 'suspended')). Restrições `EXCLUDE` impedem intervalos sobrepostos — invaluáveis para sistemas de reserva que precisam evitar reservas duplas. Essas restrições rodam no nível do banco de dados, então são impostas independentemente de qual aplicação ou script escreve dados.

Namespaces de Schema

PostgreSQL usa o termo "schema" (de forma confusa) para duas coisas diferentes: a estrutura geral do banco de dados e namespaces de schema — agrupamentos lógicos de tabelas dentro de um banco de dados. Por padrão, todas as tabelas ficam no schema public. Em aplicações maiores, você pode usar namespaces para organizar tabelas: auth.users, billing.invoices, analytics.events. Isso mantém o namespace público limpo e permite gerenciar permissões no nível do namespace com GRANT USAGE ON SCHEMA billing TO billing_role.

Particionamento para Tabelas Grandes

Quando as tabelas crescem para centenas de milhões de linhas, o particionamento de tabelas melhora a performance de consultas e operações de manutenção. PostgreSQL suporta particionamento declarativo (introduzido no PostgreSQL 10). Um padrão comum é o particionamento por intervalo de datas: uma tabela events particionada por mês de created_at significa que os dados de cada mês ficam na sua própria partição física. Consultas que filtram por intervalo de datas ignoram partições irrelevantes completamente (poda de partição). VACUUM, ANALYZE e reconstruções de índice rodam por partição, tornando a manutenção muito mais rápida.

Projetando Schemas PostgreSQL com ER Flow

ER Flow suporta tipos de dados nativos do PostgreSQL em seu editor de colunas, incluindo UUID, TEXT, JSONB, TIMESTAMPTZ, SERIAL e BIGSERIAL. Você pode definir todas as restrições discutidas aqui — chaves primárias, chaves estrangeiras, restrições únicas e restrições check — visualmente, e depois gerar SQL de migration compatível com PostgreSQL. Seja projetando um novo schema do zero ou mapeando um banco de dados PostgreSQL existente, ER Flow lhe dá um canvas visual que torna os relacionamentos e a estrutura imediatamente claros. As migrations geradas usam sintaxe PostgreSQL para que rodem sem modificação.