Skip to content

MongoDB - Database References

Unlike traditional SQL databases, MongoDB’s flexible schema provides multiple ways to model relationships between data. The choice between embedding and referencing data is one of the most critical schema design decisions, impacting application performance, data integrity, and scalability.

Pattern 1: Embedding Documents (Denormalization)

Section titled “Pattern 1: Embedding Documents (Denormalization)”

Embedding involves storing related data within a single parent document. This pattern is also known as denormalization.

Consider a product that has a few reviews. Since the reviews are tightly coupled with the product, we can embed them as an array within the product document.

{
_id: ObjectId("..."),
name: "Wireless Mouse",
price: 25,
// Reviews are embedded directly
reviews: [
{ username: "alice", rating: 5, comment: "Excellent product!" },
{ username: "bob", rating: 4, comment: "Good value for the price." }
]
}
  • One-to-Few Relationships: Ideal when the number of related items is small and finite.
  • Data Retrieval: When you need to retrieve the related data together with the main document, embedding provides the best performance as it requires a single database read.
  • Atomicity: Updates to the document, including its embedded data, are atomic.
  • Best For: Contact information for a user, items in a shopping cart, reviews for a product.

Pattern 2: Referencing Documents (Normalization)

Section titled “Pattern 2: Referencing Documents (Normalization)”

Referencing, or normalization, involves storing related data in separate collections. You create a link between them by storing the _id of one document in another. This is the recommended approach for most ‘one-to-many’ relationships.

An author can write many posts. Storing the author’s full details in every post would be redundant. Instead, we store the author’s _id in the post document.

// `authors` collection
{
_id: ObjectId("author1"),
name: "Chris",
email: "chris@example.com"
}
// `posts` collection
{
_id: ObjectId("post123"),
title: "Schema Design Best Practices",
content: "...",
// Manual reference to the author
author_id: ObjectId("author1")
}
  • One-to-Many Relationships: When the ‘many’ side is large or unbounded (e.g., logs for an event, comments on a popular post).
  • Avoiding Data Duplication: When the referenced data is large and updated frequently (e.g., user profiles).
  • Many-to-Many Relationships: For example, a students collection and a courses collection, linked by a third collection that stores references to both.
  • Exceeding Document Size Limit: When embedding would cause documents to exceed the 16MB limit.

Resolving References: The Modern Approach with $lookup

Section titled “Resolving References: The Modern Approach with $lookup”

The main drawback of referencing is that it requires a separate query to fetch the related data. The modern, efficient way to solve this is with the $lookup aggregation stage, which performs a server-side ‘join’.

// This aggregation pipeline finds a post and 'joins' its author information
db.posts.aggregate([
{
$match: { _id: ObjectId("post123") } // Find the specific post
},
{
$lookup: {
from: "authors", // The collection to join with
localField: "author_id", // The field from the input documents (posts)
foreignField: "_id", // The field from the documents of the 'from' collection (authors)
as: "authorDetails" // The new array field to add to the input documents
}
}
])
// Result includes the post with author details embedded in an array:
/*
{
_id: ObjectId("post123"),
title: "Schema Design Best Practices",
author_id: ObjectId("author1"),
authorDetails: [
{ _id: ObjectId("author1"), name: "Chris", email: "chris@example.com" }
]
}
*/

DBRefs are a legacy, formal convention for referencing documents. A DBRef is a document with three fields: $ref (the collection name), $id (the _id of the referenced document), and an optional $db.

// Example of a document using a DBRef
{
_id: ObjectId("user1"),
name: "Tom Benzamin",
// DBRef pointing to a document in the 'addresses' collection
address: {
"$ref": "addresses",
"$id": ObjectId("addr1"),
"$db": "w3"
}
}

While they exist, DBRefs are not the recommended way to model relationships. Modern best practices strongly favor manual references with the $lookup pipeline because:

  • Lack of Automatic Resolution: Most MongoDB drivers do not automatically resolve DBRefs. You still need to write application-level code to perform a second query, just as you would with a manual reference.
  • Added Complexity: They add unnecessary verbosity and complexity to your documents compared to simply storing an ObjectId.
  • $lookup is Superior: The $lookup aggregation stage is far more powerful and flexible. It works seamlessly with simple manual references and provides more control over the joined data.

Start with embedding. If your data relationships are one-to-few and data is accessed together, embedding is faster. If embedding leads to data duplication, large documents, or you have one-to-many relationships, switch to manual references. Use the $lookup aggregation pipeline to efficiently retrieve related data when needed. Avoid DBRefs unless you have a specific, well-justified legacy reason or a niche use case requiring references to multiple collections from a single field.