A plain equality filter — { status: 'pending' } — matches documents where a field equals a specific value. Real applications need more: find tasks due before Friday, find tasks that are either high priority or overdue, find users who have not set a phone number, find tasks that include a specific tag. MongoDB’s query operators extend the filter syntax to express all of these. Operators are prefixed with $ and appear as values inside the filter object. Mastering them means you can push complex filtering logic into the database — where it runs on indexed data — rather than fetching everything and filtering in Node.js.
Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
$eq |
Equal to (implicit — rarely written explicitly) | { age: { $eq: 30 } } = { age: 30 } |
$ne |
Not equal to | { status: { $ne: 'deleted' } } |
$gt |
Greater than | { dueDate: { $gt: new Date() } } |
$gte |
Greater than or equal to | { score: { $gte: 70 } } |
$lt |
Less than | { price: { $lt: 100 } } |
$lte |
Less than or equal to | { createdAt: { $lte: cutoffDate } } |
$in |
Value is in the given array | { priority: { $in: ['high', 'medium'] } } |
$nin |
Value is NOT in the given array | { status: { $nin: ['deleted', 'archived'] } } |
Logical Operators
| Operator | Meaning | Example |
|---|---|---|
$and |
All conditions must be true (implicit when multiple fields) | { $and: [{ status: 'pending' }, { priority: 'high' }] } |
$or |
At least one condition must be true | { $or: [{ priority: 'high' }, { dueDate: { $lt: now } }] } |
$nor |
None of the conditions can be true | { $nor: [{ status: 'deleted' }, { archived: true }] } |
$not |
Negates an expression (field-level) | { priority: { $not: { $eq: 'low' } } } |
Element Operators
| Operator | Meaning | Example |
|---|---|---|
$exists |
Field exists (true) or does not exist (false) | { dueDate: { $exists: true } } |
$type |
Field is of a specific BSON type | { age: { $type: 'int' } } |
Array Operators
| Operator | Meaning | Example |
|---|---|---|
$all |
Array contains ALL specified elements | { tags: { $all: ['urgent', 'Q4'] } } |
$elemMatch |
At least one element matches all conditions | { scores: { $elemMatch: { $gte: 80, $lt: 90 } } } |
$size |
Array has exactly n elements | { tags: { $size: 3 } } |
{ tags: 'urgent' } — MongoDB matches any document where the tags array contains the value 'urgent' as one of its elements. This is one of MongoDB’s most convenient behaviours: you do not need a special operator to check if an array contains a value. Only use $in when you want to match any of several possible values, or $all when you need all specified values to be present.$exists: false to find documents missing a field — extremely useful for identifying incomplete data or migration tasks. For example, { phoneNumber: { $exists: false } } finds all users who have not provided a phone number. This is impossible to express in SQL without nullable columns and an IS NULL check, but in MongoDB (where fields can be completely absent) it is a first-class query pattern.$or operator significantly affects query performance. MongoDB can use an index for each individual condition in a $or array, but only if there is a separate index for each field. If any condition in a $or cannot use an index, MongoDB falls back to a full collection scan. Always ensure each field referenced in a $or is indexed, or consider restructuring the query to use a single field with $in when possible.Complete Query Examples
const { ObjectId } = require('mongodb');
const now = new Date();
// ── Comparison operators ──────────────────────────────────────────────────
// Tasks due in the next 7 days
const sevenDays = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
await Task.find({ dueDate: { $gte: now, $lte: sevenDays } });
// Tasks that are NOT completed
await Task.find({ status: { $ne: 'completed' } });
// High or medium priority tasks
await Task.find({ priority: { $in: ['high', 'medium'] } });
// All tasks except deleted and archived
await Task.find({ status: { $nin: ['deleted', 'archived'] } });
// Tasks with a score between 70 and 90 (inclusive)
await Task.find({ score: { $gte: 70, $lte: 90 } });
// ── Logical operators ─────────────────────────────────────────────────────
// High priority OR overdue (past due date)
await Task.find({
$or: [
{ priority: 'high' },
{ dueDate: { $lt: now }, status: { $ne: 'completed' } },
],
});
// Pending AND (high priority OR has a due date)
await Task.find({
status: 'pending', // implicit $and for top-level fields
$or: [
{ priority: 'high' },
{ dueDate: { $exists: true } },
],
});
// Tasks that are neither deleted nor archived
await Task.find({
$nor: [
{ status: 'deleted' },
{ archived: true },
],
});
// ── Element operators ─────────────────────────────────────────────────────
// Tasks that have a due date set
await Task.find({ dueDate: { $exists: true } });
// Tasks with no description (field absent)
await Task.find({ description: { $exists: false } });
// Users where phone is stored as a string (not number)
await User.find({ phone: { $type: 'string' } });
// ── Array operators ───────────────────────────────────────────────────────
// Tasks tagged with 'urgent' (array contains 'urgent')
await Task.find({ tags: 'urgent' }); // MongoDB auto-matches array elements
// Tasks tagged with BOTH 'urgent' AND 'Q4'
await Task.find({ tags: { $all: ['urgent', 'Q4'] } });
// Tasks with exactly 3 tags
await Task.find({ tags: { $size: 3 } });
// Tasks with no tags (empty array)
await Task.find({ tags: { $size: 0 } });
// Tasks where at least one attachment is larger than 1MB
await Task.find({
attachments: {
$elemMatch: { size: { $gt: 1024 * 1024 } }
}
});
// ── Date range filters ─────────────────────────────────────────────────────
const startOfDay = new Date(now.setHours(0, 0, 0, 0));
const endOfDay = new Date(now.setHours(23, 59, 59, 999));
// Tasks created today
await Task.find({ createdAt: { $gte: startOfDay, $lte: endOfDay } });
// Tasks overdue (past due date and not completed)
await Task.find({
dueDate: { $lt: new Date() },
status: { $ne: 'completed' },
});
// ── Combined real-world query ──────────────────────────────────────────────
// "Find all active, non-deleted tasks for a user that are either
// high priority or overdue, created in the last 30 days"
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
await Task.find({
userId: new ObjectId(userId),
status: { $nin: ['completed', 'deleted'] },
createdAt: { $gte: thirtyDaysAgo },
$or: [
{ priority: 'high' },
{ dueDate: { $lt: new Date() } },
],
}).sort({ priority: -1, dueDate: 1 }).limit(20);
How It Works
Step 1 — Operators Are Values Inside the Filter Object
Query operators always appear as the value of a field key, inside a nested object. { age: { $gte: 18 } } means “the age field must be greater than or equal to 18.” Multiple operators on the same field are implicitly ANDed: { age: { $gte: 18, $lte: 65 } } means age is between 18 and 65. This nested syntax lets MongoDB parse the filter JSON without ambiguity.
Step 2 — Top-Level Multiple Fields Are Implicitly ANDed
{ status: 'pending', priority: 'high' } is shorthand for { $and: [{ status: 'pending' }, { priority: 'high' }] }. The explicit $and is only necessary when you need to apply multiple conditions to the same field — for example, { $and: [{ tags: 'urgent' }, { tags: 'Q4' }] } is different from { tags: { $all: ['urgent', 'Q4'] } } in some edge cases.
Step 3 — Array Queries Work Without Special Syntax for Contains
When you query { tags: 'urgent' } on a document with tags: ['urgent', 'Q4', 'project'], MongoDB returns the document. Querying an array field with a scalar value performs an “array contains” check automatically. This is intentional by design — it means the same query syntax works whether tags is a string or an array of strings in different documents.
Step 4 — $elemMatch Is Required for Multi-Condition Array Element Queries
Without $elemMatch, { scores: { $gte: 80, $lte: 90 } } matches a document where any element in scores is ≥80 AND any element (not necessarily the same one) is ≤90. With $elemMatch, both conditions must be met by the same element. The difference: a document with scores: [95, 60] would match the first query (95≥80 and 60≤90) but not the second.
Step 5 — Operators Are Evaluated Against MongoDB Indexes
MongoDB optimises queries by using indexes when available. $eq, $in, $lt, $gt, and range queries all benefit from B-tree indexes. $exists on a sparse index is efficient. $regex without an anchor (^) cannot use an index. Understanding which operators are index-compatible helps you design efficient queries and choose the right indexes.
Real-World Example: Dashboard Stats Query
// GET /api/v1/tasks/stats — returns counts grouped by status
// controllers/task.controller.js
exports.getStats = asyncHandler(async (req, res) => {
const userId = new ObjectId(req.user.id);
const now = new Date();
const [statusCounts, overdue, dueToday] = await Promise.all([
// Group by status
Task.aggregate([
{ $match: { userId, deletedAt: { $exists: false } } },
{ $group: { _id: '$status', count: { $sum: 1 } } },
]),
// Count overdue tasks (due before now, not completed)
Task.countDocuments({
userId,
dueDate: { $lt: now },
status: { $nin: ['completed', 'deleted'] },
deletedAt: { $exists: false },
}),
// Count tasks due today
Task.countDocuments({
userId,
dueDate: {
$gte: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
$lt: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
},
status: { $ne: 'completed' },
deletedAt: { $exists: false },
}),
]);
const stats = { overdue, dueToday };
statusCounts.forEach(({ _id, count }) => { stats[_id] = count; });
res.json({ success: true, data: stats });
});
Common Mistakes
Mistake 1 — Using $or on the same field when $in is simpler
❌ Wrong — verbose and less efficient:
await Task.find({
$or: [{ priority: 'high' }, { priority: 'medium' }]
});
✅ Correct — use $in for multiple values on the same field:
await Task.find({ priority: { $in: ['high', 'medium'] } });
Mistake 2 — Expecting $exists: false to match null values
❌ Wrong — null and missing are different in MongoDB:
// Document: { title: 'Task', dueDate: null }
await Task.find({ dueDate: { $exists: false } }); // does NOT match — field exists, value is null
await Task.find({ dueDate: null }); // matches both null AND missing fields
✅ Correct — match truly absent fields or handle null explicitly:
// Find documents where dueDate is absent OR null:
await Task.find({ $or: [{ dueDate: { $exists: false } }, { dueDate: null }] });
// OR simply:
await Task.find({ dueDate: { $in: [null, undefined] } });
Mistake 3 — Using $not instead of $ne for simple inequality
❌ Overcomplicated — $not wraps an expression:
await Task.find({ status: { $not: { $eq: 'completed' } } }); // verbose
✅ Simpler — use $ne directly:
await Task.find({ status: { $ne: 'completed' } });
Quick Reference
| Need | Operator | Example |
|---|---|---|
| Not equal | $ne |
{ s: { $ne: 'done' } } |
| Greater than | $gt |
{ age: { $gt: 18 } } |
| Between range | $gte + $lte |
{ score: { $gte: 70, $lte: 90 } } |
| One of many values | $in |
{ p: { $in: ['high', 'medium'] } } |
| Not any of | $nin |
{ s: { $nin: ['deleted', 'archived'] } } |
| Either condition | $or |
{ $or: [{a: 1}, {b: 2}] } |
| Field exists | $exists |
{ phone: { $exists: true } } |
| Array contains value | Scalar query on array field | { tags: 'urgent' } |
| Array contains ALL | $all |
{ tags: { $all: ['a','b'] } } |
| Array element matches multiple conditions | $elemMatch |
{ arr: { $elemMatch: { $gt:5, $lt:10 } } } |