MongoDB quick reference

156 entries — one card per concept, for looking something up rather than learning it. Each links back to the full explanation.

156

MongoDB Fundamentals

7

database > collection > document

MongoDB stores JSON-like BSON documents, grouped into collections, inside a database.

{ "title": "Dune", "tags": ["sci-fi"] }

fundamentalsdocuments
Document-oriented databases

server > database > collection > document { field: value }

Four nested containers; a document's _id is its primary key, an ObjectId by default.

{ "_id": ObjectId("..."), "title": "Dune" }

relational: JOIN at query time vs. document: nest inline

Denormalizing trades update-in-one-place for read-in-one-place — a workload decision, not "which database is better."

fundamentalsdata-modelingrelational
Document model vs. relational model

lowercase db/collection · camelCase or snake_case fields

Naming is a convention MongoDB does not enforce — pick one and stay consistent; a collection is created implicitly on first insert.

insert · find · update · replace · delete

The five conceptual CRUD verbs — update changes fields, replace swaps the whole document.

db.orders.updateOne({ _id }, { $set: { status: "shipped" } })

fundamentalscrud
CRUD, conceptually

mongosh "mongodb://localhost:27017"

The official JavaScript REPL shell — use switches database, every command is a real JS method call.

fundamentalsshellmongosh
The MongoDB shell (mongosh)

schema-flexible ≠ schema-free

No fixed table schema is enforced automatically — app code or a validator still can be.

db.createCollection("orders", { validator: { $jsonSchema: { required: ["status"] } } })

BSON and Data Types

7

string · double · int32 · int64 · decimal128 · date · objectId · binary · regex

BSON's type list — more precise than JSON's one generic string/number/boolean/null/object/array set.

{ "price": NumberDecimal("19.99"), "createdAt": ISODate("2026-01-01") }

JSON (text, display) vs. BSON (binary, stored)

MongoDB stores and transmits BSON; the JSON-like text you type or read is a rendering of it.

{ "_id": ObjectId("...") } // displayed as text, stored as binary BSON

16 MB per document — field names + type byte stored per field

Every field carries per-document name overhead; unbounded arrays are the most common way to hit the 16 MB limit.

bsondocument-sizegridfs
BSON size and serialization

int32 · int64 · double · decimal128

MongoDB compares numbers by value across types; which type a literal lands in depends on the driver, not the number itself.

bsonnumeric-typesprecision
Integer types and numeric precision

NumberDecimal("19.99")

Exact base-10 decimal, 34 significant digits — use for money; double only approximates decimal fractions.

bsondecimal128numeric-types
Decimal128

ISODate("2026-01-01T00:00:00Z")

Milliseconds since the Unix epoch, UTC — no timezone stored; convert to local time only at display/input.

bsondatetimezone
BSON Date

ObjectId = 4-byte timestamp + 5-byte random + 3-byte counter

The default _id value: unique without central coordination, roughly sortable by creation time.

ObjectId("65f1a2b3c4d5e6f7a8b9c0d1").getTimestamp()

Documents and Collections

6

embed: nest inline · reference: store an _id, look up separately

Embed data read together and bounded to one parent; reference data that grows independently or is shared.

{ "authorId": ObjectId("...") } // reference, resolved by a separate query or $lookup

"a.b" · "a.0.b"

Dot notation reaches a nested field or array element, in queries, projections and update operators alike.

db.users.updateOne({ _id: 1 }, { $set: { "address.city": "Pune" } })

["tags"] scalars vs. [{ sku, qty }] documents

Scalar arrays hold plain values; document arrays hold structured items worth naming and querying individually.

a write touches the whole document, not just the changed field

Large or unbounded arrays and deep nesting cost more to update and index — keep frequently-updated documents small.

documentsperformanceschema-design
Document shape and performance

createCollection(name, { capped, size, validator, timeseries })

Capped and time-series storage modes are chosen at creation; validators and indexes can be added at any time.

collectionsvalidatorscapped-collections
Collection-level concerns

one collection, distinguished by a "type"/"method" field

Shape a collection around what is read/written together, not the application's class hierarchy.

collectionsschema-designdata-modeling
Collections as business concepts

CRUD Operations

8

insertOne/Many · find/findOne · updateOne/Many · replaceOne · deleteOne/Many

The nine CRUD methods — "One" stops at the first match, the plural/cursor form covers every match.

db.orders.updateMany({ status: "pending" }, { $set: { status: "shipped" } })

updateOne(filter, update, { upsert: true })

Updates the matching document, or inserts one from filter + update if nothing matches.

updateOne({ _id: "orders" }, { $inc: { seq: 1 } }, { upsert: true })

crudupsert
Upserts

$set · $unset · $inc · $mul · $min · $max · $rename · $currentDate

Field update operators — atomic, in-place edits without reading the document first.

updateOne({ _id }, { $inc: { count: 1 }, $currentDate: { updatedAt: true } })

crudupdate-operators
Field update operators

$push · $addToSet · $pop · $pull · $pullAll · $each · $slice · $sort

Add with $push/$addToSet, remove with $pop/$pull/$pullAll; $each/$slice/$sort are modifiers inside $push.

crudarraysupdate-operators
Array update operators

"field.$" · "field.$[]" · "field.$[id]" + arrayFilters

$ updates the query-matched element; $[] updates all elements; $[<id>] updates elements matching arrayFilters.

crudarraysupdate-operators
Positional update patterns

db.<collection>.find(filter, { field: 1 }) // or { field: 0 }

Inclusion and exclusion projections cannot normally mix — { _id: 0 } is the one exception.

Query Language

8

{ field: value } · { field: { $gte, $lt } } · { arrayField: scalar }

Equality is a bare value; range needs an operator; a scalar against an array field matches any element.

db.products.find({ price: { $gte: 10, $lt: 50 } })

$eq · $ne · $gt · $gte · $lt · $lte · $in · $nin

The eight comparison operators — $in/$nin take an array of candidate values.

db.orders.find({ status: { $in: ["pending", "shipped"] } })

queryoperatorscomparison
Comparison operators

$and · $or · $nor · $not

{ a, b } is implicit $and. $or/$nor take a condition array; $not inverts one wrapped operator.

db.orders.find({ $or: [ { status: "pending" }, { total: { $gt: 500 } } ] })

$exists · $type

$exists tests field presence (not value); $type tests the BSON type of a present field.

db.products.find({ discount: { $exists: false } })

$all · $elemMatch · $size

$all: contains all values. $elemMatch: one element meets every condition. $size: exact array length.

db.students.find({ results: { $elemMatch: { subject: "math", score: { $gte: 90 } } } })

$regex · $mod · $expr · $text · $where

Evaluation operators run logic, not plain comparison — only an anchored $regex or $text can use an index.

db.users.find({ email: { $regex: "^alice", $options: "i" } })

"parent.child" · "array.N" · "array.field"

A quoted dotted path reaches an embedded field or array element — not real nested-object syntax.

db.users.find({ "address.city": "Austin" })

querydot-notationnested
Dot notation for nested fields

{ a: 1, b: 1, c: 1 } → prefixes: {a}, {a,b}, {a,b,c}

A compound index only serves queries matching one of its leading prefixes, in definition order.

db.inventory.find({ item: "widget", location: "east" }) // uses the { item, location } prefix

Data Modeling

9

workload → access patterns → schema shape

MongoDB schema design starts from the application's read/write patterns, not from normalized entities.

Data accessed together should be stored together.

{ query, frequency, latency } list → schema

Design input is a list of real access patterns with frequency, not just an entity-relationship diagram.

"Get order with line items" (every page load) vs "sum revenue per product" (monthly) — same entities, different schema pressure.

data-modelingschema-designaccess-patterns
Start from access patterns, not only entities

read-heavy · write-heavy · high-cardinality · time-series · hierarchical · transactional

Six recognizable workload shapes, each pointing toward known-good modeling patterns.

Order placement: write-heavy + transactional. Order history: read-heavy. Same collection, different patterns.

data-modelingworkloadperformance
Workload types to identify

data accessed together → stored together

MongoDB's core modeling principle — the reasoning behind "embed by default," applied by access pattern.

{ userId, layout, lastViewedFilters } co-located because a dashboard load always reads all three.

embed (default) vs. reference

Embed for one-read locality; reference for independent updates, wide sharing, or unbounded growth.

{ title, userId } + a separate users collection, when the user is shared across many movies.

data-modelingembeddingreferencing
Deciding what to embed and what to reference

embedding → locality + atomic updates + simple reads, costs document growth

Embedding's three benefits all come from one-document locality; its one cost is that document's size.

db.orders.updateOne({ _id, "items.sku": "X1" }, { $inc: { "items.$.qty": 1 } }) — atomic, but scales with document size.

data-modelingembeddingtradeoffs
Embedding trade-offs

referencing → independent growth + reuse + small documents, costs an extra query/$lookup

Referencing trades locality (embedding's benefit) for a per-read join cost.

$lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" }

data-modelingreferencingtradeoffs
Referencing trade-offs

bounded (safe to embed) vs. unbounded (reference instead)

Bounded means an enforced cap on array size; unbounded means nothing stops it from growing.

$push: { recentLogins: { $each: [event], $slice: -5 } } — actively enforces a bound.

data-modelingarraysembedding
Bounded vs. unbounded arrays

16 MiB document limit

An unbounded embedded array degrades gradually, then fails writes outright at the 16 MiB document limit.

WriteError: BSONObj size ... Size must be between 0 and 16793600 (16MB)

Embedding vs. Referencing

5

embed: read-together + bounded + owned · reference: independent + shared + own lifecycle

Two three-signal checklists for the embed/reference decision, applied per relationship.

Order line items embed (bounded, owned); the customer record they belong to references (shared, own lifecycle).

data-modelingembeddingreferencing
Embed-when and reference-when signals

1:1/1:few → embed · 1:many/N:N → reference · trees → parent ref or path

Cardinality narrows the strategy; boundedness (not the cardinality label) makes the final call.

db.orders.find({ customerId: 1 }) — reference on the "many" side, indexed lookup instead of a growing array.

data-modelingcardinalityreferencing
Modeling strategies by relationship cardinality

subset · bucket · extended reference · computed · attribute · outlier

Six named schema design patterns, each a named answer to one recurring modeling symptom.

Extended reference: copy { name } onto an order at write time, to skip a $lookup on every order-list read.

denormalize on purpose: copy a rarely-changing field to remove a join

MongoDB denormalization is a deliberate, bounded trade — not the relational anti-pattern of the same name.

{ _id: 101, customerId: 1, customerName: "Joel M" } — a copied display name avoids a $lookup on the order-list read.

MongoDB schema = access-pattern decision, not a normal-form decision

A schema is justified by how cheaply it answers real queries — the same entities can validly model differently for different access patterns.

Comments embedded when read with their post; referenced and indexed by postId when queried independently.

data-modelingperformanceaccess-patterns
Schema design as a performance decision

Schema Design Patterns

4

subset · bucket · extended reference · computed · attribute · outlier

Six named patterns, each a targeted trade against one specific, measured schema symptom.

Bucket: group readings by sensor + hour into one document, instead of one document per reading.

bucket width ≈ dominant query range width

Wider buckets cost more per write; narrower buckets cost more per wide query — match width to the real query pattern.

Hourly buckets for an hourly dashboard load exactly the needed document; daily buckets would over-fetch.

events/history → own collection, indexed by { parentId, at }

Event and history data is unbounded by nature — a separate collection keeps the parent document small.

db.orderEvents.find({ orderId: 101 }).sort({ at: 1 }) — full history on demand, order document stays small.

precompute + $inc on write + periodic reconciliation

A read-heavy count/summary, maintained incrementally instead of recalculated, with a reconciliation job to catch drift.

db.posts.updateOne({ _id: 1 }, { $inc: { likeCount: 1 } }) instead of counting likes on every read.

data-modelingaggregationperformance
Precomputing counters and summaries

Schema Validation

6

db.createCollection(name, { validator: { $jsonSchema: {...} } })

Attaches structural rules to a collection — enforced on insert, and on update per validationLevel.

db.runCommand({ collMod: "orders", validator: {...}, validationLevel: "moderate" })

{ bsonType, required: [...], properties: { field: {...} } }

The $jsonSchema vocabulary for a MongoDB validator — nests for embedded documents and arrays.

{ status: { enum: ["pending", "shipped"] }, total: { bsonType: "decimal", minimum: 0 } }

schemavalidationjsonschema
JSON Schema validation concepts

validate fields with a real downstream dependency on their shape

Pick validation targets by asking what breaks if a field is missing or the wrong type — not every field needs a rule.

Validate "total" (feeds a $sum aggregation); leave "notes" (display-only) unconstrained.

validationLevel: strict|moderate|off · validationAction: error|warn

Level picks which writes are checked; action picks what happens on failure. moderate+warn is the safe rollout combination.

{ collMod: "orders", validator: {...}, validationLevel: "moderate", validationAction: "warn" }

validation strength is a per-collection decision

Strict for costly-to-get-wrong, stable collections; loose for fast-evolving ones — not one project-wide policy.

Strict validator on "payments" from day one; no validator yet on a still-iterating "featureEvents" collection.

app validation covers one path; a collection validator covers every write path

A collection validator is the backstop for write paths — scripts, other services, bugs — that skip application-side checks.

A migration script inserting a malformed order is rejected by the collection validator even though it never called the API.

Indexing Fundamentals

9

no index → COLLSCAN · index → IXSCAN

An index is a sorted structure that lets a query jump to matches instead of scanning every document.

db.users.find({ email: "a@x.com" }).explain("executionStats") — check the stage name.

indexesperformance
Why indexes exist

db.collection.createIndex({ field: 1 | -1 })

An index on one field — serves equality, range, and sort queries on that field, in either direction.

db.products.createIndex({ price: 1 }) serves both find({ price: 29.99 }) and sort({ price: -1 }).

db.collection.createIndex({ field1: 1, field2: 1, ... })

An index over several fields in a fixed order — serves queries matching its leading field(s), in that order.

db.orders.createIndex({ customerId: 1, createdAt: -1 }) serves "this customer, newest first."

indexescompound-index
Compound indexes

index on an array field → automatically multikey

One index entry per array element; a query matches if any element matches.

db.products.createIndex({ tags: 1 }) matches find({ tags: "sale" }) via one element among several.

indexesarraysmultikey
Multikey indexes

db.collection.createIndex({ field: 1 }, { unique: true })

Rejects a second document with the same field value — a missing field counts as null, and only one null is allowed.

A second user with no email address fails with E11000 unless the index is also sparse.

indexesunique
Unique indexes

{ sparse: true } · { partialFilterExpression: {...} }

Both shrink an index by excluding some documents — partial is the more general, recommended tool.

db.orders.createIndex({ customerId: 1 }, { partialFilterExpression: { status: "pending" } })

indexessparsepartial
Sparse and partial indexes

createIndex({ dateField: 1 }, { expireAfterSeconds: N })

Auto-deletes documents N seconds after a Date field, via a background sweep every ~60 seconds — not instant.

db.sessions.createIndex({ lastActiveAt: 1 }, { expireAfterSeconds: 1800 }) — 30-minute session expiry.

indexesttl
TTL indexes

createIndex({ field: "text" }) · find({ $text: { $search: "..." } })

Word-presence search with stemming — one per collection, no phrase/proximity data, no covered queries.

db.clothing.find({ $text: { $search: "silk" } }) matches a stemmed word anywhere in the indexed field.

compound index order: equality → sort → range (ESR)

Field role, not just field presence, determines whether an index can serve a query's sort without an extra in-memory step.

db.orders.createIndex({ status: 1, createdAt: -1, total: 1 }) serves filter + sort + range in one pass.

Compound Index Design

7

E (equality) → S (sort) → R (range)

A repeatable procedure for compound index field order: classify each field's role in the target query, then order by role.

find({ status, total: { $gte } }).sort({ createdAt }) → createIndex({ status: 1, createdAt: -1, total: 1 })

indexescompound-indexesr
ESR: equality, sort, range

real query shape → ESR classify → build → verify with explain()

Index design starts from captured, real query patterns — a field with no real query use has no index-design reason to exist.

A slow-query log entry becomes the exact createIndex call, verified against it with explain() afterward.

{ a: 1, b: 1 } ≠ { b: 1, a: 1 }

Reordering a compound index's fields builds a genuinely different index, serving a different set of queries efficiently.

customerId-first serves customerId queries well; status-first serves status queries well — not both from one index.

prefixes of { a, b, c }: { a } · { a, b } · { a, b, c }

Only leading field subsets are efficient prefixes — a non-leading field alone generally cannot use the index.

find({ location, stock }) on createIndex({ item, location, stock }) cannot use the index — item is missing.

indexescompound-indexprefix-rule
Compound index prefix behavior

more indexes → more storage + more per-write update cost

An index speeds reads at the cost of extra storage and a write-time update on every insert/update touching its field(s).

Five single-field indexes mean six index updates (plus _id) on one insert populating all five fields.

indexes + hot data compete for the same working set (RAM)

Too many or unused indexes push the working set past available RAM, forcing disk access on reads too.

A collection accumulating one index per feature over a year ends up with 11 index updates per write.

together → compound index · independent → separate indexes

The choice follows from whether fields are actually queried together or independently — not from wanting fewer or more indexes in the abstract.

{ customerId, status } for storefront queries, plus a separate { trackingNumber } index for independent lookups.

Multikey and Array Indexes

5

multikey status is detected from data, not declared

The first array-valued document in the indexed field flips an index to multikey — MongoDB tracks this automatically.

Two scalar tags inserts, then one array-valued insert — the index becomes multikey on the third insert.

hashed index: no arrays · sort on multikey: often falls back to in-memory

Two consequences of the same "an array has no single position" fact — verify sort behavior with explain(), don't assume.

find({ category }).sort({ tags: 1 }) on a multikey tags index may still show an in-memory SORT stage.

compound index: at most one array field per document

Two fields both holding arrays on the same document in one compound index fails the write outright.

{ genres: ["Drama"], year: [2020, 2021] } fails on a { genres: 1, year: 1 } compound index — both are arrays.

indexesmultikeycompound-index
The compound multikey restriction

multikey index size ≈ Σ(array length) across the collection

Index entry count scales with total array elements, not document count — large or unbounded arrays make for disproportionately large indexes.

100,000 documents with 200-element arrays produce ~20M index entries — the same document count as a 3-element-array collection, 65x the index size.

bound the array ($slice, or reference out) before indexing it

An unbounded indexed array compounds document-growth and index-growth costs together — cap it at the source.

$push: { recentSearches: { $each: [term], $slice: -20 } } keeps both the document and its multikey index bounded.

Partial, Sparse, Unique, and TTL Indexes

4

supported: equality, $exists, comparison, $type, $and, $or, $in, geospatial

partialFilterExpression supports a real but not unlimited operator subset — measure the actual size reduction, don't assume it.

db.orders.countDocuments({ status: { $in: [...] } }) vs. countDocuments({}) gives the real reduction percentage.

sparse excludes absence, not null

A field explicitly set to null is still "present" for sparse purposes — only true field absence is excluded.

{ phone: null } is included in a sparse index on phone; only an entirely omitted phone field is excluded.

unique: true, sparse: true — optional field, unique when present

Combining sparse with unique excludes missing-field documents from the uniqueness check entirely.

db.users.createIndex({ email: 1 }, { unique: true, sparse: true }) — two users with no email, no collision.

TTL expiry time ≠ TTL deletion time

A document can be logically expired but still physically present until the next background sweep — check timestamps explicitly for exact-time logic.

findOne({ _id: sessionId, lastActiveAt: { $gte: cutoffTime } }) instead of trusting the document's mere presence.

Query Planning

7

candidate plans → trial → winning plan → cached

The query planner discovers the best plan empirically via a short trial, then caches it for queries of the same shape.

explain() always evaluates fresh, bypassing the plan cache — the direct way to confirm the current real plan.

query-planningperformance
What the query planner does

COLLSCAN (no index) vs. IXSCAN (walked an index)

The explain() stage names for the scan-vs-index distinction — usually nested (FETCH over IXSCAN), check the whole tree.

find({ email }).explain().queryPlanner.winningPlan.stage — "COLLSCAN" before an index exists, "FETCH" (over IXSCAN) after.

query-planningexplain
COLLSCAN vs. IXSCAN

FETCH: retrieve documents · SORT: order in memory

Stages nest bottom-up — a filter can use IXSCAN while the query as a whole still needs an expensive SORT stage on top.

A { status: 1 }-indexed query sorted by an unindexed field shows SORT → FETCH → IXSCAN in its stage tree.

explain("allPlansExecution") shows candidates + winner

One candidate plan per usable index; the trial winner becomes the cached plan — rejectedPlans shows what else was tried.

db.orders.find({...}).explain("allPlansExecution").queryPlanner.rejectedPlans

explain() · explain("executionStats") · explain("allPlansExecution")

Three verbosity levels trading off how much the query actually executes — always bypasses the plan cache.

db.orders.find({ status: "pending" }).explain("executionStats") for real nReturned/totalDocsExamined numbers.

rejectedPlans — compare stats to the winner before assuming a problem

Only visible via allPlansExecution — most rejections are the trial correctly measuring more work for the same result.

A rejected { status }-only index examined 4,500 keys vs. the winner's 120 for the same 118 results — a correct rejection.

IXSCAN ≠ well-aligned — check totalKeysExamined ÷ nReturned

A ratio far from 1 means the index technically applies but does not match the query's real filter/sort needs.

40,000 keys examined for 60 results (ratio ≈ 667) despite a genuine IXSCAN — the index is misaligned to this query.

explain() and Performance Analysis

5

queryPlanner (free) → executionStats (real numbers) → allPlansExecution (all candidates)

Match verbosity to the actual question — allPlansExecution costs the most and is only needed for comparative questions.

"Why did index A lose to B?" needs allPlansExecution; "is this using an index?" needs only queryPlanner.

nReturned vs. totalKeysExamined vs. totalDocsExamined

Ratio near 1 means well-aligned; a large ratio means the index is not narrowing enough for this specific query.

{ nReturned: 118, totalKeysExamined: 120 } — well-aligned. { nReturned: 60, totalKeysExamined: 40000 } — poorly aligned.

small nReturned + large docsExamined/keysExamined = worth investigating

This pattern is visible directly in slow-query logs and the profiler, without running explain() first — a triage signal.

{ nreturned: 12, keysExamined: 85000 } in a log line flags a query worth confirming with explain().

capture before → change → capture after → compare

Confirm an index change worked with a real before/after explain() comparison — not a subjective impression.

A SORT stage disappearing from the stage tree after adding a compound index is direct, structural confirmation.

db.setProfilingLevel(1, { slowms: 100 }) → db.system.profile

The profiler logs real production operations exceeding a threshold — the evidence a local benchmark cannot reproduce.

db.system.profile.find({ millis: { $gt: 200 } }).sort({ ts: -1 }).limit(20) surfaces the worst recent real offenders.

Aggregation Framework

6

db.collection.aggregate([ { $stage: {...} }, ... ])

A sequence of stages, each transforming the previous stage's output — MongoDB's tool for grouping, reshaping, and computing.

aggregate([{ $match: {...} }, { $group: { _id: "$customerId", total: { $sum: "$amount" } } }])

$match · $project · $set · $group · $sort · $limit · $facet · $count

The core stage vocabulary — each stage has one job, and pipelines compose them like a sentence describing the task.

$facet runs multiple sub-pipelines (e.g. grouped by status, grouped by region) against the same input in one call.

filter early, reshape only what's needed, $sort before $limit

Two logically identical pipelines can have very different real cost based purely on stage order.

$match before $group filters to relevant documents first; $group before $match wastes work grouping everything.

aggregationperformance
Pipeline ordering principles

leading $match → can use an index, exactly like find()

A $match placed before any reshaping stage sees the real collection documents and can use an index — verify with explain().

aggregate([{ $match: { status: "pending" } }, ...]).explain() shows the same IXSCAN a find() call would.

aggregationperformanceindexes
Pushing $match as early as practical

100MB per in-memory stage, default — auto-spills to disk (6.0+)

Stages that must hold state (mainly $group, $sort, $bucket family) are capped per-stage — spilling avoids errors but is slower.

db.events.aggregate([...], { allowDiskUse: true }) — explicit override, though 6.0+ already defaults to allowing it.

summary-shaped result → aggregate, not fetch-and-loop

A pipeline computes a summary where the data lives, transferring only the final result — a loop transfers everything first.

aggregate([{ $match }, { $group: { _id: null, total: { $sum: "$amount" } } }]) instead of find().toArray().reduce().

Aggregation Expressions

6

{ $operator: <expr> | [<expr>, ...] }

Expression operators compute a value from document fields inside a stage — grouped by arithmetic, comparison, boolean, string, date, array, conditional, object, and conversion.

{ $set: { total: { $multiply: ["$price", "$qty"] } } }

$ifNull([expr, fallback]) · $cond({if,then,else}) · $switch({branches,default})

Three ways to branch inside an expression, from simplest (one fallback) to most general (ordered multi-way branch).

{ $switch: { branches: [{ case: {...}, then: "a" }], default: "b" } }

aggregationexpressions
$cond, $ifNull, $switch

$map({input,as,in}) · $filter({input,as,cond}) · $reduce({input,initialValue,in})

Transform, keep, or fold an array field in place, without exploding the document with $unwind.

{ $reduce: { input: "$items", initialValue: 0, in: { $add: ["$$value", "$$this.price"] } } }

aggregationexpressionsarrays
$map, $filter, $reduce

$dateToString({date,format,timezone}) · $dateTrunc({date,unit,timezone})

Round a timestamp down to the bucket you want to group by — day, hour, week — respecting timezone.

{ $group: { _id: { $dateToString: { date: "$at", format: "%Y-%m", timezone: "UTC" } } } }

aggregationexpressionsdates
Date expressions for grouping

$convert: { input, to, onError?, onNull? }

Converts a value's BSON type, with explicit fallback values on failure instead of aborting the pipeline.

{ $toInt: { $ifNull: ["$qty", "0"] } }

aggregationexpressionstypes
Type conversion expressions

index bounds document count, not per-document expression cost

A well-indexed $match still hands a large or expression-heavy $group/$project stage real CPU work — that cost does not show up as a scan-to-return ratio.

$unwind and Array Processing

4

{ $unwind: "$field" }

Explodes an array field into one document per element — a document count multiplier, usually followed by $match/$group on the now-scalar field.

{ $unwind: "$tags" }, { $group: { _id: "$tags", n: { $sum: 1 } } }

{ $unwind: { path, preserveNullAndEmptyArrays, includeArrayIndex } }

Object form of $unwind: keep documents with an empty/missing array (preserveNullAndEmptyArrays), and/or record each element's original index (includeArrayIndex).

{ $unwind: { path: "$tags", preserveNullAndEmptyArrays: true } }

output count = Σ array length across surviving documents

$unwind's multiplier applies to every stage that runs after it — filter first, or avoid $unwind when a scalar result is all that is needed.

aggregationarraysperformance
The document-explosion trade-off

same doc, narrowed array → $filter/$map · one doc per element → $unwind

Reach for $filter/$map when the output should still be one document per input — $unwind is for when the result genuinely needs one document per array element.

$lookup and Join-Like Operations

5

{ $lookup: { from, localField, foreignField, as } }

Joins documents from another collection into an array field — always a left outer join, always an array output even for a single match.

{ $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } }

aggregationlookupjoins
$lookup for collection joins

{ $lookup: { from, let, pipeline, as } }

The general form of $lookup — let exposes local fields as $$vars for a nested pipeline to match on anything, not just equality.

{ $lookup: { from: "shipments", let: { id: "$_id" }, pipeline: [{ $match: { $expr: { $eq: ["$orderId", "$$id"] } } }], as: "shipment" } }

index foreignField before joining against it

A $lookup without an index on the foreign collection's matched field degrades toward a repeated collection scan.

aggregationlookupperformance
$lookup performance implications

hot path → denormalize · occasional report → $lookup

Choose based on read frequency and criticality, not on which approach is technically possible — both are valid schema tools for different access patterns.

aggregationlookupdata-modeling
$lookup vs. denormalized design

low cardinality → small joined array · high cardinality → cap with $limit inside the pipeline form

The foreignField index makes matching fast; cardinality determines how large the joined array ends up, which is a separate cost.

aggregationlookupindexes
Join field cardinality

Window Functions and Advanced Analytics

3

{ $setWindowFields: { partitionBy, sortBy, output } }

Computes a value across a window of related documents while keeping every original document — unlike $group, which collapses them.

{ output: { rank: { $rank: {} } } }

aggregationwindow-functions
$setWindowFields fundamentals

$rank/$denseRank (order only) · $sum/$avg + window (bounded accumulator)

Rank operators use sortBy alone; accumulator operators need an explicit window to bound how many neighboring documents they include.

{ output: { rank: { $rank: {} }, runningTotal: { $sum: "$amt", window: { documents: ["unbounded", "current"] } } } }

known-shape report on live data → aggregation · ad hoc / huge / heavy joins → warehouse

Aggregation fits reporting colocated with operational data; a dedicated analytics system fits exploratory, large-scale, or resource-isolated workloads.

aggregationanalyticsarchitecture
Aggregation vs. a dedicated analytics system

Transactions

8

one updateOne() call = atomic, regardless of how many fields it changes

Single-document writes are always all-or-nothing — combine related field changes into one call to get this guarantee for them.

transactionsatomicity
Single-document atomicity

session.startTransaction() → writes with { session } → commitTransaction()/abortTransaction()

Extends all-or-nothing atomicity across multiple documents and collections — real cost, use when embedding cannot avoid the cross-document need.

await session.withTransaction(async () => { ...writes with { session }... })

transactionsatomicity
Multi-document transactions

startSession() → startTransaction() → { session } writes → commitTransaction()/abortTransaction() → endSession()

The full transaction lifecycle — a session is the container, every operation inside must explicitly carry it.

transaction cost ∝ how long it stays open, not just data touched

Locks and snapshot resources are held for the whole open duration — keep slow work (network calls, external APIs) outside the boundary.

startTransaction({ readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } })

Set once for the whole transaction — snapshot reads see a consistent point in time, majority write concern makes the commit durable.

transactionsread-concernwrite-concern
Read/write concern in transactions

TransientTransactionError → retry whole transaction · UnknownTransactionCommitResult → retry commit only

Check err.hasErrorLabel(...) to distinguish an expected, retryable failure from a real error — withTransaction handles both correctly by default.

could this be one document? no → transaction · yes → embed instead

A transaction is justified for genuinely separate entities with independent lifecycles — not for data split into multiple documents out of habit.

Atomicity and Concurrency

6

one write call = atomic · read + decide + write = not atomic as a unit

Single-document atomicity covers one call, not a sequence of a read followed by a separate write — that gap is what the rest of this section addresses.

new value = f(current value) → use $inc/$push/$max/… directly, not findOne + updateOne

An atomic operator is one round trip with no race window — a read-then-write is two operations that can race under concurrency.

updateOne({ _id, stock: { $gte: n } }, { $inc: { stock: -n } })

updateOne({ _id, expected }, { $set: {...} }) → matchedCount === 0 means retry

Put the expected current value in the filter so the check-and-write happens as one atomic step — retry with fresh data on a failed match.

concurrencyoptimistic-locking
Optimistic concurrency

updateOne({ _id, version: v }, { $set: {...}, $inc: { version: 1 } })

A version field checked-and-incremented in one atomic call — catches any concurrent write, not just ones touching a specific business field.

concurrencyoptimistic-locking
Version fields and compare-and-set

read → [gap] → write, based on the read → stale-read race grows with the gap and concurrent traffic

Fix with a conditional write (optimistic concurrency) or an atomic operator — never by assuming the gap stays small enough not to matter.

concurrencyrace-conditions
Race conditions from stale reads

createIndex({ field: 1 }, { unique: true }) → handle code 11000 as expected

A unique index enforces an invariant atomically at the database level — an application-side check-then-insert always has a race window the index does not.

Read Concern and Write Concern

5

{ w: 1 | "majority" | n, j: true|false, wtimeout: ms }

w controls how many members must confirm a write before it is acknowledged — stronger means safer against failover, at the cost of latency.

{ writeConcern: { w: "majority", j: true } }

{ w, j, wtimeout }

w = how many copies, j = how durable each one, wtimeout = how long to wait — independent settings, not one combined knob.

{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }

local (fast, can roll back) < majority (durable) < linearizable (durable + latest)

readConcern controls whether returned data could still be rolled back — stronger levels trade latency for that guarantee.

{ readConcern: { level: "majority" } }

read-concernconsistency
readConcern levels

stronger concern = more latency + lower availability under partition, in exchange for a stronger guarantee

Match concern strength to the real cost of the weaker guarantee being wrong — not a single project-wide default.

read-concernwrite-concernavailability
Choosing concern strength

consistency ↔ latency ↔ availability — pick two, deliberately, per operation

Every stronger read/write concern is a real trade against latency and, under a partition, availability — never a free upgrade.

read-concernwrite-concerndistributed-systems
Consistency is a trade-off, not a free feature

Sessions

3

client.startSession() → operations with { session } → session.endSession()

A server-tracked logical identity for related operations, decoupled from any one connection — the basis for causal consistency, retryable writes, and transactions.

transaction → scoped to a session · retryable write → session ID + txnNumber identifies a retry

Both features rely on a session's identity — one to hold in-progress state, the other to let the server recognize a retried write safely.

idle session timeout: 30 min default · implicit session per operation without one

Most code benefits from an implicit, driver-managed session automatically — use explicit sessions specifically for transactions or causal consistency.

Replication

8

rs.initiate({ _id, members: [...] })

Several servers holding the same data — one primary accepts writes, secondaries replicate, and the group elects a new primary automatically on failure.

replicationhigh-availability
Replica sets

primary: accepts writes (1 at a time) · secondary: replicates, optionally serves reads

A role assigned to exactly one member at a time — writes always go to whichever member currently holds it.

local.oplog.rs — capped, idempotent, tailed continuously by secondaries

The ordered record of every write a secondary replays to stay in sync — its fixed size bounds how long a disconnect can last before a full resync is needed.

majority vote → most up-to-date candidate becomes primary → typically seconds

A deliberate or crash-triggered election picks a new primary automatically — writes are unavailable for a brief window while it completes.

replicationhigh-availability
Elections and failover

.readPref("primary" | "secondary" | ...)

Controls which member(s) a read can reach — a separate setting from write concern and read concern.

replicationread-preference
What read preference controls

primary · primaryPreferred · secondary · secondaryPreferred · nearest

The five read preference modes — "Preferred" always means try-then-fallback, never only-if-convenient.

replicationread-preference
The five read preference modes

lag = primary's latest oplog time − secondary's latest applied oplog time

Normally milliseconds, grows under load or network issues — directly bounds how stale a secondary-routed read can be.

replicationmonitoring
Replica lag

secondary read can be stale by up to current replica lag

A secondary-routed read can miss a recent write — use readPref("primary") or a shared session (causal consistency) when read-your-own-writes matters.

Replica Set Architecture

5

majority = floor(N / 2) + 1

The same threshold governs write concern, read concern, and elections — guaranteeing any two majorities overlap by at least one member.

votes: 0 → replicates and can read, cannot vote or become primary

Voting rights and holding a data copy are separate — majority is computed against voting members only, capped at 7.

majority side → elects, keeps writing · no-majority side(s) → read-only, no primary

A partition can produce at most one side with a majority, which is exactly why split-brain (two primaries) cannot happen under normal majority-based elections.

concern setting + topology (voter count, placement) = actual real-world guarantee

The same write concern or read preference setting protects against different failures depending on the current topology — re-audit after any topology change.

replica-setwrite-concernread-preference
How concern settings interact with topology

hidden · secondaryDelaySecs · priority · votes

Configuration dials on a normal secondary — combine hidden + delayed + non-voting for a recovery-window safety net.

{ hidden: true, secondaryDelaySecs: 3600, priority: 0, votes: 0 }