Documents and BSON: The Cornerstone of MongoDB Data
BSON is MongoDB's data format—it extends the capabilities of JSON and supports native types such as Date, Binary, and Decimal128.
This course provides an in-depth understanding of the BSON data format, the internal structure of ObjectId, and the field type system, and teaches best practices for document design.
1. What You'll Learn
- The Fundamental Difference Between the BSON Data Format and JSON
- The Internal Structure of MongoDB Documents (_id, Fields, Values)
- The composition of ObjectId, timestamp extraction, and guarantee of uniqueness
- 12 BSON data types (String, Number, Date, Array, Object, ObjectId, etc.)
- Field Naming Conventions (CamelCase vs. SnakeCase vs. Kebab-Case)
- The Design Philosophy Behind the Document Size Limit (16 MB)
- Strategies for Choosing Between Embedded Documentation and Citations
2. A True Story of a Full-Stack Engineer
(1) Pain Point: Dates are converted to strings when JSON is stored in MongoDB
Charlie is a full-stack Node.js engineer who is migrating MySQL data to MongoDB:
"I converted the order data from MySQL to JSON and stored it in MongoDB, only to find that all dates had been converted to the string
new Date(), which couldn't be parsed; the precision of the amounts was lost (0.1 + 0.2 ≠ 0.3); and binary profile pictures couldn't be stored at all."
He serialized the order data using JSON.stringify(), which resulted in the loss of type information:
// ❌ Error:JSON.stringify Type of Loss
const order = {
createdAt: new Date(), // Date Object
total: new Number('0.30'), // Decimal128 A more precise type should be used.
avatar: Buffer.from('...'), // Binary Avatar
_id: new ObjectId() // MongoDB Expected ObjectId
};
const json = JSON.stringify(order);
// {"createdAt":"2026-07-01T...","total":0.3,"avatar":"...","_id":"..."}
// ^^^^^^^^^^^^^^^^ String ^ Floating-point numbers(Loss of Accuracy) ^ String(Cannot be restored)
(2) The BSON Solution
MongoDB stores data directly in BSON (Binary JSON) format, preserving all type information.
// ✅ Correct:mongoose Direct Operation BSON Type
const OrderSchema = new mongoose.Schema({
createdAt: { type: Date, default: Date.now }, // BSON Date
total: { type: mongoose.Schema.Types.Decimal128 }, // BSON Decimal128(Accurate)
avatar: { type: Buffer }, // BSON Binary
_id: { type: mongoose.Schema.Types.ObjectId, auto: true } // BSON ObjectId
});
const order = await Order.create({
total: mongoose.Types.Decimal128.fromString('0.30'),
avatar: fs.readFileSync('avatar.jpg')
});
(3) Revenue
| Dimension | JSON | BSON |
|---|---|---|
| Date Type | String (requires manual parsing) | Native Date (millisecond precision) |
| Numeric Precision | Floating-point (loss of precision) | Decimal128 (34-bit precision) |
| Binary Data | Not supported | Native Binary |
| Field Order | Unordered | Ordered (Important!) |
| Size and Resource Usage | More compact | Slightly larger (5–15% larger) |
3. BSON Data Format
Concept Overview: BSON (Binary JSON) is a binary serialization format specific to MongoDB and is a superset of JSON. While JSON has only six data types (string, number, boolean, null, array, and object), BSON supports more than 12 types, including database-essential types such as Date, Binary, ObjectId, and Decimal128. The core advantages of BSON are: rich data types, ordered fields, and extremely fast parsing.
How It Works: BSON documents are stored in binary format. Each document begins with a 4-byte length header, followed by a sequence of key-value pairs, and ends with 0x00. Unlike JSON’s text-based parsing, BSON’s length header allows for the rapid skipping of unnecessary fields (similar to the “fixed-length header” design in binary protocols), resulting in parsing performance that is 3–5 times faster than JSON. The trade-off is a 5–15% increase in space overhead (to store type and length information).
graph TB
subgraph "BSON Internal Structure of the Document"
A[4 Byte<br/>Total length of the document] --> B[Type Code 1B<br/>+ Field Name<br/>+ Value]
B --> C[Type Code 1B<br/>+ Field Name<br/>+ Value]
C --> D[...More key-value pairs...]
D --> E[0x00<br/>Closing tag]
end
style A fill:#cce5ff
| Dimension | JSON | BSON |
|---|---|---|
| Type | Text Format | Binary Format |
| Readability | ✅ Human-readable | ❌ Binary |
| Performance | Slow resolution | Extremely fast resolution (3–5x) |
| Wide variety | 6 types | 12+ types |
| Field Order | Unordered | Ordered |
| Space | More compact | 5–15% more |
(1) What is BSON?
BSON (Binary JSON) is the binary serialization format used by MongoDB. Its features include:
graph LR
A[JavaScript Object] -->|JSON.stringify| B[JSON Text]
A -->|BSON Serialization| C[BSON Binary]
B --> D[Transmission / Storage]
C --> D
style C fill:#d4edda
| Dimension | JSON | BSON |
|---|---|---|
| Type | Text Format | Binary Format |
| Readability | ✅ Human-readable | ❌ Binary |
| Performance | Slow resolution | Extremely fast resolution |
| Wide variety | 6 types | 12+ types |
| Field Order | Unordered | Ordered |
| Space | More compact | 5–15% more |
(2) BSON Document Structure
Key Points Analysis:
- BSON field insertion order—this is critical for indexing and query optimization in MongoDB
- Each field is preceded by a 1-byte type code, allowing BSON to distinguish between Date and String (unlike JSON).
- Nested documents and arrays are stored recursively in BSON, with a maximum nesting depth of 100 levels.
- The
_idfield is always at the beginning of the document, optimizing query performance
// One BSON Internal Representation of a Document(Simplify)
{
_id: ObjectId("507f1f77bcf86cd799439011"), // 12 Byte ObjectId
name: "Alice", // String(UTF-8)
age: 28, // Int32
balance: Decimal128("12345.6789"), // Decimal128(High precision)
joinedAt: ISODate("2026-07-01T10:00:00Z"), // Date(64-bit Integer)
isActive: true, // Boolean
hobbies: ["reading", "coding", "hiking"], // Array
address: { // Embedded Document
city: "Tokyo",
country: "Japan"
},
profile: null, // Null
avatar: BinData(0, "..."), // Binary
// Field Order:BSON Preserve the insertion order of fields(JSON No guarantee)
}
▶ Example 1: Viewing BSON Details in mongosh
// Insert a document
db.users.insertOne({
name: "Alice",
age: 28,
joinedAt: new Date(),
balance: NumberDecimal("12345.6789"),
address: { city: "Tokyo", country: "Japan" }
});
// View BSON Details(Usage bsonSon Function)
db.users.findOne({ name: "Alice" });
// {
// _id: ObjectId('507f1f77bcf86cd799439011'),
// name: 'Alice',
// age: 28,
// joinedAt: ISODate('2026-07-01T10:00:00.000Z'),
// balance: NumberDecimal('12345.6789'),
// address: { city: 'Tokyo', country: 'Japan' }
// }
// View Field Types
const doc = db.users.findOne({ name: "Alice" });
print(typeof doc.age); // number
print(doc.joinedAt instanceof Date); // true
4. The ObjectId Primary Key Mechanism
Concept Explanation: ObjectId is MongoDB’s default primary key type, consisting of a 12-byte (96-bit) binary value. Unlike the auto-incrementing integer primary keys found in traditional databases, ObjectId employs a distributed design—composed of a timestamp, a random value, and a counter—ensuring global uniqueness without the need for centralized coordination. Another major advantage of ObjectId is that it inherently includes the creation time, which can be extracted directly without the need for additional fields.
How It Works: The 12 bytes of an ObjectId are divided into three segments: the first 4 bytes are a Unix timestamp (accurate to the second), the middle 5 bytes are a random value (determined by the machine ID and process ID when first generated, and remain unchanged thereafter), and the last 3 bytes are an incrementing counter (which increments from a random starting value within the same second). This design allows a single process to generate approximately 16.77 million unique ObjectIds within a single second.
graph LR
A[ObjectId 12 Byte] --> B[4 Byte Timestamp<br/>Accuracy to the second]
A --> C[5 Random Byte Values<br/>Machine/Unique Process]
A --> D[3 Byte-Increment Counter<br/>Unique within a single second]
style A fill:#cce5ff
| Section | Length | Content | Purpose |
|---|---|---|---|
| Timestamp | 4 bytes | Unix timestamp (seconds) | Creation time can be extracted |
| Random | 5 bytes | Machine ID + Process ID | Unique across processes |
| Counter | 3 bytes | Incremental counter | Unique within a single second |
| _id Strategy | Advantages | Disadvantages | Use Cases |
|---|---|---|---|
| Auto-generated ObjectId | Distributed, unique, timestamped, naturally ordered | 12 bytes (relatively large) | General-purpose (default) |
| String Business Keys | Clear semantics, good readability | Must be manually ensured to be unique | Order number, SKU |
| Auto-incrementing integers | Compact, readable | Requires a counter set; not suitable for sharding | Legacy systems |
| UUID | Globally unique | 16 bytes, unordered | Unique across systems |
(1) What is an ObjectId?
ObjectId is MongoDB's default primary key type, a 12-byte (96-bit) binary value:
graph LR
A[ObjectId 12 Byte] --> B[4 Byte Timestamp<br/>Accuracy to the second]
A --> C[5 Random Byte Values<br/>Machine/Unique Process]
A --> D[3 Byte-Increment Counter<br/>Unique within a single second]
style A fill:#cce5ff
| Section | Length | Content | Purpose |
|---|---|---|---|
| Timestamp | 4 bytes | Unix timestamp (seconds) | Creation time can be extracted |
| Random | 5 bytes | Machine ID + Process ID | Unique across processes |
| Counter | 3 bytes | Incremental counter | Unique within a single second |
(2) Advantages of ObjectId
Key Points Analysis:
- The timestamp portion of the ObjectId naturally sorts entries by insertion time—allowing queries by time range without the need for additional
createdAtindexes. - A 5-byte random value is generated and cached when the process starts, ensuring uniqueness across processes (2^40 ≈ 1 trillion possibilities).
- The 3-byte counter increments within a single second, generating 2^24 ≈ 16.77 million unique IDs per second.
- The
getTimestamp()method can extract the creation time directly from the ObjectId without the need for additional queries.
// Create ObjectId in mongosh
const id1 = ObjectId(); // Automatically Generated
const id2 = ObjectId("507f1f77bcf86cd799439011"); // Generate from a string
// Retrieve the creation time(Key Advantages!)
id2.getTimestamp();
// ISODate("2012-10-17T20:46:11.000Z")
// Use in Node.js with mongoose
const mongoose = require('mongoose');
const id = new mongoose.Types.ObjectId();
console.log(id.getTimestamp()); // 2026-07-01T10:00:00.000Z
(3) Ensuring the Uniqueness of ObjectId
graph TB
A[Client A<br/>Generated in the same second ID] --> A1[time=1000<br/>random=ABC<br/>counter=1]
A --> A2[time=1000<br/>random=ABC<br/>counter=2]
A --> A3[time=1000<br/>random=ABC<br/>counter=3]
B[Client B<br/>Generated in the same second ID] --> B1[time=1000<br/>random=DEF<br/>counter=1]
B --> B2[time=1000<br/>random=DEF<br/>counter=2]
style A1 fill:#d4edda
style B1 fill:#d4edda
▶ Example 2: Extracting the ObjectId Timestamp
// === In mongosh ===
const products = db.products.find().toArray();
products.forEach(p => {
print(`Product ${p._id} created at ${p._id.getTimestamp()}`);
});
// === by ObjectId Time Range Query ===
const startOfDay = ObjectId.createFromTime(
Math.floor(new Date('2026-07-01').getTime() / 1000)
);
const endOfDay = ObjectId.createFromTime(
Math.floor(new Date('2026-07-02').getTime() / 1000)
);
db.products.find({
_id: { $gte: startOfDay, $lt: endOfDay }
});
// === Node.js / mongoose ===
const Product = mongoose.model('Product', productSchema);
const products = await Product.find({
_id: {
$gte: mongoose.Types.ObjectId.createFromTime(
Math.floor(Date.parse('2026-07-01') / 1000)
),
$lt: mongoose.Types.ObjectId.createFromTime(
Math.floor(Date.parse('2026-07-02') / 1000)
)
}
});
5. BSON Data Types
Concept Explanation: BSON supports more than 12 data types, far exceeding JSON’s 6. The most significant difference lies in numeric types—JSON has only one numeric type (Number, which is IEEE 754 double-precision floating-point), while BSON provides four numeric types: Double, Int32, Int64 (Long), and Decimal128. Selecting the wrong numeric type can result in a loss of precision (e.g., in monetary calculations 0.1 + 0.2 ≠ 0.3).
Use Cases: Financial amounts must use Decimal128 (34-digit decimal precision); counters use Int32; large integer IDs use Long; and scientific calculations and statistics use Double. The Date type is stored in BSON as a 64-bit millisecond timestamp, which is fundamentally different from JSON’s string-based dates.
| Type | Type Code | Example | Purpose |
|---|---|---|---|
| Double | 1 | 3.14, 0.1+0.2 |
Floating-point (Default Number) |
| String | 2 | "Alice" |
UTF-8 string |
| Object | 3 | { key: "value" } |
Nested Document |
| Array | 4 | [1, 2, 3] |
Array |
| Binary data | 5 | BinData(0, "...") |
Binary data (images, files) |
| Undefined | 6 | undefined |
Not recommended |
| ObjectId | 7 | ObjectId("...") |
Default primary key |
| Boolean | 8 | true, false |
Boolean |
| Date | 9 | ISODate("...") |
Date and Time |
| Null | 10 | null |
Empty value |
| Regular Expression | 11 | /pattern/i |
Regular Expression |
| 32-bit Integer | 16 | NumberInt(123) |
32-bit integer |
| 64-bit Integer | 18 | NumberLong(123) |
64-bit Integer (BigInt) |
| Decimal128 | 19 | NumberDecimal("0.30") |
High-precision decimals (finance) |
| MinKey/MaxKey | -1 / 127 | MinKey(), MaxKey() |
Comparison boundary |
(1) 12 BSON Data Types
(2) Selecting a Numeric Type
Concept Explanation: Choosing the numeric data type is the most critical decision when working with BSON data types. JSON has only one Number type (double-precision floating-point), which leads to the classic problem of precision loss in financial calculations: 0.1 + 0.2 = 0.30000000000000004. BSON’s Decimal128 type solves this problem by providing 34-bit decimal precision, making it suitable for scenarios requiring precise calculations, such as amounts and tax rates.
| Numeric Type | Precision | Range | Storage Size | Use Cases |
|---|---|---|---|---|
| Double | 15–17 significant digits | ±1.7×10^308 | 8 bytes | Scientific computing, statistics, graphics |
| Int32 | Exact | -2^31 ~ 2^31-1 | 4 bytes | General-purpose counting, inventory |
| Int64/Long | Precision | -2^63 ~ 2^63-1 | 8 bytes | Large integer IDs, timestamps |
| Decimal128 | 34-bit decimal | ±10^6145 | 16 bytes | Financial Amount (Recommended) |
graph TB
A[MongoDB Numeric Types] --> B[Double<br/>Default]
A --> C[Int32<br/>32 Integer]
A --> D[Long<br/>64 Integer]
A --> E[Decimal128<br/>34 Decimal place]
B --> B1[Applicable:Scientific Computing、Statistics]
C --> C1[Applicable:Routine Count]
D --> D1[Applicable:Large integers ID]
E --> E1[Applicable:Finance、Amount]
style E fill:#d4edda
▶ Example 3: Working with Numeric Types
// === Double(Default)===
db.products.insertOne({
sku: "PHONE-001",
price: 599.99 // Save as Double
});
// === Decimal128(Financial Recommendations)===
db.accounts.insertOne({
balance: NumberDecimal("1234567890.12345678901234567890")
// Precise Storage,No loss of precision
});
// === Int32(Count)===
db.products.insertOne({
sku: "BOOK-001",
stock: NumberInt(150)
});
// === Long(Large integers ID)===
db.orders.insertOne({
_id: NumberLong("1700000000000") // Timestamps as ID
});
// === JavaScript Processing Decimal128 ===
const account = await Account.findOne({});
console.log(account.balance.toString()); // "1234567890.12345678901234567890"
// === Number The Precision Trap ===
0.1 + 0.2; // 0.30000000000000004 ❌
NumberDecimal("0.1") + NumberDecimal("0.2"); // NumberDecimal("0.3") ✅
6. Field Naming Conventions
Concept Explanation: Field naming may seem like a minor detail, but it has a significant impact on team collaboration and long-term maintenance. MongoDB imposes three strict restrictions on field names (they cannot start with $, cannot contain ., and cannot be an empty string), as well as several soft recommendations (camelCase is recommended, avoid reserved words, and limit length). A consistent naming convention is the foundation of database maintainability.
Usage Scenarios: The JavaScript/TypeScript ecosystem recommends camelCase (consistent with code variable names), while the Python/SQL ecosystem recommends snake_case (consistent with database column names). In the MongoDB + Mongoose tech stack, camelCase is recommended for database field names, with snake_case output at the API layer via Mongoose’s toJSON conversion.
| Naming Style | Example | Advantages | Disadvantages | Recommendation |
|---|---|---|---|---|
| camelCase | firstName |
Native support in JS/TS | Not SQL-friendly | ⭐⭐⭐ (Recommended) |
| snake_case | first_name |
SQL/Python friendly | Needs quotes in JS | ⭐⭐ |
| kebab-case | first-name |
URL-friendly | Requires quotes in MongoDB | ⭐ |
(1) MongoDB Field Naming Conventions
✅ Valid naming:
- Field names cannot begin with
$(reserved word) - Field names cannot contain
.(dot notation is retained) - Field names cannot be empty strings
""
// ✅ Valid field names
db.users.insertOne({
firstName: "Alice", // Hump-style
first_name: "Alice", // Snake-like
"first-name": "Alice", // kebab-case(Quotation marks are required)
"user 1": "Alice", // Contains spaces(Quotation marks are required)
age28: 28 // Ending in a number
});
// ❌ Invalid field name
db.users.insertOne({
$name: "Alice", // starts with $ ❌
"user.name": "Alice", // contains . ❌
"": "Alice" // Empty string ❌
});
(2) Comparison of Three Naming Conventions
| Style | Example | Advantages | Disadvantages |
|---|---|---|---|
| camelCase | firstName |
Native support in JS/TS | Not SQL-friendly |
| snake_case | first_name |
SQL/Python friendly | Needs quotes in JS |
| kebab-case | first-name |
URL-friendly | Requires quotes in MongoDB |
(3) Recommendation: camelCase + MongoDB official style
// ✅ Recommended Styles:camelCase
db.users.insertOne({
firstName: "Alice",
lastName: "Smith",
emailAddress: "alice@example.com",
dateOfBirth: new Date("1998-01-01"),
isActive: true,
totalSpent: NumberDecimal("1234.56")
});
▶ Example 4: Mongoose Schema Naming Conventions
// mongoose Automatically convert camelCase to database fields
const UserSchema = new mongoose.Schema({
firstName: { type: String, required: true }, // Database Fields:firstName
emailAddress: { type: String, required: true }, // Database Fields:emailAddress
createdAt: { type: Date, default: Date.now }, // Database Fields:createdAt
isActive: { type: Boolean, default: true } // Database Fields:isActive
});
// Through toJSON Convert Underscore-Based Naming Conventions(API On the way back)
UserSchema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: (doc, ret) => {
ret.first_name = ret.firstName;
delete ret.firstName;
return ret;
}
});
7. Document Size Limits
Concept Explanation: The maximum size for a single MongoDB document is 16 MB, and the maximum nesting depth is 100 levels. This limitation is a core design philosophy of MongoDB—it encourages embedding related data within a single document (to avoid JOINs), but discourages storing extremely large documents. The 16 MB limit allows MongoDB to process individual documents efficiently in memory, ensuring fast response times for queries and updates.
How It Works: The underlying reason for the 16 MB limit is that MongoDB’s WiredTiger storage engine uses an “in-place update” strategy when modifying documents—if the document becomes larger after the update and there is insufficient space at its original location, the document must be moved to a new location, which triggers an index update (all index entries pointing to that document must be updated). The larger the document, the higher the cost of moving it. Therefore, MongoDB has chosen 16 MB as a balance point.
| Dimension | Constraint | Reason |
|---|---|---|
| Single Document Size | 16 MB | Maximum BSON Document Size |
| Nesting Depth | 100 levels (default) | Prevents stack overflow |
| Field Name Length | 255 bytes | UTF-8 encoding |
| Number of indexes | 64 per collection | Index metadata size |
| Total length of a single clustered index key | 1024 bytes | Index efficiency |
| Out-of-Bounds Scenarios | Solutions | Description |
|---|---|---|
| Large files (images/videos) | GridFS | Block storage, 255 KB per block |
| Very Long Text | Elasticsearch + Citations | Documents Store IDs, ES Stores Full Text |
| Array too large (comment list) | Split into separate collections | comments collection + reference |
| Excessively Deep Hierarchy | Flat Design | Reduce Hierarchy Levels |
(1) 16 MB limit
(2) Why 16 MB?
MongoDB Design Philosophy: Avoid Storing Huge Documents:
- ✅ Returns the entire document in a single query (no JOIN)
- ✅ High document transfer efficiency (suitable for network transmission)
- ❌ Not suitable for storing large binary files (use GridFS)
- ❌ Not suitable for storing very long text (use Elasticsearch)
(3) Solutions for Large-Document Scenarios
graph TB
A[Large-Document Scenarios] --> B[Binary file<br/>Image/Video]
A --> C[Long Text<br/>Article/Log]
A --> D[The array is too large<br/>List of Comments]
B --> E[GridFS<br/>Block Storage]
C --> F[Text Search<br/>Elasticsearch]
D --> G[Split Set<br/>comments Gathering]
style E fill:#d4edda
style F fill:#d4edda
style G fill:#d4edda
▶ Example 5: Storing Large Files in GridFS
// === Storing Large Files(>16MB)===
const mongoose = require('mongoose');
const Grid = require('gridfs-stream');
const fs = require('fs');
const conn = mongoose.connection;
let gfs;
conn.once('open', () => {
gfs = Grid(conn.db, mongoose.mongo);
gfs.collection('uploads');
});
// Upload File
const writestream = gfs.createWriteStream({
filename: 'large-video.mp4',
content_type: 'video/mp4'
});
fs.createReadStream('./local-video.mp4').pipe(writestream);
writestream.on('close', (file) => {
console.log(`File stored: ${file._id}`);
});
// Download File
const readstream = gfs.createReadStream({
_id: ObjectId('507f1f77bcf86cd799439011')
});
readstream.pipe(fs.createWriteStream('./downloaded-video.mp4'));
8. Embedded Documentation vs. Citations
Concept Explanation: There are two main strategies for modeling document relationships in MongoDB—embedded (Embed) and referenced (Reference). The embedded approach embeds related data directly within the parent document, allowing all data to be retrieved in a single query; the referenced approach stores related data in separate collections, accessed via ObjectId references, requiring multiple queries using $lookup or at the application layer. Choosing between these two strategies is the most critical decision in MongoDB data modeling.
How It Works: Embedded documents and parent documents are stored in the same BSON document and share the same lifecycle—when the parent document is updated, the embedded document is also overwritten, and when the parent document is queried, the embedded document is returned along with it. Referenced documents are independent BSON documents with their own _id and lifecycle; updates to one do not affect the other, but querying them requires an additional association operation.
graph TB
A[Document Relationship Modeling] --> B{Data Characteristics}
B -->|1:1 Relationship<br/>Small data set<br/>We often read together| C[Embedded ✅<br/>Retrieve in a single query]
B -->|1:N Relationship<br/>N Smaller<br/>It is rarely checked on its own.| D[Embedded ✅<br/>Nested Arrays]
B -->|1:N Relationship<br/>N Larger<br/>Needs to be checked separately| E[Quotation Style ✅<br/>Independent Set]
B -->|N:N Relationship| F[Quotation Style ✅<br/>Two-way ID Array]
B -->|Frequent Updates to Subdocuments| G[Quotation Style ✅<br/>Avoid rewriting the entire document]
style C fill:#d4edda
style D fill:#d4edda
style E fill:#d4edda
| Scenario | Recommendation | Reason |
|---|---|---|
| 1:1 Relationship (User-Address) | Embedded (unless the address changes frequently) | Retrieve all in a single query |
| 1:N Relationship (User-Order) | Depends on the value of N: Small → Embedded; Large → Referenced |
Document Size Limit |
| N:N Relationship (User-Role) | Reference (two-way ID array) | Complex relationship |
| Frequent Updates to Subdocuments | Reference | Avoid Rewriting the Entire Document |
| Requires separate queries for subdocuments | Reference | Performance of separate queries |
(1) Two Strategies for Modeling Relationships
graph TB
subgraph "Embedded Documentation(Embed)"
A1[users Gathering] --> A2[Document 1<br/>address: {<br/> city: Tokyo<br/> country: Japan<br/>}]
end
subgraph "Citation-Style Documentation(Reference)"
B1[users Gathering] --> B2[Document 1<br/>address_id: ObjectId]
B3[addresses Gathering] --> B4[Document 1<br/>city: Tokyo]
B2 -.->|Search| B3
end
(2) Select a Strategy
| Scenario | Recommendation | Reason |
|---|---|---|
| 1:1 Relationship (User-Address) | Embedded (unless the address changes frequently) | Retrieve all in a single query |
| 1:N Relationship (User-Order) | Depends on the value of N: Small → Embedded; Large → Referenced |
Document Size Limit |
| N:N Relationship (User-Role) | Reference (two-way ID array) | Complex relationship |
| Frequent Updates to Subdocuments | Reference | Avoid Rewriting the Entire Document |
| Requires separate queries for subdocuments | Reference | Performance of separate queries |
(3) Example of an Embedded Document
// === Embedded:User + Multiple Addresses ===
db.users.insertOne({
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Alice",
email: "alice@example.com",
addresses: [ // Nested Arrays
{
type: "home",
street: "123 Main St",
city: "Tokyo",
country: "Japan",
zip: "100-0001"
},
{
type: "work",
street: "456 Office Rd",
city: "Tokyo",
country: "Japan",
zip: "100-0002"
}
]
});
// === Search:Living in Tokyo users ===
db.users.find({ "addresses.city": "Tokyo" });
▶ Example 6: Example of a Citation-Style Document
// === Quotation Style:User + Order(Many-to-one) ===
// users Gathering
db.users.insertOne({
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Alice",
email: "alice@example.com"
});
// orders Gathering
db.orders.insertMany([
{
_id: ObjectId("507f1f77bcf86cd799439012"),
user_id: ObjectId("507f1f77bcf86cd799439011"), // Quote
items: ["PHONE-001", "CASE-002"],
total: NumberDecimal("649.98"),
createdAt: new Date()
},
{
_id: ObjectId("507f1f77bcf86cd799439013"),
user_id: ObjectId("507f1f77bcf86cd799439011"), // Quote
items: ["LAPTOP-001"],
total: NumberDecimal("1299.99"),
createdAt: new Date()
}
]);
// === Usage $lookup Joined Queries(Similar SQL JOIN) ===
db.users.aggregate([
{ $match: { name: "Alice" } },
{ $lookup: {
from: "orders",
localField: "_id",
foreignField: "user_id",
as: "orders"
}}
]);
9. Comprehensive Hands-On Exercise: Designing User Documentation for E-commerce
(1) Scenario Requirements
Design user documentation for an e-commerce platform. Requirements:
- Basic user information (name, email address, registration date)
- Multiple shipping addresses (embedded)
- Preferences (Language, Currency, Notifications)
- Statistical Information (Total Number of Orders, Total Sales)
- Follow List (mentions other users)
- Profile picture (GridFS reference)
(2) Document Design
// === Comprehensive User Documentation ===
db.users.insertOne({
_id: ObjectId("507f1f77bcf86cd799439011"),
// === Basic Information ===
email: "alice@example.com",
username: "alice_chen",
displayName: "Alice Chen",
phone: "+81-90-1234-5678",
// === Certification ===
passwordHash: "$2b$10$...", // bcrypt Hash(Not explicitly stated)
emailVerified: true,
twoFactorEnabled: false,
// === Preferences(Nested Documents)===
preferences: {
language: "ja",
currency: "JPY",
timezone: "Asia/Tokyo",
notifications: {
email: true,
sms: false,
push: true,
marketing: false
}
},
// === Shipping Address(Nested Arrays)===
addresses: [
{
addressId: ObjectId("..."),
type: "home",
isDefault: true,
street: "1-2-3 Shibuya",
city: "Tokyo",
prefecture: "Tokyo",
zip: "150-0002",
country: "Japan",
phone: "+81-90-1234-5678"
}
],
// === Statistics(It is recommended to split fields that are updated frequently)===
stats: {
totalOrders: 25,
totalSpent: NumberDecimal("125430.50"),
averageRating: 4.7,
lastOrderAt: ISODate("2026-06-15T10:30:00Z")
},
// === Watchlist(Quotation Style)===
followingIds: [
ObjectId("507f1f77bcf86cd799439012"),
ObjectId("507f1f77bcf86cd799439013")
],
// === Profile Picture Citation(GridFS)===
avatarFileId: ObjectId("507f1f77bcf86cd799439099"),
// === Metadata ===
createdAt: ISODate("2025-03-01T10:00:00Z"),
updatedAt: ISODate("2026-07-01T15:23:00Z"),
lastLoginAt: ISODate("2026-07-01T10:00:00Z"),
isActive: true,
role: "customer" // customer | admin | moderator
});
(3) Mongoose Schema Mapping
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true },
username: { type: String, required: true, unique: true, index: true },
displayName: { type: String, required: true },
phone: { type: String },
passwordHash: { type: String, required: true, select: false },
emailVerified: { type: Boolean, default: false },
twoFactorEnabled: { type: Boolean, default: false },
preferences: {
language: { type: String, default: 'en' },
currency: { type: String, default: 'USD' },
timezone: { type: String, default: 'UTC' },
notifications: {
email: { type: Boolean, default: true },
sms: { type: Boolean, default: false },
push: { type: Boolean, default: true },
marketing: { type: Boolean, default: false }
}
},
addresses: [{
addressId: { type: mongoose.Schema.Types.ObjectId, default: () => new mongoose.Types.ObjectId() },
type: { type: String, enum: ['home', 'work', 'other'], default: 'home' },
isDefault: { type: Boolean, default: false },
street: { type: String, required: true },
city: { type: String, required: true },
prefecture: String,
zip: { type: String, required: true },
country: { type: String, required: true },
phone: String
}],
stats: {
totalOrders: { type: Number, default: 0 },
totalSpent: { type: mongoose.Schema.Types.Decimal128, default: 0 },
averageRating: { type: Number, default: 0 },
lastOrderAt: Date
},
followingIds: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
avatarFileId: { type: mongoose.Schema.Types.ObjectId },
role: { type: String, enum: ['customer', 'admin', 'moderator'], default: 'customer', index: true },
isActive: { type: Boolean, default: true, index: true }
}, { timestamps: true });
❓ FAQ
_id: ObjectId() or a UUID.firstName and FirstName are different fields. MongoDB is strictly case-sensitive. We recommend using a consistent naming convention throughout (camelCase is recommended)._id field be modified?_id is the document’s unique identifier, and modifying it will break the reference relationships. If you need a business primary key (such as an order number), you can use a custom primary key like _id: "ORDER-2026-07-001", but this will result in slower query performance.0.1 + 0.2 = 0.30000000000000004 problem.{ name: "Alice" } is valid, but it offers poor support for debugging, logging, and third-party tools. We recommend using English throughout.📖 Summary
- BSON is MongoDB's binary storage format, which supports more data types than JSON (such as Date, Decimal128, and Binary).
- An ObjectId is a 12-byte unique identifier consisting of a timestamp, a random value, and a counter.
- BSON supports 12+ data types, with a focus on distinguishing between Double, Int32, Long, and Decimal128
- We recommend using camelCase for field names; avoid names that start with
$or do not contain.. - The size limit for a single document is 16 MB; use GridFS for large files and Elasticsearch for long text.
- Embedded relationships are suitable for 1:1 and a small number of 1:N relationships, while referential relationships are suitable for complex relationships
- Mongoose Schema defines document structures and automatically handles BSON type conversions
📝 Exercises
-
Basic Question (⭐): Insert a product document into Mongosh (containing 6 or more fields: String, Number, Date, Array, Object, Boolean), then use
findOne()to query and verify the field types. -
Basic Exercise (⭐): Write a Node.js script to create a user schema using Mongoose (including a
balancefield of typeDecimal128and anavatarfield of typeBuffer), insert data, and print the timestamp of theObjectId. -
Advanced Exercise (⭐⭐): Design a document structure for a blog post (with 5 or more fields), use
insertManyto insert 5 posts, and demonstrate the design of an embedded comments array. -
Advanced Problem (⭐⭐): Write a script to extract the timestamps from the ObjectId fields of 100 documents, group them by date, and count the number of documents for each day.
-
Advanced Problem (⭐⭐): Compare the query performance of embedded and referenced documents: Using 1 million records, store the “user-order” relationship using both embedded and referenced storage methods, and measure response times using
$lookupand nested queries. -
Challenge (⭐⭐⭐): Use GridFS to implement a file upload/download API that supports uploading files up to 100MB in size, verifies the chunked storage mechanism, and implements download progress tracking.



