Episode 3 — NodeJS MongoDB Backend Architecture / 3.11 — Database Optimization
3.11.d — MongoDB Operators
MongoDB operators are the building blocks of every query, update, and aggregation. Mastering them gives you precise control over how you read, filter, and modify data — from simple comparisons to complex array manipulations.
< 3.11.c — Aggregation Pipeline | 3.11.e — Advanced Aggregation >
1. Comparison Operators
Used in queries to compare field values:
| Operator | Meaning | Example |
|---|---|---|
$eq | Equal to | { age: { $eq: 25 } } |
$ne | Not equal to | { status: { $ne: "deleted" } } |
$lt | Less than | { price: { $lt: 100 } } |
$gt | Greater than | { price: { $gt: 50 } } |
$lte | Less than or equal | { age: { $lte: 65 } } |
$gte | Greater than or equal | { age: { $gte: 18 } } |
$in | Matches any value in array | { status: { $in: ["active", "pending"] } } |
$nin | Matches none of the values | { role: { $nin: ["banned", "suspended"] } } |
Examples
// $eq — explicit equality (usually shorthand is used)
db.users.find({ age: { $eq: 25 } });
db.users.find({ age: 25 }); // Shorthand — same result
// $ne — not equal
db.products.find({ status: { $ne: "discontinued" } });
// Returns all products where status is NOT "discontinued"
// Also returns docs where status field does not exist
// $lt, $gt — range queries
db.products.find({ price: { $gt: 10, $lt: 100 } });
// Price between 10 (exclusive) and 100 (exclusive)
// $lte, $gte — inclusive range
db.users.find({ age: { $gte: 18, $lte: 65 } });
// Age between 18 and 65, inclusive
// $in — matches any value in the array
db.orders.find({ status: { $in: ["shipped", "delivered"] } });
// Equivalent to: status === "shipped" || status === "delivered"
// $nin — does not match any value
db.users.find({ role: { $nin: ["admin", "superadmin"] } });
// Excludes admins and superadmins
// Date comparisons
db.orders.find({
createdAt: {
$gte: new Date("2025-01-01"),
$lt: new Date("2025-02-01")
}
});
// All orders from January 2025
2. Logical Operators
Combine multiple conditions:
| Operator | Description | Syntax |
|---|---|---|
$and | All conditions must match | { $and: [ {cond1}, {cond2} ] } |
$or | At least one condition must match | { $or: [ {cond1}, {cond2} ] } |
$not | Negates a condition | { field: { $not: { operator } } } |
$nor | None of the conditions match | { $nor: [ {cond1}, {cond2} ] } |
Examples
// $and — all conditions (implicit AND is more common)
db.products.find({
$and: [
{ price: { $gte: 10 } },
{ price: { $lte: 100 } },
{ category: "electronics" }
]
});
// Implicit AND — shorter syntax (same result)
db.products.find({
price: { $gte: 10, $lte: 100 },
category: "electronics"
});
// When you NEED explicit $and — same field with different operators
db.products.find({
$and: [
{ $or: [{ category: "electronics" }, { category: "books" }] },
{ $or: [{ price: { $lt: 20 } }, { onSale: true }] }
]
});
// (electronics OR books) AND (cheap OR on sale)
// $or — at least one condition
db.users.find({
$or: [
{ email: "alice@example.com" },
{ username: "alice" }
]
});
// $not — negate a condition
db.products.find({
price: { $not: { $gt: 100 } }
});
// Matches price <= 100 AND documents where price does not exist
// $nor — none of the conditions match
db.users.find({
$nor: [
{ status: "banned" },
{ status: "deleted" }
]
});
// Not banned AND not deleted (includes docs without status field)
3. Array Operators (Query)
Used to query documents based on array field contents:
| Operator | Description | Example |
|---|---|---|
$elemMatch | Array element matches all conditions | { scores: { $elemMatch: { $gte: 80, $lte: 90 } } } |
$all | Array contains all specified elements | { tags: { $all: ["tech", "sale"] } } |
$size | Array has exact number of elements | { tags: { $size: 3 } } |
Examples
// $elemMatch — element must match ALL conditions
// Document: { name: "Alice", scores: [72, 85, 91, 68] }
db.students.find({
scores: { $elemMatch: { $gte: 80, $lte: 90 } }
});
// Matches: 85 is >= 80 AND <= 90
// Without $elemMatch — conditions can match DIFFERENT elements
db.students.find({
scores: { $gte: 80, $lte: 90 }
});
// This could match if ANY element >= 80 and ANY element <= 90
// (not necessarily the same element)
// $elemMatch with objects in arrays
// Document: { items: [{ product: "A", qty: 5 }, { product: "B", qty: 2 }] }
db.orders.find({
items: { $elemMatch: { product: "A", qty: { $gte: 3 } } }
});
// Matches only if ONE item is product "A" WITH qty >= 3
// $all — array must contain ALL specified values
db.products.find({ tags: { $all: ["electronics", "wireless", "new"] } });
// Array must contain "electronics" AND "wireless" AND "new"
// Order does not matter, extra elements are OK
// $size — exact array length
db.users.find({ roles: { $size: 2 } });
// Only users with exactly 2 roles
// NOTE: $size does not accept ranges — cannot do { $size: { $gte: 2 } }
// Workaround for array length ranges (use aggregation or $expr):
db.users.find({
$expr: { $gte: [{ $size: "$roles" }, 2] }
});
4. Array Update Operators
Used in update operations to modify arrays:
| Operator | Description | Example |
|---|---|---|
$push | Add element to array | { $push: { tags: "new" } } |
$pull | Remove matching elements | { $pull: { tags: "old" } } |
$pop | Remove first (-1) or last (1) element | { $pop: { tags: 1 } } |
$addToSet | Add only if not already present | { $addToSet: { tags: "sale" } } |
Examples
// $push — add element to array
db.users.updateOne(
{ _id: userId },
{ $push: { notifications: { message: "Welcome!", date: new Date() } } }
);
// $push with modifiers
db.users.updateOne(
{ _id: userId },
{ $push: {
scores: {
$each: [85, 92, 78], // Add multiple values
$sort: -1, // Sort array descending after push
$slice: 10 // Keep only top 10 scores
}
}}
);
// $addToSet — add only if unique
db.products.updateOne(
{ _id: productId },
{ $addToSet: { tags: "featured" } }
);
// If "featured" already exists, no change
// $addToSet with $each — add multiple unique values
db.products.updateOne(
{ _id: productId },
{ $addToSet: { tags: { $each: ["sale", "new", "popular"] } } }
);
// $pull — remove matching elements
db.users.updateOne(
{ _id: userId },
{ $pull: { notifications: { read: true } } }
);
// Removes ALL notifications where read === true
// $pull with condition
db.users.updateOne(
{ _id: userId },
{ $pull: { scores: { $lt: 50 } } }
);
// Removes all scores less than 50
// $pop — remove first or last element
db.users.updateOne(
{ _id: userId },
{ $pop: { activityLog: 1 } } // Remove last element
);
db.users.updateOne(
{ _id: userId },
{ $pop: { activityLog: -1 } } // Remove first element
);
Positional Operator ($)
// Update a specific array element that matches a condition
db.orders.updateOne(
{ _id: orderId, "items.productId": productId },
{ $set: { "items.$.quantity": 5 } }
);
// $ refers to the first matching array element
// Update all matching array elements ($[])
db.orders.updateMany(
{ status: "pending" },
{ $set: { "items.$[].shipped": false } }
);
// Filtered positional operator ($[<identifier>])
db.orders.updateOne(
{ _id: orderId },
{ $set: { "items.$[elem].discount": 10 } },
{ arrayFilters: [{ "elem.price": { $gte: 100 } }] }
);
// Apply discount only to items with price >= 100
5. Update Operators
Used in updateOne(), updateMany(), and findOneAndUpdate():
| Operator | Description | Example |
|---|---|---|
$set | Set field value | { $set: { name: "Alice" } } |
$unset | Remove a field | { $unset: { tempField: "" } } |
$inc | Increment by value | { $inc: { count: 1 } } |
$mul | Multiply by value | { $mul: { price: 1.1 } } |
$rename | Rename a field | { $rename: { "old": "new" } } |
$min | Set if value is less than current | { $min: { lowScore: 50 } } |
$max | Set if value is greater than current | { $max: { highScore: 99 } } |
$currentDate | Set to current date | { $currentDate: { updatedAt: true } } |
Examples
// $set — set one or more fields
db.users.updateOne(
{ _id: userId },
{ $set: {
name: "Alice Smith",
"address.city": "New York", // Dot notation for nested fields
updatedAt: new Date()
}}
);
// $unset — remove fields
db.users.updateOne(
{ _id: userId },
{ $unset: { temporaryToken: "", oldField: "" } }
);
// Field value in $unset does not matter — field is removed regardless
// $inc — increment (positive or negative)
db.products.updateOne(
{ _id: productId },
{ $inc: {
viewCount: 1, // Increment by 1
stock: -1 // Decrement by 1
}}
);
// $mul — multiply
db.products.updateMany(
{ category: "electronics" },
{ $mul: { price: 0.9 } } // 10% discount
);
// $rename — rename a field
db.users.updateMany(
{},
{ $rename: { "fullName": "name", "pwd": "password" } }
);
// $min / $max — conditional update
db.scores.updateOne(
{ playerId: id },
{
$min: { lowestScore: newScore }, // Only updates if newScore < current
$max: { highestScore: newScore } // Only updates if newScore > current
}
);
// $currentDate — set to current timestamp
db.posts.updateOne(
{ _id: postId },
{
$set: { title: "Updated Title" },
$currentDate: {
updatedAt: true, // Sets as Date
"metadata.lastModified": { $type: "timestamp" } // Sets as Timestamp
}
}
);
// Combining multiple update operators
db.users.updateOne(
{ _id: userId },
{
$set: { lastLogin: new Date() },
$inc: { loginCount: 1 },
$push: { loginHistory: { date: new Date(), ip: "192.168.1.1" } },
$addToSet: { devices: "mobile" }
}
);
6. Element Operators
Check field existence or type:
| Operator | Description | Example |
|---|---|---|
$exists | Field exists or not | { phone: { $exists: true } } |
$type | Field is a specific BSON type | { age: { $type: "number" } } |
Examples
// $exists — check if field exists
db.users.find({ phone: { $exists: true } });
// Only users that have a phone field (even if null)
db.users.find({ deletedAt: { $exists: false } });
// Users without a deletedAt field (not soft-deleted)
// $type — check field type
db.users.find({ age: { $type: "number" } });
// Only documents where age is a number
db.mixedData.find({ value: { $type: "string" } });
// Common types: "string", "number", "bool", "object", "array", "null", "date", "objectId"
// Multiple types
db.data.find({ field: { $type: ["string", "number"] } });
// Field is either string or number
7. Regex Operator
Pattern matching on string fields:
// $regex — regular expression matching
db.users.find({ name: { $regex: /alice/i } });
// Case-insensitive search for "alice" anywhere in name
// Explicit syntax
db.users.find({ name: { $regex: "^Alice", $options: "i" } });
// Names starting with "Alice" (case-insensitive)
// Common patterns
db.users.find({ email: { $regex: /@gmail\.com$/ } }); // Gmail users
db.products.find({ name: { $regex: /^wireless/i } }); // Starts with "wireless"
db.logs.find({ message: { $regex: /error|fail|exception/i } }); // Contains error terms
// Mongoose — can also use RegExp directly
const users = await User.find({ name: /alice/i });
const gmailUsers = await User.find({ email: /@gmail\.com$/i });
Performance notes:
- Prefix regex (
/^alice/) can use an index — fast - Non-prefix regex (
/alice/) cannot use an index efficiently — slow on large collections - For full-text search, prefer text indexes or Atlas Search over regex
8. Operators Quick Reference Table
Query Operators
| Category | Operator | Purpose |
|---|---|---|
| Comparison | $eq | Equal |
| Comparison | $ne | Not equal |
| Comparison | $gt / $gte | Greater than / or equal |
| Comparison | $lt / $lte | Less than / or equal |
| Comparison | $in / $nin | In array / not in array |
| Logical | $and | All conditions true |
| Logical | $or | Any condition true |
| Logical | $not | Negate condition |
| Logical | $nor | No condition true |
| Array | $elemMatch | Element matches all criteria |
| Array | $all | Contains all values |
| Array | $size | Exact array length |
| Element | $exists | Field exists |
| Element | $type | Field is BSON type |
| Regex | $regex | Pattern match |
Update Operators
| Category | Operator | Purpose |
|---|---|---|
| Field | $set | Set value |
| Field | $unset | Remove field |
| Field | $inc | Increment |
| Field | $mul | Multiply |
| Field | $rename | Rename field |
| Field | $min / $max | Conditional set |
| Field | $currentDate | Set current date |
| Array | $push | Add to array |
| Array | $pull | Remove from array |
| Array | $pop | Remove first/last |
| Array | $addToSet | Add unique to array |
Key Takeaways
- Comparison operators (
$eq,$gt,$in, etc.) are the foundation of every query filter - Logical operators (
$and,$or) combine conditions — implicit$andis more common - Array query operators (
$elemMatch,$all,$size) handle complex array conditions - Array update operators (
$push,$pull,$addToSet) safely modify array contents - Update operators (
$set,$inc,$unset) perform atomic field-level changes $existsand$typeare essential for schema flexibility in MongoDB- Prefix regex can use indexes; non-prefix regex should be avoided on large collections
- Combine multiple update operators in a single update for atomic multi-field changes
Explain-It Challenge
You are building a social media API. Users have profiles with arrays of
followers,following,posts, and nestedsettingsobjects. Design the following operations using appropriate operators: (1) find users who follow at least 3 specific accounts AND have more than 100 followers, (2) add a new follower without duplicates, (3) remove a specific post from a user's posts array, (4) update a nested notification setting without affecting other settings, (5) find users whose bio matches a search term but are not banned. For each, explain which operators you chose and why.