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