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.
[ ]
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.
Install & Connect
installing, connecting, a query toolbox and command builders
Command Builders: mongodump, Connection Strings, Users, Indexes, mongod.conf
Install MongoDB
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
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
mongosh Basics
navigating, printing, scripting and configuring the shell
Navigate
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
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
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
Time a query from the shell
const t0 = Date.now()
db.orders.find({ status: 'paid' }).toArray().length
print(`${Date.now() - t0} ms`)
Databases & Collections
creating, listing, renaming, views, capped and clustered collections
Databases
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
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
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
Names and types of every collection and view
db.getCollectionInfos().map(c => ({ name: c.name, type: c.type, capped: !!c.options.capped }))
BSON Types
ObjectId, dates, numbers, decimals, UUIDs and the $type table
ObjectId and Dates
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
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
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
| Type | Alias | Number |
|---|---|---|
| Double | double | 1 |
| String | string | 2 |
| Object | object | 3 |
| Array | array | 4 |
| Binary data | binData | 5 |
| ObjectId | objectId | 7 |
| Boolean | bool | 8 |
| Date | date | 9 |
| Null | null | 10 |
| Regular expression | regex | 11 |
| 32 bit integer | int | 16 |
| Timestamp | timestamp | 17 |
| 64 bit integer | long | 18 |
| Decimal128 | decimal | 19 |
| Any number | number | none |
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
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' } } }])
Schema Design
embedding, references and the patterns that keep documents small
Embed or Reference
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
| Relationship | Typical model |
|---|---|
| One to one | embed 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 many | arrays of ids on one or both sides |
Design Patterns
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
Insert Documents
insertOne, insertMany, write concern and mongoimport
insertOne and insertMany
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
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
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
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()
Query Operators
find, comparison, logical, element, array and evaluation operators
Filters
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
$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
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
The operators by group
| Group | Operators |
|---|---|
| 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 |
Projection, Sort, Paging
choosing fields, sorting, limits, collation and keyset pagination
Projection
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
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
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
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' }] } }
])
Update Operators
updateOne, updateMany, replace, upserts and update pipelines
Update Documents
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
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
| Operator | Does |
|---|---|
$set | set a field, creating it and any missing parent documents |
$unset | remove a field |
$inc / $mul | add to or multiply a number |
$rename | rename a field |
$min / $max | update only when the new value is lower or higher |
$currentDate | set to the current date or timestamp |
$setOnInsert | set only when an upsert inserts |
Upserts and findOneAndUpdate
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
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'] }
])
Arrays
querying arrays, $push, $pull, positional operators and arrayFilters
Query Arrays
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
$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
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
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
Delete & Bulk Writes
deleteOne, deleteMany, findOneAndDelete and bulkWrite
Delete
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
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
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') } })
Aggregation Basics
the pipeline model, $match, $group, $sort, $project and memory limits
The Pipeline
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
| SQL | Aggregation |
|---|---|
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
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
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 } }
])
Pipeline Stages
$lookup, $unwind, $facet, $bucket, $setWindowFields, $merge and more
Joins and Arrays
$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
$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
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
$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
| Stage | Use it to |
|---|---|
$match | filter; first, to use indexes |
$project, $set, $unset | choose, add or remove fields |
$group, $bucket, $sortByCount | group and summarise |
$sort, $limit, $skip | order and page |
$lookup, $graphLookup, $unionWith | combine collections |
$unwind | one document per array element |
$facet | several pipelines on the same input |
$setWindowFields, $densify, $fill | window functions and gap filling |
$replaceWith | promote a sub document to the top |
$merge, $out | write the result to a collection |
$search, $vectorSearch | full text and vector search |
Expressions
arithmetic, strings, dates, conditions, arrays, conversion and variables
Conditions and Arithmetic
$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
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
$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
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' } }
Indexes
single, compound, multikey, text, wildcard, TTL, partial and unique indexes
Create and Manage
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
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
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()
Explain & Tuning
reading explain(), the profiler, covered queries and query settings
Read explain()
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
| Stage | Meaning |
|---|---|
COLLSCAN | reads every document: no usable index |
IXSCAN | walks an index |
FETCH | reads the documents the index pointed to |
SORT | sorts in memory: the index does not match the sort |
PROJECTION_COVERED | answered from the index alone |
EXPRESS_IXSCAN | fast path for simple equality on a unique index, 8.0+ |
SHARD_MERGE | results 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
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
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
What each verbosity level runs
| Mode | Runs the query | Shows |
|---|---|---|
queryPlanner | no | the winning plan and the rejected plans |
executionStats | yes | plus counts and times for the winning plan |
allPlansExecution | yes | plus the trial results of every candidate plan |
Schema Validation
$jsonSchema validators, validation levels and actions
$jsonSchema Validators
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
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
| Setting | Value | Effect |
|---|---|---|
validationLevel | strict | check every insert and update (default) |
validationLevel | moderate | skip updates to documents that were already invalid |
validationLevel | constraint | 9.0+, every document in the collection is guaranteed to satisfy the rules |
validationLevel | off | no validation |
validationAction | error | reject the write (default) |
validationAction | warn | allow it, write a warning to the log |
validationAction | errorAndLog | reject 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
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 })
Transactions
multi document transactions, sessions, retries and limits
Run a Transaction
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
What to keep in mind
| Rule | Detail |
|---|---|
| Needs a replica set or sharded cluster | a single standalone mongod cannot run transactions |
| 60 second default lifetime | transactionLifetimeLimitSeconds, then the server aborts it |
| Keep them small | about 1000 modified documents is the usual guidance |
| Write conflicts abort | the second writer gets WriteConflict and must retry |
| DDL | creating collections and indexes inside is allowed since 4.4, dropping is not |
| Single document writes are already atomic | a 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
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)
Change Streams
watching inserts, updates and deletes, resuming and pre images
Watch Changes
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
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
| operationType | When |
|---|---|
insert, update, replace, delete | document changes |
drop, rename, dropDatabase | the collection or database went away |
invalidate | the stream can no longer continue; resume with startAfter |
create, createIndexes, modify, shardCollection | DDL events, with showExpandedEvents: true |
Change Streams From Code
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)
}
Search, Vector & Geo
text indexes, Atlas Search, vector search and geospatial queries
Text Index
Basic full text search built into mongod
db.articles.createIndex({ title: 'text', body: 'text' }, { weights: { title: 5 } })
db.articles.find(
{ $text: { $search: 'oak table -pine "dining room"' } },
{ score: { $meta: 'textScore' }, title: 1 }
).sort({ score: { $meta: 'textScore' } }).limit(10)
MongoDB Search
Create a search index, then query with $search
db.products.createSearchIndex('default', { mappings: { dynamic: true } })
db.products.getSearchIndexes()
db.products.aggregate([
{ $search: { index: 'default', text: { query: 'oak tabel', path: ['name', 'description'], fuzzy: { maxEdits: 1 } } } },
{ $limit: 10 },
{ $project: { name: 1, score: { $meta: 'searchScore' } } }
])
Autocomplete
db.products.createSearchIndex('auto', { mappings: { dynamic: false, fields: { name: [{ type: 'autocomplete' }] } } })
db.products.aggregate([{ $search: { index: 'auto', autocomplete: { query: 'oa', path: 'name' } } }, { $limit: 5 }, { $project: { name: 1 } }])
Vector Search
A vector index and a semantic query
db.docs.createSearchIndex({
name: 'vec',
type: 'vectorSearch',
definition: { fields: [
{ type: 'vector', path: 'embedding', numDimensions: 1024, similarity: 'cosine' },
{ type: 'filter', path: 'lang' }
] }
})
db.docs.aggregate([
{ $vectorSearch: { index: 'vec', path: 'embedding', queryVector: qv, numCandidates: 200, limit: 10, filter: { lang: 'en' } } },
{ $project: { title: 1, score: { $meta: 'vectorSearchScore' } } }
])
Hybrid search: combine vector and text results
db.docs.aggregate([
{ $rankFusion: {
input: { pipelines: {
vector: [{ $vectorSearch: { index: 'vec', path: 'embedding', queryVector: qv, numCandidates: 200, limit: 20 } }],
text: [{ $search: { index: 'default', text: { query: 'refund policy', path: 'body' } } }, { $limit: 20 }]
} },
combination: { weights: { vector: 0.7, text: 0.3 } }
} },
{ $limit: 10 }
])
Geospatial
GeoJSON points and a 2dsphere index
db.stores.insertOne({ name: 'Cluj', loc: { type: 'Point', coordinates: [23.5899, 46.7712] } }) // longitude first
db.stores.createIndex({ loc: '2dsphere' })
db.stores.find({ loc: { $near: { $geometry: { type: 'Point', coordinates: [23.6, 46.77] }, $maxDistance: 5000 } } })
Inside a shape, and distance as a field
db.stores.find({ loc: { $geoWithin: { $centerSphere: [[23.6, 46.77], 10 / 6378.1] } } }) // 10 km, radius in radians
db.stores.aggregate([
{ $geoNear: { near: { type: 'Point', coordinates: [23.6, 46.77] }, distanceField: 'meters', maxDistance: 20000, query: { open: true } } },
{ $limit: 5 }
])
Facets
Counts per category with $searchMeta
db.products.createSearchIndex('facets', { mappings: { dynamic: true, fields: { category: { type: 'token' } } } })
db.products.aggregate([
{ $searchMeta: {
index: 'facets',
facet: { operator: { text: { query: 'oak', path: 'name' } }, facets: { cats: { type: 'string', path: 'category' } } }
} }
])
Time Series Collections
storing measurements, granularity, TTL and querying buckets
Create and Insert
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
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
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
Users & Security
authentication, users, roles, TLS, encryption and auditing
Turn On Authentication
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
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
| Role | Grants |
|---|---|
read / readWrite | read, or read and write, the collections of one database |
dbAdmin | indexes, validation, stats; no data access |
dbOwner | readWrite plus dbAdmin plus userAdmin on one database |
userAdmin | create users and roles in one database, which means it can grant itself anything there |
clusterMonitor | read only monitoring: serverStatus, currentOp, rs.status |
clusterManager / clusterAdmin | replication and sharding management |
backup / restore | mongodump and mongorestore |
readAnyDatabase / readWriteAnyDatabase | every database |
root | everything |
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
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
Before a server faces a network
| Check | How |
|---|---|
| Authentication on | security.authorization: enabled |
| Listen only where needed | net.bindIp, never 0.0.0.0 without a firewall |
| TLS on | net.tls.mode: requireTLS |
| One user per application, least privilege | readWrite on one database, not root |
| No server side JavaScript if unused | security.javascriptEnabled: false |
| Patched | follow the minor releases of your major version |
| Backups encrypted and tested | restore one regularly |
Network and Log Protection
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
Replica Sets
initiating, members, elections, oplog and read preferences
Set Up
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
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
| Member | Priority | Votes | Serves reads | Use |
|---|---|---|---|---|
| Normal secondary | 1 | 1 | yes | failover |
| Priority 0 | 0 | 1 | yes | never becomes primary, another data centre |
| Hidden | 0 | 0 or 1 | no | backups, reporting |
| Delayed | 0 | 0 or 1 | no | undo window for bad writes |
| Arbiter | 0 | 1 | no data | avoid: breaks w: majority when a data member is down |
Oplog and Lag
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
Who is primary, from any member
db.hello().primary db.hello().isWritablePrimary rs.status().members.find(m => m.stateStr === 'PRIMARY').name
Sharding
shard keys, sharding collections, balancer, resharding and removing shards
Shard a Collection
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
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
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
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')
Backup & Restore
mongodump, mongorestore, mongoexport, snapshots and point in time recovery
mongodump and mongorestore
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
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
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
| Method | Good for | Point in time |
|---|---|---|
| mongodump / mongorestore | small databases, moving data | only to the end of the dump |
| Filesystem snapshots | large self-managed deployments | no, plus oplog replay by hand |
| Ops Manager or Cloud Manager | Enterprise deployments | yes |
| Atlas cloud backup | Atlas clusters | yes, with continuous backup |
| Percona Backup for MongoDB | open source alternative | yes |
Copy Between Clusters
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
Monitoring
serverStatus, connections, cache, mongostat, mongotop and logs
Server Health
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
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
| Metric | Alert when |
|---|---|
| Replication lag | above a few seconds for minutes |
| Oplog window | below the longest planned maintenance |
| Connections | close to the limit, or climbing steadily |
| Cache dirty share | above 20% |
| Queued readers and writers | growing, from serverStatus().queues |
| Disk free | below the size of the largest collection plus its indexes |
| Page faults and swap | any sustained swap use |
Sizes and Managed Metrics
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
Config & Upgrades
mongod.conf, parameters, FCV and rolling upgrades
Configuration
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
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
| From | To | Path |
|---|---|---|
| 6.0 | 8.0 | 6.0 to 7.0 to 8.0, one major at a time |
| 7.0 | 8.0 | direct |
| 7.0 | 9.0 | 7.0 to 8.0 to 9.0 |
| 8.0 | 9.0 | direct |
| Drivers | any | upgrade 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
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
MongoDB From Code
Node.js, Python, Go, Java and C# drivers, pooling and ObjectId handling
Connect and Query
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
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
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 })
}
Error Codes
the server errors and driver messages you will meet, with fixes
Server Error Codes
Codes to handle in application code
| Code | Name | Usual cause and fix |
|---|---|---|
| 11000 | DuplicateKey | a unique index rejected the value; return a conflict or update instead |
| 121 | DocumentValidationFailure | the document breaks the collection validator; read errInfo |
| 13 | Unauthorized | the user lacks a role for this action or database |
| 18 | AuthenticationFailed | wrong password or wrong authSource |
| 50 | MaxTimeMSExpired | the operation hit maxTimeMS; add an index or raise the limit |
| 112 | WriteConflict | two transactions wrote the same document; retry the transaction |
| 251 | NoSuchTransaction | the transaction was aborted or timed out, often after 60 seconds |
| 10107 | NotWritablePrimary | a write reached a secondary after a failover; the driver retries |
| 189 | PrimarySteppedDown | an election is in progress; retryable |
| 146 | ExceededMemoryLimit | the operation passed the 9.0 per operation memory limit |
| 292 | QueryExceededMemoryLimitNoDiskUseAllowed | a sort or group needed disk with allowDiskUse off |
| 10334 | BSONObjectTooLarge | a document or an upsert result would exceed 16 MiB |
| 286 | ChangeStreamHistoryLost | the resume token fell out of the oplog |
| 26 | NamespaceNotFound | the collection or database does not exist |
| 85 / 86 | IndexOptionsConflict / IndexKeySpecsConflict | an 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
The messages and what they mean
| Message | Cause |
|---|---|
MongoServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017 | mongod 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 failed | Atlas user or password is wrong, or the password needs percent encoding |
Server selection timed out after 30000 ms | firewall, IP access list, or wrong replica set name |
connection <monitor> to ... closed | TLS mismatch or a proxy cutting idle connections |
not primary and secondaryOk=false | a 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
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
Versions & SQL
release support, what changed per version and SQL to MongoDB
Release Support
Versions and end of life, as of September 29, 2026
| Version | Type | Released | End of life |
|---|---|---|---|
| 9.0 | major | September 2026 | October 31, 2031 |
| 8.3 | minor release | May 4, 2026 | October 31, 2029 |
| 8.2 | minor release | 2025 | ended July 31, 2026 |
| 8.0 | major | October 2024 | October 31, 2029 |
| 7.0 | major | August 2023 | August 31, 2027 |
| 6.0 | major | July 2022 | ended July 31, 2025 |
What each recent version added
| Version | Highlights |
|---|---|
| 7.0 | compound wildcard indexes, $median and $percentile, Queryable Encryption equality GA, analyzeShardKey |
| 8.0 | faster 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
Terms
| SQL | MongoDB |
|---|---|
| database | database |
| table | collection |
| row | document |
| column | field |
| primary key | _id |
| index | index |
| JOIN | $lookup, or an embedded document |
| GROUP BY | $group |
| view | view |
| transaction | transaction, with a session |
The same statements side by side
| SQL | MongoDB |
|---|---|
SELECT * FROM users WHERE age > 30 ORDER BY name LIMIT 10 | db.users.find({ age: { $gt: 30 } }).sort({ name: 1 }).limit(10) |
SELECT name, email FROM users | db.users.find({}, { name: 1, email: 1, _id: 0 }) |
SELECT COUNT(*) FROM users WHERE active | db.users.countDocuments({ active: true }) |
INSERT INTO users (name) VALUES ('Ana') | db.users.insertOne({ name: 'Ana' }) |
UPDATE users SET plan = 'pro' WHERE id = 7 | db.users.updateOne({ _id: 7 }, { $set: { plan: 'pro' } }) |
DELETE FROM users WHERE active = false | db.users.deleteMany({ active: false }) |
SELECT country, COUNT(*) FROM users GROUP BY country | db.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
| Question | MongoDB | PostgreSQL or MySQL |
|---|---|---|
| Data shape | nested documents that vary per record | tables with a fixed schema |
| Relations | embedded or referenced, joins with $lookup | foreign keys and joins everywhere |
| Schema changes | add fields as you go, validate with $jsonSchema | migrations with ALTER TABLE |
| Scaling writes | built in sharding | replicas, partitioning, external sharding |
| Transactions | multi document, best kept small | the default way of working |
| Ad hoc reporting | aggregation pipeline | SQL, the widest tool support |
Checking and Changing Versions
Server, shell and FCV versions
db.version()
version()
db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 }).featureCompatibilityVersion.version
Removed, deprecated and replaced
| Feature | Status | Use instead |
|---|---|---|
legacy mongo shell | removed in 6.0 | mongosh |
count(), insert(), update(), remove() | deprecated in drivers and mongosh | countDocuments(), insertOne(), updateOne(), deleteMany() |
| index filters | deprecated since 8.0 | query settings |
removeShard | deprecated in 8.3 | startShardDraining and commitShardRemoval |
mongocryptd | deprecated in 9.0 | the Automatic Encryption Shared Library |
$where, $function, $accumulator | deprecated in 8.0, re-enabled in 9.0 | $expr when possible |
net.wireObjectCheck | deprecated in 9.0 | BSON is validated by default |
| Motor for Python | deprecated | AsyncMongoClient in PyMongo |
The same JSON query in MongoDB and PostgreSQL
| Task | MongoDB | PostgreSQL jsonb |
|---|---|---|
| Field equals | { 'meta.tier': 'gold' } | meta ->> 'tier' = 'gold' |
| Contains | { tags: 'oak' } | tags @> '["oak"]' |
| Field exists | { phone: { $exists: true } } | meta ? 'phone' |
| Index it | createIndex({ '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.
- Foundations: installing MongoDB and connecting with the toolbox and builders, mongosh basics, databases, collections and views, BSON types such as ObjectId, dates and Decimal128, and schema design with embedding, references and patterns.
- CRUD: inserting documents and mongoimport, query operators, projection, sorting and pagination, update operators and upserts, array queries and updates, and deletes and bulk writes.
- Aggregation: the aggregation pipeline, stages such as $lookup, $facet and $setWindowFields, and expressions for strings, dates, arrays and conversions.
- Performance: indexes from compound to TTL and partial, explain() and query tuning, and schema validation.
- Features: transactions, change streams, text search, MongoDB Search, vector search and geospatial queries, and time series collections.
- Operations: users, roles and security, replica sets, sharding, backup and restore, monitoring, and configuration and upgrades.
- Apps and help: MongoDB from Node.js, Python, Go, Java and C#, error codes with fixes, and versions and SQL to MongoDB.
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.
| Task | Command |
|---|---|
| Connect with mongosh | mongosh "mongodb+srv://cluster0.example.net/shop" -u app |
| Show the server version | db.version() |
| List databases | show dbs |
| Switch database | use shop |
| List collections | show collections |
| Create a collection | db.createCollection('orders') |
| Delete a collection | db.orders.drop() |
| Delete a database | db.dropDatabase() |
| Insert a document | db.orders.insertOne({ status: 'new', total: 40 }) |
| Insert many documents | db.orders.insertMany([{ ... }, { ... }]) |
| Find documents | db.orders.find({ status: 'paid' }) |
| Find one document | db.orders.findOne({ _id: ObjectId('...') }) |
| Choose fields | db.orders.find({}, { total: 1, _id: 0 }) |
| Sort and limit | db.orders.find().sort({ created_at: -1 }).limit(10) |
| Count documents | db.orders.countDocuments({ status: 'paid' }) |
| Distinct values | db.orders.distinct('status') |
| Update one field | db.orders.updateOne({ _id: id }, { $set: { status: 'shipped' } }) |
| Update many documents | db.orders.updateMany({ status: 'new' }, { $set: { status: 'expired' } }) |
| Insert or update | db.visits.updateOne({ page }, { $inc: { n: 1 } }, { upsert: true }) |
| Add to an array | db.posts.updateOne({ _id: 1 }, { $push: { tags: 'db' } }) |
| Delete documents | db.sessions.deleteMany({ expires_at: { $lt: new Date() } }) |
| Group and count | db.orders.aggregate([{ $group: { _id: '$status', n: { $sum: 1 } } }]) |
| Join two collections | { $lookup: { from: 'customers', localField: 'customer_id', foreignField: '_id', as: 'customer' } } |
| Create an index | db.orders.createIndex({ customer_id: 1, created_at: -1 }) |
| List indexes | db.orders.getIndexes() |
| See a query plan | db.orders.find({ ... }).explain('executionStats') |
| Create a user | db.createUser({ user: 'app', pwd: passwordPrompt(), roles: ['readWrite'] }) |
| Replica set status | rs.status() |
| Sharding status | sh.status() |
| Running operations | db.currentOp({ active: true }) |
| Back up a database | mongodump --uri="$URI" --db=shop --gzip --archive=shop.gz |
| Restore a backup | mongorestore --uri="$URI" --gzip --archive=shop.gz |
| Export to CSV | mongoexport --collection=orders --type=csv --fields=_id,total --out=o.csv |
| Leave mongosh | exit |
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.
| Helper | What it does |
|---|---|
help, db.help(), db.orders.help() | help at each level |
show dbs, show collections, show users, show roles | list objects |
use name | switch database |
it | the 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 x | edit 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.
- 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.
- The server parses the filter into a query shape and checks the plan cache for a plan that won before for the same shape.
- 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.
- 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.
- 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.
- 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.
- 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.
| Question | MongoDB | PostgreSQL | MySQL |
|---|---|---|---|
| Data model | documents in collections | tables, with jsonb for documents | tables, with a JSON type |
| Schema | flexible, optional $jsonSchema validation | fixed, changed with migrations | fixed, changed with migrations |
| Query language | MQL and the aggregation pipeline | SQL | SQL |
| Joins | $lookup, usually avoided by embedding | full SQL joins | full SQL joins |
| Scaling out | built in sharding | extensions such as Citus | Vitess or application sharding |
| High availability | replica sets with automatic failover | streaming replication plus a tool such as Patroni | Group Replication or InnoDB Cluster |
| License | SSPL for the server, Apache for drivers | PostgreSQL License | GPL 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.