CRUD Operations
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.
Text Search
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 ofupdateOne/updateMany - Forgetting that
updateOneonly modifies the first match — useupdateManyfor 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
findOneAndUpdatefor atomic operations - Batch writes with
bulkWritefor migrations - Match indexes to filter + sort patterns
- Use
Decimal128for monetary values in updates
What Comes Next
Indexing builds directly on these query patterns — every find() filter and sort should inform your index design.