TG
data modeling·Banco de Dados·software architecture·8 min de leitura

Modelagem de dados: quando normalizar e quando deixar flexível

Modelagem de dados: decida quando apertar o schema, deixar campos flexíveis e separar CRUD, dashboards e relatórios usando cardinalidade e acessos reais.

Read in English
Modelagem de dados: quando normalizar e quando deixar flexível

Modelagem de dados começa por duas perguntas: que regra não pode quebrar e como esse dado será lido. Normalize quando a regra protege dinheiro, estoque, permissão, auditoria, contrato ou consistência entre módulos. Deixe flexível quando a forma ainda muda, o dado é local, a cardinalidade é limitada e existe validação clara.

Não escolha banco antes de entender a regra. Postgres, MongoDB e Firestore resolvem problemas diferentes. O erro é usar flexibilidade onde você precisava de integridade, ou usar rigidez onde o produto ainda está descobrindo o formato.

Como decidir o que vira schema?

Comece pelas invariantes. Uma invariante é uma regra que o sistema deve impedir mesmo quando a aplicação erra: pedido sem cliente, item com quantidade negativa, nota fiscal duplicada, permissão sem usuário, pagamento sem estado auditável.

Depois classifique o dado:

Tipo de dadoModelo mais seguro
Dinheiro, estoque, imposto, permissão, contratoTabela explícita, constraint, transação e auditoria
Entidade usada em vários fluxos de CRUDTabela ou coleção própria com identidade estável
Relação com filho ilimitadoTabela filha, coleção separada ou subcoleção
Dados pequenos sempre lidos juntosEmbutir no documento ou usar composição
Campo experimental ou customizado por clienteMetadata ou JSON versionado com validação
Métrica de dashboard ou relatórioProjeção, materialized view, tabela de resumo ou modelo dimensional

Flexível não significa sem regra. Significa que a regra está em outro lugar: validação de entrada, schema_version, limite de tamanho, índice planejado e política para promover o campo quando ele virar parte do negócio.

Como a cardinalidade muda o modelo?

Cardinalidade é a quantidade possível de relações entre entidades. O desenho muda quando uma relação cresce, quando o filho tem vida própria ou quando a aplicação deixa de ler tudo junto.

Use este guia:

RelaçãoExemploDecisão comum
1:1perfil e preferências pequenasMesma tabela, tabela separada ou documento embutido
1:N pequeno e lido juntopedido e poucos itensEmbutir em documento ou usar tabela filha
1:N sem limiteusuário e logs, post e comentáriosSeparar o filho
N:Nprodutos e categoriasTabela de junção ou coleção de relação
Filho sem vida própriaitem de pedidoComposição ou tabela filha com cascade
Filho consultado sozinhopagamento, evento, ticketEntidade própria

Em Postgres, pedido e itens normalmente viram tabelas explícitas:

create table sales_orders (
  id uuid primary key,
  customer_id uuid not null references customers (id),
  status text not null check (status in ('draft', 'confirmed', 'cancelled'))
);
 
create table sales_order_items (
  order_id uuid not null references sales_orders (id) on delete cascade,
  line_number integer not null,
  product_id uuid not null,
  quantity numeric(12, 3) not null check (quantity > 0),
  primary key (order_id, line_number)
);

O ponto não é escrever mais SQL. O ponto é fazer o banco recusar um estado inválido. A documentação de constraints do PostgreSQL cobre essas garantias: primary key, foreign key, unique, not null e check.

Em MongoDB, o mesmo pedido pode embutir itens quando o conjunto é pequeno e sempre lido junto. A própria documentação compara documentos embutidos com referências. O limite aparece quando o array cresce sem controle. MongoDB trata unbounded arrays como antipadrão.

Firestore segue lógica parecida. Mapas e arrays servem para listas pequenas. Subcoleções servem quando a lista cresce ou precisa ser consultada separadamente. A página de estrutura de dados do Firestore deixa esse trade-off explícito.

Quando apertar o modelo?

Aperte o modelo quando o dado é parte do núcleo transacional. Isso vale para ERP, financeiro, pedidos, estoque, faturamento, assinatura, permissões, workflow de aprovação e qualquer fluxo em que corrigir depois custa caro.

Sinais fortes:

  • A regra aparece em mais de uma tela ou serviço.
  • O relatório precisa confiar no dado.
  • Uma integração externa pode mandar payload ruim.
  • A operação precisa de histórico.
  • Existe risco financeiro, fiscal, jurídico ou operacional.
  • A mesma entidade será consultada por muitos caminhos diferentes.

ERP é um bom exemplo porque uma entidade raramente fica isolada. Cliente se conecta a pedido, endereço, crédito, cobrança, entrega, imposto e relatório. O guia de domain modeling do SAP CAP trata entidades, tipos, associations e compositions como parte do modelo de domínio. O material de associations e compositions é útil aqui porque composition representa a ideia de que itens pertencem ao pedido e podem ser apagados junto com ele.

Nesses casos, prefira:

NecessidadeFerramenta
Identidadeprimary key
Existência referencialforeign key
Unicidade de negóciounique
Campo obrigatórionot null
Regra local simplescheck
Mudança atômicatransação
Rastreabilidadetabela de auditoria ou evento de domínio

Se a regra é importante, deixe o banco participar da proteção. Validação em TypeScript ou no formulário ajuda, mas não substitui constraint.

Quando deixar flexível?

Deixe flexível quando a variação é esperada e controlada:

  1. Campos customizados por cliente.
  2. Payload bruto de integração externa.
  3. Configuração de interface por usuário.
  4. Formulário experimental.
  5. Snapshot histórico de um pedido, contrato ou evento.
  6. Metadata usada por uma feature ainda instável.

Mas defina limites desde o primeiro dia:

GuardrailRegra prática
schema_versionTodo payload flexível precisa de versão
validaçãoRejeite formatos inválidos antes de salvar
limite de tamanhoEvite documento que cresce sem fim
índiceSó prometa filtro que o banco consegue executar bem
donoTodo campo customizado precisa de owner
promoçãoCampo crítico vira coluna, tabela ou dimensão

Um sinal simples: se o campo entrou em filtro, permissão, cobrança, contrato público ou relatório recorrente, ele deixou de ser detalhe flexível.

Como escolher entre Postgres, MongoDB e Firestore?

Escolha pelo padrão de escrita e leitura, não pela moda.

CenárioEscolha provável
Regras fortes entre entidadesPostgres
Transação envolvendo várias tabelasPostgres
Dado que será particionado por tenant, usuário ou contaModele a chave de partição cedo
Query ad hoc com muitos filtrosPostgres ou modelo analítico
Agregado pequeno lido e escrito juntoMongoDB
Payload externo com formato variávelMongoDB ou Postgres com JSONB
App mobile com sync em tempo realFirestore
Sublista que cresce por entidade paiFirestore subcollection ou coleção separada
Relatório financeiro ou gerencialWarehouse, data mart ou tabelas de resumo

Firestore tem limites importantes de consulta, incluindo restrições em or, in e array-contains-any, descritas em Firestore queries. Se o produto precisa de filtros combináveis sem muita previsibilidade, isso precisa entrar na decisão cedo.

Escala também transforma decisão de acesso em decisão de modelagem. O texto da Shopify sobre mover o backend do Shop app para Vitess mostra um caso concreto: antes de particionar, eles precisaram adicionar e preencher user_id porque muitas tabelas grandes tinham sido modeladas em torno de account_id. A lição é simples: se tenant, usuário, merchant ou conta será sua chave de partição, coloque isso no modelo antes de as tabelas ficarem enormes.

Como separar CRUD, dashboard e relatório?

CRUD significa Create, Read, Update, Delete. O modelo de CRUD deve proteger escrita correta. Dashboard deve responder rápido. Relatório deve explicar números com histórico e grão claro.

Não force tudo no mesmo modelo.

CargaPerguntaModelo
CRUDComo impedir escrita errada?Modelo transacional normalizado
DashboardComo ler o estado atual rápido?Projeção, cache, tabela de resumo ou materialized view
RelatórioComo analisar fatos por dimensões?Modelo dimensional, data mart ou warehouse

Um caminho comum:

orders + order_items + payments
  -> job, stream or scheduled refresh
  -> dashboard_order_summary
  -> cards, charts and reports

Postgres oferece materialized views para guardar resultado de consulta pesada. Em BI, o guia de star schema do Power BI resume bem a separação entre fatos e dimensões.

Relatório financeiro pede a mesma disciplina. O guia da Stripe para consultar dados transacionais aponta balance_transactions como ponto de partida estilo ledger para relatórios. Esse é o conceito a copiar, não o schema da Stripe: relatórios funcionam melhor quando leem fatos estáveis em vez de reconstruir números a partir de tabelas operacionais.

O núcleo transacional não deve virar tabela torta só para salvar um gráfico lento. Crie um modelo de leitura.

Qual checklist usar antes de modelar?

Antes de criar tabela, documento ou JSON genérico, responda:

  1. Qual é a fonte da verdade?
  2. Qual regra o banco deve impedir sozinho?
  3. A cardinalidade é limitada ou cresce sem controle?
  4. O filho existe sem o pai?
  5. A leitura principal precisa do conjunto inteiro?
  6. O dado entra em relatório, permissão, cobrança ou auditoria?
  7. O campo é estável, experimental ou customizado por cliente?
  8. Qual índice sustenta a consulta principal?
  9. Quem é dono da evolução desse campo?
  10. Quando esse campo flexível deve virar coluna, tabela ou dimensão?

Que referências valem a leitura?

Qual é a regra prática?

Aperte o modelo onde erro vira prejuízo, retrabalho ou relatório sem confiança. Deixe flexível onde a variação é real, limitada e validada. Separe o modelo de escrita do modelo de leitura quando dashboard e relatório começarem a distorcer o CRUD.

Resumo: modele invariantes e cardinalidade primeiro. Use Postgres para o núcleo transacional. Use MongoDB, Firestore, JSONB ou metadata para variação controlada. Use projeções e modelos analíticos para dashboards e relatórios. O melhor modelo deixa claro onde a verdade mora e onde a aplicação pode mudar sem quebrar o negócio.

Escrito por IA, revisado por Thiago Marinho

10 de agosto de 2026 · Brazil