MongoDB Document Modeling: Embedding vs Referencing, Indexes, and Aggregation Pipelines

数据库

The First Principle of MongoDB Modeling

Relational databases draw the ER diagram first, then split tables. MongoDB flips it — decide what a single query must return, then decide how to store it. The core trade-off is one word: embed or reference.

Dimension Embedding Referencing
Read one fetch, fast needs $lookup, slower
Write large-doc update cost each updates lightly
Consistency single-doc atomicity cross-doc needs transaction
Fits one-to-few, read together one-to-many, evolves independently

Embed vs Reference: How to Choose

Embedding: blog + comments (one-to-few)

{
  "_id": "post-1",
  "title": "MongoDB Modeling",
  "comments": [
    { "user": "alice", "text": "useful", "at": "2026-07-01" },
    { "user": "bob", "text": "saved", "at": "2026-07-02" }
  ]
}

Good for sub-data that is "always read together and grows slowly." Once comments hit thousands, the document balloons — switch to reference + a separate collection.

Referencing: user + orders (one-to-many)

// users
{ "_id": "u-1", "name": "Alice" }
// orders
{ "_id": "o-1", "userId": "u-1", "total": 99 }

Join with $lookup:

db.orders.aggregate([
  { $match: { userId: "u-1" } },
  { $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" } }
]);

For complex document shapes, run a sample through the JSON Formatter tool to clarify the embed level before modeling.


Three Classic Design Patterns

1. Extended Reference

Beyond the reference, denormalize a few "frequently displayed together" fields into the main doc to avoid $lookup:

{ "_id": "o-1", "userId": "u-1", "userName": "Alice", "total": 99 }

2. Subset

Embed only the "most recent N," push history to a separate collection. Great for activity feeds and notifications.

3. Bucket

Bucket time-series/logs by time to cut document count and index overhead:

{
  "_id": "metrics-2026-07-01",
  "device": "d-9",
  "points": [ { "t": "08:00", "v": 12 }, { "t": "09:00", "v": 15 } ]
}

Indexes: The Performance Linchpin

Compound index and leftmost prefix

db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });

A query must hit the leftmost prefix to use it: {userId} ✅, {userId, status} ✅, {status} ❌.

Covered query

The index already holds the needed fields, so no document fetch:

db.orders.createIndex({ userId: 1, total: 1 });
db.orders.find({ userId: "u-1" }, { total: 1, _id: 0 });

TTL index for auto-expiry

db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });

For data masking or signing, use the Hash Digest tool to produce a stable fingerprint instead of storing plaintext.


Aggregation Pipeline: Built-in ETL

Aggregation is MongoDB's strongest capability — stages chained into a pipeline:

db.orders.aggregate([
  { $match: { status: "paid" } },
  { $group: { _id: "$userId", spend: { $sum: "$total" } } },
  { $sort: { spend: -1 } },
  { $limit: 10 }
]);

Common stages: $match filter, $group group, $project project, $lookup join, $unwind flatten arrays.

If you think in SQL, use the SQL Formatter tool to clarify the GROUP BY logic before translating to $group.


Transactions and Consistency

Single-document operations are atomic by nature. Cross-document/collection needs a transaction (replica set / sharded cluster):

const session = client.startSession();
await session.withTransaction(async () => {
  await accounts.updateOne({ _id: "A" }, { $inc: { bal: -100 } }, { session });
  await accounts.updateOne({ _id: "B" }, { $inc: { bal: 100 } }, { session });
});

Caveat: transactions have a performance cost — prefer "embed + single-doc atomicity" over transactions when possible.


Sharding Basics

When data outgrows a single node, split horizontally by shard key. Choosing it:

  • High cardinality (many values) to avoid a "hot shard."
  • Aligned with query patterns so queries hit few shards.
  • Avoid monotonically increasing keys (e.g. auto-increment) that concentrate writes.

FAQ

Q1: Is there a quantitative rule for embed vs reference?

Rule of thumb: sub-documents < a few hundred, read together, not evolving independently → embed; otherwise reference.

Q2: What if an array grows unbounded?

Use the "Subset" pattern (keep only recent) or "Bucket" pattern (aggregate by time window) to stay under the 16MB single-document limit.

Q3: Why isn't my index used?

Usually it misses the leftmost prefix, or the query uses $ne/range then an equality that prevents the index from continuing.

Q4: Aggregation or MapReduce?

Always prefer the aggregation pipeline; MapReduce is essentially deprecated, kept only for very rare cases.

Q5: Transactions are slow — how to optimize?

Shrink the transaction scope, shorten lock duration, or turn strong-consistency needs into "embed single doc + compensation."


In MongoDB modeling and ops, these tools help:


MongoDB modeling is not "swap tables for collections" but a paradigm inversion: organize data around queries. Embedding lets hot paths fetch everything at once, referencing lets big data evolve independently, and indexes with aggregation pipelines push compute down to the storage layer. Know what a single query must return, and the model falls into place.

Try these browser-local tools — no sign-up required →

#MongoDB#文档建模#嵌入式#引用#聚合#索引#NoSQL