A modelagem de dados é o processo de mapear entidades, atributos e relacionamentos antes de decidir como o banco de dados vai armazená-los. As entidades são a parte fácil — todo produto conhece seus usuários, pedidos e posts. O ofício está nos relacionamentos: quais registros apontam para quais, quantos, e onde essa aresta vive fisicamente. Acerte as arestas e as queries se escrevem sozinhas; erre e cada feature briga com o schema.
Principais pontos
| Pergunta | Resposta |
|---|---|
| Os três níveis | Conceitual (o quê) → lógico (estrutura) → físico (motor) |
| As três arestas | Um-para-um · um-para-muitos · muitos-para-muitos |
| Ferramentas relacionais | Chaves estrangeiras; tabelas de junção para muitos-para-muitos |
| Ferramentas de documentos | Embutir, Pointers (referências), Relations/arrays de IDs |
| A regra moderna | Modele pelos padrões de acesso — dados lidos juntos vivem juntos |
Os mesmos relacionamentos, nos dois mundos
Relacional primeiro — a chave estrangeira carrega o um-para-muitos, e o muitos-para-muitos precisa se decompor por uma junção:
-- 1:N — a chave estrangeira vive no lado "muitos"
CREATE TABLE books (
id bigint PRIMARY KEY,
title text NOT NULL,
author_id bigint NOT NULL REFERENCES authors(id) ON DELETE CASCADE
);
-- M:N — nenhuma FK sozinha expressa isso; a tabela de junção guarda as duas chaves
CREATE TABLE book_genres (
book_id bigint REFERENCES books(id),
genre_id bigint REFERENCES genres(id),
added_at timestamptz DEFAULT now(), -- junções podem carregar atributos
PRIMARY KEY (book_id, genre_id) -- chave composta: uma aresta, uma vez
);
Mundo dos documentos, mesmo modelo: a aresta um-para-muitos é um Pointer tipado, a muitos-para-muitos é uma Relation — nenhuma tabela de junção para inventar, e a aresta é declarada no código da aplicação:
// JavaScript / Node.js — Back4app JS SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
const author = await new Parse.Query('Author').get(authorId);
const book = new Parse.Object('Book');
book.set('title', 'Dune');
book.set('author', author); // Pointer — typed one-to-many edge
await book.save();
const genres = book.relation('genres'); // Relation — many-to-many, unbounded
genres.add([sciFi, classics]);
await book.save(); // Flutter / Dart — Back4app Flutter SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
final book = ParseObject('Book')
..set('title', 'Dune')
..set('author', author.toPointer()); // Pointer — typed one-to-many edge
await book.save();
book.addRelation('genres', [sciFi, classics]); // Relation — many-to-many
await book.save(); // iOS / Swift — Back4app Swift SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
var book = Book()
book.title = "Dune"
book.author = try author.toPointer() // Pointer — typed one-to-many edge
let saved = try await book.save()
let relation = try saved.relation("genres")
try await relation.add([sciFi, classics]).save() // Relation — many-to-many // Android / Kotlin — Back4app Android SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
val book = ParseObject("Book").apply {
put("title", "Dune")
put("author", author) // Pointer — typed one-to-many edge
}
book.save()
val genres = book.getRelation<ParseObject>("genres") // Relation — many-to-many
genres.add(sciFi)
genres.add(classics)
book.save() Conceitual vs. lógico vs. físico: os três níveis
| Dimensão | Conceitual | Lógico | Físico |
|---|---|---|---|
| Pergunta respondida | O que existe, como se relaciona | Quais campos, quais chaves | Como armazena, quão rápido |
| Público | Todo mundo | Designers do modelo | Engenheiros + o motor |
| Contém | Entidades, relacionamentos | + atributos, tipos, cardinalidade | + índices, constraints, partições |
| Muda quando | O negócio muda | Os requisitos se refinam | O motor ou a escala mudam |
A disciplina que os níveis impõem: não discuta índices enquanto o time ainda discorda sobre o que é um “pedido”.
Todo relacionamento, toda implementação
A tabela que a SERP nunca montou — cada tipo de aresta com sua implementação nos dois mundos:
| Relacionamento | Exemplo | Implementação relacional | Implementação em documentos |
|---|---|---|---|
| Um-para-um | Usuário ↔ perfil | FK com UNIQUE, ou a mesma linha | Embutir — quase sempre |
| Um-para-poucos | Pessoa → endereços | Tabela filha + FK | Array embutido (limitado) |
| Um-para-muitos | Autor → livros | FK no lado “muitos” | Pointer no lado “muitos” |
| Um-para-enormes | Dispositivo → eventos | FK + particionamento | Pointer no filho; nunca um array |
| Muitos-para-muitos | Livros ↔ gêneros | Tabela de junção, PK composta | Relation ou arrays de pointers |
| Autorreferência | Funcionário → gerente | FK para a própria tabela | Pointer para a própria classe |
Notação de cardinalidade, para ler diagramas: o pé-de-galinha desenha uma barra para “um” e um garfo de três pontas para “muitos”; a tradição ER escreve 1 e N nas linhas; a UML escreve multiplicidades como 1 e *. Três dialetos, uma gramática.
Normalizar, desnormalizar ou embutir?
A modelagem relacional parte da normalização — separar os dados para que cada fato viva uma vez só, protegendo as escritas de anomalias. A modelagem analítica e a de documentos a dobram de propósito: a denormalização duplica campos quentes de leitura para matar joins, e embutir é a prima nativa dos documentos da denormalização. O checklist embutir-vs-referenciar se comprime em três perguntas: lidos juntos? de um dono só? tamanho limitado? Três sins embutem; qualquer não referencia. E, acima de tudo isso, a regra moderna que os guias generalistas pulam: liste as queries primeiro. Um modelo está correto quando os padrões de acesso que ele precisa servir saem baratos — as entidades sozinhas não contam isso.
Casos de uso comuns
- Projetar um backend novo. O rito clássico: nomear entidades, desenhar arestas, escolher implementações pela tabela acima — antes da primeira linha de código.
- O padrão junção-com-atributos. Matrículas, assinaturas, itens de pedido — quando a própria aresta tem dados (data, quantidade, papel), a junção (ou uma classe de aresta com dois pointers) é entidade de primeira classe.
- Desembaraçar um schema que cresceu. Sintomas mapeiam para arestas: linhas duplicadas revelam um muitos-para-muitos mal modelado; documentos inchados revelam um embed sem limite.
- Migrar entre os mundos. Relacional→documentos significa re-decidir cada FK como embed vs. pointer; a tabela acima é o dicionário de tradução.
- Alimentar AI e analytics. Warehouses querem as arestas explícitas e estáveis — a dívida de modelagem aparece no dia em que você tenta exportar.
Como modelar cada aresta? Matriz de decisão
| Escolha… | Quando… | Cuidado com… |
|---|---|---|
| Embutir | Lidos juntos, um dono, tamanho limitado | Crescimento: o “poucos” de hoje vira os milhares de amanhã |
| Pointer / FK | Ciclo de vida independente, alta cardinalidade | Indexe — todo lookup e include paga |
| Relation / junção | Muitos-para-muitos, ou a aresta carrega dados | Direção da query: saiba de que lado você vai perguntar |
| Duplicar (denormalizar) | Um campo quente de leitura cruzando uma aresta | Fan-out de update — duplicação é dívida com juros |
Limitações e trade-offs
- Modelos congelam premissas. Decisões de cardinalidade (“um usuário tem um endereço”) viram schema; baratas no papel, caras depois do lançamento — modele para a cardinalidade de amanhã.
- Os dois mundos punem a aresta sem índice. Colunas de FK e campos de pointer são caminhos de join; esquecer os índices deles é o bug silencioso de performance mais comum.
- Embutir troca integridade por localidade. Nenhum motor garante que uma cópia embutida continue consistente com a fonte — isso agora é a sua lógica de update.
- Junções multiplicam joins; Relations os escondem. Toda leitura muitos-para-muitos atravessa o armazém de arestas — orce o caminho da query, em qualquer um dos mundos.
- Modelar por padrão de acesso também tem custo. Otimizar para as queries de hoje pode casar o schema com o produto de hoje; mantenha o modelo conceitual por perto como o terreno neutro da verdade.
Modelagem de dados no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. Seu vocabulário de modelagem é a coluna de documentos das tabelas acima, elevada a primeira classe: Pointers são arestas um-para-muitos tipadas, Relations carregam o muitos-para-muitos sem junção feita à mão, e arrays cobrem o “poucos e limitado”. Toda aresta que você declara é atravessável na hora — include() percorre pointers em uma requisição, o GraphQL aninha relações em uma query — e o schema segue visível e editável no dashboard, para que o modelo físico nunca saia do campo de visão do time que desenhou o conceitual.
Perguntas frequentes
O que é modelagem de dados?
O processo de mapear a informação de uma aplicação — suas entidades, os atributos delas e os relacionamentos entre elas — num design que um banco de dados consegue armazenar. Ele costuma atravessar três níveis de detalhe: um modelo conceitual nomeando as entidades, um modelo lógico adicionando atributos e chaves, e um modelo físico se comprometendo com tabelas, colunas e índices num motor específico.
Quais são os três tipos de modelos de dados?
Conceitual, lógico e físico — o mesmo design em zoom crescente. O conceitual responde "o que existe e como se relaciona" para o lado de negócio. O lógico adiciona atributos, tipos e chaves sem se prender a tecnologia. O físico se compromete com um motor real: tabelas ou coleções, índices, constraints. Cada nível é o anterior mais decisões.
O que é um relacionamento um-para-muitos?
Um registro pai relacionado a muitos filhos, cada filho pertencendo a exatamente um pai — um cliente e seus pedidos, um autor e seus livros. É o relacionamento mais comum de qualquer schema. Bancos relacionais o implementam com uma chave estrangeira no lado "muitos"; bancos de documentos usam um array embutido para conjuntos pequenos e limitados, ou um pointer para o pai em todo o resto.
O que é um relacionamento muitos-para-muitos e por que ele precisa de tabela de junção?
Os dois lados se relacionam com muitos do outro — alunos e cursos, livros e gêneros. Uma coluna de chave estrangeira só consegue apontar para uma linha, então bancos relacionais decompõem o muitos-para-muitos em dois um-para-muitos através de uma tabela de junção com as duas chaves (e, muitas vezes, atributos da relação, como a data de matrícula). Bancos de documentos dispensam a junção: arrays de referências, ou um tipo dedicado de relação, carregam a aresta diretamente.
Como diferenciar um-para-muitos de muitos-para-muitos?
Faça a pergunta de posse nos dois sentidos. "Um autor pode ter muitos livros?" Sim. "Um livro pode ter muitos autores?" Se não — um-para-muitos, chave estrangeira no livro. Se sim — muitos-para-muitos, tabela de junção ou Relation. Errar aqui é o erro clássico de modelagem: um muitos-para-muitos modelado como um-para-muitos ou duplica linhas ou perde arestas em silêncio.
O que é cardinalidade em um diagrama ER?
Quantas instâncias de uma entidade podem se relacionar com instâncias de outra — um-para-um, um-para-muitos, muitos-para-muitos. Os diagramas expressam isso em uma de três notações: números e letras nas linhas de conexão, os símbolos pé-de-galinha (uma barra para "um", um garfo de três pontas para "muitos"), ou faixas de multiplicidade como 1 e asterisco. Mesma semântica, convenções de desenho diferentes.
Como bancos de documentos modelam relacionamentos sem chaves estrangeiras?
Com três ferramentas: embutir (aninhar os dados relacionados dentro do documento pai — certo quando são lidos juntos, têm um dono só e tamanho limitado), pointers ou referências (guardar o ID do documento relacionado como campo tipado — certo para ciclos de vida independentes e alta cardinalidade), e objetos Relation ou arrays de IDs para muitos-para-muitos. A regra-guia troca a normalização pelos padrões de acesso: dados lidos juntos devem viver juntos.
Quais são os erros mais comuns de modelagem de dados?
Um top cinco estável: modelar muitos-para-muitos como um-para-muitos; esquecer índices nos campos de chave estrangeira e de pointer (todo join e lookup paga por isso); arrays embutidos sem limite de crescimento em schemas de documento; denormalização prematura antes de qualquer query se provar lenta; e modelar só a partir das entidades, sem listar as queries — os padrões de acesso — que o modelo precisa servir.