Array and Nested Document Queries
Nested documents and arrays are core features of MongoDB—you need to master them to make the most of the document model.
This course provides an in-depth look at array queries, nested document point notation, $elemMatch exact matching, and array update operations.
1. What You'll Learn
- Querying Nested Documents Using Dot Notation
- Array Field Queries (Single Element, Multiple Elements)
- $elemMatch: Exact match for an element in the same array
$Location placeholder update- Array index (multikey index)
- Design Trade-offs Between Arrays and References
2. The True Story of an E-commerce Data Engineer
(1) Pain Point: Queries on nested documents return incorrect results
Alice was maintaining the product review system when she encountered a query bug:
// ❌ Counterexample:Search"Rating >= 4 and includes 'good' Keywords"Comments on
db.products.find({
"reviews.rating": { $gte: 4 },
"reviews.content": /good/i
});
// ⚠️ Question:May match different comments(One comment, high rating,Another one containing good)
// Expectations:A comment meets both conditions at the same time
(2) Solution for an Exact Match Using $elemMatch
// ✅ Correct Example:Use $elemMatch
db.products.find({
reviews: {
$elemMatch: {
rating: { $gte: 4 },
content: /good/i
}
}
});
// ✅ Correct:A single comment that meets all of the following criteria
graph TB
subgraph "Embedded Documentation"
A1[products Gathering] --> A2[Document 1<br/>address: {<br/> city: Tokyo<br/> country: Japan<br/>}]
end
subgraph "Citation-Style Documentation"
B1[products Gathering] --> B2[Document 1<br/>addressId: ObjectId]
B3[addresses Gathering] --> B4[Document 1<br/>city: Tokyo]
B2 -.->|Search| B3
end
style A2 fill:#d4edda
3. Notation for Nested Document Nodes
Concept Explanation: An embedded document refers to a document that contains another document as a field value. MongoDB supports up to 100 levels of nesting; however, in production environments, it is recommended not to exceed 3 levels. Dot notation is a method of accessing nested fields using the "field.subfield.subsubfield" syntax, and it is the core mechanism for querying and updating nested data in MongoDB.
How It Works: When MongoDB parses dot notation, it locates field paths layer by layer from the outer to the inner levels. The query engine breaks down "specs.screen.size" into doc.specs.screen.size and traverses the BSON document tree level by level. Indexes also support dot notation fields; for example, { "specs.screen.size": 1 } can be used to create efficient indexes.
graph TD
A[BSON Document] --> B[specs: Object]
B --> C[screen: Object]
C --> D["size: '6.5'"]
C --> E["type: 'OLED'"]
B --> F[battery: Object]
F --> G["capacity: '4500mAh'"]
H["specs.screen.size"] -->|Analysis of Dot Notation| D
I["specs.battery.capacity"] -->|Analysis of Dot Notation| G
style D fill:#d4edda
style G fill:#d4edda
| Dimension | Description |
|---|---|
| Applicable Scenarios | Data with a fixed hierarchy that is read and written together (e.g., addresses, specifications) |
| Not applicable scenarios | Data with an uncertain hierarchy or that is frequently updated independently (use references) |
| Performance Impact | Single level ≈ regular field; multiple levels require traversal—recommended ≤ 3 levels |
| Index Support | Fully supported; { "a.b.c": 1 } is equivalent to a regular index |
(1) Querying and Updating Nested Documents
// === Nested Document Structure ===
db.products.insertOne({
sku: 'PHONE-001',
title: 'Smartphone X',
specs: {
screen: { size: '6.5', type: 'OLED' },
battery: { capacity: '4500mAh', type: 'Li-Po' }
}
});
// === Dot Notation Query ===
db.products.find({ "specs.screen.size": '6.5' });
// === Multi-level nesting ===
db.products.find({ "specs.battery.capacity": '4500mAh' });
// === Updating Nested Fields ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "specs.battery.capacity": "5000mAh" } }
);
4. Array Field Queries
Concept Explanation: Arrays are one of the most flexible data structures in the MongoDB document model. A single field can store multiple values, and MongoDB queries on array fields follow the "any-match" principle—as long as one element in the array meets the condition, the document is selected. This makes array queries both powerful and prone to unexpected results.
How It Works: The MongoDB query engine uses an "implicit expansion matching" strategy for array fields. When querying { tags: '5g' }, the engine checks the array elements one by one; if any element equals '5g', it is considered a match. For multi-condition queries (such as {"reviews.rating": {$gte:4}, "reviews.content": /good/}), each condition matches array elements independently, potentially matching different elements—this is why $elemMatch exists.
graph LR
A[Query Criteria] --> B{Array Matching Strategies}
B -->|Single condition| C[Any element matches<br/>tags: '5g']
B -->|Multi-condition point notation| D[Independent matching of each condition<br/>May hit different elements]
B -->|Multiple conditions $elemMatch| E[The same element must<br/>Meets all conditions at the same time]
C --> F[✅ Simple and Efficient]
D --> G[⚠️ May not be accurate]
E --> H[✅ Exact Match]
style F fill:#d4edda
style G fill:#fff3cd
style H fill:#d4edda
| Search Method | Matching Rules | Applicable Scenarios |
|---|---|---|
{ tags: '5g' } |
Any element equals '5g' | Single-value match |
{ tags: { $all: ['5g','amoled'] } } |
Must include all values | Multiple values |
{ tags: { $in: ['5g','fast'] } } |
Contains any value | Multiple values or matches |
{ "tags.0": '5g' } |
Specify index position | Position match |
{ tags: { $size: 3 } } |
Exact Match for Array Length | Length Filter |
(1) Single-element match
// === An array contains the specified elements ===
db.products.find({ tags: '5g' });
// tags The array contains '5g' All Products
// === The array contains all the specified elements($all)===
db.products.find({ tags: { $all: ['5g', 'amoled'] } });
// Must include both '5g' and 'amoled'
// === The array contains any element($in)===
db.products.find({ tags: { $in: ['5g', 'fast-charging'] } });
// Includes '5g' or 'fast-charging'
(2) Querying Array Elements by Index
// === Get the first element of an array ===
db.products.find({ "tags.0": '5g' });
// tags[0] = '5g'
// === Retrieve the second element of the array ===
db.products.find({ "tags.1": 'amoled' });
// tags[1] = 'amoled'
// === Querying the Range of Array Elements ===
db.products.find({ "tags.0": { $in: ['new', 'sale'] } });
(3) Checking the Length of an Array
// === Exact Match for Array Length ===
db.products.find({ tags: { $size: 3 } });
// Number of tags = 3
// === Query on Array Length Range ===
db.products.find({
$expr: { $gt: [{ $size: '$tags' }, 3] }
});
// Number of tags > 3
5. $elemMatch Exact Match
Concept Explanation: $elemMatch is an exact match operator designed specifically for array fields in MongoDB. It ensures that the same array element satisfies all query conditions simultaneously, rather than different elements each satisfying only some of the conditions. When array elements are objects (such as comments or grades) and a multi-field join query is required, $elemMatch is the only correct choice.
How It Works: $elemMatch Applies all conditions to each element in the array one by one. An element is considered a match only if it passes all condition checks simultaneously. Key difference from dot notation: In dot notation, multiple conditions each scan the array and may match different elements; $elemMatch requires that a single element pass all tests.
sequenceDiagram
participant Query as Search Engine
participant Doc as Document<br/>reviews: [{rating:5,content:"bad"},<br/>{rating:3,content:"good"}]
Note over Query,Doc: Dot Notation Query: reviews.rating>=4 AND reviews.content=/good/
Query->>Doc: Conditions1: rating>=4 → Hit Element0
Query->>Doc: Conditions2: /good/ → Hit Element1
Doc-->>Query: ⚠️ Different elements each meet some of the conditions → Return to this document
Note over Query,Doc: $elemMatch Search
Query->>Doc: Element0: rating>=4 ✅, /good/ ❌ → Mismatch
Query->>Doc: Element1: rating>=4 ❌ → Mismatch
Doc-->>Query: ✅ No elements satisfy all of the following conditions → Does not return
| Comparison Dimension | Point Notation | $elemMatch |
|---|---|---|
| Match Granularity | Each condition is matched independently | All conditions for the same element must be met |
| Syntax | { "a.b": x, "a.c": y } |
{ a: { $elemMatch: { b: x, c: y } } } |
| Accuracy | ⚠️ May target different elements | ✅ Accurate down to a single element |
| Performance | Slightly faster (can use indexes separately) | Slightly slower (requires checking each element) |
| Index Support | Multiple conditions can be processed separately via indexes | A composite index is required |
(1) The Core Issue
// === Counterexample:Matching Elements in Different Arrays ===
db.products.find({
"reviews.rating": { $gte: 4 },
"reviews.content": /good/i
});
// Possible matches:review1.rating=5, review2.content="good"
// That is, different comments each satisfy the condition
// ✅ Correct Example:$elemMatch Match the same element
db.products.find({
reviews: {
$elemMatch: {
rating: { $gte: 4 },
content: /good/i
}
}
});
// A single comment must meet all of the following criteria at the same time
(2) Complex Conditions
// === Multiple conditions elemMatch ===
db.products.find({
reviews: {
$elemMatch: {
rating: { $gte: 4 },
helpful: { $gt: 10 },
createdAt: { $gte: new Date('2026-01-01') }
}
}
});
// === Nested elemMatch ===
db.students.find({
courses: {
$elemMatch: {
name: 'Math',
score: { $gte: 90 },
assignments: {
$elemMatch: {
submitted: true,
grade: { $gte: 80 }
}
}
}
}
});
6. Array Update Modifiers
Concept Explanation: MongoDB provides a set of update modifiers specifically for manipulating array fields: $push (append), $pull (delete), $addToSet (append without duplicates), $pop (remove first and last elements). When used in conjunction with the $ position placeholder and arrayFilters, these operators allow you to precisely update specific elements in an array without replacing the entire array.
How It Works: Array update modifiers are executed atomically on the MongoDB server. $push Appends an element to the end of the array; $addToSet Checks if the element already exists before appending it; $pull appends an element to the end of the array; $addToSet checks for the element’s existence before appending; $pull deletes an element based on a condition; $ the placeholder points to the position of the first array element that matches the query condition; $[] points to all elements; $[filter] works in conjunction with arrayFilters for conditional updates.
graph TB
A[Array Update Modifiers] --> B[$push<br/>Additional Elements]
A --> C[$pull<br/>Conditional Deletion]
A --> D[$addToSet<br/>Remove Duplicates and Append]
A --> E[$pop<br/>Remove the first and last characters]
F[Position Operators] --> G[$<br/>Update the first match]
F --> H[$[]<br/>Update All Elements]
F --> I[$[filter]<br/>Conditional Batch Update<br/>In conjunction witharrayFilters]
style B fill:#d4edda
style C fill:#f8d7da
style D fill:#cce5ff
style I fill:#fff3cd
| Modifier | Function | Example |
|---|---|---|
$push |
Add an element (repeatable) | { $push: { tags: 'new' } } |
$each |
Use $push to append multiple | { $push: { tags: { $each: ['a','b'] } } } |
$addToSet |
Remove Duplicates and Add | { $addToSet: { tags: 'new' } } |
$pull |
Delete by Criteria | { $pull: { tags: 'old' } } |
$pop |
Delete first/last | { $pop: { tags: 1 } } (Delete last) |
$ |
Update the first matching element | { $set: { "reviews.$.flagged": true } } |
$[] |
Update All Elements | { $set: { "reviews.$[].status": "ok" } } |
$[f] |
Conditional Batch Update | { arrayFilters: [{ "f.rating": {$lt:2} }] } |
(1) $push: Add an element
// === Add a single item ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $push: { tags: 'bestseller' } }
);
// === Add multiple($each)===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $push: { tags: { $each: ['5g', 'amoled', 'fast-charging'] } } }
);
(2) $pull: Deletes an element
// === Delete a Specified Value ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pull: { tags: 'old-tag' } }
);
// === Delete Multiple ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pull: { tags: { $in: ['outdated1', 'outdated2'] } } }
);
// === Delete elements that meet the criteria ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pull: { reviews: { rating: { $lt: 2 } } } }
);
// Delete Rating < 2 Comments on
(3) $ Position Placeholder
// === Update the first matching array element ===
db.products.updateOne(
{ sku: 'PHONE-001', "reviews.userId": 'user_001' },
{ $set: { "reviews.$.helpful": 10 } }
);
// Update user_001 Comments on
// === Update all matching array elements ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "reviews.$[].status": "approved" } }
);
// === Conditional Update(arrayFilters)===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "reviews.$[lowRating].flagged": true } },
{ arrayFilters: [{ "lowRating.rating": { $lt: 2 } }] }
);
7. Multikey Index: Array Indexing
Concept Explanation: When an index field is an array, MongoDB automatically creates a multikey index. Its key feature is that each element in the array generates a separate index key. For example, tags: ['5g', 'amoled'] would create two entries in the index, each pointing to the same document. This ensures that query performance for array fields is equivalent to that of regular field indexes.
How It Works: When inserting a document, MongoDB checks whether the indexed field is an array. If so, it creates an index entry for each element. During a query, MongoDB uses multi-key indexes to quickly locate the document containing the target element. Limitation: A composite index can contain at most one array field; otherwise, the number of index entries will explode (Cartesian product).
graph LR
A[Document<br/>tags: 5g, amoled, fast] --> B[multikey index]
B --> C["Index Entries: '5g' → docId"]
B --> D["Index Entries: 'amoled' → docId"]
B --> E["Index Entries: 'fast' → docId"]
F["Search {tags: '5g'}"] -->|Index Lookup| C
C --> G[✅ Document in Chinese]
style B fill:#d4edda
style G fill:#d4edda
| multikey restrictions | description |
|---|---|
| Each array element has an index | The array length affects the index size |
| Composite indexes can have at most one array field | Prevent index explosion |
| High performance for array index queries | Equivalent to regular field indexes |
| Cannot index the entire array | Index only the elements, not the array itself |
▶ Example 1: Practical Guide to Querying Nested Documents
// Scene:ShopHub Checking Product Specifications on E-commerce Platforms
db.products.insertOne({
sku: 'PHONE-002',
title: 'Smartphone Y',
specs: {
screen: { size: '6.7', type: 'AMOLED', refreshRate: '120Hz' },
battery: { capacity: '5000mAh', type: 'Li-Po', fastCharging: true },
storage: { ram: '12GB', internal: '256GB' }
}
});
// Search:AMOLED Products on the Screen
db.products.find({ "specs.screen.type": 'AMOLED' });
// Search:Supports fast charging and has a battery ≥ 5000mAh Products
db.products.find({
"specs.battery.fastCharging": true,
"specs.battery.capacity": { $in: ['5000mAh', '6000mAh'] }
});
// Update:Add Screen Protector Information
db.products.updateOne(
{ sku: 'PHONE-002' },
{ $set: { "specs.screen.protection": "Gorilla Glass 5" } }
);
Output: Dot notation precisely targets multi-level nested fields; queries and updates affect only the target fields and do not overwrite the entire nested object.
▶ Example 2: Comprehensive Practical Application of $elemMatch and Array Updates
// Scene:ShopHub Product Review Management
db.products.insertOne({
sku: 'LAPTOP-001',
title: 'Laptop Pro',
reviews: [
{ userId: 'user_010', rating: 2, content: 'Poor quality', helpful: 0, createdAt: new Date('2026-05-01') },
{ userId: 'user_011', rating: 5, content: 'Excellent good value', helpful: 30, createdAt: new Date('2026-06-01') },
{ userId: 'user_012', rating: 4, content: 'Good performance', helpful: 12, createdAt: new Date('2026-07-01') }
]
});
// 1. Exact Search:The Same Comment rating>=4 and includes 'good'
db.products.find({
reviews: { $elemMatch: { rating: { $gte: 4 }, content: /good/i } }
});
// Hit only user_011 Comments on(rating=5 and includes "good")
// 2. Use $ to update the first top-rated review
db.products.updateOne(
{ sku: 'LAPTOP-001', "reviews.rating": { $gte: 5 } },
{ $set: { "reviews.$.featured": true } }
);
// 3. Use arrayFilters Mark all low-rated reviews
db.products.updateOne(
{ sku: 'LAPTOP-001' },
{ $set: { "reviews.$[low].flagged": true } },
{ arrayFilters: [{ "low.rating": { $lt: 3 } }] }
);
// 4. Delete all low-rated reviews
db.products.updateOne(
{ sku: 'LAPTOP-001' },
{ $pull: { reviews: { rating: { $lt: 3 } } } }
);
Output: $elemMatch exactly matches the same comment;
$updates the first match;arrayFiltersperforms a batch update based on conditions;$pulldeletes comments that meet the conditions.
// === Creating an Array Field Index ===
db.products.createIndex({ tags: 1 });
// Automatically become multikey index
// === Composite multikey index ===
db.products.createIndex({ category: 1, tags: 1 });
// === View Index ===
db.products.getIndexes();
▶ Example 3: Comprehensive Hands-On Exercise Combining Arrays and Nested Documents
// Complete Scene:Nested Queries for E-commerce Product Reviews
// 1. Preparing the Data:Products with nested reviews
db.products.insertOne({
sku: 'PHONE-001',
title: 'Smartphone X',
specs: {
screen: { size: '6.5', type: 'OLED' },
battery: { capacity: '4500mAh', type: 'Li-Po' }
},
reviews: [
{ userId: 'user_001', rating: 5, content: 'Excellent phone! Great battery', helpful: 15, createdAt: new Date('2026-06-01') },
{ userId: 'user_002', rating: 3, content: 'Good but expensive', helpful: 5, createdAt: new Date('2026-06-15') },
{ userId: 'user_003', rating: 5, content: 'Best phone ever, amazing good quality', helpful: 25, createdAt: new Date('2026-07-01') }
]
});
// 2. Search:Rating >= 4 and includes 'good' Comments on Keywords(The same comment meets the criteria)
db.products.find({
reviews: {
$elemMatch: {
rating: { $gte: 4 },
content: /good/i
}
}
}, { sku: 1, title: 1, reviews: 1 });
// 3. Update:Feature the top-rated comment
db.products.updateOne(
{ sku: 'PHONE-001', 'reviews.rating': { $gte: 5 } },
{ $set: { 'reviews.$.featured': true } }
);
// 4. Query Array Elements:tags Includes '5g' And the array length = 3
db.products.find({ tags: { $all: ['5g', 'amoled'], $size: 3 } });
// 5. Multi-level nested queries:Screen Size = 6.5
db.products.find({ 'specs.screen.size': '6.5' });
Output: Returns the PHONE-001 product; in the
reviewsarray, only the third review meets both conditions—rating >= 4and contains 'good'—which perfectly demonstrates the exact matching capability of$elemMatch.
❓ FAQ
{ tags: '5g' }), $elemMatch is not needed.$elemMatch?{ "a.b": 1, "a.c": 2 } may match elements in the same document but from different arrays; $elemMatch must match elements from the same array.📖 Summary
- Querying nested documents using dot notation:
field.subfield - Single-element array match:
{ tags: '5g' } - $all matches all:
{ tags: { $all: ['a', 'b'] } } - $elemMatch: Exact match for an element in the same array
- Array update:
$push/$pull/$pop/$addToSet $Update the placeholder with the first matching element
📝 Exercises
- Basic Question (⭐): Query for products whose tags include "5g" or "amoled."
- Basic Problem (⭐): Use $pull to delete all comments with a rating less than 2.
- Advanced Problem (⭐⭐): Use $elemMatch to find products in the comments that have a "rating >= 4 and contain the keyword 'good'."
- Advanced Exercise (⭐⭐): Use the $ placeholder to update the
helpfulfield in a specified user's comments. - Challenge (⭐⭐⭐): Implement the "like" feature (helpful +1) for the comment system, filter out low-rated comments, and batch-mark comments as reviewed.



