DS DevShelfHub Projects · AI tools
Cheatsheets / MongoDB
Cheatsheet · Dev tooling

MongoDB: CRUD, Aggregation and Indexes Reference Guide

By DevShelfHub

mongosh, CRUD, query/update operators, aggregation pipeline, indexes, transactions, schema design, replica sets — the day-to-day Mongo surface.

119 items 9 min CRUD Aggregation Indexes

Start hereQuick start · 6 you’ll reach for daily

Find onedb.t.findOne({"{ _id: id }"})
Insert manydb.t.insertMany([...])
Updatedb.t.updateOne(q, {"{ $set: {...} }"})
UpsertupdateOne(q, u, {"{ upsert: true }"})
Aggregatedb.t.aggregate([{"{$match}"}, {"{$group}"}])
Explaindb.t.find(q).explain("executionStats")

Target versions · paceVersions

Targets: mongodb ≥ 7.0 mongosh ≥ 2.0 pymongo ≥ 4 motor ≥ 3

The legacy mongo shell is gone — use mongosh. Sharded transactions are stable (4.2+), time-series collections landed in 5, queryable encryption + clustered collections in 6, JSON Schema validation has been around since 3.6. License changed to SSPL in 2018 — community forks (FerretDB, Percona) exist if you need OSS-pure. This sheet targets MongoDB 7.

Install · connectSetup

bash
# macOS — Homebrew tap
brew tap mongodb/brew
brew install mongodb-community@7.0
brew services start mongodb/brew/mongodb-community

# Linux — official repo (Debian/Ubuntu)
# See https://www.mongodb.com/docs/manual/installation/ for the apt key block
sudo systemctl enable --now mongod

# Docker — disposable dev instance
docker run --name mongo -p 27017:27017 -d mongo:7

# Modern shell — mongosh (the legacy "mongo" binary is gone in 6+)
mongosh                                              # default localhost
mongosh "mongodb://user:pw@host:27017/shop"
mongosh "mongodb+srv://cluster.example/shop?retryWrites=true&w=majority"

# Python driver
pip install "pymongo>=4" "motor>=3"   # sync + async

# Ping
mongosh --eval 'db.runCommand({ ping: 1 })'

The shellmongosh essentials

use shopSwitch database. Creates it on first write.
show dbs / show collectionsInventory.
db.getCollection("user-events").find()Quote collection names with hyphens.
db.stats() / db.t.stats()Sizes, counts, index footprint.
db.t.countDocuments(q)Accurate count. Preferred over deprecated .count().
db.t.estimatedDocumentCount()Fast metadata count. Use for "roughly N".
db.t.drop() / db.dropDatabase()Destructive. Wrap in a script with confirmation in prod.
itIterate the last cursor.
load("script.js")Run a JS file.
db.adminCommand({"{ currentOp: 1 }"})In-flight operations — find the runaway query.

Beyond JSONBSON types

ObjectId()12-byte default _id. Time-ordered — cheap for sort-by-creation.
UUID()128-bit. Use uuidRepresentation="standard" in pymongo.
ISODate("2026-05-18T00:00:00Z") / new Date()UTC datetime — 8 bytes.
NumberDecimal("9.99")128-bit decimal. Preferred for money over double.
NumberLong("9007199254740993")64-bit integer. JS native numbers are 53-bit.
NumberInt(7)Force 32-bit int (default in JS is double).
BinData(0, base64)Binary blob with subtype.
DBRef(coll, id, db)Legacy — resolve manually with $lookup instead.
null vs $exists: falseStored null vs missing field — very different queries.

insert · find · update · deleteCRUD

Insert

db.t.insertOne({"{ _id, ... }"})Returns { insertedId }. Generates _id if omitted.
db.t.insertMany([d1, d2], {"{ ordered: false }"})Unordered keeps going after a failure — faster bulk loads.
db.t.bulkWrite([{"{ insertOne }"}, {"{ updateOne }"}])Mix ops in one round-trip.

Read

db.t.findOne(q, projection)One doc.
db.t.find(q, {"{ field: 1, _id: 0 }"})Projection — include vs exclude. Can’t mix except for _id.
.sort({"{ ts: -1 }"}).skip(0).limit(20)Cursor chain. Index the sort key.
.hint({"{ idx: 1 }"})Force an index. Debugging tool.
.collation({"{ locale: 'en', strength: 2 }"})Case-insensitive sort / match.
db.t.distinct("field", q)Unique values.

Update

db.t.updateOne(q, {"{ $set: { name: 'Ana' } }"})First match only.
db.t.updateMany(q, u)All matches.
db.t.replaceOne(q, full_doc)Replace whole document (keeps _id).
db.t.updateOne(q, u, {"{ upsert: true }"})Insert if missing.
db.t.findOneAndUpdate(q, u, {"{ returnDocument: 'after' }"})Atomic read-modify-write.
db.t.updateOne(q, [{"{ $set: ... }"}])Pipeline-style update — can reference other fields (4.2+).

Delete

db.t.deleteOne(q) / db.t.deleteMany(q)Targeted delete.
db.t.findOneAndDelete(q)Returns the deleted doc.

$eq · $gt · $in · $and · ...Query operators

Comparison & logic

{"{ price: { $gt: 10, $lte: 100 } }"}Range.
{"{ status: { $in: ['a', 'b'] } }"}Set membership. $nin for "not in".
{"{ $and: [c1, c2] }"}Explicit AND. Implicit when fields differ.
{"{ $or: [c1, c2] }"}OR. Each branch ideally hits an index.
{"{ name: { $regex: '^A', $options: 'i' } }"}Regex. Anchored prefix uses an index.
{"{ tags: { $all: ['red', 'sale'] } }"}Array must contain all.
{"{ tags: { $size: 3 } }"}Exact array length.
{"{ field: { $type: 'string' } }"}BSON type filter.
{"{ field: { $exists: true } }"}Field present (even if null).
{"{ items: { $elemMatch: { sku: 'a', qty: { $gt: 0 } } } }"}All conditions on the SAME array element.
{"{ $expr: { $gt: ['$a', '$b'] } }"}Compare fields to each other.

$set · $inc · $push · $pullUpdate operators

{"{ $set: { name: 'Ana', 'addr.city': 'NYC' } }"}Replace fields. Dotted paths reach nested docs.
{"{ $unset: { tmp: '' } }"}Remove fields.
{"{ $inc: { loginCount: 1 } }"}Atomic counter.
{"{ $mul: { price: 1.1 } }"}Multiply.
{"{ $min: { lowScore: 50 } }"}Update only if new is smaller. $max for the opposite.
{"{ $rename: { oldName: 'newName' } }"}Rename a field.
{"{ $currentDate: { updatedAt: true } }"}Server-side timestamp — no clock skew.
{"{ $setOnInsert: { createdAt: new Date() } }"}Apply only when an upsert inserts.
{"{ $push: { tags: 'new' } }"}Append to array.
{"{ $push: { tags: { $each: ['a','b'], $slice: -10 } } }"}Bounded append (rolling activity).
{"{ $addToSet: { tags: 'red' } }"}Push only if not present.
{"{ $pull: { tags: 'red' } }"}Remove matching elements.
{"{ $pop: { tags: -1 } }"}Remove first (-1) / last (1).
arrayFilters: [{"{ 'el.qty': { $lt: 0 } }"}]Filter on positional $[el] updates.

$match · $group · $lookup · ...Aggregation pipeline

Stages

$match: {"{ field: value }"}Filter early. Push to top to use indexes.
$project: {"{ field: 1, derived: { $multiply: [...] } }"}Reshape + compute.
$addFields / $setAdd fields without dropping others.
$group: {"{ _id: '$customerId', total: { $sum: '$amount' } }"}Aggregate. _id is the group key.
$sort: {"{ ts: -1 }"}Sort. Index the key when first.
$limit: 100 / $skip: 0Pagination.
$lookup: {"{ from, localField, foreignField, as }"}Left-outer join. Pipeline form supports filters + correlation.
$unwind: "$tags"Explode array into one doc per element.
$facet: {"{ a: [...], b: [...] }"}Run multiple sub-pipelines on the same input.
$bucket / $bucketAutoHistogram by manual or auto-chosen boundaries.
$count: "n"Replace pipeline output with the count.
$merge / $outMaterialize results into a collection. $merge upserts.

Expressions

$sum, $avg, $min, $max, $countNumeric aggregators.
$first, $last, $push, $addToSetPer-group collectors.
$dateTrunc: {"{ date: '$ts', unit: 'day' }"}Round a date for time bucketing (5+).
$cond: [if, then, else]Ternary.
$switch: {"{ branches: [...], default: ... }"}Multi-branch.
$ifNull: ["$a", 0]Default for missing / null.
$concat, $toLower, $regexMatchString ops.
$setWindowFieldsWindow functions (rank, lag, moving avg) — 5+.
javascript
// Top 5 customers by spend in the last 30 days
db.orders.aggregate([
  { $match: {                                   // narrow early — uses indexes
      placedAt: { $gte: new Date(Date.now() - 30*24*3600*1000) },
      status:   "paid"
  }},
  { $group: {
      _id:   "$customerId",
      spend: { $sum: "$total" },
      n:     { $sum: 1 }
  }},
  { $sort:  { spend: -1 } },
  { $limit: 5 },
  { $lookup: {                                  // join-ish
      from: "customers",
      localField:   "_id",
      foreignField: "_id",
      as: "customer"
  }},
  { $project: {
      _id: 0,
      customerId: "$_id",
      email: { $arrayElemAt: ["$customer.email", 0] },
      spend: 1,
      n: 1
  }}
]);

// Bucketize order sizes
db.orders.aggregate([
  { $bucket: {
      groupBy: "$total",
      boundaries: [0, 50, 200, 1000, 10000],
      default: "10k+",
      output: { count: { $sum: 1 } }
  }}
]);

Single · compound · partial · text · geoIndexes

db.t.createIndex({"{ email: 1 }"}, {"{ unique: true }"})Single-field unique. 1 = asc, -1 = desc.
db.t.createIndex({"{ a: 1, b: -1 }"})Compound. Leftmost-prefix rule applies.
{"{ partialFilterExpression: { active: true } }"}Index only matching docs — smaller, cheaper.
{"{ sparse: true }"}Skip docs missing the field. Mostly superseded by partial.
{"{ expireAfterSeconds: 3600 }"}TTL index on a date field.
db.t.createIndex({"{ '$**': 1 }"})Wildcard — every field.
db.t.createIndex({"{ title: 'text', body: 'text' }"})Full-text. Only one per collection.
db.t.createIndex({"{ loc: '2dsphere' }"})Geospatial.
db.t.createIndex({"{ field: 'hashed' }"})For shard keys with uniform distribution.
db.t.getIndexes() / dropIndex(name)Inspect / remove.
db.t.hideIndex(name)Hide without dropping. Test impact safely.
javascript
// Single field, ascending
db.users.createIndex({ email: 1 }, { unique: true });

// Compound — leftmost-prefix rule. Order matters:
// can serve queries on {customerId}, {customerId, placedAt}, but not {placedAt} alone.
db.orders.createIndex({ customerId: 1, placedAt: -1 });

// Partial — index only the rows that match the filter
db.users.createIndex(
  { lastLoginAt: -1 },
  { partialFilterExpression: { active: true } }
);

// TTL — expire docs N seconds after the field
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });

// Wildcard — index every field under a path
db.events.createIndex({ "metadata.$**": 1 });

// Text — full-text search
db.posts.createIndex({ title: "text", body: "text" });
db.posts.find({ $text: { $search: "redis cache" } });

// 2dsphere — geo
db.places.createIndex({ location: "2dsphere" });

// Find queries that aren't using an index
db.orders.find({ customerId: 7 }).explain("executionStats");

// Hide an index without dropping (test impact)
db.orders.hideIndex("customerId_1_placedAt_-1");

Embed vs referenceSchema design

EmbedRead together, write together, bounded size. Comments on a blog post.
ReferenceReused across docs, unbounded list, or independently mutable. Users referenced by orders.
Bucket patternGroup small time-series points into N-per-bucket docs. Cheaper than one-doc-per-event.
Computed patternPre-aggregate on write (e.g. $inc a count) instead of recomputing on read.
Outlier patternMove rarely-huge subdocs to a side collection; keep the common case fast.
Schema versioningStore schema_v: 2 per doc — you can mix versions during migrations.
$jsonSchema validatorReject malformed inserts at the server. collMod to add.
Time-series collectionFirst-class type for metrics (5+). Auto-buckets, big disk savings.

Read the plan, fix the queryExplain & performance

db.t.find(q).explain()Query plan only.
db.t.find(q).explain("executionStats")Plan + real timings + index hits.
db.t.find(q).explain("allPlansExecution")All candidate plans considered.
winningPlan.stageIXSCAN good, COLLSCAN = table scan, fix it.
totalKeysExamined vs nReturnedHigh ratio = poor index selectivity.
db.currentOp({"{ secs_running: { $gte: 5 } }"})In-flight ops over 5s.
db.killOp(opid)Kill a runaway op.
db.serverStatus()Connection counts, opcounters, mem.
db.setProfilingLevel(1, {"{ slowms: 200 }"})Log queries slower than 200 ms.
db.system.profile.find().sort({"{ ts: -1 }"})Read the profile log.

Multi-doc ACID · sessionsTransactions

session = db.getMongo().startSession()Sessions are required for transactions.
session.startTransaction()Begin.
session.commitTransaction() / abortTransaction()Finish.
{"{ readConcern: 'snapshot', writeConcern: { w: 'majority' } }"}Default tx options. Snapshot isolation across the docs touched.
writeConcern: {"{ w: 'majority', wtimeout: 5000 }"}Wait for majority ack. Preferred for durable writes.
readPreference: 'secondaryPreferred'Route reads to a replica. Watch for staleness.
transactionLifetimeLimitSeconds (server)Server-side cap, default 60s. Keep tx short.
Transactions only work against a replica set or sharded cluster — not a standalone mongod. For a single doc, normal updates are already atomic.

Replica sets · shardingReplica sets & sharding

rs.initiate({"{ _id: 'rs0', members: [...] }"})Bootstrap a replica set.
rs.status() / rs.conf()Topology + members.
rs.add("h:27017") / rs.remove("h:27017")Membership changes.
rs.stepDown()Trigger a primary election (force failover).
sh.enableSharding("shop")Turn on sharding for a db.
sh.shardCollection("shop.orders", {"{ customerId: 'hashed' }"})Pick a shard key — you can’t easily change it later.
sh.status()Chunks per shard, balancer state.
sh.balancerCollectionStatus("shop.orders")Is the data spread evenly?
mongosync / mongodump --archiveCluster-to-cluster sync / logical backup.

Roles · SCRAM · x509Users & security

db.createUser({"{ user, pwd, roles: [{ role: 'readWrite', db: 'shop' }] }"})Create a user with role(s).
db.updateUser(user, {"{ pwd: '...' }"})Rotate password.
db.dropUser(user)Remove.
db.grantRolesToUser / revokeRolesFromUserAdjust privileges.
db.createRole({"{ role, privileges: [...], roles: [] }"})Custom role.
security.authorization: enabledmongod.conf — turn on auth. Off by default in local installs.
SCRAM-SHA-256Default auth mechanism. Preferred over SCRAM-SHA-1.
net.tls.mode: requireTLSForce TLS on the wire. Pair with a real CA.
db.runCommand({"{ rolesInfo: 1, showPrivileges: true }"})Audit roles.

Sync · async · bulk · txPython with pymongo

python
from pymongo import MongoClient, ASCENDING, ReturnDocument
from pymongo.errors import DuplicateKeyError

client = MongoClient("mongodb://localhost:27017", uuidRepresentation="standard")
db = client.shop

# Insert with retry on duplicate-key (e.g. unique email)
try:
    db.users.insert_one({"email": "a@x.com", "name": "Ana"})
except DuplicateKeyError:
    pass

# Upsert + return the new value in one round-trip
doc = db.users.find_one_and_update(
    {"email": "a@x.com"},
    {"$inc": {"loginCount": 1},
     "$setOnInsert": {"createdAt": datetime.utcnow()}},
    upsert=True,
    return_document=ReturnDocument.AFTER,
)

# Bulk write — mix of operations, one network call
from pymongo import InsertOne, UpdateOne
res = db.events.bulk_write([
    InsertOne({"type": "login", "user": 42}),
    UpdateOne({"user": 42}, {"$inc": {"hits": 1}}, upsert=True),
], ordered=False)

# Transactions — require a replica set / sharded cluster
with client.start_session() as session:
    with session.start_transaction():
        db.accounts.update_one({"_id": "A"}, {"$inc": {"bal": -50}}, session=session)
        db.accounts.update_one({"_id": "B"}, {"$inc": {"bal":  50}}, session=session)

# Async with motor
# from motor.motor_asyncio import AsyncIOMotorClient
# m = AsyncIOMotorClient(...); await m.shop.users.find_one(...)

Validator + index + aggregationEnd-to-end · Order analytics

A schema validator, a hot-path index, and one analytical aggregation — the three things every Mongo collection eventually needs.

javascript
// One-pass: validate a shop collection, add an index, run analytics.

// 1. Schema validation — reject malformed inserts
db.runCommand({
  collMod: "orders",
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["customerId", "total", "placedAt"],
      properties: {
        customerId: { bsonType: "objectId" },
        total:      { bsonType: "decimal", minimum: 0 },
        placedAt:   { bsonType: "date" },
        status:     { enum: ["pending", "paid", "refunded"] }
      }
    }
  },
  validationLevel:  "strict",
  validationAction: "error"
});

// 2. Composite index for the hot read path
db.orders.createIndex({ customerId: 1, placedAt: -1 });

// 3. Insert sample data
db.orders.insertMany([
  { customerId: ObjectId(), total: NumberDecimal("99.50"),
    placedAt: new Date(), status: "paid" },
  { customerId: ObjectId(), total: NumberDecimal("12.00"),
    placedAt: new Date(), status: "paid" }
]);

// 4. Daily revenue for the last 7 days
db.orders.aggregate([
  { $match: { placedAt: { $gte: new Date(Date.now() - 7*24*3600*1000) },
              status:   "paid" } },
  { $group: {
      _id:     { $dateTrunc: { date: "$placedAt", unit: "day" } },
      revenue: { $sum: "$total" }
  }},
  { $sort: { _id: 1 } }
]);

Best practiceGood to know

Pick the shard key on day one, not day 365. Changing it later requires a full collection rebuild. Optimize for write distribution + the dominant query — usually a compound of high-cardinality fields, possibly hashed.
Put $match and $project as early as possible in a pipeline. Mongo can push early $match through an index and skip COLLSCAN entirely. Trim fields with $project before $lookup to cut bytes-shipped.
Use NumberDecimal for money. JavaScript / BSON doubles can’t represent 0.1 + 0.2 = 0.3. NumberDecimal("9.99") stays exact and aggregates correctly.

Common trapsWatch out for

The 16 MB document limit is a hard cap. Append-only arrays (chat logs, audit trails, event streams) will eventually hit it — usually in production, late at night. Use bucket pattern or a separate collection.
Range queries on the wrong compound-index column don’t use the index. With {a:1, b:1, c:1}, a query with equality on a, range on b, equality on c can’t use the c portion — the range on b stops the prefix.
Default writeConcern isn’t always majority. On older drivers and standalones, writes can ACK before replication. Set w: 'majority' explicitly for anything you can’t afford to lose on failover.

Go deeperSee also

MongoDB FAQ

What is MongoDB used for?

MongoDB is a document-oriented NoSQL database that stores data as BSON (binary JSON) documents. It is used for content management, real-time analytics, catalogs, user profiles, and any use case where flexible schema-less documents are preferable to rigid relational tables.

What is the MongoDB aggregation pipeline?

The aggregation pipeline processes documents through a sequence of stages — $match filters rows, $group aggregates them, $sort orders results, $lookup joins collections, and $project shapes output. Each stage transforms the stream of documents and passes results to the next stage.

How does MongoDB handle transactions?

MongoDB supports multi-document ACID transactions since version 4.0. Start a session with client.startSession(), call session.startTransaction(), run your operations with the session option, then commit or abort. Transactions work across collections and, since 4.2, across shards.

What indexes does MongoDB support?

MongoDB supports single-field, compound, multikey (arrays), text, geospatial, hashed, wildcard, partial, sparse, and TTL indexes. Always run explain('executionStats') to confirm an index is used and check the IXSCAN vs COLLSCAN stage in the winning plan.

Is MongoDB free to use?

Yes. MongoDB Community Server is free to download and self-host. The source-available SSPL license allows self-hosting for most uses. MongoDB Atlas has a free M0 tier. MongoDB Enterprise Advanced is the commercial edition for on-prem deployments with advanced security.