The MongoDB facts interviewers ask, from BSON to sharding. Shell examples use mongosh; defaults are for current releases (5.0 and later) unless a version is given.
Documents, BSON & _id
A document is a BSON object: ordered fields, plus types JSON lacks (ObjectId, Date, Int32, Long, Decimal128, binary).
mongosh numbers are doubles by default. Use NumberInt(5), NumberLong(5) or Decimal128("9.99") (money as Decimal128 or integer cents).
Every document in a standard collection needs a unique _id: the primary key, always indexed, immutable. If you omit it, the driver generates an ObjectId.
_id can be any BSON type except an array, regex or undefined, so a natural key such as an email works.
ObjectId = 12 bytes: 4-byte timestamp (seconds), 5-byte per-process random value, 3-byte counter. It is roughly time-ordered; ObjectId().getTimestamp() returns its creation time.
Max document size is 16 MiB and max nesting 100 levels; store larger files with GridFS.
Dot notation reaches into embedded documents and array positions: "address.city", "items.0.sku".
Collections are schemaless by default; enforce a shape with a $jsonSchema validator (validationLevel, validationAction).
SQL
MongoDB
table / row / column
collection / document / field
primary key
_id
JOIN
embedding, or $lookup
WHERE / SELECT a, b
filter / projection ($match / $project)
GROUP BY
$group
foreign key
reference (never enforced)
materialized view
$merge into a collection
CRUD in mongosh
Task
Method
Note
Insert
insertOne(doc), insertMany(docs)
insertMany is ordered by default: it stops at the first error
{ tags: "red" } matches arrays containing "red"; { tags: ["red", "blank"] } matches that exact array in that order; $all ignores order.
{ dims: { $gt: 15, $lt: 20 } } can be satisfied by two different elements; { dims: { $elemMatch: { $gt: 15, $lt: 20 } } } needs one element to satisfy both.
$ne, $nin and $regex count as range predicates, not equality.
Prefix rule: { a: 1, b: 1, c: 1 } serves queries on a, a, b and a, b, c, not on b or c alone.
{ a: 1, b: -1 } can sort by { a: 1, b: -1 } or { a: -1, b: 1 }, but not { a: 1, b: 1 }.
Covered query: every filtered and returned field is in one index and _id is excluded ({ _id: 0 }) unless indexed.
A collection allows 64 indexes; each costs RAM and write time. Since 4.2 builds lock the collection only at the start and end, and the background option is ignored.
explain()
db.orders.find({ status: "A" }).explain("executionStats"); for pipelines, db.orders.explain("executionStats").aggregate([...]). Modes: queryPlanner (default), executionStats, allPlansExecution.
Stage
Meaning
COLLSCAN
full collection scan: no usable index
IXSCAN
index scan
FETCH
loads documents after the index scan
SORT
in-memory sort: no index supports the sort
IXSCAN with no FETCH above it
covered query
SHARD_MERGE, SHARDING_FILTER
merges shard results, filters orphan documents
Healthy: nReturned ≈ totalKeysExamined ≈ totalDocsExamined. totalDocsExamined far above nReturned means a weak index; 0 means covered.
Find slow queries with the profiler: db.setProfilingLevel(1, { slowms: 100 }), then db.system.profile.find(). Force a plan with .hint({ status: 1 }); check index use with $indexStats.
Aggregation pipeline
Stage
Does
SQL analogue
$match
filters; put it first to use indexes
WHERE / HAVING
$project, $set, $unset
reshapes, adds or removes fields
select list
$group
groups by _id with $sum, $avg, $min, $max, $push, $addToSet, $first
GROUP BY
$sort, $skip, $limit
orders and pages
ORDER BY, LIMIT
$lookup
left outer join to a collection in the same database
Each stage may use 100 MB of RAM; since 6.0 allowDiskUseByDefault is true, so big $sort and $group stages spill to disk.
A leading $match or $sort can use indexes (the optimizer moves filters forward where it can); $lookup uses indexes on the foreign collection. Each output document must fit in 16 MiB.
Transactions & consistency
Single-document writes are always atomic, embedded arrays included. Multi-document ACID transactions arrived in 4.0 (replica sets) and 4.2 (sharded clusters).
Transactions are aborted after 60 s by default (transactionLifetimeLimitSeconds). Keep them short; a schema that needs one per request should probably embed more.
The callback API (withTransaction) retries on TransientTransactionError and UnknownTransactionCommitResult; with the core API you write the retries. Pass the session to every operation.
Heartbeats every 2 s. If the primary is silent for electionTimeoutMillis (10 s), a secondary calls an election; median failover is about 12 s, with no writes until it ends.
Winning needs a majority of voting members, so use an odd number. Arbiters vote but hold no data. Special members: priority 0 (never primary), hidden (no client reads), delayed (a lagging copy to recover from mistakes).
Writes not yet on a majority can be rolled back after a failover: that is why w: "majority" is the default.
Sharded cluster: shards (each a replica set), mongos routers (clients connect here) and config servers (metadata).
Data is split into ranges (chunks) of shard key values, 128 MB by default. The balancer migrates data once two shards differ by three times the range size.
Queries that include the shard key are targeted; the rest scatter-gather to every shard.
Change the key with reshardCollection (5.0+), or add suffix fields with refineCollectionShardKey.
Unique indexes must be prefixed by the shard key, and _id is unique only per shard unless it is the shard key.
Shard key
Result
{ createdAt: 1 } or { _id: 1 } with ObjectId
monotonic: every insert hits one shard
{ country: 1 }
low cardinality: few chunks that cannot split (jumbo)
{ userId: "hashed" }
even writes, targeted equality; range queries broadcast
{ customerId: 1, orderDate: 1 }
targeted per customer, spreads if there are many customers
Limits, pagination & change streams
Limit
Value
Document size / nesting
16 MiB / 100 levels
Indexes per collection / fields per compound index
64 / 32
Replica set members / voting members
50 / 7
Aggregation stages / memory per stage
1000 / 100 MB before spilling
Transaction lifetime
60 s (default)
Idle cursor timeout
10 min
Namespace db.collection
255 bytes (235 when sharded)
skip(n) scans from the start of the results, so it slows down as the offset grows. Paginate by range, with a unique tiebreaker such as _id in the sort.
Change streams: watch() on a collection, a database (db.watch()) or a deployment (Mongo.watch()). They need a replica set or sharded cluster and read from the oplog.
Each event’s _id is a resume token: reopen with resumeAfter or startAfter (which also works after an invalidate event), as long as the oplog still covers that point.
fullDocument: "updateLookup" attaches the current document to update events; pre- and post-images need 6.0+ and changeStreamPreAndPostImages.
unique: true builds a unique index; it is not a validator.
populate() runs a separate query per populated path, not a $lookup.
lean() returns plain objects (the docs measure about 3× smaller): no getters, setters, virtuals, save() or validation.
strict (on by default) drops unknown fields on save; strictQuery has defaulted to false since Mongoose 7.
findOneAndUpdate returns the old document unless you pass { returnDocument: "after" } (new: true is deprecated). Update validators need runValidators: true.
timestamps: true adds createdAt and updatedAt; __v is the version key. autoIndex is on by default; turn it off in production.
Performance anti-patterns
Unbounded arrays: they approach 16 MiB and slow indexes; use buckets, subsets or child references.
Too many collections (one per customer): use one collection with a key field.
Unnecessary indexes: find them with $indexStats; { a: 1 } is usually redundant next to { a: 1, b: 1 }.
Bloated documents: move rarely used data out (subset pattern) and project only what you need.
Too many $lookups: embed or copy what is read together (extended reference).
Case-insensitive regex such as /^ann/i cannot use an index efficiently: use a collation index ({ collation: { locale: "en", strength: 2 } }, queried with the same collation) or store a lowercase copy.
Unanchored regexes, $where, large skip(), SORT stages without an index, and a working set (hot data plus indexes) bigger than RAM.
SQL vs MongoDB
Need
PostgreSQL / SQL
MongoDB
Schema
fixed, migrations
flexible per document, optional validation
Relationships
joins, enforced foreign keys
embedding, $lookup, no enforcement
Transactions
the default model
single document atomic; multi-document costs more
Horizontal write scaling
not built in
built-in sharding
Queries
ad hoc SQL, complex reporting
shaped by access patterns, aggregation pipeline
Good fit
ledgers, strict integrity, analytics
catalogs, content, user profiles, events, IoT
Quick answers
Is MongoDB ACID? Single-document operations always are; multi-document transactions since 4.0 (replica sets) and 4.2 (sharded).
Replication vs sharding? Replication copies the same data for availability and read scaling; sharding partitions data for write and storage scale.
What happens when the primary dies? An election (median about 12 s); writes wait, and drivers retry once.
What is the oplog? A capped collection of idempotent operations that secondaries replay; its size sets how far a member can lag or a change stream can resume.
How do joins work?$lookup does a left outer join; better, embed data that is read together.
Why ObjectId? Generated client-side with no coordination, 12 bytes, roughly time-ordered; that also makes it a poor ranged shard key.
countDocuments vs estimatedDocumentCount? Accurate count with a filter vs fast metadata count, which can drift (orphans, unclean shutdown).
Capped collection? Fixed size, insertion order, oldest documents overwritten. For expiry by age use a TTL index.
Time series collections (5.0+)? They store measurements bucketed automatically.
How do you enforce a schema?$jsonSchema validation on the collection, or Mongoose in the app.
Gotchas & traps
updateOne(filter, { name: "x" }) fails (“Update document requires atomic operators”): use $set, or replaceOne.
{ f: null } matches missing fields too; { f: { $type: "null" } } matches only real nulls.
A unique index lets only one document lack the field; add partialFilterExpression: { f: { $exists: true } }.
Matching a whole embedded document is order-sensitive: { size: { h: 14, w: 21 } } ≠ { size: { w: 21, h: 14 } }. Use dot notation.
Array conditions without $elemMatch can be met by different elements.
TTL deletes are not instant: the monitor runs every 60 s, and documents without a date field never expire.
find() returns a cursor that fetches in batches (mongosh prints the first 20); idle cursors time out after 10 minutes.