Loading content...
A database is only as valuable as your ability to retrieve meaningful information from it. MongoDB's query language evolved from the document model's philosophy: queries are documents themselves. Instead of a string-based language like SQL, you construct queries as JSON objects that describe the shape of documents you want to find.
This document-based approach offers unique advantages:
Mastering MongoDB queries means understanding not just syntax, but also how queries interact with indexes, when to use aggregation, and how to optimize for performance at scale.
By the end of this page, you will master the MongoDB query language including all major operators, understand how to query nested documents and arrays effectively, build sophisticated aggregation pipelines for data transformation, and design indexes that make your queries scale.
MongoDB queries match documents based on criteria you specify. The simplest query is an empty document {}, which matches all documents. From there, you add field conditions that documents must satisfy.
Basic Query Structure:
1234567891011121314151617181920212223242526272829
// Find all documents in collectiondb.users.find({}) // Find by exact field matchdb.users.find({ email: "alice@example.com" }) // Find by multiple fields (implicit AND)db.users.find({ status: "active", role: "admin"})// Returns documents where status="active" AND role="admin" // Find with projection (select specific fields)db.users.find( { status: "active" }, // Query filter { email: 1, // Include email name: 1, // Include name _id: 0 // Exclude _id }) // Nested field access using dot notationdb.users.find({ "address.city": "Portland" })db.users.find({ "name.first": "Alice" }) // Multiple levels of nestingdb.orders.find({ "shipping.address.zipCode": "97201" })Comparison Operators
Beyond exact matches, MongoDB provides operators for ranges, inequality, and set membership:
123456789101112131415161718192021222324252627282930313233343536373839404142
// $eq - Equal (usually implicit)db.products.find({ price: { $eq: 29.99 } })db.products.find({ price: 29.99 }) // Same as above // $ne - Not equaldb.users.find({ status: { $ne: "deleted" } }) // $gt, $gte, $lt, $lte - Comparisonsdb.products.find({ price: { $gt: 100 } }) // Greater thandb.products.find({ price: { $gte: 100 } }) // Greater than or equaldb.products.find({ price: { $lt: 50 } }) // Less thandb.products.find({ price: { $lte: 50 } }) // Less than or equal // Range query (combine operators)db.products.find({ price: { $gte: 50, $lte: 100 } // Between 50 and 100 inclusive}) // Date comparisonsconst lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);db.orders.find({ createdAt: { $gte: lastWeek } }) // $in - Match any value in arraydb.products.find({ category: { $in: ["electronics", "computers", "accessories"] }}) // $nin - Not in arraydb.users.find({ role: { $nin: ["admin", "superadmin"] }}) // $exists - Field exists or notdb.users.find({ phoneNumber: { $exists: true } }) // Has phonedb.users.find({ deletedAt: { $exists: false } }) // Not deleted // $type - Match by BSON typedb.mixed.find({ value: { $type: "string" } })db.mixed.find({ value: { $type: "number" } })db.mixed.find({ value: { $type: ["string", "null"] } }) // Multiple typesLogical Operators
Combine multiple conditions with logical operators:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// $and - All conditions must match (explicit form)db.products.find({ $and: [ { price: { $gte: 100 } }, { price: { $lte: 500 } }, { inStock: true } ]}) // Implicit $and (preferred when possible)db.products.find({ price: { $gte: 100, $lte: 500 }, inStock: true}) // $or - At least one condition matchesdb.products.find({ $or: [ { category: "sale" }, { discount: { $gt: 0 } } ]}) // Combining $and and $ordb.products.find({ status: "active", // AND $or: [ // (category=electronics OR category=computers) { category: "electronics" }, { category: "computers" } ]}) // $not - Negates an operator expressiondb.users.find({ age: { $not: { $lt: 21 } } // age >= 21 (or age doesn't exist)}) // $nor - None of the conditions matchdb.products.find({ $nor: [ { status: "discontinued" }, { stock: 0 } ]})// Returns products that are NOT discontinued AND NOT out of stockUse explicit $and when the same field needs multiple operators on the same element: { $and: [{ tags: 'sale' }, { tags: 'featured' }] } means has BOTH tags. The implicit form { tags: 'sale', tags: 'featured' } would overwrite the first condition (JavaScript object behavior).
Arrays are a powerful feature of document databases, but querying them has nuances. MongoDB provides operators specifically designed for array matching:
123456789101112131415161718192021222324252627282930313233343536373839404142
// Sample documents// { name: "Product A", tags: ["electronics", "sale", "featured"] }// { name: "Product B", tags: ["clothing", "sale"] }// { name: "Product C", tags: ["electronics"] } // Match if array contains valuedb.products.find({ tags: "sale" })// Returns: Product A, Product B (any element matches) // Exact array match (order and size matter)db.products.find({ tags: ["electronics", "sale", "featured"] })// Returns: Product A only // $all - Array contains all specified values (any order)db.products.find({ tags: { $all: ["electronics", "sale"] } })// Returns: Product A (has both, regardless of order) // $size - Array has exactly N elementsdb.products.find({ tags: { $size: 3 } })// Returns: Product A (exactly 3 tags) // Note: $size doesn't work with ranges! Use aggregation for that. // $elemMatch - Element matches multiple conditions// Sample: { name: "Store A", reviews: [{ rating: 5, verified: true }, { rating: 3, verified: false }] }db.stores.find({ reviews: { $elemMatch: { rating: { $gte: 4 }, verified: true } }})// Returns stores where SAME review has rating >= 4 AND is verified // Without $elemMatch (different behavior!)db.stores.find({ "reviews.rating": { $gte: 4 }, "reviews.verified": true})// Returns stores that have ANY review >= 4 AND have ANY verified review// (could be different reviews!)Array Index Queries
You can query by specific array positions:
123456789101112131415161718192021222324252627282930313233
// Query first elementdb.products.find({ "tags.0": "electronics" })// Products where first tag is "electronics" // Query last element using aggregation// (No direct operator for last element in find) // Query by array index in nested documents// { items: [{ productId: "A", qty: 5 }, { productId: "B", qty: 3 }] }db.orders.find({ "items.0.productId": "A" })// Orders where first item is product A // Positional $ operator in projections and updatesdb.orders.find( { "items.productId": "A" }, { "items.$": 1 } // Returns only the matched element) // $slice - Return subset of arraydb.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: [20, 10] } } // Skip 20, then take 10)A common bug: using dot notation on arrays when you mean $elemMatch. { 'reviews.rating': 5, 'reviews.verified': true } can match a document where one review has rating 5 and a DIFFERENT review is verified. Use $elemMatch when multiple conditions must apply to the same array element.
MongoDB provides multiple ways to search text, from simple regex to full-text search indexes:
Regular Expression Queries
12345678910111213141516171819202122232425
// Basic regex - case sensitivedb.products.find({ name: /keyboard/i }) // Case insensitivedb.products.find({ name: { $regex: "keyboard", $options: "i" } }) // Same // Starts withdb.products.find({ name: /^Pro/ }) // Starts with "Pro" // Ends withdb.products.find({ sku: /-XL$/ }) // Ends with "-XL" // Containsdb.products.find({ description: /wireless/i }) // Contains "wireless" // Complex patternsdb.products.find({ sku: { $regex: "^[A-Z]{3}-\d{4}$" } // Pattern: ABC-1234}) // CAUTION: Regex without anchor (^) can't use indexes efficiently!// These are slow on large collections:db.products.find({ name: /wireless/ }) // Scans all documentsdb.products.find({ name: { $regex: "keyboard$" } }) // Ends with - no index // This CAN use an index (prefix match):db.products.find({ name: /^Pro/ }) // Starts withFull-Text Search
For serious text search, use text indexes:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// Create text indexdb.products.createIndex({ name: "text", description: "text", tags: "text" }, { weights: { name: 10, // Name matches weighted 10x description: 5, // Description weighted 5x tags: 1 // Tags normal weight }, default_language: "english"}); // Basic text searchdb.products.find({ $text: { $search: "wireless keyboard" } })// Finds documents with "wireless" OR "keyboard" in indexed fields // Phrase searchdb.products.find({ $text: { $search: '"mechanical keyboard"' } })// Exact phrase match // Exclude termsdb.products.find({ $text: { $search: "keyboard -wireless" } })// Has "keyboard" but NOT "wireless" // Score-based sortingdb.products.find( { $text: { $search: "pro gaming keyboard" } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } }) // Combine with other filtersdb.products.find({ $text: { $search: "keyboard" }, price: { $lte: 100 }, inStock: true}) // Text search with languagedb.articles.find({ $text: { $search: "café résumé", $language: "french" } })MongoDB text search is basic compared to dedicated search engines like Elasticsearch. It lacks fuzzy matching, typo tolerance, faceted search, and sophisticated relevance tuning. For complex search requirements, consider MongoDB Atlas Search (Lucene-based) or a dedicated search service.
The aggregation framework is MongoDB's most powerful querying feature. It processes documents through a pipeline of stages, where each stage transforms the documents for the next stage. Think of it as a data processing assembly line.
Core Pipeline Stages:
| Stage | Purpose | SQL Equivalent |
|---|---|---|
| $match | Filter documents | WHERE |
| $project | Reshape documents, include/exclude fields | SELECT |
| $group | Group by key and aggregate | GROUP BY |
| $sort | Order documents | ORDER BY |
| $limit | Limit number of documents | LIMIT |
| $skip | Skip documents | OFFSET |
| $unwind | Deconstruct array into multiple documents | UNNEST/LATERAL |
| $lookup | Join with another collection | LEFT JOIN |
| $addFields | Add new fields | SELECT *, expr AS field |
| $count | Count documents | COUNT(*) |
| $facet | Multiple pipelines on same input | GROUPING SETS (kind of) |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// Example: Sales analytics pipelinedb.orders.aggregate([ // Stage 1: Filter to completed orders in 2024 { $match: { status: "completed", createdAt: { $gte: new Date("2024-01-01"), $lt: new Date("2025-01-01") } }}, // Stage 2: Unwind items array (one doc per item) { $unwind: "$items" }, // Stage 3: Group by product and calculate metrics { $group: { _id: "$items.productId", totalRevenue: { $sum: { $multiply: ["$items.quantity", "$items.price"] } }, totalQuantity: { $sum: "$items.quantity" }, orderCount: { $sum: 1 }, avgOrderValue: { $avg: { $multiply: ["$items.quantity", "$items.price"] } } }}, // Stage 4: Lookup product details { $lookup: { from: "products", localField: "_id", foreignField: "_id", as: "product" }}, // Stage 5: Flatten product array { $unwind: "$product" }, // Stage 6: Reshape output { $project: { _id: 0, productId: "$_id", productName: "$product.name", category: "$product.category", totalRevenue: { $round: ["$totalRevenue", 2] }, totalQuantity: 1, orderCount: 1, avgOrderValue: { $round: ["$avgOrderValue", 2] } }}, // Stage 7: Sort by revenue descending { $sort: { totalRevenue: -1 } }, // Stage 8: Top 20 products { $limit: 20 }]);The $unwind Stage: Deconstructing Arrays
$unwind is crucial for array analytics. It creates one document per array element:
12345678910111213141516171819202122232425262728
// Original document{ _id: "order1", customer: "Alice", items: [ { product: "A", qty: 2 }, { product: "B", qty: 1 }, { product: "C", qty: 3 } ]} // After { $unwind: "$items" }{ _id: "order1", customer: "Alice", items: { product: "A", qty: 2 } }{ _id: "order1", customer: "Alice", items: { product: "B", qty: 1 } }{ _id: "order1", customer: "Alice", items: { product: "C", qty: 3 } } // Preserve documents with empty arrays{ $unwind: { path: "$items", preserveNullAndEmptyArrays: true }} // Include array index{ $unwind: { path: "$items", includeArrayIndex: "itemIndex" }}// Result includes: itemIndex: 0, itemIndex: 1, itemIndex: 2The $lookup Stage: Joining Collections
$lookup performs left outer joins between collections:
123456789101112131415161718192021222324252627282930313233343536
// Basic lookup{ $lookup: { from: "customers", // Target collection localField: "customerId", // Field in current docs foreignField: "_id", // Field in target collection as: "customerInfo" // Output array field}}// Result: customerInfo is an array (usually with 0 or 1 element) // Pipeline lookup (more powerful){ $lookup: { from: "orders", let: { customerId: "$_id" }, // Variables from current doc pipeline: [ { $match: { $expr: { $eq: ["$customerId", "$$customerId"] }, status: "completed" // Additional filter }}, { $sort: { createdAt: -1 } }, { $limit: 5 } // Only last 5 orders ], as: "recentOrders"}} // Unwind after lookup (common pattern){ $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer"}},{ $unwind: { path: "$customer", preserveNullAndEmptyArrays: true // Keep orders without customer}}// Now customer is an object, not an array$lookup is essentially a nested loop join and can be slow on large collections. Always ensure the foreignField is indexed. For high-performance joins, consider denormalizing the data at write time instead of joining at read time.
$facet: Multiple Pipelines in One Query
$facet runs multiple aggregation pipelines in parallel on the same input, returning all results together:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// E-commerce search with facetsdb.products.aggregate([ // Initial filter { $match: { category: "electronics", inStock: true } }, // Multiple pipelines for different purposes { $facet: { // Facet 1: Price ranges for filter UI priceRanges: [ { $bucket: { groupBy: "$price", boundaries: [0, 50, 100, 200, 500, 1000, Infinity], default: "Other", output: { count: { $sum: 1 } } }} ], // Facet 2: Brand counts brands: [ { $group: { _id: "$brand", count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 10 } ], // Facet 3: Rating distribution ratings: [ { $group: { _id: { $floor: "$rating" }, count: { $sum: 1 } }}, { $sort: { _id: -1 } } ], // Facet 4: Actual results (paginated) results: [ { $sort: { rating: -1 } }, { $skip: 0 }, { $limit: 20 }, { $project: { name: 1, price: 1, rating: 1, brand: 1 } } ], // Facet 5: Total count totalCount: [ { $count: "count" } ] }}]); // Result:{ priceRanges: [ { _id: 0, count: 42 }, { _id: 50, count: 128 }, // ... ], brands: [ { _id: "Samsung", count: 89 }, { _id: "Apple", count: 76 }, // ... ], ratings: [...], results: [...], totalCount: [{ count: 542 }]}Window Functions with $setWindowFields
MongoDB 5.0+ supports window functions for running calculations across document sets:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
// Running total and rankingdb.sales.aggregate([ { $setWindowFields: { // Partition by category (like PARTITION BY) partitionBy: "$category", // Sort within partition sortBy: { date: 1 }, // Output fields with window functions output: { // Running total of revenue per category runningTotal: { $sum: "$revenue", window: { documents: ["unbounded", "current"] } }, // 7-day moving average movingAvg7Day: { $avg: "$revenue", window: { range: [-6, "current"], unit: "day" } }, // Rank within category by revenue revenueRank: { $rank: {} }, // Dense rank denseRevenueRank: { $denseRank: {} }, // Previous day's revenue previousDayRevenue: { $shift: { output: "$revenue", by: -1, default: 0 } }, // Percent change from previous percentChange: { $let: { vars: { prev: { $shift: { output: "$revenue", by: -1 } } }, in: { $cond: { if: { $eq: ["$$prev", null] }, then: null, else: { $multiply: [ { $divide: [ { $subtract: ["$revenue", "$$prev"] }, "$$prev" ]}, 100 ] } } } } } } }}]);Indexes are the difference between queries that complete in milliseconds and queries that timeout. Understanding MongoDB's index types and when to use them is essential for production systems.
Index Types:
| Index Type | Use Case | Notes |
|---|---|---|
| Single Field | Simple equality and range queries | Most common; ascending/descending order affects sort performance |
| Compound | Multi-field queries and sorts | Field order matters for prefix matching |
| Multikey | Queries on array fields | Automatically created when indexing arrays |
| Text | Full-text search | One text index per collection |
| Hashed | Equality queries, shard keys | Cannot support range queries |
| Geospatial | 2d/2dsphere location queries | Required for $near, $geoWithin |
| Wildcard | Dynamic/unknown field names | Use sparingly; consider compound indexes first |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Single field indexdb.users.createIndex({ email: 1 }) // Ascendingdb.products.createIndex({ price: -1 }) // Descending // Compound index - field ORDER matters!db.orders.createIndex({ customerId: 1, createdAt: -1 })// Supports:// - find({ customerId: X })// - find({ customerId: X, createdAt: { $gte: date } })// - find({ customerId: X }).sort({ createdAt: -1 })// Does NOT support efficiently:// - find({ createdAt: { $gte: date } }) // Missing prefix // Unique indexdb.users.createIndex({ email: 1 }, { unique: true }) // Partial index - only index matching documentsdb.orders.createIndex( { customerId: 1, createdAt: -1 }, { partialFilterExpression: { status: { $in: ["pending", "processing"] } } })// Smaller index, only for active orders // Sparse index - only index documents with the fielddb.users.createIndex( { phoneNumber: 1 }, { sparse: true })// Documents without phoneNumber are not indexed // TTL index - auto-delete old documentsdb.sessions.createIndex( { createdAt: 1 }, { expireAfterSeconds: 86400 } // 24 hours) // Background index creation (default in MongoDB 4.2+)db.largeCollection.createIndex( { commonField: 1 }, { background: true } // No longer needed in 4.2+)Using explain() to Analyze Queries
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Explain query executionconst explain = await db.orders.find({ customerId: ObjectId("..."), status: "completed", createdAt: { $gte: new Date("2024-01-01") }}).explain("executionStats"); // Key metrics to examine:// 1. executionStats.nReturned - Documents returned// 2. executionStats.totalDocsExamined - Documents scanned// 3. executionStats.totalKeysExamined - Index entries scanned// 4. executionStats.executionTimeMillis - Time taken// 5. queryPlanner.winningPlan.stage - Should be IXSCAN, not COLLSCAN // Ideal ratio: nReturned ≈ totalDocsExamined ≈ totalKeysExamined// If totalDocsExamined >> nReturned, query is inefficient // Example output analysis:{ executionStats: { nReturned: 42, totalDocsExamined: 42, // Good: examined = returned totalKeysExamined: 42, // Good: using index executionTimeMillis: 3 }, queryPlanner: { winningPlan: { stage: "FETCH", // Fetching docs inputStage: { stage: "IXSCAN", // Using index (good!) indexName: "customerId_1_createdAt_-1" } } }} // Bad example - collection scan:{ queryPlanner: { winningPlan: { stage: "COLLSCAN" // Full collection scan! } }}For compound indexes, order fields by: Equality first (exact matches), Sort fields next, Range fields last. Example: If you query { status: 'active', category: 'electronics' } and sort by { createdAt: -1 } with a date range, index as { status: 1, category: 1, createdAt: -1 }.
MongoDB's query capabilities range from simple lookups to sophisticated analytics pipelines. Mastering them transforms how you work with document data.
What's Next:
With querying mastered, we'll conclude with use cases and trade-offs—understanding when document databases are the right choice and when other database types are more appropriate.
You now have comprehensive knowledge of MongoDB's query capabilities. You can write efficient filters with operators, query complex nested structures and arrays, build aggregation pipelines for analytics, and design indexes that make queries fast at scale. Next, we'll examine when to choose document databases versus alternatives.