MongoDB Cheat Sheet

This MongoDB cheat sheet covers 30 sections: mongosh commands, BSON types, schema design, find and the query operators, update operators and arrays, the aggregation pipeline, indexes and explain(), transactions, change streams, search and vector search, replica sets, sharding, mongodump and the error codes you will actually meet. Search it, filter it by level or by your server version, copy any line with one click, and format, lint or convert a query from SQL without leaving the page.

30sections
276snippets
7.0 to 9.0versions covered
5command builders

Updated September 29, 2026, checked against the MongoDB 9.0 and 8.0 documentation and release notes. Print it for a PDF copy.

/
level

[ ]

Nothing matches that search. Try a shorter keyword such as index, $lookup or upsert. Several words are combined, so every one of them has to match. Keys: / focuses the search, Esc clears it, t switches the theme.

01

Install & Connect

installing, connecting, a query toolbox and command builders

Query Toolbox: Format, Lint, SQL to MongoDB, Read explain(), Extended JSON

core
Ctrl+Enter formats. Lint flags updates with an empty filter, updates without an operator, _id compared as a string, unanchored or case insensitive regex, $where, operators missing their $, large skip values, $match after $group, $out or $merge that is not last and the legacy count() and insert() methods. SQL to MongoDB handles SELECT with JOIN, WHERE, GROUP BY, HAVING, ORDER BY and LIMIT, plus INSERT, UPDATE and DELETE. Read explain() takes queryPlanner or executionStats output and suggests an index that follows the ESR rule. mongosh to PyMongo rewrites find, aggregate, update and index calls with Python types and keyword options.
output

          
Paste a query, a pipeline, SQL, explain() output, CSV or JSON above, then pick an action.

Command Builders: mongodump, Connection Strings, Users, Indexes, mongod.conf

everyday
commands

          

Install MongoDB

core

A throwaway server in Docker, with a root user and a volume for the data

docker run -d --name mongo -p 127.0.0.1:27017:27017 \
  -e MONGO_INITDB_ROOT_USERNAME=root \
  -e MONGO_INITDB_ROOT_PASSWORD="$(openssl rand -base64 24)" \
  -v mongo-data:/data/db \
  mongo:8.0

docker exec -it mongo mongosh -u root -p            # prompts for the password
docker logs -f mongo

A single node replica set, so transactions and change streams work locally

docker run -d --name mongo-rs -p 127.0.0.1:27017:27017 mongo:8.0 --replSet rs0 --bind_ip_all
docker exec -it mongo-rs mongosh --eval "rs.initiate()"
mongosh "mongodb://localhost:27017/?replicaSet=rs0&directConnection=true"

Ubuntu 24.04 from the official repository (the distribution package is old or missing)

curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
  sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | \
  sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
sudo apt-get update && sudo apt-get install -y mongodb-org
sudo systemctl enable --now mongod
systemctl status mongod

Homebrew, from MongoDB's own tap

brew tap mongodb/brew
brew install mongodb-community            # or mongodb-community@8.0 to pin a version
brew services start mongodb-community
brew install mongosh mongodb-database-tools

A local Atlas deployment with Search and Vector Search, or a free cloud cluster

brew install mongodb-atlas-cli
atlas deployments setup local1 --type local      # runs in Docker or Podman, includes mongot
atlas deployments connect local1 --connectWith mongosh

atlas setup                                       # sign in and create a free M0 cluster
atlas clusters connectionStrings describe Cluster0

Connect With mongosh

core

Connection strings: local, replica set, Atlas

mongosh                                                        # mongodb://127.0.0.1:27017
mongosh "mongodb://localhost:27017/shop"
mongosh "mongodb://db1:27017,db2:27017,db3:27017/shop?replicaSet=rs0&tls=true" -u app
mongosh "mongodb+srv://cluster0.example.net/shop" --username app   # prompts for the password
mongosh "$MONGODB_URI" --quiet --eval "db.runCommand({ ping: 1 })"

Where am I

db                                  // current database
db.getMongo()                       // connection
db.version()                        // server version
db.hello()                          // role in the replica set, limits
db.runCommand({ connectionStatus: 1 }).authInfo

Common startup options

mongosh "$MONGODB_URI" --file seed.js          # run a script and exit
mongosh "$MONGODB_URI" --nodb                   # a JavaScript REPL without a connection
mongosh "$MONGODB_URI" --apiVersion 1 --apiStrict
mongosh "$MONGODB_URI" --tls --tlsCAFile ca.pem --tlsCertificateKeyFile client.pem
mongosh "$MONGODB_URI" --authenticationMechanism MONGODB-X509
02

mongosh Basics

navigating, printing, scripting and configuring the shell

Navigate

core

Databases and collections

show dbs                     // databases that hold data
use shop                     // switch; created on the first write
show collections
db.getCollectionNames()
db.orders.find().limit(3)
db.getCollection('order-items').find()      // names with dashes or dots

Help at every level

help
db.help()
db.orders.help()
db.orders.find().help()
db.orders.find            // print the method without calling it

Leave the shell

exit
quit()
// or Ctrl+D; Ctrl+C twice while nothing is running

Results and Cursors

core

Iterate a cursor

db.orders.find({ status: 'paid' })   // prints the first 20 documents
it                                   // the next batch
config.set('displayBatchSize', 50)
db.orders.find().toArray()           // everything, into memory
db.orders.find().forEach(d => print(d._id))

Printing and JSON

printjson(db.orders.findOne())
print(EJSON.stringify(db.orders.findOne(), null, 2))   // Extended JSON, keeps types
db.orders.findOne({}, { _id: 0, total: 1 })
JSON.stringify({ at: new Date() })                      // plain JSON loses the type

Scripts and Configuration

everyday

Run scripts and one liners

mongosh "$MONGODB_URI" --quiet --eval 'db.orders.countDocuments({ status: "new" })'
mongosh "$MONGODB_URI" --quiet --json=relaxed --eval 'db.orders.findOne()' > one.json
load('scripts/seed.js')              // inside mongosh

Settings and the startup file

config                                    // every setting
config.set('editor', 'code --wait')
edit myPipeline                           // edit a variable in that editor
config.set('inspectDepth', 10)
config.set('historyLength', 5000)
// ~/.mongoshrc.js runs at every start:
prompt = () => `${db.getName()}> `

Tab completion, history, snippets

db.ord<Tab>                  // completes collection names and methods
// Ctrl+R searches the history in ~/.mongodb/mongosh/
snippet search               // community snippets package manager
snippet install analyze-schema

Connect to a second server from the same shell

const other = connect('mongodb://reports.internal:27017/archive')
other.orders.countDocuments()
const m = new Mongo('mongodb://db2.internal:27017')
m.getDB('shop').orders.findOne()
db.getSiblingDB('admin').runCommand({ listDatabases: 1, nameOnly: true })

Timing and Output

everyday

Time a query from the shell

const t0 = Date.now()
db.orders.find({ status: 'paid' }).toArray().length
print(`${Date.now() - t0} ms`)
03

Databases & Collections

creating, listing, renaming, views, capped and clustered collections

Databases

core

Create, inspect and drop

use shop
db.orders.insertOne({ status: 'new' })     // the database now exists
db.stats()
db.getSiblingDB('reports').getCollectionNames()
db.dropDatabase()                          // the current database, no undo

Database name rules

// max 64 bytes; no / \ . " $ * < > : | ? space or null character
// case matters but two databases cannot differ only by case (Shop vs shop)
// admin, local and config are reserved

Collections

core

Create explicitly with options, or implicitly on the first insert

db.createCollection('orders')
db.createCollection('events', { validator: { $jsonSchema: { required: ['type', 'at'] } } })
db.orders.renameCollection('orders_2025')
db.orders_tmp.drop()
db.getCollectionInfos({ name: 'orders' })

Size of a collection and its indexes

db.orders.aggregate([{ $collStats: { storageStats: { scale: 1024 * 1024 } } }])
  .next().storageStats
db.orders.totalIndexSize()
db.orders.estimatedDocumentCount()

Modify options later with collMod

db.runCommand({ collMod: 'orders', validationLevel: 'moderate' })
db.runCommand({ collMod: 'sessions', index: { keyPattern: { last_seen: 1 }, expireAfterSeconds: 3600 } })
db.runCommand({ collMod: 'orders', changeStreamPreAndPostImages: { enabled: true } })

Views, Capped and Clustered Collections

everyday

A read only view defined by a pipeline

db.createView('paid_orders', 'orders', [
  { $match: { status: 'paid' } },
  { $project: { customer_id: 1, total: 1, created_at: 1 } }
])
db.paid_orders.find({ total: { $gt: 100 } })

Capped collection: fixed size, insertion order, oldest out first

db.createCollection('log', { capped: true, size: 64 * 1024 * 1024, max: 100000 })
db.log.find().sort({ $natural: -1 }).limit(10)     // newest first
db.log.isCapped()

Clustered collection: documents stored in _id order

db.createCollection('events', {
  clusteredIndex: { key: { _id: 1 }, unique: true },
  expireAfterSeconds: 60 * 60 * 24 * 30
})

Inspect Collections

everyday

Names and types of every collection and view

db.getCollectionInfos().map(c => ({ name: c.name, type: c.type, capped: !!c.options.capped }))
04

BSON Types

ObjectId, dates, numbers, decimals, UUIDs and the $type table

ObjectId and Dates

core

ObjectId: 12 bytes, sortable by creation time

const id = ObjectId()
id.getTimestamp()
ObjectId('66f9a1c2e4b0d7a1c3f2e9b4').toHexString()
db.orders.find({ _id: { $gt: ObjectId.createFromTime(Math.floor(Date.now() / 1000) - 3600) } })   // last hour

Dates are UTC milliseconds; Date() without new is a string

new Date()                             // Date, stored as a BSON date
ISODate('2026-09-29T10:00:00Z')        // same type, mongosh helper
Date()                                 // a string: do not store this
db.orders.find({ created_at: { $gte: ISODate('2026-09-01'), $lt: ISODate('2026-10-01') } })

Numbers

core

Int32, Long, Double, Decimal128

db.t.insertOne({
  qty: 3,                            // mongosh sends whole numbers as int32
  views: Long('9007199254740993'),   // 64 bit integer, beyond JavaScript precision
  ratio: 0.25,                       // double
  price: Decimal128('19.99')         // exact decimal for money
})
db.t.find({ qty: { $type: 'int' } })

Money: Decimal128, or integer cents

0.1 + 0.2                                    // 0.30000000000000004
db.products.updateOne({ sku: 'OAK-1' }, { $inc: { price: Decimal128('0.10') } })
db.products.insertOne({ sku: 'OAK-2', price_cents: 1999 })

Other Types

everyday

UUID, binary data, booleans, null and missing fields

UUID()                                         // random v4, binary subtype 4
UUID('4f6c8d0e-2b1a-4c3d-9e8f-7a6b5c4d3e2f')
BinData(0, 'aGVsbG8=')
db.users.find({ deleted_at: null })            // null or missing
db.users.find({ deleted_at: { $exists: false } })   // missing only
db.users.find({ deleted_at: { $type: 'null' } })    // explicitly null only

$type names and numbers

TypeAliasNumber
Doubledouble1
Stringstring2
Objectobject3
Arrayarray4
Binary databinData5
ObjectIdobjectId7
Booleanbool8
Datedate9
Nullnull10
Regular expressionregex11
32 bit integerint16
Timestamptimestamp17
64 bit integerlong18
Decimal128decimal19
Any numbernumbernone

Sort and comparison order across types

// MinKey < null < numbers < strings < objects < arrays < binData
//   < ObjectId < booleans < dates < timestamps < regex < MaxKey
db.t.find().sort({ v: 1 })      // mixed types come out in this order

Limits worth knowing

// document: 16 MiB, nesting depth: 100 levels
// field names may contain . and $ since 5.0, but most operators need $getField for them
Object.bsonsize(db.orders.findOne())       // bytes of one document

Fix Types in Place

advanced

Convert string prices to Decimal128 and string dates to dates

db.products.updateMany({ price: { $type: 'string' } }, [{ $set: { price: { $toDecimal: '$price' } } }])
db.orders.updateMany({ created_at: { $type: 'string' } }, [{ $set: { created_at: { $toDate: '$created_at' } } }])
05

Schema Design

embedding, references and the patterns that keep documents small

Embed or Reference

core

Embed what is read together and bounded in size

{
  _id: ObjectId('66f9a1c2e4b0d7a1c3f2e9b4'),
  customer_id: ObjectId('66f99f10e4b0d7a1c3f2d001'),
  status: 'paid',
  shipping: { city: 'Cluj-Napoca', country: 'RO' },   // one to one: embed
  items: [                                              // one to few: embed
    { sku: 'OAK-1', qty: 2, price: Decimal128('19.99') }
  ],
  total: Decimal128('39.98')
}

Reference what grows without limit or is shared

// customers: { _id, name, email }
// orders:    { _id, customer_id, ... }    one to many with an unbounded many
db.orders.find({ customer_id: cid }).sort({ created_at: -1 }).limit(20)
db.orders.createIndex({ customer_id: 1, created_at: -1 })

A rule of thumb

RelationshipTypical model
One to oneembed the child document
One to few (under a few hundred)embed an array
One to many (thousands)array of ids in the parent, or parent id in the child
One to squillions (logs, events)parent id in the child, never an array in the parent
Many to manyarrays of ids on one or both sides

Design Patterns

everyday

Extended reference: copy the few fields you display

{ _id: 1, customer: { _id: cid, name: 'Ana Pop', country: 'RO' }, total: 129.9 }

Subset: the latest items embedded, the rest elsewhere

{ _id: 'OAK-1', name: 'Oak table', recent_reviews: [ /* last 10 */ ], review_count: 1482 }
db.reviews.find({ product_id: 'OAK-1' }).sort({ at: -1 }).skip(10)

Computed: keep totals up to date on write

db.products.updateOne(
  { _id: 'OAK-1' },
  { $inc: { review_count: 1, rating_sum: 5 } }
)

Bucket: group many small readings into one document per period

db.readings.updateOne(
  { sensor: 's1', day: ISODate('2026-09-29'), n: { $lt: 200 } },
  { $push: { r: { t: new Date(), v: 21.4 } }, $inc: { n: 1 } },
  { upsert: true }
)

Schema versioning and polymorphic documents

{ _id: 1, schema_version: 2, name: { first: 'Ana', last: 'Pop' } }
{ _id: 2, name: 'Radu Ionescu' }                          // version 1, migrated lazily
db.people.find({ schema_version: { $ne: 2 } }).limit(1000)   // batch migration

Anti patterns

// unbounded arrays that grow forever (followers, logs)
// one collection per customer or per day: thousands of collections and indexes
// huge documents read in full to show two fields
// $lookup on every request to rebuild a relational schema
// fields that hold different types in different documents
06

Insert Documents

insertOne, insertMany, write concern and mongoimport

insertOne and insertMany

core

Insert one document; _id is added when missing

db.customers.insertOne({ name: 'Ana Pop', email: 'ana@example.com', country: 'RO', created_at: new Date() })

Insert many; ordered: false keeps going after an error

db.products.insertMany([
  { sku: 'OAK-1', name: 'Oak table', price: Decimal128('199.00'), tags: ['wood', 'table'] },
  { sku: 'ASH-2', name: 'Ash chair', price: Decimal128('79.50'), tags: ['wood', 'chair'] }
], { ordered: false })

Duplicate key error

db.customers.createIndex({ email: 1 }, { unique: true })
db.customers.insertOne({ email: 'ana@example.com' })

Write Concern

everyday

How many members must confirm a write

db.orders.insertOne(doc, { writeConcern: { w: 'majority', wtimeout: 5000 } })
db.orders.insertOne(doc, { writeConcern: { w: 1 } })          // primary only
db.adminCommand({ getDefaultRWConcern: 1 })

Change the cluster default

db.adminCommand({
  setDefaultRWConcern: 1,
  defaultWriteConcern: { w: 'majority' },
  defaultReadConcern: { level: 'majority' }
})

Bulk Loading Files

everyday

mongoimport from JSON, JSON arrays and CSV

mongoimport --uri "$MONGODB_URI" --collection products --file products.json          # one document per line
mongoimport --uri "$MONGODB_URI" --collection products --jsonArray --file products.json
mongoimport --uri "$MONGODB_URI" --collection products --type csv --headerline --file products.csv
mongoimport --uri "$MONGODB_URI" --collection products --type csv --headerline \
  --mode upsert --upsertFields sku --file products.csv

Typed CSV columns

mongoimport --uri "$MONGODB_URI" -c products --type csv --file products.csv \
  --columnsHaveTypes --fields "sku.string(),price.decimal(),stock.int32(),added.date_go(2006-01-02)"

Custom Ids and Scripted Loads

everyday

Your own _id instead of an ObjectId

db.products.insertOne({ _id: 'OAK-1', name: 'Oak table' })          // natural key
db.events.insertOne({ _id: UUID(), type: 'signup', at: new Date() })
db.order_items.insertOne({ _id: { order: 9001, line: 1 }, sku: 'OAK-1', qty: 2 })   // compound key

Generate test data in batches

const batch = []
for (let i = 1; i <= 100000; i++) {
  batch.push({ n: i, status: ['new', 'paid', 'shipped'][i % 3], total: Math.round(Math.random() * 50000) / 100, created_at: new Date(Date.now() - i * 60000) })
  if (batch.length === 1000) { db.orders_test.insertMany(batch, { ordered: false }); batch.length = 0 }
}
if (batch.length) db.orders_test.insertMany(batch)
db.orders_test.countDocuments()
07

Query Operators

find, comparison, logical, element, array and evaluation operators

Filters

core

Equality and comparison

db.orders.find({ status: 'paid' })
db.orders.find({ total: { $gte: 100, $lt: 500 } })
db.orders.find({ status: { $in: ['paid', 'shipped'] } })
db.orders.find({ status: { $ne: 'cancelled' } })       // also matches documents without status
db.orders.findOne({ _id: ObjectId('66f9a1c2e4b0d7a1c3f2e9b4') })

AND is implicit; OR needs $or

db.orders.find({ status: 'paid', country: 'RO' })
db.orders.find({ $or: [{ status: 'paid' }, { total: { $gt: 1000 } }] })
db.orders.find({ $and: [{ $or: [{ a: 1 }, { b: 1 }] }, { $or: [{ c: 1 }, { d: 1 }] }] })
db.orders.find({ total: { $not: { $gt: 100 } } })       // also matches missing total

Embedded documents: dotted path versus exact match

db.orders.find({ 'shipping.country': 'RO' })               // field inside
db.orders.find({ shipping: { city: 'Cluj', country: 'RO' } }) // whole document, same field order

Count and distinct

db.orders.countDocuments({ status: 'paid' })
db.orders.estimatedDocumentCount()              // metadata, fast, no filter
db.orders.distinct('status')
db.orders.distinct('shipping.country', { status: 'paid' })

Existence, Types and Nulls

everyday

$exists, $type and null

db.users.find({ phone: { $exists: true } })
db.users.find({ phone: { $exists: true, $ne: null } })
db.users.find({ zip: { $type: 'string' } })
db.users.find({ zip: { $type: ['int', 'long'] } })
db.users.find({ phone: null })                  // null or missing

null no longer matches undefined

db.t.find({ v: null })       // 8.0+: matches null and missing, not the deprecated undefined type

Dotted paths compared with null

db.orders.find({ 'items.discount': null })
db.orders.find({ 'items.discount': { $in: [null, 0] } })

Regex, $expr and Other Evaluation Operators

everyday

Regular expressions

db.users.find({ email: /@example\.com$/ })
db.users.find({ name: /^ana/i })
db.users.find({ name: { $regex: '^ana', $options: 'i' } })
db.users.find({ sku: { $regex: '^OAK-' } })          // anchored and case sensitive: uses an index

$expr: compare two fields of the same document

db.products.find({ $expr: { $lt: ['$stock', '$reorder_level'] } })
db.orders.find({ $expr: { $gt: [{ $size: '$items' }, 10] } })
db.orders.find({ $expr: { $eq: [{ $year: '$created_at' }, 2026] } })

$mod, bitwise, $jsonSchema in a query

db.orders.find({ number: { $mod: [100, 0] } })
db.users.find({ flags: { $bitsAllSet: [0, 2] } })
db.users.find({ $jsonSchema: { required: ['email'], properties: { email: { bsonType: 'string' } } } })

$where: server side JavaScript

db.orders.find({ $where: 'this.total > this.budget' })        // prefer $expr

Query Operator Reference

core

The operators by group

GroupOperators
Comparison$eq $ne $gt $gte $lt $lte $in $nin
Logical$and $or $nor $not
Element$exists $type
Evaluation$expr $regex $mod $text $jsonSchema $where
Array$all $elemMatch $size
Geospatial$geoWithin $geoIntersects $near $nearSphere
Bitwise$bitsAllSet $bitsAnySet $bitsAllClear $bitsAnyClear
08

Projection, Sort, Paging

choosing fields, sorting, limits, collation and keyset pagination

Projection

core

Include or exclude fields

db.customers.find({}, { name: 1, email: 1 })             // plus _id
db.customers.find({}, { name: 1, email: 1, _id: 0 })
db.customers.find({}, { password_hash: 0, tokens: 0 })   // everything else
db.orders.find({}, { 'shipping.city': 1 })

Arrays and computed fields in a projection

db.posts.find({}, { title: 1, comments: { $slice: -3 } })                 // last 3
db.orders.find({ 'items.sku': 'OAK-1' }, { 'items.$': 1 })                // first matching element
db.orders.find({}, { items: { $elemMatch: { qty: { $gte: 5 } } } })
db.orders.find({}, { total: 1, year: { $year: '$created_at' } })          // aggregation expressions

Sort, Limit, Skip

core

Chain them in any order; the server applies sort, then skip, then limit

db.orders.find({ status: 'paid' }).sort({ created_at: -1 }).limit(20)
db.orders.find().sort({ total: -1, _id: 1 }).skip(40).limit(20)
db.orders.find().sort({ $natural: -1 }).limit(5)       // storage order, no index

Keyset pagination instead of large skip values

const page = db.orders.find({ status: 'paid' }).sort({ created_at: -1, _id: -1 }).limit(20).toArray()
const last = page[page.length - 1]
db.orders.find({
  status: 'paid',
  $or: [
    { created_at: { $lt: last.created_at } },
    { created_at: last.created_at, _id: { $lt: last._id } }
  ]
}).sort({ created_at: -1, _id: -1 }).limit(20)

Collation, Hints and Limits

everyday

Case insensitive sorting and matching with a collation

db.users.createIndex({ name: 1 }, { collation: { locale: 'en', strength: 2 } })
db.users.find({ name: 'ana pop' }).collation({ locale: 'en', strength: 2 })
db.users.find().sort({ name: 1 }).collation({ locale: 'ro' })              // ă, â, î, ș, ț in Romanian order
db.items.find().sort({ code: 1 }).collation({ locale: 'en', numericOrdering: true })   // 2 before 10

Time limits, hints, comments

db.orders.find({ status: 'new' }).maxTimeMS(2000)
db.orders.find({ status: 'new' }).hint({ status: 1, created_at: -1 })
db.orders.find({ status: 'new' }).comment('dashboard: new orders')
db.orders.find().sort({ note: 1 }).allowDiskUse()

A default time limit for every query

db.adminCommand({ setClusterParameter: { defaultMaxTimeMS: { readOperations: 30000 } } })

Read preference and read concern per query

db.orders.find({ status: 'paid' }).readPref('secondaryPreferred')
db.orders.find({ status: 'paid' }).readConcern('majority')

Distinct Pages and Totals

everyday

One page of results and the total count in a single aggregation

db.orders.aggregate([
  { $match: { status: 'paid' } },
  { $sort: { created_at: -1, _id: -1 } },
  { $facet: { page: [{ $skip: 0 }, { $limit: 20 }], total: [{ $count: 'n' }] } }
])
09

Update Operators

updateOne, updateMany, replace, upserts and update pipelines

Update Documents

core

updateOne and updateMany

db.customers.updateOne({ email: 'ana@example.com' }, { $set: { country: 'DE', updated_at: new Date() } })
db.orders.updateMany({ status: 'new', created_at: { $lt: ISODate('2026-09-01') } }, { $set: { status: 'expired' } })

The update needs operators, or it is a replacement

db.customers.updateOne({ _id: id }, { $set: { name: 'Ana' } })      // changes one field
db.customers.replaceOne({ _id: id }, { name: 'Ana' })               // the document is now only { _id, name }
db.customers.updateOne({ _id: id }, { name: 'Ana' })                // error: needs atomic operators

Field Update Operators

core

Set, remove, increment, rename

db.products.updateOne({ sku: 'OAK-1' }, {
  $set: { 'dims.width': 120, active: true },
  $unset: { legacy_code: '' },
  $inc: { stock: -1, sold: 1 },
  $mul: { price: Decimal128('1.10') },
  $rename: { desc: 'description' },
  $currentDate: { updated_at: true }
})

Keep the lowest or highest value

db.stats.updateOne({ _id: 'day-2026-09-29' }, { $min: { low: 12.4 }, $max: { high: 31.2 } }, { upsert: true })

Update operator reference

OperatorDoes
$setset a field, creating it and any missing parent documents
$unsetremove a field
$inc / $muladd to or multiply a number
$renamerename a field
$min / $maxupdate only when the new value is lower or higher
$currentDateset to the current date or timestamp
$setOnInsertset only when an upsert inserts

Upserts and findOneAndUpdate

core

Insert when nothing matches

db.daily_visits.updateOne(
  { page: '/pricing', day: '2026-09-29' },
  { $inc: { count: 1 }, $setOnInsert: { first_seen: new Date() } },
  { upsert: true }
)

Read and modify in one atomic step

db.counters.findOneAndUpdate(
  { _id: 'invoice' },
  { $inc: { seq: 1 } },
  { upsert: true, returnDocument: 'after' }
)

Update the first document in a given order

db.jobs.updateOne(
  { state: 'queued' },
  { $set: { state: 'running', worker: 'w1', started_at: new Date() } },
  { sort: { priority: -1, created_at: 1 } }
)

Update With an Aggregation Pipeline

advanced

Use other fields of the document in the update

db.orders.updateMany({}, [
  { $set: { total: { $sum: { $map: { input: '$items', in: { $multiply: ['$$this.qty', '$$this.price'] } } } } } },
  { $set: { tier: { $switch: { branches: [{ case: { $gte: ['$total', 1000] }, then: 'gold' }], default: 'standard' } } } },
  { $unset: 'tmp' }
])

Move fields into a sub document

db.users.updateMany({ street: { $exists: true } }, [
  { $set: { address: { street: '$street', city: '$city' } } },
  { $unset: ['street', 'city'] }
])
10

Arrays

querying arrays, $push, $pull, positional operators and arrayFilters

Query Arrays

core

A value inside an array

db.products.find({ tags: 'wood' })                    // any element equals
db.products.find({ tags: { $all: ['wood', 'table'] } })
db.products.find({ tags: ['wood', 'table'] })         // exactly this array, this order
db.products.find({ tags: { $size: 2 } })
db.products.find({ 'tags.0': 'wood' })                // by position

$elemMatch: all conditions on the same element

db.orders.find({ 'items.sku': 'OAK-1', 'items.qty': { $gte: 5 } })            // may match two different items
db.orders.find({ items: { $elemMatch: { sku: 'OAK-1', qty: { $gte: 5 } } } }) // the same item

Add and Remove Elements

core

$push, $addToSet, $pull, $pop

db.posts.updateOne({ _id: 1 }, { $push: { tags: 'mongodb' } })
db.posts.updateOne({ _id: 1 }, { $addToSet: { tags: { $each: ['nosql', 'db'] } } })   // no duplicates
db.posts.updateOne({ _id: 1 }, { $pull: { tags: 'db' } })
db.posts.updateOne({ _id: 1 }, { $pull: { comments: { spam: true } } })               // by condition
db.posts.updateOne({ _id: 1 }, { $pop: { tags: 1 } })                                 // last; -1 first

Keep only the latest N elements, sorted

db.users.updateOne({ _id: uid }, {
  $push: { recent: { $each: [{ at: new Date(), page: '/pricing' }], $sort: { at: -1 }, $slice: 10 } }
})

Insert at a position

db.lists.updateOne({ _id: 1 }, { $push: { items: { $each: ['first'], $position: 0 } } })

Update Elements in Place

everyday

Positional operators

db.orders.updateOne({ _id: oid, 'items.sku': 'OAK-1' }, { $inc: { 'items.$.qty': 1 } })     // first match
db.orders.updateOne({ _id: oid }, { $set: { 'items.$[].shipped': true } })                  // every element
db.orders.updateMany(
  {},
  { $set: { 'items.$[it].price': Decimal128('17.99') } },
  { arrayFilters: [{ 'it.sku': 'OAK-1', 'it.price': { $gt: Decimal128('17.99') } }] }
)

Nested arrays

db.courses.updateOne(
  { _id: 1 },
  { $set: { 'modules.$[m].lessons.$[l].done': true } },
  { arrayFilters: [{ 'm.code': 'M2' }, { 'l.id': 7 }] }
)

Index Arrays of Documents

everyday

A compound multikey index for $elemMatch queries

db.orders.createIndex({ 'items.sku': 1, 'items.qty': 1 })
db.orders.find({ items: { $elemMatch: { sku: 'OAK-1', qty: { $gte: 5 } } } }).explain('executionStats').executionStats.totalKeysExamined
11

Delete & Bulk Writes

deleteOne, deleteMany, findOneAndDelete and bulkWrite

Delete

core

Delete one or many

db.sessions.deleteOne({ _id: sid })
db.sessions.deleteMany({ expires_at: { $lt: new Date() } })
db.jobs.findOneAndDelete({ state: 'done' }, { sort: { finished_at: 1 } })

Empty a collection

db.logs.deleteMany({})     // deletes one by one, keeps indexes, slow on big collections
db.logs.drop()             // instant, drops the indexes too

Delete in batches on a busy server

let n
do {
  const ids = db.events.find({ at: { $lt: ISODate('2025-01-01') } }, { _id: 1 }).limit(5000).toArray().map(d => d._id)
  n = ids.length ? db.events.deleteMany({ _id: { $in: ids } }).deletedCount : 0
  sleep(200)
} while (n > 0)

Bulk Writes

everyday

Mixed writes to one collection in one round trip

db.products.bulkWrite([
  { insertOne: { document: { sku: 'ELM-3', stock: 5 } } },
  { updateOne: { filter: { sku: 'OAK-1' }, update: { $inc: { stock: -1 } } } },
  { updateMany: { filter: { stock: 0 }, update: { $set: { active: false } } } },
  { replaceOne: { filter: { sku: 'ASH-2' }, replacement: { sku: 'ASH-2', stock: 3 }, upsert: true } },
  { deleteOne: { filter: { sku: 'OLD-9' } } }
], { ordered: false })

One bulk write across several collections

db.getMongo().bulkWrite([
  { insertOne: { namespace: 'shop.orders', document: { _id: 9001, total: 40 } } },
  { updateOne: { namespace: 'shop.products', filter: { sku: 'OAK-1' }, update: { $inc: { stock: -1 } } } }
])

Cleanup Patterns

everyday

Remove duplicates, keeping the newest document per key

const dupes = db.customers.aggregate([
  { $sort: { email: 1, created_at: -1 } },
  { $group: { _id: '$email', keep: { $first: '$_id' }, all: { $push: '$_id' }, n: { $sum: 1 } } },
  { $match: { n: { $gt: 1 } } }
], { allowDiskUse: true }).toArray()
const extra = dupes.flatMap(d => d.all.filter(id => !id.equals(d.keep)))
db.customers.deleteMany({ _id: { $in: extra } })
db.customers.createIndex({ email: 1 }, { unique: true })

Soft delete with a partial index

db.posts.updateOne({ _id: pid }, { $set: { deleted_at: new Date() } })
db.posts.createIndex({ author_id: 1, created_at: -1 }, { partialFilterExpression: { deleted_at: null } })
db.posts.find({ author_id: aid, deleted_at: null }).sort({ created_at: -1 })

Copy then delete: archive old documents

db.orders.aggregate([
  { $match: { created_at: { $lt: ISODate('2025-01-01') } } },
  { $merge: { into: 'orders_archive', on: '_id', whenMatched: 'keepExisting', whenNotMatched: 'insert' } }
])
db.orders.deleteMany({ created_at: { $lt: ISODate('2025-01-01') } })
12

Aggregation Basics

the pipeline model, $match, $group, $sort, $project and memory limits

The Pipeline

core

Filter, group, sort

db.orders.aggregate([
  { $match: { status: 'paid', created_at: { $gte: ISODate('2026-01-01') } } },
  { $group: { _id: '$country', orders: { $sum: 1 }, revenue: { $sum: '$total' }, avg: { $avg: '$total' } } },
  { $sort: { revenue: -1 } },
  { $limit: 5 }
])

SQL to aggregation

SQLAggregation
WHERE$match
GROUP BY$group
HAVING$match after $group
SELECT columns$project, $set
ORDER BY$sort
LIMIT / OFFSET$limit / $skip
JOIN$lookup
UNION ALL$unionWith
COUNT(*)$count or $group with $sum: 1
INSERT INTO ... SELECT$merge or $out

Group by several fields, then HAVING

db.orders.aggregate([
  { $group: { _id: { country: '$country', status: '$status' }, n: { $sum: 1 } } },
  { $match: { n: { $gt: 100 } } },
  { $project: { _id: 0, country: '$_id.country', status: '$_id.status', n: 1 } }
])

Count, and group everything

db.orders.aggregate([{ $match: { status: 'paid' } }, { $count: 'paid' }])
db.orders.aggregate([{ $group: { _id: null, total: { $sum: '$total' }, max: { $max: '$total' } } }])

Accumulators

everyday

The $group accumulators

db.orders.aggregate([{ $group: {
  _id: '$customer_id',
  n: { $sum: 1 },
  spent: { $sum: '$total' },
  first_order: { $min: '$created_at' },
  last_order: { $max: '$created_at' },
  statuses: { $addToSet: '$status' },
  latest3: { $topN: { n: 3, sortBy: { created_at: -1 }, output: '$_id' } },
  p90: { $percentile: { input: '$total', p: [0.9], method: 'approximate' } }
} }])

Memory limits and spilling to disk

db.orders.aggregate(pipeline, { allowDiskUse: true })       // the default since 6.0
db.orders.aggregate(pipeline, { maxTimeMS: 60000, comment: 'nightly report' })

Explain a pipeline

db.orders.explain('executionStats').aggregate(pipeline)
db.orders.aggregate(pipeline, { explain: true })

Group by Time

everyday

Revenue per month, in the business time zone

db.orders.aggregate([
  { $match: { status: 'paid' } },
  { $group: {
      _id: { $dateTrunc: { date: '$created_at', unit: 'month', timezone: 'Europe/Bucharest' } },
      revenue: { $sum: '$total' },
      orders: { $sum: 1 }
  } },
  { $sort: { _id: 1 } }
])
13

Pipeline Stages

$lookup, $unwind, $facet, $bucket, $setWindowFields, $merge and more

Joins and Arrays

core

$lookup: a left outer join

db.orders.aggregate([
  { $match: { status: 'paid' } },
  { $lookup: { from: 'customers', localField: 'customer_id', foreignField: '_id', as: 'customer' } },
  { $unwind: '$customer' },
  { $project: { total: 1, 'customer.name': 1, 'customer.email': 1 } }
])

$lookup with conditions and a sub pipeline

db.customers.aggregate([
  { $lookup: {
      from: 'orders',
      localField: '_id',
      foreignField: 'customer_id',
      pipeline: [{ $match: { status: 'paid' } }, { $sort: { created_at: -1 } }, { $limit: 3 }],
      as: 'recent_paid'
  } }
])

$unwind: one document per array element

db.orders.aggregate([
  { $unwind: { path: '$items', preserveNullAndEmptyArrays: true, includeArrayIndex: 'pos' } },
  { $group: { _id: '$items.sku', sold: { $sum: '$items.qty' } } },
  { $sort: { sold: -1 } }
])

$graphLookup: follow references recursively

db.employees.aggregate([
  { $match: { name: 'Ana' } },
  { $graphLookup: { from: 'employees', startWith: '$manager_id', connectFromField: 'manager_id', connectToField: '_id', as: 'chain', depthField: 'level' } }
])

Reshaping

everyday

$set, $project, $replaceWith, $unset

db.orders.aggregate([
  { $set: { item_count: { $size: '$items' } } },
  { $unset: ['internal_notes', 'items'] },
  { $replaceWith: { $mergeObjects: ['$shipping', { order_id: '$_id', total: '$total' }] } }
])

$facet: several aggregations over the same input

db.products.aggregate([
  { $match: { active: true } },
  { $facet: {
      by_category: [{ $sortByCount: '$category' }],
      price_bands: [{ $bucket: { groupBy: '$price', boundaries: [0, 50, 200, 1000], default: '1000+' } }],
      total: [{ $count: 'n' }]
  } }
])

$unionWith, $sample, $documents

db.orders_2025.aggregate([{ $unionWith: 'orders_2026' }, { $count: 'all' }])
db.users.aggregate([{ $sample: { size: 5 } }])
db.aggregate([{ $documents: [{ x: 1 }, { x: 2 }] }, { $set: { y: { $multiply: ['$x', 10] } } }])

Window Functions

everyday

Running totals, moving averages and ranks with $setWindowFields

db.sales.aggregate([
  { $setWindowFields: {
      partitionBy: '$store',
      sortBy: { day: 1 },
      output: {
        running: { $sum: '$amount', window: { documents: ['unbounded', 'current'] } },
        avg7: { $avg: '$amount', window: { range: [-6, 0], unit: 'day' } },
        rank: { $denseRank: {} }
      }
  } }
])

Fill gaps in a time series

db.sales.aggregate([
  { $densify: { field: 'day', partitionByFields: ['store'], range: { step: 1, unit: 'day', bounds: 'partition' } } },
  { $fill: { partitionBy: '$store', sortBy: { day: 1 }, output: { amount: { value: 0 } } } }
])

Writing Results

everyday

$merge into a collection, $out to replace one

db.orders.aggregate([
  { $match: { created_at: { $gte: ISODate('2026-09-01') } } },
  { $group: { _id: { $dateTrunc: { date: '$created_at', unit: 'day' } }, revenue: { $sum: '$total' } } },
  { $merge: { into: 'daily_revenue', on: '_id', whenMatched: 'replace', whenNotMatched: 'insert' } }
])
db.orders.aggregate([{ $match: { status: 'paid' } }, { $out: 'paid_snapshot' }])

Stage reference

StageUse it to
$matchfilter; first, to use indexes
$project, $set, $unsetchoose, add or remove fields
$group, $bucket, $sortByCountgroup and summarise
$sort, $limit, $skiporder and page
$lookup, $graphLookup, $unionWithcombine collections
$unwindone document per array element
$facetseveral pipelines on the same input
$setWindowFields, $densify, $fillwindow functions and gap filling
$replaceWithpromote a sub document to the top
$merge, $outwrite the result to a collection
$search, $vectorSearchfull text and vector search
14

Expressions

arithmetic, strings, dates, conditions, arrays, conversion and variables

Conditions and Arithmetic

core

$cond, $ifNull, $switch

db.orders.aggregate([{ $project: {
  total: 1,
  size: { $cond: [{ $gte: ['$total', 500] }, 'large', 'small'] },
  coupon: { $ifNull: ['$coupon', 'none'] },
  band: { $switch: {
    branches: [
      { case: { $lt: ['$total', 50] }, then: 'low' },
      { case: { $lt: ['$total', 200] }, then: 'mid' }
    ],
    default: 'high'
  } }
} }])

Arithmetic

{ $add: ['$price', '$shipping'] }
{ $subtract: ['$total', '$discount'] }
{ $multiply: ['$qty', '$price'] }
{ $divide: ['$total', '$qty'] }
{ $round: [{ $multiply: ['$total', 1.19] }, 2] }
{ $mod: ['$number', 2] }
{ $abs: '$delta' }

Strings and Dates

everyday

String operators

{ $concat: ['$first', ' ', '$last'] }
{ $toLower: '$email' }
{ $trim: { input: '$name' } }
{ $split: ['$path', '/'] }
{ $substrCP: ['$name', 0, 3] }
{ $strLenCP: '$name' }
{ $regexMatch: { input: '$email', regex: /@example\.com$/ } }
{ $replaceAll: { input: '$phone', find: ' ', replacement: '' } }

Date operators with a time zone

db.orders.aggregate([{ $group: {
  _id: { $dateToString: { format: '%Y-%m-%d', date: '$created_at', timezone: 'Europe/Bucharest' } },
  n: { $sum: 1 }
} }])
{ $dateTrunc: { date: '$created_at', unit: 'week', startOfWeek: 'monday' } }
{ $dateAdd: { startDate: '$created_at', unit: 'day', amount: 30 } }
{ $dateDiff: { startDate: '$created_at', endDate: '$$NOW', unit: 'day' } }
{ $dateFromString: { dateString: '$date_text', format: '%d.%m.%Y' } }
{ $year: { date: '$created_at', timezone: '+03:00' } }

Arrays in Expressions

everyday

$map, $filter, $reduce

{ $map: { input: '$items', as: 'i', in: { $multiply: ['$$i.qty', '$$i.price'] } } }
{ $filter: { input: '$items', as: 'i', cond: { $gte: ['$$i.qty', 5] }, limit: 3 } }
{ $reduce: { input: '$items', initialValue: 0, in: { $add: ['$$value', '$$this.qty'] } } }
{ $sum: '$items.qty' }                          // simpler when you only need the sum

The position of each element

{ $map: { input: '$steps', arrayIndexAs: 'i', in: { n: { $add: ['$$IDX', 1] }, step: '$$this' } } }

Other array expressions

{ $size: '$items' }
{ $arrayElemAt: ['$items', -1] }            // last
{ $in: ['vip', '$tags'] }
{ $slice: ['$items', 3] }
{ $concatArrays: ['$a', '$b'] }
{ $setUnion: ['$a', '$b'] }
{ $sortArray: { input: '$items', sortBy: { price: -1 } } }
{ $firstN: { input: '$scores', n: 3 } }

Conversion and Variables

advanced

Type conversion

{ $toString: '$_id' }
{ $toObjectId: '$customer_ref' }
{ $toDate: '$ts_millis' }
{ $toDecimal: '$price_text' }
{ $convert: { input: '$qty', to: 'int', onError: 0, onNull: 0 } }
{ $toUUID: '$uuid_text' }

Newer conversion helpers

{ $convert: { input: 'ff', to: 'int', base: 16 } }
{ $toArray: '$tags_json' }
{ $hexHash: { input: '$email', algorithm: 'sha256' } }
{ $createObjectId: {} }

System variables and $let

'$$ROOT'           // the whole input document
'$$CURRENT'
'$$NOW'            // same value for the whole operation
'$$REMOVE'         // leave the field out
{ $let: { vars: { net: { $subtract: ['$total', '$tax'] } }, in: { $multiply: ['$$net', 0.1] } } }
{ $project: { email: { $cond: ['$public', '$email', '$$REMOVE'] } } }

Fields with dots or dollar signs in the name

{ $getField: { field: 'price.usd', input: '$$ROOT' } }
{ $setField: { field: '$unit', input: '$$ROOT', value: 'kg' } }
15

Indexes

single, compound, multikey, text, wildcard, TTL, partial and unique indexes

Create and Manage

core

Create, list, drop

db.orders.createIndex({ customer_id: 1, created_at: -1 })
db.orders.createIndex({ email: 1 }, { unique: true, name: 'email_unique' })
db.orders.getIndexes()
db.orders.dropIndex('email_unique')

Compound indexes follow the ESR rule

// query: status = 'paid', sorted by created_at, total > 100
db.orders.createIndex({ status: 1, created_at: -1, total: 1 })
// Equality fields first, then Sort fields, then Range fields

Prefixes: one compound index serves several queries

db.orders.createIndex({ customer_id: 1, status: 1, created_at: -1 })
// used by: { customer_id }, { customer_id, status }, { customer_id, status, created_at }
// not by:  { status } alone, { created_at } alone

Build indexes on a live replica set

db.orders.createIndex({ sku: 1 })                 // does not block reads and writes for its duration
db.currentOp({ 'command.createIndexes': { $exists: true } })
db.orders.createIndexes([{ a: 1 }, { b: 1, c: -1 }])

Special Index Types

everyday

TTL: delete documents after a time

db.sessions.createIndex({ last_seen: 1 }, { expireAfterSeconds: 3600 })
db.events.createIndex({ expire_at: 1 }, { expireAfterSeconds: 0 })     // at the date in the field

Partial and unique

db.users.createIndex({ email: 1 }, { unique: true, partialFilterExpression: { email: { $type: 'string' } } })
db.orders.createIndex({ created_at: -1 }, { partialFilterExpression: { status: { $in: ['new', 'paid'] } } })

Multikey: an index on an array field

db.products.createIndex({ tags: 1 })          // one entry per element
db.products.find({ tags: 'oak' })

Text, wildcard and hashed

db.articles.createIndex({ title: 'text', body: 'text' }, { weights: { title: 5 }, default_language: 'english' })
db.products.createIndex({ 'attrs.$**': 1 })        // any field under attrs
db.users.createIndex({ user_id: 'hashed' })        // for hashed shard keys

Case insensitive index

db.users.createIndex({ name: 1 }, { collation: { locale: 'en', strength: 2 } })
db.users.find({ name: 'ANA POP' }).collation({ locale: 'en', strength: 2 })

Maintain

advanced

Hide an index before dropping it

db.orders.hideIndex('status_1')        // kept up to date, ignored by the planner
db.orders.unhideIndex('status_1')

Which indexes are used

db.orders.aggregate([{ $indexStats: {} }, { $project: { name: 1, 'accesses.ops': 1, 'accesses.since': 1 } }])

Index sizes

db.orders.stats({ indexDetails: false }).indexSizes
db.orders.totalIndexSize()
16

Explain & Tuning

reading explain(), the profiler, covered queries and query settings

Read explain()

core

executionStats runs the query and reports what it did

db.orders.find({ status: 'paid' }).sort({ created_at: -1 }).explain('executionStats')

The stages you will see

StageMeaning
COLLSCANreads every document: no usable index
IXSCANwalks an index
FETCHreads the documents the index pointed to
SORTsorts in memory: the index does not match the sort
PROJECTION_COVEREDanswered from the index alone
EXPRESS_IXSCANfast path for simple equality on a unique index, 8.0+
SHARD_MERGEresults merged from several shards

Covered queries

db.users.createIndex({ email: 1, name: 1 })
db.users.find({ email: 'ana@example.com' }, { _id: 0, email: 1, name: 1 }).explain('executionStats').executionStats.totalDocsExamined

Profiler and Running Operations

everyday

Log and profile slow operations

db.setProfilingLevel(1, { slowms: 100 })          // profile operations slower than 100 ms
db.system.profile.find().sort({ ts: -1 }).limit(5).pretty()
db.system.profile.find({ planSummary: 'COLLSCAN' }, { ns: 1, millis: 1, command: 1 })
db.setProfilingLevel(0)

What is running now, and stopping it

db.currentOp({ active: true, secs_running: { $gte: 5 } })
db.killOp(12345)
db.aggregate([{ $currentOp: { allUsers: true } }, { $match: { secs_running: { $gt: 10 } } }])

Query statistics per query shape

db.getSiblingDB('admin').aggregate([
  { $queryStats: {} },
  { $sort: { 'metrics.totalExecMicros.sum': -1 } },
  { $limit: 10 }
])

Fix a Plan

advanced

Pin a plan or reject a query shape with query settings

db.adminCommand({
  setQuerySettings: { find: 'orders', filter: { status: 'paid' }, sort: { created_at: -1 }, $db: 'shop' },
  settings: { indexHints: { ns: { db: 'shop', coll: 'orders' }, allowedIndexes: ['status_1_created_at_-1'] } }
})
db.adminCommand({ setQuerySettings: querySettingsHash, settings: { reject: true } })
db.aggregate([{ $querySettings: {} }])

Query settings for one command only

db.runCommand({
  find: 'orders',
  filter: { status: 'paid' },
  querySettings: { indexHints: { ns: { db: 'shop', coll: 'orders' }, allowedIndexes: [{ status: 1 }] } }
})

The plan cache

db.orders.getPlanCache().list()
db.orders.getPlanCache().clear()

Explain Modes

core

What each verbosity level runs

ModeRuns the queryShows
queryPlannernothe winning plan and the rejected plans
executionStatsyesplus counts and times for the winning plan
allPlansExecutionyesplus the trial results of every candidate plan
17

Schema Validation

$jsonSchema validators, validation levels and actions

$jsonSchema Validators

core

Create a collection with rules

db.createCollection('customers', {
  validator: { $jsonSchema: {
    bsonType: 'object',
    required: ['email', 'name', 'created_at'],
    properties: {
      email: { bsonType: 'string', pattern: '^[^@\\s]+@[^@\\s]+$' },
      name: { bsonType: 'string', minLength: 1, maxLength: 200 },
      tier: { enum: ['free', 'pro', 'enterprise'] },
      credits: { bsonType: ['int', 'long'], minimum: 0 },
      created_at: { bsonType: 'date' }
    }
  } },
  validationAction: 'error'
})

What a rejected write looks like

db.customers.insertOne({ email: 'not-an-email', name: 'Ana', created_at: new Date() })

Levels and Actions

everyday

Add or change rules on an existing collection

db.runCommand({
  collMod: 'customers',
  validator: { $jsonSchema: { required: ['email'] } },
  validationLevel: 'moderate',
  validationAction: 'warn'
})
db.getCollectionInfos({ name: 'customers' })[0].options.validator

What each setting does

SettingValueEffect
validationLevelstrictcheck every insert and update (default)
validationLevelmoderateskip updates to documents that were already invalid
validationLevelconstraint9.0+, every document in the collection is guaranteed to satisfy the rules
validationLeveloffno validation
validationActionerrorreject the write (default)
validationActionwarnallow it, write a warning to the log
validationActionerrorAndLogreject it and log it, 8.1+

Find documents that break the current rules

db.customers.find({ $nor: [db.getCollectionInfos({ name: 'customers' })[0].options.validator] })

Bypass validation for a one off fix

db.runCommand({ insert: 'customers', documents: [legacyDoc], bypassDocumentValidation: true })

Other Validation Tools

everyday

Validators with query operators

db.runCommand({
  collMod: 'orders',
  validator: { $and: [
    { total: { $gte: 0 } },
    { status: { $in: ['new', 'paid', 'shipped', 'refunded', 'cancelled'] } },
    { $expr: { $lte: ['$discount', '$total'] } }
  ] }
})

Check a collection's data files and indexes

db.orders.validate({ full: true })
18

Transactions

multi document transactions, sessions, retries and limits

Run a Transaction

core

withTransaction commits, and retries on transient errors

const session = db.getMongo().startSession()
session.withTransaction(() => {
  const s = session.getDatabase('shop')
  s.accounts.updateOne({ _id: 'A' }, { $inc: { balance: -100 } })
  s.accounts.updateOne({ _id: 'B' }, { $inc: { balance: 100 } })
  s.transfers.insertOne({ from: 'A', to: 'B', amount: 100, at: new Date() })
}, { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } })
session.endSession()

The same in Node.js

const session = client.startSession()
try {
  await session.withTransaction(async () => {
    await accounts.updateOne({ _id: 'A' }, { $inc: { balance: -100 } }, { session })
    await accounts.updateOne({ _id: 'B' }, { $inc: { balance: 100 } }, { session })
  })
} finally {
  await session.endSession()
}

Manual control

session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } })
try {
  session.getDatabase('shop').orders.insertOne({ total: 50 })
  session.commitTransaction()
} catch (e) {
  session.abortTransaction()
  throw e
}

Limits and Good Practice

everyday

What to keep in mind

RuleDetail
Needs a replica set or sharded clustera single standalone mongod cannot run transactions
60 second default lifetimetransactionLifetimeLimitSeconds, then the server aborts it
Keep them smallabout 1000 modified documents is the usual guidance
Write conflicts abortthe second writer gets WriteConflict and must retry
DDLcreating collections and indexes inside is allowed since 4.4, dropping is not
Single document writes are already atomica transaction is only needed across documents

A cap on open transactions

db.adminCommand({ getParameter: 1, maxConcurrentMultiDocumentTransactions: 1 })

Retryable writes are on by default

mongodb+srv://cluster0.example.net/shop?retryWrites=true&w=majority

Consistency Options

advanced

Causal consistency: read your own writes on secondaries

const s = db.getMongo().startSession({ causalConsistency: true })
const sdb = s.getDatabase('shop')
sdb.orders.insertOne({ _id: 9002, total: 75 })
sdb.orders.find({ _id: 9002 }).readPref('secondary').readConcern('majority')

A transaction in Python

with client.start_session() as session:
    def transfer(s):
        accounts = s.client.shop.accounts
        accounts.update_one({"_id": "A"}, {"$inc": {"balance": -100}}, session=s)
        accounts.update_one({"_id": "B"}, {"$inc": {"balance": 100}}, session=s)
    session.with_transaction(transfer)
19

Change Streams

watching inserts, updates and deletes, resuming and pre images

Watch Changes

core

Watch a collection

const cs = db.orders.watch(
  [{ $match: { operationType: { $in: ['insert', 'update'] } } }],
  { fullDocument: 'updateLookup' }
)
while (cs.hasNext()) { printjson(cs.next()) }

Watch a database or the whole deployment

db.watch()
db.getMongo().watch([{ $match: { 'ns.coll': 'orders' } }])

Resume after a restart

const token = lastEvent._id                         // store it after handling each event
db.orders.watch([], { resumeAfter: token })
db.orders.watch([], { startAfter: token })          // also works after an invalidate event
db.orders.watch([], { startAtOperationTime: Timestamp({ t: 1790678034, i: 0 }) })

Pre and Post Images

advanced

The document before the change

db.runCommand({ collMod: 'orders', changeStreamPreAndPostImages: { enabled: true } })
db.orders.watch([], { fullDocumentBeforeChange: 'whenAvailable', fullDocument: 'whenAvailable' })

Events larger than 16 MB

db.orders.watch([{ $changeStreamSplitLargeEvent: {} }], { fullDocument: 'updateLookup', fullDocumentBeforeChange: 'required' })

Event types

operationTypeWhen
insert, update, replace, deletedocument changes
drop, rename, dropDatabasethe collection or database went away
invalidatethe stream can no longer continue; resume with startAfter
create, createIndexes, modify, shardCollectionDDL events, with showExpandedEvents: true

Change Streams From Code

everyday

Node.js, with a stored resume token

const stream = orders.watch([{ $match: { operationType: 'insert' } }], { resumeAfter: await loadToken() })
for await (const ev of stream) {
  await handle(ev.fullDocument)
  await saveToken(ev._id)
}
21

Time Series Collections

storing measurements, granularity, TTL and querying buckets

Create and Insert

core

A time series collection

db.createCollection('metrics', {
  timeseries: { timeField: 'ts', metaField: 'meta', granularity: 'minutes' },
  expireAfterSeconds: 60 * 60 * 24 * 90
})
db.metrics.insertMany([
  { ts: new Date(), meta: { host: 'web1', metric: 'cpu' }, value: 41.2 },
  { ts: new Date(), meta: { host: 'web2', metric: 'cpu' }, value: 17.9 }
])

Granularity or custom bucket spans

db.createCollection('ticks', { timeseries: { timeField: 't', metaField: 'sym', bucketMaxSpanSeconds: 300, bucketRoundingSeconds: 300 } })
db.runCommand({ collMod: 'metrics', timeseries: { granularity: 'hours' } })     // can only grow

Secondary indexes

db.metrics.createIndex({ 'meta.host': 1, ts: -1 })
db.metrics.createIndex({ ts: -1 })

Query

everyday

Downsample to five minute averages

db.metrics.aggregate([
  { $match: { 'meta.host': 'web1', ts: { $gte: ISODate('2026-09-29T00:00:00Z') } } },
  { $group: {
      _id: { $dateTrunc: { date: '$ts', unit: 'minute', binSize: 5 } },
      avg: { $avg: '$value' },
      max: { $max: '$value' }
  } },
  { $sort: { _id: 1 } }
])

Storage in 9.0

db.adminCommand({ renameCollection: 'shop.metrics', to: 'shop.metrics_2026' })

When not to use one

// frequent updates to single measurements
// documents looked up one by one by _id
// data without a real time dimension

Latest Values and Archiving

everyday

The latest reading per series

db.metrics.aggregate([
  { $match: { ts: { $gte: new Date(Date.now() - 3600000) } } },
  { $group: { _id: '$meta.host', last: { $top: { sortBy: { ts: -1 }, output: { ts: '$ts', value: '$value' } } } } }
])

Move cold data to cheaper storage with Online Archive

atlas clusters onlineArchives create --clusterName Cluster0 --db shop --collection metrics \
  --dateField ts --archiveAfter 90
22

Users & Security

authentication, users, roles, TLS, encryption and auditing

Turn On Authentication

core

Enable authorization, then create the first user through the localhost exception

# /etc/mongod.conf
security:
  authorization: enabled

mongosh --host 127.0.0.1                  # on the server itself, before any user exists
use admin
db.createUser({ user: 'root', pwd: passwordPrompt(), roles: ['root'] })

Log in

mongosh "mongodb://db1.internal/shop?authSource=admin" -u app
db.auth('app', passwordPrompt())          // inside an open shell
db.runCommand({ connectionStatus: 1 }).authInfo.authenticatedUsers

Users and Roles

core

Create, change and remove users

use admin
db.createUser({ user: 'app', pwd: passwordPrompt(), roles: [{ role: 'readWrite', db: 'shop' }] })
db.grantRolesToUser('app', [{ role: 'read', db: 'reports' }])
db.revokeRolesFromUser('app', [{ role: 'read', db: 'reports' }])
db.changeUserPassword('app', passwordPrompt())
db.getUsers()
db.dropUser('old_app')

Built-in roles

RoleGrants
read / readWriteread, or read and write, the collections of one database
dbAdminindexes, validation, stats; no data access
dbOwnerreadWrite plus dbAdmin plus userAdmin on one database
userAdmincreate users and roles in one database, which means it can grant itself anything there
clusterMonitorread only monitoring: serverStatus, currentOp, rs.status
clusterManager / clusterAdminreplication and sharding management
backup / restoremongodump and mongorestore
readAnyDatabase / readWriteAnyDatabaseevery database
rooteverything

A custom role with exact privileges

db.createRole({
  role: 'orders_writer',
  privileges: [{ resource: { db: 'shop', collection: 'orders' }, actions: ['find', 'insert', 'update'] }],
  roles: []
})
db.grantRolesToUser('app', ['orders_writer'])
db.getRole('orders_writer', { showPrivileges: true })

Encryption and Auditing

everyday

Require TLS

net:
  tls:
    mode: requireTLS
    certificateKeyFile: /etc/ssl/mongodb.pem
    CAFile: /etc/ssl/ca.pem
    disabledProtocols: TLS1_0,TLS1_1

Encryption at rest with the WiredTiger encrypted storage engine

mongod --enableEncryption --encryptionKeyFile /etc/mongodb/master.key --dbpath /var/lib/mongodb
# or KMIP: --kmipServerName kms.internal --kmipServerCAFile ca.pem

Auditing

auditLog:
  destination: file
  format: JSON
  path: /var/log/mongodb/audit.json
  filter: '{ atype: { $in: ["authenticate", "createUser", "dropCollection"] } }'

LDAP, Kerberos and OpenID Connect

mongosh "mongodb://db1.internal/?authMechanism=PLAIN&authSource=$external" -u ana
mongosh "mongodb://db1.internal/?authMechanism=GSSAPI&authSource=$external" -u ana@EXAMPLE.COM
mongosh "mongodb://db1.internal/?authMechanism=MONGODB-OIDC"

Queryable Encryption: query fields the server cannot read

const encryptedFields = { fields: [
  { path: 'ssn', bsonType: 'string', queries: { queryType: 'equality' } },
  { path: 'salary', bsonType: 'int', queries: { queryType: 'range', min: 0, max: 1000000 } }
] }
// the driver encrypts before sending; the server stores and compares ciphertext

Hardening Checklist

core

Before a server faces a network

CheckHow
Authentication onsecurity.authorization: enabled
Listen only where needednet.bindIp, never 0.0.0.0 without a firewall
TLS onnet.tls.mode: requireTLS
One user per application, least privilegereadWrite on one database, not root
No server side JavaScript if unusedsecurity.javascriptEnabled: false
Patchedfollow the minor releases of your major version
Backups encrypted and testedrestore one regularly

Network and Log Protection

everyday

Allow an IP address on Atlas

atlas accessLists create 203.0.113.10 --type ipAddress --comment "office"
atlas accessLists list

Keep document contents out of the logs

security:
  redactClientLogData: true
23

Replica Sets

initiating, members, elections, oplog and read preferences

Set Up

core

Start three members, then initiate once

mongod --replSet rs0 --bind_ip localhost,db1.internal --dbpath /var/lib/mongodb --keyFile /etc/mongodb/keyfile
rs.initiate({
  _id: 'rs0',
  members: [
    { _id: 0, host: 'db1.internal:27017' },
    { _id: 1, host: 'db2.internal:27017' },
    { _id: 2, host: 'db3.internal:27017' }
  ]
})

Status

rs.status().members.map(m => ({ name: m.name, state: m.stateStr, health: m.health, lag: m.optimeDate }))

Add, remove, step down

rs.add('db4.internal:27017')
rs.add({ host: 'db5.internal:27017', priority: 0, votes: 0 })
rs.remove('db4.internal:27017')
rs.stepDown(60)                       // primary steps down for 60 seconds, triggers an election
rs.freeze(120)                        // this secondary will not seek election for 120 seconds

Member Options

everyday

Change priorities, hide a member, delay one

cfg = rs.conf()
cfg.members[0].priority = 2                      // preferred primary
cfg.members[2].priority = 0
cfg.members[2].hidden = true                     // invisible to clients: analytics, backups
cfg.members[2].secondaryDelaySecs = 3600         // one hour behind, protects against mistakes
rs.reconfig(cfg)

Member roles

MemberPriorityVotesServes readsUse
Normal secondary11yesfailover
Priority 001yesnever becomes primary, another data centre
Hidden00 or 1nobackups, reporting
Delayed00 or 1noundo window for bad writes
Arbiter01no dataavoid: breaks w: majority when a data member is down

Oplog and Lag

everyday

The oplog window and replication lag

rs.printReplicationInfo()
rs.printSecondaryReplicationInfo()

Resize the oplog without a restart

db.adminCommand({ replSetResizeOplog: 1, size: 16384 })          // MB, run on each member
db.adminCommand({ replSetResizeOplog: 1, minRetentionHours: 72 })

Read preference

db.orders.find().readPref('secondaryPreferred', [{ region: 'eu-west' }])
mongodb+srv://cluster0.example.net/shop?readPreference=secondaryPreferred&maxStalenessSeconds=120

Quick Checks

core

Who is primary, from any member

db.hello().primary
db.hello().isWritablePrimary
rs.status().members.find(m => m.stateStr === 'PRIMARY').name
24

Sharding

shard keys, sharding collections, balancer, resharding and removing shards

Shard a Collection

advanced

Pick the key, then shard

sh.enableSharding('shop')                                             // optional in recent versions
db.orders.createIndex({ customer_id: 1, created_at: 1 })
sh.shardCollection('shop.orders', { customer_id: 1, created_at: 1 })   // ranged
sh.shardCollection('shop.events', { device_id: 'hashed' })             // hashed: even writes

Test a candidate key on real traffic

db.adminCommand({ analyzeShardKey: 'shop.orders', key: { customer_id: 1 }, keyCharacteristics: true, readWriteDistribution: true })
db.adminCommand({ configureQueryAnalyzer: 'shop.orders', mode: 'full', samplesPerSecond: 10 })

Where the data is

sh.status()
db.orders.getShardDistribution()
db.getSiblingDB('admin').aggregate([{ $shardedDataDistribution: {} }])

Change the Layout

advanced

Change the shard key online

db.adminCommand({ reshardCollection: 'shop.orders', key: { customer_id: 'hashed' } })
db.adminCommand({ refineCollectionShardKey: 'shop.orders', key: { customer_id: 1, created_at: 1, _id: 1 } })   // add suffix fields
db.adminCommand({ reshardCollection: 'shop.orders', key: { customer_id: 1 }, forceRedistribution: true })       // same key, spread again

Move or unshard a whole collection

db.adminCommand({ moveCollection: 'shop.audit', toShard: 'shard02' })
db.adminCommand({ unshardCollection: 'shop.logs', toShard: 'shard01' })

The balancer

sh.getBalancerState()
sh.stopBalancer()                  // before a manual backup of the cluster
sh.startBalancer()
db.getSiblingDB('config').settings.updateOne(
  { _id: 'balancer' },
  { $set: { activeWindow: { start: '01:00', stop: '05:00' } } },
  { upsert: true }
)

Remove a shard with the draining commands

db.adminCommand({ startShardDraining: 'shard03' })
db.adminCommand({ shardDrainingStatus: 'shard03' })
db.adminCommand({ commitShardRemoval: 'shard03' })

Queries on a Sharded Cluster

advanced

Targeted versus scatter gather

db.orders.find({ customer_id: cid })            // goes to one shard
db.orders.find({ status: 'paid' })              // asks every shard
db.orders.find({ status: 'paid' }).explain().queryPlanner.winningPlan.stage

Zones

advanced

Keep EU customers on EU shards

sh.addShardToZone('shard-eu-1', 'EU')
sh.addShardToZone('shard-us-1', 'US')
sh.shardCollection('shop.customers', { region: 1, _id: 1 })
sh.updateZoneKeyRange('shop.customers', { region: 'EU', _id: MinKey() }, { region: 'EU', _id: MaxKey() }, 'EU')
sh.updateZoneKeyRange('shop.customers', { region: 'US', _id: MinKey() }, { region: 'US', _id: MaxKey() }, 'US')
25

Backup & Restore

mongodump, mongorestore, mongoexport, snapshots and point in time recovery

mongodump and mongorestore

core

Dump and restore one database

mongodump --uri="$MONGODB_URI" --db=shop --gzip --archive=shop-2026-09-29.archive.gz
mongorestore --uri="$RESTORE_URI" --gzip --archive=shop-2026-09-29.archive.gz --nsInclude='shop.*'

One collection, a subset, or into another database

mongodump --uri="$MONGODB_URI" --db=shop --collection=orders --query='{"status":"paid"}' --out=dump/
mongorestore --uri="$MONGODB_URI" --nsFrom='shop.*' --nsTo='shop_copy.*' dump/
mongorestore --uri="$MONGODB_URI" --drop --nsInclude='shop.orders' dump/

Consistent dump of a running replica set

mongodump --uri="$MONGODB_URI" --oplog --gzip --archive=full.archive.gz
mongorestore --uri="$RESTORE_URI" --oplogReplay --gzip --archive=full.archive.gz

Export and Import

everyday

mongoexport to JSON or CSV

mongoexport --uri="$MONGODB_URI" --collection=orders --query='{"status":"paid"}' --out=paid.json
mongoexport --uri="$MONGODB_URI" --collection=orders --type=csv --fields=_id,status,total,created_at --out=orders.csv
mongoexport --uri="$MONGODB_URI" --collection=orders --jsonArray --pretty --limit=5

Inspect a BSON file

bsondump dump/shop/orders.bson | head -n 3
bsondump --type=debug dump/shop/orders.bson

Snapshots and Point in Time

advanced

Filesystem snapshot of a member

db.fsyncLock()          // flush and block writes on this member
// take the LVM, EBS or ZFS snapshot here
db.fsyncUnlock()

Managed backups with point in time restore

atlas backups snapshots list Cluster0
atlas backups restores start pointInTime --clusterName Cluster0 --pointInTimeUTCSeconds 1790678034 --targetClusterName Cluster0-restore

Choosing a method

MethodGood forPoint in time
mongodump / mongorestoresmall databases, moving dataonly to the end of the dump
Filesystem snapshotslarge self-managed deploymentsno, plus oplog replay by hand
Ops Manager or Cloud ManagerEnterprise deploymentsyes
Atlas cloud backupAtlas clustersyes, with continuous backup
Percona Backup for MongoDBopen source alternativeyes

Copy Between Clusters

everyday

Stream a collection from one cluster to another, with no file in between

mongodump --uri="$SOURCE_URI" --db=shop --collection=orders --archive | \
  mongorestore --uri="$TARGET_URI" --archive --nsInclude='shop.orders' --drop
26

Monitoring

serverStatus, connections, cache, mongostat, mongotop and logs

Server Health

core

Connections and operation counters

db.serverStatus().connections
db.serverStatus().opcounters

The WiredTiger cache

const c = db.serverStatus().wiredTiger.cache
({ usedGB: c['bytes currently in the cache'] / 2 ** 30, maxGB: c['maximum bytes configured'] / 2 ** 30, dirtyGB: c['tracked dirty bytes in the cache'] / 2 ** 30 })

Latency per collection

db.orders.aggregate([{ $collStats: { latencyStats: { histograms: false } } }]).next().latencyStats
db.adminCommand({ top: 1 }).totals['shop.orders']

Command Line Tools

everyday

mongostat and mongotop

mongostat --uri="$MONGODB_URI" 5                 # every 5 seconds
mongotop --uri="$MONGODB_URI" 10                 # time spent per collection

Logs from inside the shell

db.adminCommand({ getLog: 'global' }).log.slice(-5)
db.adminCommand({ getLog: 'startupWarnings' })
db.adminCommand({ setParameter: 1, logLevel: 1 })

What to alert on

MetricAlert when
Replication lagabove a few seconds for minutes
Oplog windowbelow the longest planned maintenance
Connectionsclose to the limit, or climbing steadily
Cache dirty shareabove 20%
Queued readers and writersgrowing, from serverStatus().queues
Disk freebelow the size of the largest collection plus its indexes
Page faults and swapany sustained swap use

Sizes and Managed Metrics

everyday

Every database with its size, largest first

db.adminCommand({ listDatabases: 1 }).databases
  .sort((a, b) => b.sizeOnDisk - a.sizeOnDisk)
  .map(d => `${d.name}: ${(d.sizeOnDisk / 2 ** 30).toFixed(2)} GiB`)

Atlas metrics from the command line

atlas processes list
atlas metrics processes cluster0-shard-00-00.example.net:27017 --granularity PT1M --period PT1H --type CONNECTIONS,OPCOUNTER_QUERY
27

Config & Upgrades

mongod.conf, parameters, FCV and rolling upgrades

Configuration

everyday

Read the running configuration

db.adminCommand({ getCmdLineOpts: 1 }).parsed
db.adminCommand({ getParameter: '*' })
db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })

Change a parameter at runtime

db.adminCommand({ setParameter: 1, cursorTimeoutMillis: 1200000 })
db.adminCommand({ setParameter: 1, transactionLifetimeLimitSeconds: 120 })

Production settings on Linux

# XFS for the data directory, noatime
# ulimit -n 64000 and ulimit -u 64000 for the mongod user (the packaged systemd unit sets them)
# 8.0 and newer: Transparent Huge Pages enabled (always); earlier versions: disabled
# vm.swappiness = 1, and NTP or chrony on every member

Upgrades

everyday

Rolling upgrade of a replica set

# 1. upgrade each secondary: stop mongod, install the new binaries, start, wait for SECONDARY
# 2. step down the primary, then upgrade it the same way
rs.stepDown()
# 3. when every member runs the new version and the application works:
db.adminCommand({ setFeatureCompatibilityVersion: '9.0', confirm: true })

Upgrade paths

FromToPath
6.08.06.0 to 7.0 to 8.0, one major at a time
7.08.0direct
7.09.07.0 to 8.0 to 9.0
8.09.0direct
Driversanyupgrade drivers first; check the compatibility table

Stable API: keep code working across upgrades

const client = new MongoClient(uri, { serverApi: { version: '1', strict: true, deprecationErrors: true } })

Service and Logs

everyday

Control the service on Linux

sudo systemctl start mongod
sudo systemctl restart mongod
sudo systemctl status mongod
journalctl -u mongod --since "10 min ago"
tail -f /var/log/mongodb/mongod.log | jq -c 'select(.s == "W" or .s == "E")'

Rotate the log

db.adminCommand({ logRotate: 1 })
# mongod.conf, when logrotate moves the file:
systemLog:
  logRotate: reopen

Change an Atlas cluster tier without downtime

atlas clusters update Cluster0 --tier M30
atlas clusters watch Cluster0

The in-memory storage engine

storage:
  engine: inMemory
  inMemory:
    engineConfig:
      inMemorySizeGB: 8
28

MongoDB From Code

Node.js, Python, Go, Java and C# drivers, pooling and ObjectId handling

Connect and Query

core

The official driver

import { MongoClient, ObjectId } from 'mongodb'

const client = new MongoClient(process.env.MONGODB_URI, { maxPoolSize: 50 })
const orders = client.db('shop').collection('orders')

const recent = await orders.find({ status: 'paid' }).sort({ created_at: -1 }).limit(20).toArray()
const one = await orders.findOne({ _id: new ObjectId(req.params.id) })
await orders.updateOne({ _id: one._id }, { $set: { status: 'shipped' } })

Mongoose

import mongoose from 'mongoose'
await mongoose.connect(process.env.MONGODB_URI)
const Order = mongoose.model('Order', new mongoose.Schema({
  status: { type: String, enum: ['new', 'paid', 'shipped'], index: true },
  total: Number
}, { timestamps: true }))
const paid = await Order.find({ status: 'paid' }).sort({ createdAt: -1 }).limit(20).lean()

PyMongo, with the async client

import os
from bson import ObjectId
from pymongo import MongoClient, AsyncMongoClient

client = MongoClient(os.environ["MONGODB_URI"])
orders = client.shop.orders
recent = list(orders.find({"status": "paid"}).sort("created_at", -1).limit(20))
one = orders.find_one({"_id": ObjectId(order_id)})

aclient = AsyncMongoClient(os.environ["MONGODB_URI"])
doc = await aclient.shop.orders.find_one({"status": "new"})

Go driver v2

client, err := mongo.Connect(options.Client().ApplyURI(os.Getenv("MONGODB_URI")))
if err != nil { log.Fatal(err) }
defer client.Disconnect(context.Background())

orders := client.Database("shop").Collection("orders")
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}).SetLimit(20)
cur, err := orders.Find(ctx, bson.D{{Key: "status", Value: "paid"}}, opts)
var recent []Order
err = cur.All(ctx, &recent)

Java sync driver

MongoClient client = MongoClients.create(System.getenv("MONGODB_URI"));
MongoCollection<Document> orders = client.getDatabase("shop").getCollection("orders");
List<Document> recent = orders.find(Filters.eq("status", "paid"))
    .sort(Sorts.descending("created_at"))
    .limit(20)
    .into(new ArrayList<>());
orders.updateOne(Filters.eq("_id", new ObjectId(id)), Updates.set("status", "shipped"));

C# driver with typed models

var client = new MongoClient(Environment.GetEnvironmentVariable("MONGODB_URI"));
var orders = client.GetDatabase("shop").GetCollection<Order>("orders");
var recent = await orders.Find(o => o.Status == "paid")
    .SortByDescending(o => o.CreatedAt)
    .Limit(20)
    .ToListAsync();
await orders.UpdateOneAsync(o => o.Id == id, Builders<Order>.Update.Set(o => o.Status, "shipped"));

Driver Habits

everyday

ObjectId from a string, safely

// Node.js
if (!ObjectId.isValid(id)) return res.status(400).end()
const doc = await orders.findOne({ _id: ObjectId.createFromHexString(id) })

Never build filters from raw request bodies

// { "email": { "$ne": null } } in a login body would match any user
const email = String(req.body.email)
const user = await users.findOne({ email })

Pool and timeout options in the connection string

mongodb+srv://cluster0.example.net/shop?maxPoolSize=50&minPoolSize=5&maxIdleTimeMS=60000&serverSelectionTimeoutMS=5000&timeoutMS=10000

Serverless and Short Lived Processes

advanced

Reuse the client across function invocations

let clientPromise
export async function handler(event) {
  clientPromise ??= new MongoClient(process.env.MONGODB_URI, { maxPoolSize: 5 }).connect()
  const client = await clientPromise
  return client.db('shop').collection('orders').findOne({ _id: event.id })
}
29

Error Codes

the server errors and driver messages you will meet, with fixes

Server Error Codes

core

Codes to handle in application code

CodeNameUsual cause and fix
11000DuplicateKeya unique index rejected the value; return a conflict or update instead
121DocumentValidationFailurethe document breaks the collection validator; read errInfo
13Unauthorizedthe user lacks a role for this action or database
18AuthenticationFailedwrong password or wrong authSource
50MaxTimeMSExpiredthe operation hit maxTimeMS; add an index or raise the limit
112WriteConflicttwo transactions wrote the same document; retry the transaction
251NoSuchTransactionthe transaction was aborted or timed out, often after 60 seconds
10107NotWritablePrimarya write reached a secondary after a failover; the driver retries
189PrimarySteppedDownan election is in progress; retryable
146ExceededMemoryLimitthe operation passed the 9.0 per operation memory limit
292QueryExceededMemoryLimitNoDiskUseAlloweda sort or group needed disk with allowDiskUse off
10334BSONObjectTooLargea document or an upsert result would exceed 16 MiB
286ChangeStreamHistoryLostthe resume token fell out of the oplog
26NamespaceNotFoundthe collection or database does not exist
85 / 86IndexOptionsConflict / IndexKeySpecsConflictan index with that key or name already exists with other options

Catch duplicates in Node.js

try {
  await users.insertOne({ email })
} catch (e) {
  if (e.code === 11000) return res.status(409).json({ error: 'email already registered' })
  throw e
}

Connection Errors

everyday

The messages and what they mean

MessageCause
MongoServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017mongod is not running, or listens on another address
querySrv ENOTFOUND _mongodb._tcp.cluster0...DNS cannot resolve the SRV record: check the host name or the DNS server
bad auth : authentication failedAtlas user or password is wrong, or the password needs percent encoding
Server selection timed out after 30000 msfirewall, IP access list, or wrong replica set name
connection <monitor> to ... closedTLS mismatch or a proxy cutting idle connections
not primary and secondaryOk=falsea direct connection to a secondary; add the replica set to the URI

Diagnose from the client machine

mongosh "$MONGODB_URI" --eval "db.runCommand({ ping: 1 })"
nslookup -type=SRV _mongodb._tcp.cluster0.example.net
nc -vz db1.internal 27017
curl -s https://checkip.amazonaws.com          # the public IP to add to the Atlas access list

More Errors and Their Fixes

everyday

Sort without an index on a large result

db.orders.find().sort({ note: 1 }).allowDiskUse(false)

Cursor timed out in a long loop

db.orders.find().forEach(d => slowWork(d))

Write concern not satisfied in time

db.orders.insertOne({ total: 10 }, { writeConcern: { w: 'majority', wtimeout: 2000 } })

Retry a transaction in Python

from pymongo.errors import PyMongoError
try:
    session.with_transaction(transfer)
except PyMongoError as e:
    if e.has_error_label("TransientTransactionError"):
        pass  # with_transaction already retried for 120 seconds; alert or queue the work
    raise
30

Versions & SQL

release support, what changed per version and SQL to MongoDB

Release Support

core

Versions and end of life, as of September 29, 2026

VersionTypeReleasedEnd of life
9.0majorSeptember 2026October 31, 2031
8.3minor releaseMay 4, 2026October 31, 2029
8.2minor release2025ended July 31, 2026
8.0majorOctober 2024October 31, 2029
7.0majorAugust 2023August 31, 2027
6.0majorJuly 2022ended July 31, 2025

What each recent version added

VersionHighlights
7.0compound wildcard indexes, $median and $percentile, Queryable Encryption equality GA, analyzeShardKey
8.0faster reads and writes, express query path, bulkWrite across collections, query settings, sort option on updateOne, QE range queries, moveCollection and unshardCollection, defaultMaxTimeMS
8.3$scoreFusion GA, $$IDX and arrayIndexAs, $convert base, $toArray and $toObject, $hash and $hexHash, $createObjectId, cost based ranker by default, shard draining commands
9.0$where, $function and $accumulator back on WebAssembly, per operation memory limit, cap on open transactions, querySettings per command, constraint validation level, QE prefix, suffix and substring GA, single namespace time series, $queryStats on by default, null semantics on dotted paths

SQL to MongoDB

core

Terms

SQLMongoDB
databasedatabase
tablecollection
rowdocument
columnfield
primary key_id
indexindex
JOIN$lookup, or an embedded document
GROUP BY$group
viewview
transactiontransaction, with a session

The same statements side by side

SQLMongoDB
SELECT * FROM users WHERE age > 30 ORDER BY name LIMIT 10db.users.find({ age: { $gt: 30 } }).sort({ name: 1 }).limit(10)
SELECT name, email FROM usersdb.users.find({}, { name: 1, email: 1, _id: 0 })
SELECT COUNT(*) FROM users WHERE activedb.users.countDocuments({ active: true })
INSERT INTO users (name) VALUES ('Ana')db.users.insertOne({ name: 'Ana' })
UPDATE users SET plan = 'pro' WHERE id = 7db.users.updateOne({ _id: 7 }, { $set: { plan: 'pro' } })
DELETE FROM users WHERE active = falsedb.users.deleteMany({ active: false })
SELECT country, COUNT(*) FROM users GROUP BY countrydb.users.aggregate([{ $group: { _id: '$country', n: { $sum: 1 } } }])
WHERE name LIKE 'An%'{ name: /^An/ }
WHERE email IS NULL{ email: null }
CREATE INDEX ON users (email)db.users.createIndex({ email: 1 })

MongoDB or a relational database

QuestionMongoDBPostgreSQL or MySQL
Data shapenested documents that vary per recordtables with a fixed schema
Relationsembedded or referenced, joins with $lookupforeign keys and joins everywhere
Schema changesadd fields as you go, validate with $jsonSchemamigrations with ALTER TABLE
Scaling writesbuilt in shardingreplicas, partitioning, external sharding
Transactionsmulti document, best kept smallthe default way of working
Ad hoc reportingaggregation pipelineSQL, the widest tool support

Checking and Changing Versions

everyday

Server, shell and FCV versions

db.version()
version()
db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 }).featureCompatibilityVersion.version

Removed, deprecated and replaced

FeatureStatusUse instead
legacy mongo shellremoved in 6.0mongosh
count(), insert(), update(), remove()deprecated in drivers and mongoshcountDocuments(), insertOne(), updateOne(), deleteMany()
index filtersdeprecated since 8.0query settings
removeSharddeprecated in 8.3startShardDraining and commitShardRemoval
mongocryptddeprecated in 9.0the Automatic Encryption Shared Library
$where, $function, $accumulatordeprecated in 8.0, re-enabled in 9.0$expr when possible
net.wireObjectCheckdeprecated in 9.0BSON is validated by default
Motor for PythondeprecatedAsyncMongoClient in PyMongo

The same JSON query in MongoDB and PostgreSQL

TaskMongoDBPostgreSQL jsonb
Field equals{ 'meta.tier': 'gold' }meta ->> 'tier' = 'gold'
Contains{ tags: 'oak' }tags @> '["oak"]'
Field exists{ phone: { $exists: true } }meta ? 'phone'
Index itcreateIndex({ 'meta.tier': 1 })CREATE INDEX ON t ((meta ->> 'tier'))
Set a field$set: { 'meta.tier': 'pro' }jsonb_set(meta, '{tier}', '"pro"')

What Is a MongoDB Cheat Sheet

A MongoDB cheat sheet is a one page reference to the mongosh commands, query and update operators, aggregation stages and administration tasks of the MongoDB database, laid out so you can find the right syntax in seconds instead of searching the manual. People also call it a mongosh cheat sheet or a Mongo shell cheat sheet, and they mean the same mix of shell commands, queries and pipelines.

This MongoDB cheat sheet covers 30 sections and 276 snippets for MongoDB 7.0, 8.0, 8.3 and 9.0. Every snippet that needs a newer release carries a version chip, and the server version picker in the toolbar hides everything your server cannot run. Rows that need Enterprise Advanced or Atlas carry a chip too, and community mode hides them. Each card holds copyable examples with a one line description, the output mongosh prints where it helps, and a short explanation of why MongoDB behaves that way.

The query toolbox at the top formats and minifies mongosh code, lints queries and pipelines for common mistakes, converts SQL statements into find and aggregate calls, reads explain() output in plain words and suggests an index, converts between mongosh syntax and Extended JSON, rewrites mongosh calls as PyMongo code, and turns CSV or JSON into insertMany calls. The command builders write mongodump and mongorestore commands, connection strings, users with the right roles, indexes that follow the ESR rule and a starting mongod.conf. None of it sends anything to a server.

How It Differs From the Documentation

The MongoDB manual is thorough and spread across hundreds of pages. This page shows the shortest working form of each command next to the mistake that usually comes with it. For the SQL side of the same ideas, see the SQL cheat sheet; for a relational database with strong JSON support, the PostgreSQL cheat sheet.


What This MongoDB Cheat Sheet Covers

The 30 sections follow the life of a database, from the first connection to running it in production, and each link jumps to that section above.


MongoDB Commands List

The commands people look up most often, one line each. The sections above show each of them with options, output and the mistakes that come with them.

TaskCommand
Connect with mongoshmongosh "mongodb+srv://cluster0.example.net/shop" -u app
Show the server versiondb.version()
List databasesshow dbs
Switch databaseuse shop
List collectionsshow collections
Create a collectiondb.createCollection('orders')
Delete a collectiondb.orders.drop()
Delete a databasedb.dropDatabase()
Insert a documentdb.orders.insertOne({ status: 'new', total: 40 })
Insert many documentsdb.orders.insertMany([{ ... }, { ... }])
Find documentsdb.orders.find({ status: 'paid' })
Find one documentdb.orders.findOne({ _id: ObjectId('...') })
Choose fieldsdb.orders.find({}, { total: 1, _id: 0 })
Sort and limitdb.orders.find().sort({ created_at: -1 }).limit(10)
Count documentsdb.orders.countDocuments({ status: 'paid' })
Distinct valuesdb.orders.distinct('status')
Update one fielddb.orders.updateOne({ _id: id }, { $set: { status: 'shipped' } })
Update many documentsdb.orders.updateMany({ status: 'new' }, { $set: { status: 'expired' } })
Insert or updatedb.visits.updateOne({ page }, { $inc: { n: 1 } }, { upsert: true })
Add to an arraydb.posts.updateOne({ _id: 1 }, { $push: { tags: 'db' } })
Delete documentsdb.sessions.deleteMany({ expires_at: { $lt: new Date() } })
Group and countdb.orders.aggregate([{ $group: { _id: '$status', n: { $sum: 1 } } }])
Join two collections{ $lookup: { from: 'customers', localField: 'customer_id', foreignField: '_id', as: 'customer' } }
Create an indexdb.orders.createIndex({ customer_id: 1, created_at: -1 })
List indexesdb.orders.getIndexes()
See a query plandb.orders.find({ ... }).explain('executionStats')
Create a userdb.createUser({ user: 'app', pwd: passwordPrompt(), roles: ['readWrite'] })
Replica set statusrs.status()
Sharding statussh.status()
Running operationsdb.currentOp({ active: true })
Back up a databasemongodump --uri="$URI" --db=shop --gzip --archive=shop.gz
Restore a backupmongorestore --uri="$URI" --gzip --archive=shop.gz
Export to CSVmongoexport --collection=orders --type=csv --fields=_id,total --out=o.csv
Leave mongoshexit

mongosh Cheat Sheet

mongosh is the MongoDB Shell, a Node.js based REPL that replaced the legacy mongo shell. Besides every JavaScript expression, it understands these helpers.

HelperWhat it does
help, db.help(), db.orders.help()help at each level
show dbs, show collections, show users, show roleslist objects
use nameswitch database
itthe next batch of a cursor
db.getSiblingDB('admin')another database without switching
db.getCollection('a-b')a collection whose name is not a valid identifier
printjson(x), EJSON.stringify(x)print a value, or as Extended JSON
load('file.js')run a script in the current shell
passwordPrompt()ask for a password without echoing it
config.set('editor', 'vim'), edit xedit a variable in an external editor
sleep(ms)pause a script
rs.*, sh.*replica set and sharding helpers
exit, quit()leave

What MongoDB Is

MongoDB is a document database: instead of rows in tables, it stores records as documents, JSON like objects saved in a binary format called BSON, grouped into collections. A document can hold nested objects and arrays, and documents in the same collection do not need the same fields, so the shape of the data can follow the shape of the objects in your code.

It was first released in 2009 by 10gen, the company now called MongoDB, Inc. The server is source available under the Server Side Public License; Community Edition is free, Enterprise Advanced adds features such as LDAP, auditing and encryption at rest, and Atlas is the managed cloud service. MongoDB 8.0 arrived in October 2024, and MongoDB 9.0 was released in September 2026.

Its strengths are a flexible schema, a query language that works naturally with nested data, built in replication with automatic failover, and horizontal scaling through sharding. The aggregation pipeline, change streams, time series collections, and full text and vector search cover much of what used to need separate systems.

Where It Fits

MongoDB suits product catalogues, content, user profiles, event and IoT data, mobile and web back ends and other workloads where records vary in shape and are read as a whole. It is a common choice for a cloud-based app and for microservices that each own their data. For heavily relational data with many joins and ad hoc reporting, a relational database is usually the simpler tool; our guide to the best database for mobile apps compares the options.


How MongoDB Runs a Query

A find or aggregate goes through the same steps on every server, and knowing them explains most of this cheat sheet.

  1. The driver picks a server using the read preference and sends the command over a pooled connection; on a sharded cluster it goes to mongos, which routes it to the shards that own the data.
  2. The server parses the filter into a query shape and checks the plan cache for a plan that won before for the same shape.
  3. Without a cached plan, the planner lists candidate plans from the available indexes, runs them side by side for a short trial, and keeps the one that returns results with the least work.
  4. The executor walks the chosen index (IXSCAN), fetches the documents it points to (FETCH), applies the rest of the filter, and sorts in memory only when no index provides the order.
  5. Results go back in batches of up to 16 MiB; the cursor stays open on the server and getMore fetches the next batch as the client iterates.
  6. A write changes the document in the WiredTiger cache, records the change in the journal and the oplog, and is acknowledged once the members required by the write concern have it.
  7. Secondaries copy the oplog and apply the same changes, which is what drives replication, change streams and point in time recovery.

Most slow queries go wrong at step 3 or 4, when no index matches the filter and sort, so the server reads far more documents than it returns. The explain section shows how to see this, and the index section how to fix it.


MongoDB, PostgreSQL and MySQL Compared

The differences that matter when choosing, or when moving code from one to another.

QuestionMongoDBPostgreSQLMySQL
Data modeldocuments in collectionstables, with jsonb for documentstables, with a JSON type
Schemaflexible, optional $jsonSchema validationfixed, changed with migrationsfixed, changed with migrations
Query languageMQL and the aggregation pipelineSQLSQL
Joins$lookup, usually avoided by embeddingfull SQL joinsfull SQL joins
Scaling outbuilt in shardingextensions such as CitusVitess or application sharding
High availabilityreplica sets with automatic failoverstreaming replication plus a tool such as PatroniGroup Replication or InnoDB Cluster
LicenseSSPL for the server, Apache for driversPostgreSQL LicenseGPL plus commercial

Moving from SQL is mostly a change of habits: model around how the data is read, embed instead of joining where the child data is small and bounded, and let indexes follow the queries. The SQL to MongoDB button in the toolbox converts statements, and the comparison section maps the terms. For MySQL specifics, see the MySQL cheat sheet.


Common MongoDB Mistakes

The same problems show up in most MongoDB deployments that grew without someone watching them.

In Schema and Queries

  • Arrays that grow without limit inside one document, until updates slow down and the 16 MiB limit is near.
  • A relational schema copied as is, with a $lookup on every request.
  • Compound indexes in the wrong order, so the server sorts in memory; follow the equality, sort, range rule.
  • Case insensitive or unanchored regex searches on large collections instead of a collation index or search.
  • Large skip values for pagination, where keyset pagination stays fast on every page.
  • Dates stored as strings and money stored as doubles.

In Operations

  • Servers reachable from the internet without authentication, or with bindIp set to 0.0.0.0 and no firewall.
  • A new MongoClient per request, which exhausts connections under load.
  • Two member replica sets, or arbiters, that cannot keep a majority when one member fails.
  • An oplog window shorter than the longest maintenance, forcing full resyncs.
  • Backups that were never restored.
  • Raising the feature compatibility version the moment the binaries are upgraded, which removes the easy way back.

How to Learn MongoDB Well

Start With Documents and Queries

Run a local server in a container with the Docker cheat sheet, load a sample dataset and practise find, the update operators and the aggregation pipeline in mongosh. Because documents are JSON at heart, the JSON cheat sheet helps with the format itself, and the CSV to JSON converter turns spreadsheets into documents you can import. Then read explain('executionStats') for every query that matters until IXSCAN, FETCH and SORT feel familiar.

Then Learn to Design and Operate It

Schema design is where MongoDB projects succeed or fail, so study the embedding rules and patterns before a large project. After that, run a three member replica set, practise a restore, and learn how the choice of shard key limits software scalability later. On Android, our guide on how to use MongoDB in Android Studio covers the mobile side, and the Python cheat sheet pairs well with PyMongo.


How This MongoDB Cheat Sheet Is Maintained

This page is written and maintained by Bogdan Sandu for TMS Outsource, a software development company. It was checked against the MongoDB 9.0, 8.3 and 8.0 release notes and the current manual on September 29, 2026. If a snippet is wrong or out of date, the date in the byline shows when it was last reviewed.


MongoDB Questions People Actually Ask

The questions developers search for most about MongoDB and about this cheat sheet, answered in a few sentences each.

What is MongoDB used for?

MongoDB stores application data as JSON like documents: product catalogues, user profiles, content, orders, sessions, events and sensor readings. It runs web and mobile back ends, real time analytics with change streams and the aggregation pipeline, IoT platforms with time series collections, and AI features with vector search, either self-managed or as the Atlas cloud service.

Is MongoDB a SQL or NoSQL database?

MongoDB is a NoSQL document database. It does not use SQL tables or the SQL language; queries are written as documents, such as { status: 'paid' }, and more complex work goes through the aggregation pipeline. It still offers what many people associate with SQL databases, including secondary indexes, joins through $lookup, schema validation and multi document ACID transactions.

What is the difference between mongo and mongosh?

mongo was the legacy shell, deprecated in 5.0 and removed from the server packages in 6.0. mongosh is its replacement: a Node.js based shell with syntax highlighting, autocompletion, better error messages and the same helpers such as show dbs and use. Scripts written for mongo mostly run unchanged, apart from number handling, since mongosh stores whole numbers as 32 bit integers instead of doubles.

How do I list databases and collections in MongoDB?

In mongosh, show dbs lists the databases that contain data, use name switches to one, and show collections or db.getCollectionNames() lists its collections. From a driver, call listDatabases on the admin database and listCollections on a database. A database created with use appears in the list only after its first write.

Which MongoDB version should I use?

Use the latest major version your platform and drivers support. As of September 2026 that is MongoDB 9.0 for new projects, with 8.0 as the conservative choice, supported until October 31, 2029. MongoDB 7.0 reaches end of life on August 31, 2027, so plan the move from it, one major version at a time, and raise the feature compatibility version only after the application has run on the new binaries for a while.

How do I join collections in MongoDB?

Use the $lookup stage in an aggregation pipeline: it matches localField in the input documents with foreignField in the other collection and adds the matches as an array. Index the foreign field, and $unwind the result when you expect one match. Often the better answer is to embed the related data, or copy the few fields you display, so no join is needed at read time.

How do I speed up a slow MongoDB query?

Run explain('executionStats') and compare nReturned with totalKeysExamined and totalDocsExamined. A COLLSCAN or a large gap between documents examined and returned means a missing or poorly ordered index; a SORT stage means the index does not match the sort. Build a compound index in equality, sort, range order, project only the fields you need, and use the profiler or $queryStats to find the queries worth fixing first.

How do I back up and restore a MongoDB database?

For small and medium databases, mongodump with --gzip and --archive creates a compressed backup that mongorestore loads back, and --oplog makes a dump of a running replica set consistent. For large deployments use filesystem snapshots of a hidden secondary, Ops Manager or Cloud Manager, Percona Backup for MongoDB, or Atlas cloud backup with point in time restore. Restore a backup regularly to prove it works.

Is this MongoDB cheat sheet up to date?

Yes. It covers MongoDB 7.0, 8.0, 8.3 and 9.0 and was checked against the release notes and the manual on September 29, 2026. Every snippet that needs a newer release carries a version chip, the server picker in the toolbar hides what your version cannot run, community mode hides Enterprise and Atlas only rows, and the date in the byline changes with every revision.

Can I download this MongoDB cheat sheet as a PDF?

Yes. The page has a print stylesheet, so your browser's print command with Save as PDF produces a clean copy: the dark theme switches to a print palette, every output and explanation is expanded, and the search box, navigation and footer are left out. The print core button prints only the core snippets for a short reference, and you can collapse sections or run a search first to print only what is still visible.