Boas Práticas28 Mar 20268 min de leitura

Top 10 Erros de Design de Banco de Dados que Desenvolvedores Cometem (e Como Evitá-los)

Design ruim de banco de dados aparece meses depois — em queries lentas, refatorações dolorosas e relatórios financeiros incorretos. Veja os 10 erros mais comuns e como evitar cada um.

Design ruim de banco de dados não se anuncia no primeiro dia. Aparece seis meses depois quando queries levam 30 segundos, quando uma funcionalidade "simples" requer refatorar metade do schema, ou quando você percebe que vinha armazenando preços como floats e seus relatórios financeiros não fecham.

Estes são os dez erros mais comuns que vemos desenvolvedores cometendo ao projetar bancos de dados — e como evitar cada um.

1. Não projetar antes de codificar

O erro mais caro acontece antes de qualquer SQL ser escrito: pular direto para o código sem pensar através do modelo de dados. Desenvolvedores criam tabelas conforme precisam delas, adicionam colunas reativamente e acabam com um schema que reflete a ordem em que construíram funcionalidades em vez da estrutura lógica do domínio.

A correção é direta: gaste 30 minutos com um diagrama ER antes de escrever qualquer código. Mapeie suas entidades, relacionamentos e atributos principais. Você não precisa de perfeição — você precisa de um ponto de partida que capture as principais decisões estruturais. Uma ferramenta visual torna isso mais rápido do que você imagina. Você consegue projetar um schema de 20 tabelas em ER Flow em menos de 30 minutos, e o tempo economizado por não ter que refatorar depois é medido em dias.

2. Usar o tipo de dado errado para dinheiro

Armazenar preços, totais ou qualquer valor financeiro como FLOAT ou DOUBLE é um erro clássico. Aritmética de ponto flutuante é inerentemente imprecisa — 0.1 + 0.2 resulta em 0.30000000000000004 na maioria das linguagens. Ao longo de milhões de transações, erros de arredondamento se acumulam em discrepâncias financeiras reais.

Use DECIMAL(10,2) para valores de moeda, ou armazene valores como inteiros na menor unidade de moeda (centavos). Um produto com preço de $29.99 é armazenado como 2999. Aritmética inteira é exata, e você converte para formato de exibição apenas na camada de apresentação.

3. Não indexar foreign keys

Cada coluna de foreign key deve ter um índice. Quando você faz query de todos os pedidos de um usuário (WHERE user_id = 123), o banco de dados escaneia a tabela inteira de orders se user_id não está indexado. Alguns bancos de dados (como MySQL com InnoDB) criam índices em foreign keys automaticamente. PostgreSQL não.

Além de foreign keys, adicione índices em qualquer coluna que você frequentemente filtra, ordena ou faz join: status, created_at, email, slug. Mas não sobre-indexe — cada índice desacelera escritas e consome storage. Indexe as colunas que suas queries realmente usam, não cada coluna que teoricamente poderia ser consultada.

4. Armazenar dados repetidos em vez de usar relacionamentos

Quando um desenvolvedor armazena customer_name, customer_email e customer_phone diretamente em cada pedido em vez de referenciar uma tabela de customers, criou um problema de denormalização. Quando o cliente atualiza seu email, você precisa atualizá-lo em cada pedido. Se perder um, seus dados ficam inconsistentes.

A correção é normalização: armazene cada fato em um lugar e use foreign keys para referenciar. Orders têm um customer_id que aponta para a tabela customers. O email do cliente existe em exatamente uma linha.

Há razões legítimas para denormalizar — armazenar o nome do produto e preço em itens de pedido (porque o produto pode mudar após o pedido), ou cachear valores computados para performance. Mas denormalize intencionalmente, não acidentalmente. Saiba quando você está fazendo um trade-off versus quando está criando uma bagunça.

5. Usar uma única "God table"

Todos já vimos: uma tabela data ou uma tabela records com 50+ colunas, onde diferentes tipos de registros usam diferentes subconjuntos de colunas. Uma linha pode ser um usuário, um produto ou um pedido, distinguido por uma coluna type. A maioria das colunas é NULL para qualquer linha dada.

Isso acontece quando desenvolvedores tentam evitar criar novas tabelas. O resultado é um schema que é impossível de raciocinar, impossível de indexar eficientemente e um pesadelo para manter.

Cada entidade distinta merece sua própria tabela. Se usuários, produtos e pedidos têm diferentes atributos e diferentes ciclos de vida, devem ser tabelas separadas. O ligeiro overhead de criar uma nova tabela e migration é trivial comparado ao custo de longo prazo de uma God table.

6. Ignorar relacionamentos muitos-para-muitos

Quando duas entidades têm um relacionamento muitos-para-muitos (alunos inscritos em cursos, produtos em categorias, usuários com papéis), alguns desenvolvedores tentam armazená-lo como uma lista separada por vírgulas: categories = "1,2,5,12". Isso torna queries quase impossíveis — como você encontra todos os produtos na categoria 5? — e viola a primeira forma normal.

A abordagem correta é uma junction table: product_categories com colunas product_id e category_id, cada uma com uma foreign key. Isso é queryável, indexável e permite atributos adicionais no relacionamento (como sort_order ou added_at).

7. Não planejar para soft deletes

Deletar um usuário do banco de dados parece simples até você perceber que seus pedidos, comentários, reviews e outros dados associados ou são cascade-deletados (perdendo registros críticos para negócios) ou deixam foreign keys órfãs.

Soft deletes — adicionar uma coluna de timestamp deleted_at que marca registros como deletados sem realmente removê-los — resolvem isso limpa. Um deleted_at não-nulo significa que o registro está "deletado." Sua aplicação os filtra por padrão, mas os dados permanecem intactos para registros históricos, conformidade e audit trails.

Planeje isso desde o início. Adicionar soft deletes a um schema existente com milhões de linhas e dezenas de queries é significativamente mais difícil do que incluir deleted_at desde o dia um.

8. Armazenar JSON para tudo

O jsonb do PostgreSQL e o tipo de coluna JSON do MySQL são poderosos — mas não são um substituto para design de schema adequado. Armazenar dados estruturados como JSON significa que você perde type safety (um "price" dentro de JSON pode ser string, número ou null), você não consegue forçar constraints (sem foreign keys, sem NOT NULL, sem unique constraints dentro de JSON), suporte a índice é limitado (índices GIN ajudam, mas não são tão eficientes quanto índices B-tree em colunas tipadas) e mudanças de schema se tornam invisíveis (qualquer um consegue adicionar ou remover uma chave no JSON, sem migration ou documentação).

Colunas JSON são excelentes para dados verdadeiramente não-estruturados: preferências do usuário, caches de resposta de API, metadata que varia por registro. São uma escolha ruim para dados que têm estrutura consistente e relacionamentos — esses dados pertencem em colunas apropriadamente tipadas e tabelas relacionadas.

9. Não considerar padrões de query durante o design

Um schema normalizado é teoricamente correto, mas se sua query mais comum requer fazer join de 8 tabelas, você tem um problema de performance. Design de banco de dados deve balancear normalização com as queries que sua aplicação realmente executa.

Antes de finalizar seu schema, liste suas dez queries mais comuns. Se uma query crítica requer joins caros através de muitas tabelas, considere denormalização estratégica: adicione uma coluna computada, crie uma materialized view ou reestruture as tabelas para reduzir profundidade de join.

Essa é uma decisão de tempo de design, não uma tarefa de "vamos otimizar depois". Adicionar uma coluna total_orders_count na tabela users durante design inicial é fácil. Adicioná-la depois de um ano de dados em produção requer uma migration que atualiza milhões de linhas.

10. Sem convenções de naming

Naming inconsistente torna schemas confusos: algumas tabelas são singulares (user), outras plurais (orders). Algumas colunas usam camelCase (firstName), outras usam snake_case (first_name). Foreign keys às vezes são userId, às vezes user_id, às vezes fk_user.

Escolha uma convenção e mantenha-a. A mais comum na indústria é nomes de tabelas plurais, snake_case (users, order_items), nomes de coluna singulares, snake_case (first_name, created_at), e foreign keys nomeadas como singular_table_id (user_id, order_id). Colunas booleanas prefixadas com is_ ou has_ (is_active, has_verified).

Documente sua convenção e force-a em code reviews. Um schema consistente é dramaticamente mais fácil de trabalhar do que um onde cada tabela segue regras diferentes.

Prevenção é Mais Barata que Cura

Todos esses erros compartilham um traço comum: são fáceis de evitar no tempo de design e caros de corrigir depois. Gastar uma hora projetando seu schema visualmente — vendo todas as tabelas, relacionamentos e tipos de dados em uma visão — detecta a maioria desses problemas antes que se tornem embedded em código de produção e dados.

Ferramentas como ER Flow tornam esse passo de design rápido e colaborativo. Design visual superficializa problemas estruturais (relacionamentos faltantes, problemas de denormalização, índices faltantes) que são invisíveis em uma lista linear de arquivos de migration. E com funcionalidades como design assistido por IA e geração de migration, o passo de design não desacelera seu desenvolvimento — ele o acelera.