CRUD (Create, Read, Update, Delete) is the foundation of every MongoDB application. This guide covers the MongoDB Query API with practical patterns, operator reference, and production best practices.

Create Operations

insertOne and insertMany

  // Single document
db.orders.insertOne({
  userId: ObjectId("507f1f77bcf86cd799439011"),
  items: [{ sku: "WIDGET-01", qty: 2, price: NumberDecimal("19.99") }],
  status: "pending",
  createdAt: new Date()
})

// Batch insert
db.orders.insertMany([
  { userId: ObjectId("..."), status: "pending", total: NumberDecimal("49.99") },
  { userId: ObjectId("..."), status: "completed", total: NumberDecimal("29.99") }
], { ordered: false })  // continue on duplicate key errors
  

insertMany with ordered: false is faster for bulk loads — failed documents do not block subsequent inserts.

Write Concern

  db.orders.insertOne(
  { status: "pending" },
  { writeConcern: { w: "majority", j: true } }
)
  

Use w: "majority" in production replica sets to ensure durability across nodes.

Read Operations

find and findOne

  // All matching documents (cursor)
db.users.find({ status: "active" })

// Single document
db.users.findOne({ email: "[email protected]" })

// With projection — only return needed fields
db.users.find(
  { status: "active" },
  { name: 1, email: 1, _id: 0 }
)
  

Comparison Operators

Operator Meaning Example
$eq Equal { age: { $eq: 30 } }
$ne Not equal { status: { $ne: "deleted" } }
$gt / $gte Greater than (or equal) { price: { $gt: 10 } }
$lt / $lte Less than (or equal) { stock: { $lte: 0 } }
$in In array { category: { $in: ["electronics", "books"] } }
$nin Not in array { role: { $nin: ["banned"] } }

Logical Operators

  // AND (implicit in same object)
db.products.find({ category: "electronics", inStock: true })

// OR
db.products.find({
  $or: [
    { category: "electronics" },
    { price: { $lt: 10 } }
  ]
})

// AND + OR combined
db.products.find({
  inStock: true,
  $or: [{ category: "electronics" }, { featured: true }]
})
  

Element and Array Operators

  // Field exists
db.users.find({ phone: { $exists: true } })

// Array contains value
db.articles.find({ tags: "mongodb" })

// Array contains all values
db.articles.find({ tags: { $all: ["mongodb", "database"] } })

// Array element match
db.orders.find({ items: { $elemMatch: { sku: "WIDGET-01", qty: { $gt: 1 } } } })
  

Sort, Skip, Limit

  db.logs.find({ level: "error" })
  .sort({ timestamp: -1 })
  .limit(50)

// Keyset pagination (preferred at scale)
const lastTimestamp = ISODate("2024-06-01T00:00:00Z")
db.logs.find({ timestamp: { $lt: lastTimestamp } })
  .sort({ timestamp: -1 })
  .limit(50)
  

Avoid large skip() values — MongoDB scans and discards skipped documents.

  db.articles.createIndex({ title: "text", body: "text" })
db.articles.find({ $text: { $search: "mongodb indexing" } })
  

Update Operations

updateOne and updateMany

  db.users.updateOne(
  { email: "[email protected]" },
  { $set: { lastLogin: new Date(), loginCount: 1 } }
)

db.products.updateMany(
  { category: "clearance" },
  { $mul: { price: 0.5 } }
)
  

Update Operators Reference

Operator Purpose Example
$set Set field value { $set: { status: "active" } }
$unset Remove field { $unset: { tempField: "" } }
$inc Increment number { $inc: { views: 1 } }
$mul Multiply number { $mul: { price: 0.9 } }
$push Add to array { $push: { tags: "new" } }
$pull Remove from array { $pull: { tags: "old" } }
$addToSet Add if not present { $addToSet: { tags: "mongodb" } }
$rename Rename field { $rename: { "name": "fullName" } }

Array Update Modifiers

  // Push with position, slice to cap array size
db.posts.updateOne(
  { _id: postId },
  {
    $push: {
      comments: {
        $each: [{ user: "Bob", text: "Great post!" }],
        $slice: -100  // keep last 100 comments
      }
    }
  }
)

// Update specific array element
db.students.updateOne(
  { _id: studentId, "grades.subject": "math" },
  { $set: { "grades.$.score": 95 } }
)
  

Upsert — Insert If Not Found

  db.counters.updateOne(
  { _id: "orderId" },
  { $inc: { seq: 1 } },
  { upsert: true }
)
// Returns updated doc with new seq value — common ID generation pattern
  

findOneAndUpdate

  const result = db.inventory.findOneAndUpdate(
  { sku: "WIDGET-01", stock: { $gte: 1 } },
  { $inc: { stock: -1 } },
  { returnDocument: "after" }  // return updated document
)
  

Atomic read-modify-write — use for inventory decrements and counter updates.

Delete Operations

  // Single document
db.sessions.deleteOne({ token: "abc123" })

// Multiple documents
db.logs.deleteMany({ createdAt: { $lt: ISODate("2023-01-01") } })

// findOneAndDelete — return deleted document
db.queue.findOneAndDelete({}, { sort: { priority: -1 } })
  

Bulk Write Operations

  db.products.bulkWrite([
  { insertOne: { document: { sku: "NEW-01", price: NumberDecimal("9.99") } } },
  { updateOne: {
      filter: { sku: "OLD-01" },
      update: { $set: { discontinued: true } }
  }},
  { deleteOne: { filter: { sku: "OBSOLETE-01" } } }
], { ordered: false })
  

Bulk writes reduce round trips — use for ETL, migrations, and batch processing.

Replace Operations

  db.users.replaceOne(
  { email: "[email protected]" },
  { email: "[email protected]", name: "Alice Chen", role: "admin" }
)
  

replaceOne replaces the entire document — prefer $set for partial updates to avoid overwriting fields.

Production Query Patterns

Covered Queries

Include all queried and sorted fields in the index for index-only scans:

  db.users.createIndex({ status: 1, createdAt: -1, name: 1 })
db.users.find(
  { status: "active" },
  { name: 1, createdAt: 1, _id: 0 }
).sort({ createdAt: -1 })
  

Avoiding Common Anti-Patterns

  // Bad: unanchored regex — collection scan
db.users.find({ name: { $regex: "alice", $options: "i" } })

// Better: anchored regex with index
db.users.find({ name: { $regex: "^alice", $options: "i" } })

// Bad: $where JavaScript — slow, disabled in Atlas free tier
db.products.find({ $where: "this.price > this.cost * 2" })

// Good: use query operators
db.products.find({ $expr: { $gt: ["$price", { $multiply: ["$cost", 2] }] } })
  

Common Mistakes

  • Using update() (deprecated) instead of updateOne/updateMany
  • Forgetting that updateOne only modifies the first match — use updateMany for all
  • Not using $set{ updateOne: { filter, update: { status: "done" } } } replaces the document
  • Reading stale data from secondaries without understanding replication lag
  • Missing write concern configuration for critical financial writes

Troubleshooting

  // Explain any query
db.orders.find({ userId: ObjectId("...") }).explain("executionStats")

// Check for COLLSCAN (bad) vs IXSCAN (good)
// totalDocsExamined should be close to nReturned

// Profile slow operations
db.setProfilingLevel(1, { slowms: 50 })
db.system.profile.find({ op: "query", millis: { $gt: 100 } }).sort({ ts: -1 })
  

Best Practices

  • Always use projection to return only needed fields
  • Use findOneAndUpdate for atomic operations
  • Batch writes with bulkWrite for migrations
  • Match indexes to filter + sort patterns
  • Use Decimal128 for monetary values in updates

What Comes Next

Indexing builds directly on these query patterns — every find() filter and sort should inform your index design.