Whether you're new to MongoDB or a seasoned developer, these query patterns and techniques will help you write more efficient, readable queries.
1. Use Projection to Limit Returned Fields
Don't fetch entire documents when you only need a few fields. Projection reduces network transfer and memory usage.
// Bad: fetches all fields
db.users.find({ status: "active" })
// Good: only fetches what you need
db.users.find(
{ status: "active" },
{ name: 1, email: 1, _id: 0 }
) 2. Use $exists to Find Documents with Missing Fields
MongoDB's flexible schema means fields can be missing. Use $exists to find documents with or without specific fields.
// Find users without an email field
db.users.find({ email: { $exists: false } })
// Find users who have a phone number set
db.users.find({ phone: { $exists: true, $ne: null } }) 3. Combine $elemMatch for Array Queries
When querying arrays of objects, $elemMatch ensures all conditions match the same array element.
// Wrong: matches if ANY element has quantity > 5
// AND ANY element has status "pending"
db.orders.find({
"items.quantity": { $gt: 5 },
"items.status": "pending"
})
// Right: matches elements where BOTH conditions are true
db.orders.find({
items: {
$elemMatch: {
quantity: { $gt: 5 },
status: "pending"
}
}
}) 4. Use $expr for Field-to-Field Comparisons
Compare two fields in the same document using $expr.
// Find products where inventory is below reorder level
db.products.find({
$expr: {
$lt: ["$inventory", "$reorderLevel"]
}
})
// Find orders where discount exceeds 20% of subtotal
db.orders.find({
$expr: {
$gt: ["$discount", { $multiply: ["$subtotal", 0.2] }]
}
}) 5. Leverage Compound Indexes
Create indexes that match your query patterns. The order of fields matters—put equality conditions first, then sort fields, then range conditions.
// If you frequently query like this:
db.orders.find({
status: "shipped", // equality
customerId: "abc123" // equality
}).sort({ createdAt: -1 }) // sort
// Create this compound index:
db.orders.createIndex({
status: 1,
customerId: 1,
createdAt: -1
}) 6. Use $facet for Multiple Aggregations in One Query
Run multiple aggregation pipelines on the same input documents with a single database round-trip.
db.orders.aggregate([
{ $match: { year: 2026 } },
{
$facet: {
"totalRevenue": [
{ $group: { _id: null, sum: { $sum: "$total" } } }
],
"byStatus": [
{ $group: { _id: "$status", count: { $sum: 1 } } }
],
"topProducts": [
{ $unwind: "$items" },
{ $group: { _id: "$items.productId", sold: { $sum: "$items.quantity" } } },
{ $sort: { sold: -1 } },
{ $limit: 5 }
]
}
}
]) 7. Use $merge to Save Aggregation Results
Write aggregation results directly to a collection for caching or reporting.
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: {
_id: { year: { $year: "$createdAt" }, month: { $month: "$createdAt" } },
revenue: { $sum: "$total" },
orderCount: { $sum: 1 }
}},
{ $merge: {
into: "monthly_reports",
on: "_id",
whenMatched: "replace",
whenNotMatched: "insert"
}}
]) 8. Use countDocuments() Wisely
For large collections, estimatedDocumentCount() is much faster when you don't need an exact count.
// Slow: scans matching documents
const exactCount = await db.logs.countDocuments({ level: "error" })
// Fast: uses collection metadata (but no filter support)
const approxTotal = await db.logs.estimatedDocumentCount()
// Compromise: limit the count operation
const cappedCount = await db.logs.countDocuments(
{ level: "error" },
{ limit: 1000 } // Stop counting after 1000
) 9. Use $lookup with Pipeline for Complex Joins
The pipeline form of $lookup gives you more control over the join conditions and allows you to filter and transform the joined documents.
db.orders.aggregate([
{
$lookup: {
from: "products",
let: { productIds: "$items.productId" },
pipeline: [
{ $match: {
$expr: { $in: ["$_id", "$$productIds"] },
inStock: true // Additional filter
}},
{ $project: { name: 1, price: 1 } } // Limit fields
],
as: "productDetails"
}
}
]) 10. Use explain() to Understand Query Performance
Always check the query plan for slow queries. Look for COLLSCAN (collection scan) which indicates a missing index.
// Check if your query uses an index
db.users.find({ email: "test@example.com" }).explain("executionStats")
// Key fields to check in the output:
// - winningPlan.stage: "IXSCAN" (good) vs "COLLSCAN" (bad)
// - executionStats.totalDocsExamined: should be close to nReturned
// - executionStats.executionTimeMillis: total query time Bonus: Query Patterns Cheat Sheet
| Pattern | Use Case |
|---|---|
$in | Match any value in an array |
$nin | Match none of the values |
$regex | Pattern matching (use sparingly) |
$text | Full-text search (requires text index) |
$near | Geospatial queries (requires 2dsphere index) |
$unwind | Flatten arrays in aggregation |
$bucket | Group into ranges (histograms) |
Try These in Sutido
All of these queries work great in Sutido's IntelliShell editor. The autocomplete will help you with field names, and you can switch between table, JSON, and tree views to inspect your results.
Level Up Your MongoDB Workflow
Sutido makes writing and testing MongoDB queries faster and more enjoyable.
Download Sutido