Modelagem define o custo antes da primeira consulta

Todo sistema nasce com um schema que parece provisório. Dois anos depois, esse schema é a parte mais cara de mudar, porque já existem relatórios, integrações e rotinas noturnas apoiados nele. Performance de banco raramente é um problema de hardware: na maioria dos casos é consequência de decisões de modelagem tomadas sem pensar em como o dado seria consultado e mantido.

Um caso concreto: uma distribuidora de materiais de construção com ERP próprio em MySQL guardava o status do pedido em um campo de texto livre e não tinha chave estrangeira entre pedidos e itens. O resultado aparecia no fechamento mensal, quando o time precisava conciliar variações como "Cancelado", "cancelado" e "CANC" antes de faturar. A correção não foi criar índice nem trocar servidor: foi transformar o status em tabela de domínio com chave estrangeira e constraint, e recriar o vínculo entre pedidos e itens. O mesmo tipo de ajuste, feito no início, custaria uma tarde de modelagem.

Chaves, tipos e constraints: onde PostgreSQL e MySQL divergem

As decisões abaixo são as que mais impactam operação e custo de manutenção nos dois bancos:

  • Chave primária: BIGINT sequencial é compacto e previsível. UUID aleatório (v4) fragmenta índices e incha páginas, o que pesa em tabelas grandes. Se a unicidade global é requisito, prefira UUIDv7 ou ULID, ou mantenha BIGINT interno e exponha um UUID apenas como identificador público.
  • Valores monetários: NUMERIC/DECIMAL sempre. FLOAT e DOUBLE introduzem erro de arredondamento que aparece em relatório fiscal, não em teste unitário.
  • Datas e fuso: no PostgreSQL, TIMESTAMPTZ grava em UTC e converte conforme a sessão. No MySQL, TIMESTAMP tem faixa limitada e depende de time_zone do servidor, enquanto DATETIME não carrega fuso. Escolha uma convenção e documente.
  • Domínios de valor: PostgreSQL permite ENUM e CHECK nomeados; no MySQL, alterar ENUM é DDL. Uma tabela de lookup costuma envelhecer melhor que qualquer enum.
  • JSON: JSONB no PostgreSQL e JSON no MySQL resolvem atributos esparsos, mas não substituem modelagem. Campo filtrado com frequência pertence a coluna própria.
  • Integridade: chaves estrangeiras e constraints no banco reduzem a dependência de scripts de correção. No MySQL, isso exige InnoDB e atenção ao foreign_key_checks durante migrações.

Normalização, desnormalização e evolução de schema

Terceira forma normal é o ponto de partida, não uma regra religiosa. Desnormalizar faz sentido quando existe leitura comprovadamente custosa e o dado derivado tem rotina de reconciliação. Materialized views no PostgreSQL e colunas geradas no MySQL cobrem parte desses casos com menos risco que duplicação manual.

Para dados históricos, particionamento por faixa de data (declarative partitioning no PostgreSQL, RANGE no MySQL) ajuda em retenção e arquivamento, mas não corrige modelo errado. O mesmo vale para migrações: use versionamento, aplique o padrão expandir e contrair (criar coluna nullable, preencher em lotes, migrar a leitura, remover a antiga) e teste com volume parecido ao de produção. No MySQL, alterações grandes podem copiar a tabela inteira e exigem ferramentas de alteração online. No PostgreSQL, valide constraints com NOT VALID antes de validar de fato.

Na prática

Quem mantém software em produção deveria tratar schema como código revisado:

  1. Toda coluna nova precisa de tipo, nulidade e justificativa no pull request.
  2. Mantenha um dicionário de dados acessível a quem escreve consulta e a quem decide produto.
  3. Rode migrações primeiro em ambiente com volume realista e meça o tempo antes de aplicar.
  4. Acompanhe crescimento das tabelas e revise o modelo quando um campo de texto começa a virar filtro.
  5. Trate migração como parte do pipeline de entrega, nunca como passo manual de madrugada.

Modelagem não é etapa encerrada: é a decisão que continua determinando quanto custa cada mudança futura.