136: MongoDB CRUD and Query Language — Filters, Operators, Arrays, Projection, Cursors, Bulk Writes, and Safe Updates
Learning objectives
You will learn to:
- insert one/many documents;
- query with comparison, logical, element, array, and regex operators;
- understand missing versus null;
- use projection;
- sort/limit/skip;
- work with cursors;
- count documents;
- update with atomic operators;
- use array update operators;
- replace documents;
- delete safely;
- use upsert and bulkWrite;
- understand query injection risks;
- build safe pagination/query filters.
Insert
db.tasks.insertOne({
tenantId: ObjectId("..."),
title: "Learn CRUD",
completed: false,
priority: "normal",
tags: ["mongodb"],
createdAt: new Date()
})
Many:
db.tasks.insertMany([
{...},
{...}
])
Decide ordered/unordered bulk semantics when failures may occur.
Find one
db.tasks.findOne({
_id: ObjectId("...")
})
Find cursor
db.tasks.find({
completed: false
})
find returns cursor in shell/driver.
Do not assume database sends entire collection instantly.
Equality
{
priority: "high"
}
Comparison operators
{
createdAt: {
$gte: ISODate("2026-08-01"),
$lt: ISODate("2026-09-01")
}
}
Common:
$eq $ne $gt $gte $lt $lte $in $nin
$ne/$nin can match broad sets and often be less index-selective.
Do not assume every operator query is efficient.
Logical
{
$or: [
{ priority: "high" },
{ overdue: true }
]
}
Common:
$and $or $nor $not
Mongo implicitly ANDs top-level fields:
{
tenantId: ObjectId(...),
completed: false
}
Element
{
dueDate: {
$exists: true,
$type: "date"
}
}
Use $exists to distinguish absent field.
Null versus missing
Query:
{ field: null }
has special behavior and can match null/missing depending query form.
If you need explicit existence/type:
{
field: null,
other...
}
Use documented patterns such as $type/$exists.
Test null semantics; do not assume SQL NULL behavior.
Dot notation
{
"profile.city": "Madurai"
}
Nested fields query without loading entire document.
Arrays
Contains element:
{
tags: "mongodb"
}
Matches array containing value.
All:
{
tags: {
$all: [
"node",
"mongodb"
]
}
}
Size:
{
tags: {
$size: 2
}
}
$elemMatch
For array of documents:
{
items: {
$elemMatch: {
sku: "A",
quantity: {
$gte: 2
}
}
}
}
This requires predicates to match same array element.
Without $elemMatch, separate elements may satisfy separate dot predicates.
Understand carefully.
Regex
{
title: {
$regex: "^Mongo",
$options: "i"
}
}
Regex can be expensive and index-unfriendly depending pattern.
User-provided regex is dangerous:
- regex DoS;
- broad scan;
- unexpected metacharacters.
Do not pass raw search string as regex without escaping/limits.
For full text/search use appropriate text/Atlas Search design.
Projection
Include:
db.tasks.find(
{ completed: false },
{
title: 1,
priority: 1,
createdAt: 1
}
)
_id included by default unless excluded.
Exclusion:
{
largeField: 0
}
Generally do not mix inclusion/exclusion except _id rules.
Projection reduces data transfer but does not automatically guarantee covered query.
Sort
.sort({
createdAt: -1,
_id: -1
})
Add deterministic tiebreaker for pagination.
Sort without supporting index can consume memory/CPU.
Limit
.limit(25)
Always limit public list APIs.
Skip
.skip(1000)
Useful for small offset pagination.
Deep skip requires walking past many results and can degrade.
Cursor/range pagination preferred for large changing lists.
Count
db.tasks.countDocuments({
completed: false
})
Exact count can be expensive on huge filters.
estimatedDocumentCount() uses metadata for collection estimate and different semantics.
Choose based on need.
Distinct
db.tasks.distinct("priority", {
tenantId: ObjectId(...)
})
Do not use distinct as replacement for proper aggregation if you need counts/sorts.
Cursor iteration
Node driver later:
const cursor = collection.find(filter);
for await (const doc of cursor) {
...
}
This streams batches rather than converting all to array.
Avoid:
await cursor.toArray()
for millions of documents.
Cursor batch size
Drivers fetch batches.
Batch size tuning can affect memory/network.
Defaults usually fine.
Update one
db.tasks.updateOne(
{
_id: ObjectId("..."),
tenantId: ObjectId("...")
},
{
$set: {
completed: true,
updatedAt: new Date()
},
$inc: {
version: 1
}
}
)
Atomic on one document.
Update operators
Common:
$set $unset $inc $mul $min $max $currentDate $rename
Array:
$push $addToSet $pull $pop
Use update operators instead of read-modify-write where possible.
Lost update
Bad:
read document modify in app replace
Two clients can overwrite.
Use atomic operator:
$inc
or version predicate:
{
_id,
version: expectedVersion
}
Update:
{
$set: patch,
$inc: { version: 1 }
}
If matched count 0, conflict.
$push
{
$push: {
tags: "node"
}
}
Duplicates allowed.
$addToSet avoids duplicate exact values.
Do not use either on unbounded arrays without model bound.
Push modifiers
Mongo supports modifiers such as:
$each $slice $sort $position
Can maintain bounded recent-items array.
Example:
{
$push: {
recentEvents: {
$each: [newEvent],
$position: 0,
$slice: 20
}
}
}
Useful subset pattern.
Array filters
Update selected array elements:
db.orders.updateOne(
{ _id: orderId },
{
$set: {
"items.$[item].status": "ready"
}
},
{
arrayFilters: [
{
"item.sku": "A"
}
]
}
)
Validate identifiers/conditions.
Complex array updates can signal overly large embedded model.
Replace
replaceOne(filter, replacement)
Replaces document content except immutable _id.
Easy to accidentally drop fields.
Use PATCH-style update operators for partial update.
Delete
db.tasks.deleteOne({
_id,
tenantId
})
Scope tenant/authorization.
Delete many:
deleteMany({
archived: true,
archivedAt: {
$lt: cutoff
}
})
Before destructive bulk:
- run
findsame filter; - count;
- inspect sample;
- backup/transaction/change process;
- execute.
Upsert
updateOne(
{ externalId },
{
$set: {...},
$setOnInsert: {
createdAt: new Date()
}
},
{
upsert: true
}
)
Use unique index to enforce uniqueness under concurrency.
Filter alone without unique index can race.
findOneAndUpdate
Returns document according to options.
Useful for atomic claim/update workflows.
Example job claiming needs robust predicate/index and return-after semantics.
Bulk write
db.tasks.bulkWrite([
{
updateOne: {
filter: { _id: id1 },
update: { $set: { completed: true } }
}
},
{
deleteOne: {
filter: { _id: id2 }
}
}
])
Useful for many operations in fewer round trips.
Ordered default stops after first error; unordered can continue independent operations.
Understand partial success.
Retryable writes
MongoDB supports retryable writes for selected operations/configurations.
Driver may retry transient failures safely when operation has retryable semantics.
Do not build manual retries around non-idempotent operations without understanding driver/server behavior.
Consistency lesson goes deeper.
Query injection
Danger:
const filter = req.body.filter;
collection.find(filter);
Attacker can supply:
{
"$where": "...",
"$ne": ...
}
depending server/API capabilities.
Build filter from validated values:
const filter = {
tenantId: auth.tenantId,
};
if (input.status) {
filter.status = input.status;
}
Do not allow arbitrary Mongo operators from public client unless endpoint intentionally exposes a safe query DSL.
Cursor pagination
Sort:
{
createdAt: -1,
_id: -1
}
Next page filter after cursor (createdAt, _id):
{
tenantId,
$or: [
{
createdAt: {
$lt: cursor.createdAt
}
},
{
createdAt: cursor.createdAt,
_id: {
$lt: cursor.id
}
}
]
}
Requires supporting compound index.
Do not use _id timestamp alone when actual business order is another field.
Common mistakes
- raw client filter;
- regex from user;
- deep skip at scale;
- unbounded
toArray; - replace loses fields;
- upsert without unique index;
- update by
_idwithout tenant scope; - read-modify-write lost update;
- unbounded push;
- bulk delete without preview;
- inconsistent field types;
$nescan surprises.
Exercises
- Insert/find/update/delete tasks.
- Query nested and array fields.
- Demonstrate
$elemMatch. - Project small shape.
- Build cursor iteration.
- Compare skip and range pagination.
- Implement version update conflict.
- Maintain recent-events bounded array.
- Build upsert with unique index plan.
- Sanitize a public filter builder.
- Use bulkWrite and inspect partial errors.
Mastery checklist
Explain:
- CRUD;
- operators;
- null/missing;
- arrays/elemMatch;
- projection;
- cursor;
- skip/limit;
- atomic updates;
- upsert;
- bulkWrite;
- retryable writes concept;
- query injection;
- cursor pagination.
