Loading learning content...
The power of a database is measured not just by how it stores data, but by how expressively and efficiently you can retrieve it. Document databases offer a query language that feels natural to developers—working with JSON-like objects rather than SQL strings.
MongoDB's query language has evolved into a rich system of operators, modifiers, and patterns that can handle everything from simple lookups to complex analytical queries. Mastering this query language is essential for building performant applications.
In this page, we explore the full spectrum of MongoDB query capabilities—from fundamental CRUD operations through advanced text search and geospatial queries.
By the end of this page, you will understand: query filter operators (comparison, logical, element, array); projection for shaping results; sorting and pagination patterns; text search capabilities; geospatial queries; query performance optimization; and common query patterns for real-world applications.
MongoDB queries use a filter document to specify matching criteria. The filter is a BSON object where keys are field names and values are matching conditions.
Find One Document:
// Find a single document by exact match
const user = await db.users.findOne({ email: "alice@example.com" });
// Find by ObjectId
const order = await db.orders.findOne({
_id: ObjectId("507f1f77bcf86cd799439011")
});
Find Multiple Documents:
// Find all matching documents
const activeUsers = await db.users.find({ status: "active" }).toArray();
// With chained modifiers
const recentOrders = await db.orders
.find({ status: "pending" })
.sort({ createdAt: -1 })
.limit(10)
.toArray();
| Operation | Method | Description |
|---|---|---|
| Create | insertOne(doc) / insertMany([docs]) | Insert one or multiple documents |
| Read | findOne(filter) / find(filter) | Retrieve matching documents |
| Update | updateOne(filter, update) / updateMany() | Modify matching documents |
| Delete | deleteOne(filter) / deleteMany(filter) | Remove matching documents |
| Replace | replaceOne(filter, replacement) | Completely replace a document |
| Upsert | updateOne(filter, update, {upsert: true}) | Update or insert if not exists |
Update Operators:
// $set: Set field values
await db.users.updateOne(
{ _id: userId },
{ $set: { "profile.bio": "New bio", lastModified: new Date() } }
);
// $inc: Increment numeric fields
await db.products.updateOne(
{ _id: productId },
{ $inc: { viewCount: 1, stock: -1 } }
);
// $push: Add to array
await db.users.updateOne(
{ _id: userId },
{ $push: { tags: "premium" } }
);
// $addToSet: Add to array only if not present
await db.users.updateOne(
{ _id: userId },
{ $addToSet: { roles: "editor" } }
);
// $pull: Remove from array
await db.users.updateOne(
{ _id: userId },
{ $pull: { tags: "trial" } }
);
// $unset: Remove field
await db.users.updateOne(
{ _id: userId },
{ $unset: { temporaryFlag: "" } }
);
Comparison operators enable queries beyond simple equality matching.
| Operator | Meaning | SQL Equivalent |
|---|---|---|
$eq | Equals | = value |
$ne | Not equals | <> value |
$gt | Greater than | > value |
$gte | Greater than or equal | >= value |
$lt | Less than | < value |
$lte | Less than or equal | <= value |
$in | Matches any value in array | IN (values) |
$nin | Matches none of the values | NOT IN (values) |
// Greater than / less than (range queries)
db.products.find({
price: { $gte: 100, $lte: 500 }
});
// Not equal
db.users.find({ status: { $ne: "banned" } });
// In a set of values
db.orders.find({
status: { $in: ["pending", "processing", "shipped"] }
});
// Not in a set
db.users.find({
country: { $nin: ["US", "CA", "UK"] }
});
// Comparison with dates
db.events.find({
timestamp: {
$gte: ISODate("2024-01-01"),
$lt: ISODate("2024-02-01")
}
});
// Comparison with nested fields
db.users.find({
"profile.age": { $gte: 18 }
});
Prefer $in over $or when matching multiple values on the same field:
// ✅ Better - uses index efficiently
{ status: { $in: ["a", "b", "c"] } }
// ❌ Avoid - creates separate index scans
{ $or: [{ status: "a" }, { status: "b" }, { status: "c" }] }
Logical operators combine multiple conditions for complex queries.
| Operator | Meaning | Syntax |
|---|---|---|
$and | All conditions must match | { $and: [{ cond1 }, { cond2 }] } |
$or | At least one condition matches | { $or: [{ cond1 }, { cond2 }] } |
$not | Negates a condition | { field: { $not: { condition } } } |
$nor | None of the conditions match | { $nor: [{ cond1 }, { cond2 }] } |
$and (explicit and implicit):
// Implicit AND (comma-separated conditions)
db.products.find({
price: { $gte: 100 },
category: "electronics",
inStock: true
});
// Explicit $and (needed when querying same field multiple times)
db.products.find({
$and: [
{ tags: { $elemMatch: { $eq: "sale" } } },
{ tags: { $elemMatch: { $eq: "featured" } } }
]
});
$or:
// Match users from different conditions
db.users.find({
$or: [
{ role: "admin" },
{ "permissions.canEdit": true }
]
});
// Combined with other conditions
db.products.find({
category: "electronics", // AND
$or: [
{ brand: "Apple" },
{ price: { $gt: 1000 } }
]
});
$not:
// Negate a condition
db.products.find({
price: { $not: { $gt: 100 } } // price <= 100 or price doesn't exist
});
// $not with regex
db.users.find({
email: { $not: /.*@gmail\.com$/ } // Not a Gmail address
});
$nor:
// Exclude multiple conditions
db.users.find({
$nor: [
{ status: "banned" },
{ role: "guest" },
{ verified: false }
]
});
// Matches users who are NOT banned AND NOT guests AND ARE verified
For $or queries to use indexes efficiently, each clause should have an index:
// If you have this query:
{ $or: [{ status: "active" }, { priority: "high" }] }
// You need BOTH indexes:
db.collection.createIndex({ status: 1 });
db.collection.createIndex({ priority: 1 });
// Or a compound index won't help here—$or requires separate index scans
Document databases excel at querying arrays. MongoDB provides powerful operators for array matching and manipulation.
| Operator | Purpose | Example |
|---|---|---|
$all | Match arrays containing all specified elements | { tags: { $all: ['a', 'b'] } } |
$elemMatch | Match documents where array element matches all conditions | { scores: { $elemMatch: { $gt: 80, $lt: 90 } } } |
$size | Match arrays with exact length | { tags: { $size: 3 } } |
$ | Positional operator (first matching element) | { "items.$.qty": 5 } in update |
$[] | All elements operator | { "items.$[].price": 10 } in update |
$[identifier] | Filtered positional operator | Update specific elements matching filter |
Basic Array Matching:
// Match if array CONTAINS the value
db.products.find({ tags: "electronics" });
// Matches: { tags: ["electronics", "sale"] }
// Matches: { tags: ["electronics"] }
// Match if array contains ALL values
db.products.find({ tags: { $all: ["electronics", "sale"] } });
// Matches: { tags: ["electronics", "sale", "featured"] }
// Does NOT match: { tags: ["electronics", "new"] }
// Match exact array (order and content)
db.products.find({ tags: ["electronics", "sale"] });
// Only matches exactly: { tags: ["electronics", "sale"] }
$elemMatch for Complex Conditions:
// Sample document:
// { scores: [{ subject: "math", score: 85 }, { subject: "english", score: 72 }] }
// Find where at least one element matches ALL conditions
db.students.find({
scores: {
$elemMatch: {
subject: "math",
score: { $gte: 80 }
}
}
});
// Without $elemMatch (matches across different elements - usually wrong!):
db.students.find({
"scores.subject": "math",
"scores.score": { $gte: 80 }
});
// This could match if math=60 and english=85!
Array Updates:
// Update first matching element using $
db.orders.updateOne(
{ _id: orderId, "items.product": "SKU123" },
{ $set: { "items.$.status": "shipped" } }
);
// Update ALL array elements using $[]
db.products.updateOne(
{ _id: productId },
{ $mul: { "variants.$[].price": 1.1 } } // 10% price increase
);
// Update specific elements using $[identifier] with arrayFilters
db.orders.updateOne(
{ _id: orderId },
{ $set: { "items.$[item].discount": 10 } },
{ arrayFilters: [{ "item.price": { $gt: 100 } }] }
);
// Only items with price > 100 get the discount
$size only matches exact lengths and cannot use indexes. For range queries on array length:
// Store array length as a separate field
{ tags: [...], tagCount: 5 }
db.products.createIndex({ tagCount: 1 });
db.products.find({ tagCount: { $gte: 3 } });
These operators check for field existence, types, and evaluate expressions.
// $exists: Check if field exists
db.users.find({ phone: { $exists: true } }); // Has phone field
db.users.find({ deletedAt: { $exists: false } }); // Not soft-deleted
// $type: Check BSON type
db.data.find({ value: { $type: "string" } }); // value is a string
db.data.find({ value: { $type: "number" } }); // any number type
db.data.find({ _id: { $type: "objectId" } }); // ObjectId type
// $type with number codes
db.data.find({ field: { $type: 2 } }); // 2 = string
db.data.find({ field: { $type: [2, 16, 18] } }); // string or int32 or int64
// $regex: Regular expression matching
db.users.find({
email: { $regex: /@example\.com$/i } // Case-insensitive
});
// With options
db.users.find({
email: { $regex: "@EXAMPLE\.COM$", $options: "i" }
});
// $expr: Use aggregation expressions in queries
db.products.find({
$expr: { $gt: ["$soldCount", "$stockCount"] } // Sold more than in stock
});
// $expr for comparing two fields
db.orders.find({
$expr: {
$lt: [
"$amount",
{ $multiply: ["$budget", 0.8] } // Amount < 80% of budget
]
}
});
// $mod: Modulo operation
db.items.find({ quantity: { $mod: [10, 0] } }); // Divisible by 10
// $where: JavaScript expression (use sparingly - slow!)
db.users.find({
$where: function() {
return this.firstName.length + this.lastName.length > 20;
}
});
// Prefer $expr over $where:
db.users.find({
$expr: {
$gt: [
{ $add: [
{ $strLenCP: "$firstName" },
{ $strLenCP: "$lastName" }
]},
20
]
}
});
Regular expressions have index limitations:
// ✅ Uses index - anchored at start
{ name: /^John/ }
// ❌ Cannot use index - not anchored
{ name: /John/ }
// ❌ Cannot use index - case-insensitive
{ name: /^john/i }
For better text matching, use text indexes or store lowercase versions.
Controlling what data is returned and in what order is essential for performance and usability.
Projection specifies which fields to include or exclude:
// Include specific fields (1 = include)
db.users.find(
{ status: "active" },
{ name: 1, email: 1, createdAt: 1 }
);
// Returns: { _id: ..., name: ..., email: ..., createdAt: ... }
// Exclude specific fields (0 = exclude)
db.users.find(
{ status: "active" },
{ password: 0, internalNotes: 0 }
);
// Returns all fields EXCEPT password and internalNotes
// Exclude _id
db.users.find(
{ status: "active" },
{ _id: 0, name: 1, email: 1 }
);
// Nested field projection
db.users.find(
{},
{ "profile.name": 1, "profile.avatar": 1 }
);
Projection Rules:
_id is included by default, can be explicitly excluded$slice, $elemMatch available// $slice: Limit array elements
db.posts.find(
{ _id: postId },
{ comments: { $slice: 10 } } // First 10 comments
);
db.posts.find(
{ _id: postId },
{ comments: { $slice: -5 } } // Last 5 comments
);
db.posts.find(
{ _id: postId },
{ comments: { $slice: [10, 5] } } // Skip 10, take 5
);
// $elemMatch: Project first matching array element
db.products.find(
{ _id: productId },
{ variants: { $elemMatch: { color: "red" } } }
);
// $ positional: Project first matching element (requires match in query)
db.products.find(
{ "variants.color": "red" },
{ "variants.$": 1 }
);
// Ascending (1) and descending (-1)
db.products.find().sort({ price: 1 }); // Cheapest first
db.products.find().sort({ createdAt: -1 }); // Newest first
// Multiple fields (order matters)
db.products.find().sort({
category: 1, // Primary sort
price: -1 // Secondary sort (within category)
});
// Text score sorting
db.articles.find(
{ $text: { $search: "mongodb performance" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
Skip/Limit (simple but inefficient for large offsets):
const page = 5;
const pageSize = 20;
db.products
.find({ category: "electronics" })
.sort({ _id: 1 })
.skip((page - 1) * pageSize)
.limit(pageSize);
Cursor-based Pagination (efficient for large datasets):
// First page
const firstPage = await db.products
.find({ category: "electronics" })
.sort({ _id: 1 })
.limit(20)
.toArray();
const lastId = firstPage[firstPage.length - 1]._id;
// Next page - uses index efficiently
const nextPage = await db.products
.find({
category: "electronics",
_id: { $gt: lastId } // Start after last item
})
.sort({ _id: 1 })
.limit(20)
.toArray();
Skip/limit is O(skip + limit) — page 1000 with pageSize 20 scans 20,000 documents!
Cursor-based pagination is O(limit) regardless of position. Always prefer cursor-based for: • Large collections • Deep pagination (beyond page 100) • Real-time feeds where data changes frequently
MongoDB provides built-in text search capabilities, enabling search across string content with linguistic processing.
// Single field text index
db.articles.createIndex({ content: "text" });
// Multiple fields with weights
db.articles.createIndex(
{
title: "text",
content: "text",
tags: "text"
},
{
weights: {
title: 10, // Title matches score 10x
tags: 5, // Tag matches score 5x
content: 1 // Content is baseline
},
name: "article_text_index",
default_language: "english"
}
);
// Basic text search
db.articles.find({ $text: { $search: "mongodb performance" } });
// Phrase search (exact phrase)
db.articles.find({ $text: { $search: "\"database optimization\"" } });
// Negation (exclude terms)
db.articles.find({ $text: { $search: "mongodb -deprecated" } });
// Case-sensitive (requires case-sensitive text index)
db.articles.find({
$text: {
$search: "MongoDB",
$caseSensitive: true
}
});
// With text score for relevance ranking
db.articles.find(
{ $text: { $search: "database performance tuning" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
Stemming and Language Processing:
Diacritics:
// Diacritic-insensitive by default
// "café" matches "cafe"
// For diacritic-sensitive:
db.articles.find({
$text: {
$search: "café",
$diacriticSensitive: true
}
});
Limitations of MongoDB Text Search:
$text with $regex in same queryFor advanced search requirements (fuzzy matching, faceted search, auto-suggest), consider MongoDB Atlas Search (Lucene-based) or external search engines like Elasticsearch.
MongoDB Atlas Search provides Lucene-powered full-text search with: • Fuzzy matching • Auto-complete • Custom analyzers • Faceted search • Highlighting • Synonyms
Integrated directly into MongoDB queries via $search aggregation stage.
MongoDB offers powerful geospatial query capabilities for location-based applications.
// Store location as GeoJSON
{
_id: "store_1",
name: "Downtown Coffee",
location: {
type: "Point",
coordinates: [-73.97, 40.77] // [longitude, latitude]
}
}
// Polygon (area)
{
_id: "zone_1",
name: "Manhattan",
area: {
type: "Polygon",
coordinates: [[
[-74.01, 40.70],
[-73.97, 40.70],
[-73.97, 40.80],
[-74.01, 40.80],
[-74.01, 40.70] // Close the polygon
]]
}
}
// 2dsphere index for GeoJSON (Earth-like sphere)
db.places.createIndex({ location: "2dsphere" });
// 2d index for flat coordinates (legacy, simple use cases)
db.places.createIndex({ location: "2d" });
// $near: Find locations near a point (sorted by distance)
db.stores.find({
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [-73.99, 40.73]
},
$maxDistance: 5000, // meters
$minDistance: 100 // meters
}
}
});
// $geoWithin: Find locations within a polygon
db.stores.find({
location: {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [[...]] // Polygon vertices
}
}
}
});
// $geoWithin: Find within a circle (using $centerSphere)
db.stores.find({
location: {
$geoWithin: {
$centerSphere: [
[-73.99, 40.73], // Center point
5 / 6378.1 // Radius in radians (5km / Earth radius in km)
]
}
}
});
// $geoIntersects: Find geometries that intersect
db.routes.find({
path: {
$geoIntersects: {
$geometry: {
type: "Polygon",
coordinates: [[...]] // Area to check
}
}
}
});
| Operator | Returns | Requires Index | Sorted |
|---|---|---|---|
$near | Documents near a point | 2dsphere or 2d | Yes (by distance) |
$nearSphere | Like $near, spherical geometry | 2dsphere or 2d | Yes |
$geoWithin | Documents within an area | No (but recommended) | No |
$geoIntersects | Documents intersecting geometry | 2dsphere | No |
We've covered MongoDB's complete query language. Let's consolidate the key concepts:
What's Next:
With thorough knowledge of document querying, we'll explore document database use cases—understanding when document databases are the right choice, common patterns, anti-patterns to avoid, and real-world scenarios where the document model excels.
You now have comprehensive knowledge of MongoDB's query language—from basic CRUD through advanced text search and geospatial queries. This foundation enables you to build efficient, expressive queries for any application requirement.