2.3 Array Queries & Element Evaluation
Key Takeaways
- Querying an array field with a scalar value ('{ tags: "mongodb" }') matches any document where the array contains that element, whereas querying with an array literal ('{ tags: ["mongodb", "nosql"] }') requires an exact, order-sensitive match of the entire array.
- The '$all' operator matches documents where an array contains all specified elements regardless of their order or the presence of additional elements.
- The '$size' operator matches arrays with an exact number of elements; it does not accept range operators like '$gt' or '$lt'.
- The '$elemMatch' operator on scalar arrays matches documents where at least one single array element satisfies all specified query conditions simultaneously, avoiding false positives caused by different elements satisfying separate conditions.
- When querying arrays of embedded subdocuments, standard dot notation ('items.sku') evaluates conditions across any combination of subdocuments, whereas subdocument '$elemMatch' requires a single subdocument to satisfy all combined criteria.
Array Queries & Element Evaluation
In MongoDB's document model, fields can contain arrays of scalar values (strings, numbers, dates) or arrays of embedded subdocuments. Querying array fields requires understanding how MongoDB inspects array elements, matches exact arrays, and prevents false-positive query evaluations across multiple elements.
Array Querying Fundamentals: Scalar Match vs. Exact Match
When querying an array field, the syntax chosen determines whether MongoDB performs element containment or exact array matching.
+-------------------------------------------------------------------------+
| Array Querying Paradigms |
| |
| 1. Scalar Containment: { tags: "mongodb" } |
| - Matches if "mongodb" is ONE of the elements in the array. |
| |
| 2. Exact Array Match: { tags: [ "mongodb", "database" ] } |
| - Matches ONLY if the array has EXACTLY those elements in that |
| EXACT order with NO extra elements. |
| |
| 3. Index-Specific: { "tags.0": "mongodb" } |
| - Matches ONLY if the FIRST element (index 0) is "mongodb". |
+-------------------------------------------------------------------------+
Example Document Dataset
Consider a courses collection containing the following documents:
{ "_id": 1, "title": "DB101", "tags": ["database", "sql", "relational"] }
{ "_id": 2, "title": "M103", "tags": ["database", "mongodb", "nosql"] }
{ "_id": 3, "title": "M201", "tags": ["mongodb", "nosql", "database"] }
{ "_id": 4, "title": "M320", "tags": ["mongodb"] }
1. Scalar Element Containment
Passing a single scalar value checks if that value exists anywhere inside the array:
// Returns documents _id: 2, _id: 3, and _id: 4
db.courses.find({ tags: "mongodb" });
2. Exact Array Match
Passing an array literal requires an exact match on element values, array length, and element order:
// Returns ONLY document _id: 2
db.courses.find({ tags: ["database", "mongodb", "nosql"] });
// Returns NOTHING! (Order differs from _id: 2 and _id: 3)
db.courses.find({ tags: ["nosql", "database", "mongodb"] });
3. Querying by Array Index Position
Using dot notation with a numeric index targets a specific position in the array (0-indexed):
// Returns documents where the very first tag is "database" (Returns _id: 1 and _id: 2)
db.courses.find({ "tags.0": "database" });
Array Operators: $all and $size
MongoDB provides specialized operators designed specifically for array field evaluation.
1. The $all Operator
The $all operator matches documents where the array field contains all the specified elements, regardless of their order or the presence of other elements in the array.
// Returns documents _id: 2 and _id: 3
db.courses.find({
tags: { $all: ["mongodb", "database"] }
});
Equivalent $and Logic
Under the hood, { tags: { $all: [ "A", "B" ] } } is functionally equivalent to an explicit logical $and:
db.courses.find({
$and: [
{ tags: "mongodb" },
{ tags: "database" }
]
});
2. The $size Operator
The $size operator matches documents where the array field contains an exact number of elements.
// Returns document _id: 4 (tags array has exactly 1 element)
db.courses.find({ tags: { $size: 1 } });
// Returns documents _id: 1, 2, and 3 (tags array has exactly 3 elements)
db.courses.find({ tags: { $size: 3 } });
The $size Range Trap (Crucial Exam Concept)
The $size operator only accepts a single integer representing exact length. It cannot be combined with comparison operators like $gt, $gte, $lt, or $lte.
// SYNTAX ERROR / INVALID QUERY:
// db.courses.find({ tags: { $size: { $gt: 2 } } }); // Throws MongoServerError!
// Workaround 1: Check if index position 2 exists (matches arrays with >= 3 elements)
db.courses.find({ "tags.2": { $exists: true } });
// Workaround 2: Use $expr with the $size aggregation operator
db.courses.find({
$expr: { $gt: [{ $size: "$tags" }, 2] }
});
The $elemMatch Operator on Scalar Arrays
When querying an array of scalar numbers or strings with multiple range conditions, omitting $elemMatch introduces a subtle false-positive bug.
The Scalar Range Pitfall
Suppose a students collection contains test scores:
{ "_id": 101, "student": "Alice", "scores": [ 45, 88 ] }
{ "_id": 102, "student": "Bob", "scores": [ 12, 95 ] }
{ "_id": 103, "student": "Carol", "scores": [ 72, 78 ] }
Suppose we want to find students who scored between 80 and 90 (inclusive) on a single exam.
// WRONG: Multi-condition query without $elemMatch
db.students.find({
scores: { $gte: 80, $lte: 90 }
});
Why does the query above return Bob (_id: 102)?
MongoDB evaluates { scores: { $gte: 80, $lte: 90 } } by checking if any element in scores is $\ge 80$ AND any element in scores is $\le 90$.
- Bob's score
95satisfiesscores >= 80. - Bob's score
12satisfiesscores <= 90. - Because both conditions were satisfied (even though by two completely different elements!), Bob is returned as a false positive.
Correcting with $elemMatch
The $elemMatch operator forces MongoDB to evaluate all criteria against the same individual array element:
// CORRECT: Matches only if AT LEAST ONE single element satisfies both >= 80 AND <= 90
db.students.find({
scores: { $elemMatch: { $gte: 80, $lte: 90 } }
});
// Returns ONLY Alice (_id: 101, because 88 is between 80 and 90)!
Querying Arrays of Embedded Subdocuments
In real-world data models, collections frequently store arrays of embedded subdocuments (e.g., line items in an order, comments on a post, addresses for a customer).
// Sample Order Document in 'orders' collection
{
"_id": "ORD-9901",
"customer": "Marcus Brody",
"items": [
{ "sku": "LAPTOP-X1", "qty": 1, "price": NumberDecimal("1200.00") },
{ "sku": "MOUSE-W02", "qty": 5, "price": NumberDecimal("25.00") },
{ "sku": "CABLE-U03", "qty": 2, "price": NumberDecimal("15.00") }
]
}
There are three distinct ways to query arrays of subdocuments, each with different semantics:
1. Exact Subdocument Match
Passing an embedded document literal matches only if the subdocument has the exact fields in the exact order:
// Exact match (Fragile: sensitive to field order and omitted fields)
db.orders.find({
items: { sku: "MOUSE-W02", qty: 5, price: NumberDecimal("25.00") }
});
2. Dot Notation Across Subdocuments (Cross-Element Match)
Using dot notation on subdocument fields ("items.sku", "items.qty") tests whether the conditions are met by the document's array, but does not require the conditions to be met by the same subdocument:
// Searches for orders containing SKU "LAPTOP-X1" AND quantity >= 5
db.orders.find({
"items.sku": "LAPTOP-X1",
"items.qty": { $gte: 5 }
});
The Problem: The order document ORD-9901 above matches! Why? Because item 0 has SKU LAPTOP-X1, and item 1 has quantity 5. The query matched across two different subdocuments!
3. Subdocument $elemMatch (Single-Element Match)
To ensure that a single subdocument meets all the query criteria simultaneously, you must use $elemMatch:
// CORRECT: Matches only if at least ONE item has BOTH sku: "LAPTOP-X1" AND qty >= 5
db.orders.find({
items: {
$elemMatch: {
sku: "LAPTOP-X1",
qty: { $gte: 5 }
}
}
});
// ORD-9901 does NOT match, because the laptop item only has qty: 1!
Array Query Semantics Comparison Table
| Query Pattern | Example Syntax | Matching Requirement |
|---|---|---|
| Scalar Containment | { tags: "db" } | Any array element equals "db" |
| Exact Array | { tags: ["a", "b"] } | Entire array exactly equals ["a", "b"] in order |
| All Elements | { tags: { $all: ["a", "b"] } } | Array contains both "a" and "b" in any order |
| Exact Length | { tags: { $size: 3 } } | Array length is exactly 3 |
Scalar $elemMatch | { scores: { $elemMatch: { $gt: 50, $lt: 60 } } } | Single scalar element satisfies both $gt and $lt |
| Subdoc Dot Notation | { "items.sku": "A", "items.qty": 2 } | Can match SKU on subdoc 1 and QTY on subdoc 2 |
Subdoc $elemMatch | { items: { $elemMatch: { sku: "A", qty: 2 } } } | Single subdocument must satisfy BOTH SKU and QTY |
An orders collection contains documents with an 'items' array of subdocuments: { _id: 1, items: [ { product: "A", qty: 2 }, { product: "B", qty: 10 } ] }. Which query ensures that a single line item has BOTH product: "A" AND qty >= 5?
A collection contains a document with an array of integers: { _id: 50, readings: [ 15, 85 ] }. Which of the following queries will match this document?
A developer attempts to query a collection to find all documents where an array field named 'tags' contains more than 3 elements. Which statement regarding the '$size' operator is correct?
What is the expected behavior when executing 'db.articles.find({ categories: { $all: ["tech", "cloud"] } })'?