DevTools

Cheatsheet MongoDB

Base de dados NoSQL orientada a documentos (JSON/BSON)

Back to languages
MongoDB
78 cards found
Categories:
Versions:

Databases and Collections


8 cards
List databases
show dbs

// In the Node.js driver:
const dbs = await client.db().admin().listDatabases();
dbs.databases.forEach(d => console.log(d.name, d.sizeOnDisk));

show dbs lists all databases in mongosh. It only shows databases with at least one document. Empty databases do not appear.

Create collection with options
// Simple collection
db.createCollection("customers")

// Capped collection (fixed size, log-like)
db.createCollection("logs", {
  capped: true,
  size: 1048576,    // 1 MB
  max: 5000         // max. documents
})

createCollection() creates explicitly. capped collections have a fixed size and overwrite the oldest ones — ideal for logs. Most collections do not need this.

Select / create database
use store

// The database is created automatically
// when the first document is inserted
db.products.insertOne({ name: "Test" });

use name switches to the given database. If it does not exist, it is created implicitly on the first insert. There is no explicit CREATE DATABASE command.

Drop collection
db.customers.drop()

// Also drops all associated indexes
// Returns true if it existed, false otherwise

// Check first:
if (db.getCollectionNames().includes("temp")) {
  db.temp.drop();
}

drop() deletes the collection, its documents and all indexes. It is irreversible. To clear data while keeping the structure, use deleteMany({}).

Current database
db              // shows the current database name
db.getName()    // explicit alternative

// Statistics
db.stats()
db.stats().objects  // total documents

db returns the reference to the current database. db.stats() shows statistics such the size, number of documents and indexes.

Drop database
// Drops the current database
db.dropDatabase()

// Confirmation:
show dbs  // no longer appears

db.dropDatabase() deletes the entire current database — data, collections and indexes. A destructive and irreversible operation. Always confirm with db.getName() first.

List collections
show collections

// Or programmatically:
db.getCollectionNames()

// With details:
db.getCollectionInfos()

show collections lists the collections of the current database. getCollectionNames() returns an array. getCollectionInfos() includes options such the capped and validator.

Rename collection
db.oldCustomers.renameCollection("customers")

// With the option to overwrite the target:
db.temp.renameCollection("customers", true)

renameCollection() changes the collection name. The second argument true allows overwriting an existing collection with the same name. Indexes are preserved.

Insert Documents


8 cards
insertOne
const result = db.customers.insertOne({
  name: "Anna",
  age: 30,
  city: "Lisbon",
  active: true
});

console.log(result.insertedId);

insertOne() inserts a single document. It returns insertedId with the generated ObjectId. If the _id field is not provided, MongoDB creates it automatically.

Arrays in documents
db.customers.insertOne({
  name: "Ray",
  tags: ["vip", "newsletter"],
  phones: [
    { type: "personal", number: "912345678" },
    { type: "work", number: "213456789" }
  ]
});

Fields can contain arrays of simple values or of objects. Arrays of objects are useful for lists with metadata. Limit: a document up to 16 MB.

insertMany
const result = db.customers.insertMany([
  { name: "Ray", age: 25 },
  { name: "Mia", age: 40 },
  { name: "Joe", age: 35 }
]);

console.log(result.insertedCount); // 3
console.log(result.insertedIds);

insertMany() inserts multiple documents in an array. insertedCount confirms how many were inserted. By default, it stops at the first error (ordered).

Data types
db.types.insertOne({
  text: "string",
  integer: NumberInt(42),
  long: NumberLong(9999999999),
  double: 3.14,
  boolean: true,
  date: new Date(),
  null: null,
  objectId: new ObjectId(),
  regex: /pattern/i
});

BSON supports String, Int32, Int64, Double, Boolean, Date, null, ObjectId, Array, Object and Regex.

insertMany (unordered)
db.customers.insertMany(
  [
    { name: "A", _id: 1 },
    { name: "B", _id: 1 },  // duplicate!
    { name: "C", _id: 3 }
  ],
  { ordered: false }
);
// Inserts A and C, ignores B

With { ordered: false }, MongoDB keeps inserting the rest even if one fails. Faster for bulk inserts. Errors end up in writeErrors.

Custom _id
// Use your own ID instead of ObjectId
db.customers.insertOne({
  _id: "anna@email.com",
  name: "Anna"
});

// Or UUID
db.customers.insertOne({
  _id: UUID(),
  name: "Ray"
});

The _id field can be any unique value (string, number, UUID). If omitted, MongoDB generates a 12-byte ObjectId automatically.

Nested documents
db.customers.insertOne({
  name: "Anna",
  address: {
    street: "Liberty Ave",
    number: 42,
    city: "Lisbon",
    zip: "1000-001"
  }
});

MongoDB supports nested objects (embedded documents). Ideal for data accessed together. Maximum depth: 100 levels. Query with dot notation.

Schema validation
db.createCollection("customers", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "email"],
      properties: {
        name: { bsonType: "string" },
        age: { bsonType: "int", minimum: 0 }
      }
    }
  }
});

$jsonSchema validates documents on insert/update. Fields in required are mandatory. bsonType defines the expected type. It rejects invalid documents.

Queries (find)


8 cards
find (all documents)
db.customers.find()

// With readable formatting:
db.customers.find().pretty()

// In the Node.js driver:
const docs = await db.collection("customers").find({}).toArray();

find() without a filter returns all documents in the collection. It returns a cursor — data is loaded in batches, not all at once.

Sorting (sort)
// Ascending by age
db.customers.find().sort({ age: 1 })

// Descending by age
db.customers.find().sort({ age: -1 })

// Multiple fields
db.customers.find().sort({ city: 1, age: -1 })

sort() orders the results. 1 = ascending, -1 = descending. You can combine fields. Without an index, it does an in-memory sort (100 MB limit).

Simple filter
// Exact equality
db.customers.find({ city: "Lisbon" })

// Multiple conditions (implicit AND)
db.customers.find({ city: "Lisbon", active: true })

// By _id
db.customers.find({ _id: ObjectId("65f...") })

Filters use equality by default. Multiple fields in the same object work the AND. To compare by _id, use ObjectId() the a wrapper.

Limit and Skip (pagination)
// First 10
db.customers.find().limit(10)

// Page 3 (skip 20, limit 10)
db.customers.find().sort({ name: 1 }).skip(20).limit(10)

// Recommended order: sort → skip → limit

limit() restricts the number of results. skip() skips documents (pagination). For large datasets, prefer pagination by _id instead of skip.

findOne
const customer = db.customers.findOne({ name: "Anna" });

// Returns the document or null
if (customer) {
  console.log(customer.age);
}

findOne() returns the first matching document (or null). It does not return a cursor — it is direct. Useful when you know there is only one result.

Count documents
// Collection total
db.customers.countDocuments()

// With filter
db.customers.countDocuments({ active: true })

// Fast estimate (uses metadata, does not scan)
db.customers.estimatedDocumentCount()

countDocuments() counts accurately (scans documents). estimatedDocumentCount() is instant but approximate — it uses collection metadata.

Projection (fields)
// Include only name and city (exclude _id)
db.customers.find({}, { name: 1, city: 1, _id: 0 })

// Exclude specific fields
db.customers.find({}, { password: 0, notes: 0 })

// Nested field
db.customers.find({}, { "address.city": 1 })

The second argument of find() is the projection. 1 includes, 0 excludes. _id always comes by default — use _id: 0 to omit it.

distinct
// Unique values of a field
db.customers.distinct("city")
// ["Lisbon", "Porto", "Braga"]

// With filter
db.customers.distinct("city", { active: true })

// In arrays, it returns unique elements
db.customers.distinct("tags")

distinct() returns an array with the unique values of a field. It accepts a filter the the second argument. In array fields, it returns unique individual elements.

Query Operators


8 cards
Comparison ($gt, $lt, $ne)
{ age: { $gt: 18 } }    // greater than
{ age: { $gte: 18 } }   // greater or equal
{ age: { $lt: 65 } }    // less than
{ age: { $lte: 65 } }   // less or equal
{ age: { $ne: 30 } }    // not equal to
{ age: { $eq: 30 } }    // equal to (explicit)

Comparison operators: $gt, $gte, $lt, $lte, $ne, $eq. They work with numbers, strings and dates.

$exists and $type
// Field exists
{ phone: { $exists: true } }

// Field does not exist
{ fax: { $exists: false } }

// Check BSON type
{ age: { $type: "int" } }
{ date: { $type: "date" } }

$exists checks the presence of the field (even if null). $type filters by BSON type: "string", "int", "double", "bool", "date", "array", "object", "null".

$in and $nin
// City is in the list
{ city: { $in: ["Lisbon", "Porto", "Braga"] } }

// City is NOT in the list
{ city: { $nin: ["Faro", "Evora"] } }

// Works with arrays: field contains some value
{ tags: { $in: ["vip", "premium"] } }

$in matches if the value is in the list. $nin is the inverse. In array fields, it checks whether any element matches. More efficient than multiple $or.

$regex (text search)
// Starts with "Anna" (case-insensitive)
{ name: { $regex: "^Anna", $options: "i" } }

// Contains "smith"
{ name: { $regex: "smith", $options: "i" } }

// Alternative syntax
{ name: /^anna/i }

$regex allows regular expression patterns. $options: "i" ignores case. Without an index, it does a collection scan. Prefixes (^) can use an index.

$and and $or
// OR: one of the conditions
{ $or: [
  { age: { $lt: 18 } },
  { vip: true }
]}

// Explicit AND (needed for the same field)
{ $and: [
  { age: { $gt: 18 } },
  { age: { $lt: 65 } }
]}

$or requires at least one true condition. Explicit $and is needed when you apply two operators to the same field (e.g. a range with $gt and $lt).

Nested fields and arrays
// Nested field (dot notation)
{ "address.city": "Porto" }

// Array contains value
{ tags: "vip" }

// Array with a specific condition
{ grades: { $elemMatch: { $gt: 15, $lt: 20 } } }

// Array size
{ tags: { $size: 3 } }

Dot notation accesses nested fields. In arrays, equality checks whether it contains the value. $elemMatch applies multiple conditions to the same element. $size filters by length.

$not and $nor
// NOT: inverts a condition
{ age: { $not: { $gt: 65 } } }

// NOR: no condition can be true
{ $nor: [
  { city: "Lisbon" },
  { vip: true }
]}

$not inverts an operator (includes documents without the field). $nor is the opposite of $or — none can match. Careful: $not includes docs where the field does not exist.

$expr (expressions)
// Compare two fields of the same document
{ $expr: { $gt: ["$spending", "$budget"] } }

// With aggregation inside find
{ $expr: {
  $and: [
    { $eq: ["$status", "active"] },
    { $gte: ["$balance", 100] }
  ]
}}

$expr allows using aggregation expressions inside find(). Useful for comparing fields against each other. The $ prefix references field values.

Update Documents


8 cards
updateOne ($set)
db.customers.updateOne(
  { name: "Anna" },
  { $set: { age: 31, city: "Porto" } }
);

// Result: { matchedCount: 1, modifiedCount: 1 }

updateOne() updates the first document that matches the filter. $set defines new values for fields. If the field does not exist, it is created.

$push and $pull (arrays)
// Add to the array
db.customers.updateOne(
  { _id: id },
  { $push: { tags: "premium" } }
);

// Remove from the array
db.customers.updateOne(
  { _id: id },
  { $pull: { tags: "old" } }
);

// Add several
{ $push: { tags: { $each: ["a", "b"] } } }

$push adds an element to the array (creates it if it does not exist). $pull removes all matching elements. $each adds multiple at once.

updateMany
db.customers.updateMany(
  { city: "Lisbon" },
  { $set: { region: "Center-South" } }
);

// Result: { matchedCount: 150, modifiedCount: 150 }

updateMany() updates all documents that match the filter. It returns matchedCount and modifiedCount. Use an empty filter {} to update all.

Upsert
db.customers.updateOne(
  { email: "anna@mail.com" },
  {
    $set: { name: "Anna", visits: 1 },
    $setOnInsert: { createdAt: new Date() }
  },
  { upsert: true }
);

upsert: true creates the document if none matches the filter. $setOnInsert is only applied on creation (not on update). A powerful combination for "create or update".

$inc (increment)
// Add 10 to the points
db.customers.updateOne(
  { _id: id },
  { $inc: { points: 10 } }
);

// Decrement
db.customers.updateOne(
  { _id: id },
  { $inc: { stock: -1 } }
);

$inc adds a value to the field (creates it with that value if it does not exist). It accepts negatives to decrement. An atomic operation — safe under concurrency.

replaceOne
db.customers.replaceOne(
  { _id: id },
  { name: "New Name", age: 25 }
);
// Replaces EVERYTHING (except _id)
// Fields not included are removed!

replaceOne() replaces the entire document with the new object. Fields not included are lost. Use updateOne() with $set for partial updates.

$unset (remove field)
// Remove a field
db.customers.updateOne(
  { _id: id },
  { $unset: { fax: "" } }
);

// Remove a nested field
db.customers.updateOne(
  { _id: id },
  { $unset: { "address.floor": "" } }
);

$unset removes a field from the document. The value ("") is irrelevant. It works with nested fields via dot notation. It does not remove the document.

$rename and $mul
// Rename field
db.customers.updateMany(
  {},
  { $rename: { "tel": "phone" } }
);

// Multiply value
db.products.updateOne(
  { _id: id },
  { $mul: { price: 1.1 } }  // +10%
);

$rename changes the name of a field in all documents. $mul multiplies the numeric value. If the field does not exist, $mul creates it with value 0.

Delete Documents


8 cards
deleteOne
const result = db.customers.deleteOne({ name: "Anna" });
console.log(result.deletedCount); // 1

// Only deletes the FIRST that matches
// even if several have the name "Anna"

deleteOne() removes only the first document that matches the filter. It returns deletedCount. To delete by _id, it is always unique.

Delete by _id
// By ObjectId
db.customers.deleteOne({ _id: ObjectId("65f3a...") });

// Multiple IDs
db.customers.deleteMany({
  _id: { $in: [ObjectId("..."), ObjectId("...")] }
});

Deleting by _id is the most efficient way — it uses the primary index automatically. ObjectId() converts the string. $in allows deleting several at once.

deleteMany
const result = db.customers.deleteMany({ active: false });
console.log(result.deletedCount); // N

// With a complex condition
db.customers.deleteMany({
  $and: [
    { lastLogin: { $lt: new Date("2023-01-01") } },
    { plan: "free" }
  ]
});

deleteMany() removes all documents that match. It accepts complex filters with $and, $or, comparison operators, etc.

Bulk delete (bulkWrite)
db.customers.bulkWrite([
  { deleteOne: { filter: { name: "A" } } },
  { deleteOne: { filter: { name: "B" } } },
  { deleteMany: { filter: { active: false } } }
]);

bulkWrite() runs multiple operations (delete, insert, update) in a single request. More efficient than individual calls. It reduces round-trips to the server.

Delete all documents
// Removes all (keeps collection and indexes)
db.customers.deleteMany({});

// Faster alternative (removes everything):
db.customers.drop();
db.createCollection("customers");
// ⚠️ drop() also deletes indexes!

deleteMany({}) clears data but keeps the collection and the indexes. drop() is faster but removes everything — you will have to recreate the indexes.

TTL (auto-expiration)
// Documents expire after 1 hour
db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 3600 }
);

// Insert with a date
db.sessions.insertOne({
  userId: "u1",
  createdAt: new Date()
});

TTL (Time-To-Live) indexes delete documents automatically after X seconds. MongoDB checks every 60s. Ideal for sessions, tokens and temporary data.

findOneAndDelete
const doc = db.customers.findOneAndDelete(
  { email: "anna@mail.com" },
  { projection: { name: 1, email: 1 } }
);

// Returns the deleted document (or null)
console.log("Deleted:", doc.name);

findOneAndDelete() removes and returns the document in an atomic operation. Useful for processing queues. It accepts a projection to limit the returned fields.

Care when deleting
// ❌ Dangerous: deletes EVERYTHING without a filter
db.customers.deleteMany()

// ✅ Always with a specific filter
db.customers.deleteMany({ _id: id })

// ✅ Check before deleting
const count = db.customers.countDocuments({ active: false });
print(`Will delete ${count} documents`);

Always confirm the filter before deleteMany(). Use countDocuments() first to check how many will be affected. In production, do a backup before mass operations.

Aggregation Pipeline


10 cards
Basic pipeline
db.customers.aggregate([
  { $match: { active: true } },
  { $sort: { age: -1 } },
  { $limit: 5 },
  { $project: { name: 1, age: 1, _id: 0 } }
]);

aggregate() processes documents in a sequence of stages (pipeline). Each stage transforms the result of the previous one. Typical order: $match$sort$limit$project.

$lookup (join)
db.orders.aggregate([
  { $lookup: {
    from: "customers",
    localField: "customerId",
    foreignField: "_id",
    the: "customer"
  }},
  { $unwind: "$customer" }
]);

$lookup does a left outer join with another collection. from is the target collection, localField/foreignField are the keys. The result is an array — use $unwind for a single object.

$lookup (JOIN)
// "JOIN" between collections:
db.orders.aggregate([
  {
    $lookup: {
      from: "customers",        // collection
      localField: "customer_id", // local field
      foreignField: "_id",      // field in the other
      the: "customer"            // result name
    }
  },
  { $unwind: "$customer" }  // optional: 1 object
]);

// Each order ends up with the
// customer document embedded

$lookup is the equivalent of a JOIN: it links documents of two collections by localField = foreignField. The result goes into an array (the). Use $unwind to turn it into a single object.

$group (group by)
db.customers.aggregate([
  { $group: {
    _id: "$city",
    total: { $sum: 1 },
    avgAge: { $avg: "$age" },
    oldest: { $max: "$age" }
  }}
]);

$group groups by _id (the grouping expression). $sum: 1 counts. $avg, $min, $max compute statistics. The $ prefix references fields.

$addFields and $set
{ $addFields: {
  total: { $multiply: ["$price", "$quantity"] },
  vat: { $multiply: ["$price", 0.23] },
  formattedDate: {
    $dateToString: { format: "%d/%m/%Y", date: "$created" }
  }
}}

$addFields (alias $set) adds fields without removing the existing ones. Useful for intermediate calculations in the pipeline. $dateToString formats dates.

$unwind
// Document with an array:
// { name: "Anna", tags: ["a", "b"] }

db.products.aggregate([
  { $unwind: "$tags" }
]);

// Generates 1 document per element:
// { name: "Anna", tags: "a" }
// { name: "Anna", tags: "b" }

// With preserveNullAndEmptyArrays
// it keeps docs with an empty array:
{ $unwind: {
    path: "$tags",
    preserveNullAndEmptyArrays: true
} }

$unwind denormalizes an array: it creates one document per element. Essential before $group by array values. preserveNullAndEmptyArrays: true does not discard documents without the array.

$project (reshape)
{ $project: {
  name: 1,
  isAdult: { $gte: ["$age", 18] },
  fullName: { $concat: ["$name", " ", "$surname"] },
  birthYear: { $subtract: [2025, "$age"] }
}}

$project reshapes documents: it includes, excludes, renames or computes fields. Expressions such the $concat, $subtract, $gte create derived fields.

$bucket and $facet
// Group into ranges
{ $bucket: {
  groupBy: "$age",
  boundaries: [0, 18, 35, 65, 120],
  default: "other",
  output: { total: { $sum: 1 } }
}}

// Multiple aggregations in parallel
{ $facet: {
  byCity: [{ $group: { _id: "$city", n: { $sum: 1 } } }],
  byAge: [{ $group: { _id: "$age", n: { $sum: 1 } } }]
}}

$bucket groups into numeric ranges. $facet runs multiple pipelines in parallel over the same data — it returns an object with each result.

$unwind (unfold arrays)
// Before: { name: "Anna", tags: ["a", "b", "c"] }
// After: 3 documents, one per tag

db.customers.aggregate([
  { $unwind: "$tags" },
  { $group: { _id: "$tags", total: { $sum: 1 } } }
]);
// Counts occurrences of each tag

$unwind creates one document per array element. Combined with $group, it counts frequencies. preserveNullAndEmptyArrays: true keeps docs without the array.

Available accumulators
// In $group:
$sum     // sums values (or $sum: 1 to count)
$avg     // average
$min     // minimum
$max     // maximum
$first   // first value of the group
$last    // last value of the group
$push    // array with all the values
$addToSet // array with unique values
$count   // count (MongoDB 5.0+)

Accumulators are used inside $group. $push creates an array with all values; $addToSet only unique values. $count is a shortcut for $sum: 1.

Indices


10 cards
Create simple index
// Ascending index
db.customers.createIndex({ email: 1 });

// Descending index
db.customers.createIndex({ createdAt: -1 });

// With a custom name
db.customers.createIndex({ email: 1 }, { name: "idx_email" });

createIndex() creates an index to speed up queries. 1 = ascending, -1 = descending. Without an index, MongoDB does a collection scan (scans everything).

List and drop indexes
// List all
db.customers.getIndexes();

// Drop by name
db.customers.dropIndex("email_1");

// Drop all (except _id)
db.customers.dropIndexes();

getIndexes() shows the name, fields and options of each index. dropIndex() removes it by name (default: field_order). dropIndexes() removes all except _id.

Text index
// Full-text search:
db.articles.createIndex(
  { title: "text", body: "text" }
);

// Search:
db.articles.find(
  { $text: { $search: "mongodb indexes" } }
);

// With a relevance score:
db.articles.find(
  { $text: { $search: "mongodb" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });

// Only 1 text index
// per collection

text indexes allow word search across several fields. Use $text + $search. $meta: "textScore" gives relevance for sorting. Limit: one per collection.

Unique index
db.customers.createIndex(
  { email: 1 },
  { unique: true }
);

// Allows multiple null (sparse):
db.customers.createIndex(
  { phone: 1 },
  { unique: true, sparse: true }
);

unique: true prevents duplicate values (error code 11000). sparse: true ignores documents without the field — it allows multiple null. Essential for emails, usernames.

Partial index
// Only indexes active documents
db.customers.createIndex(
  { email: 1 },
  {
    unique: true,
    partialFilterExpression: { active: true }
  }
);

partialFilterExpression creates an index only over documents that meet the condition. Smaller and faster than sparse. Useful when only a fraction of the docs is queried.

TTL index
// Deletes documents automatically
// after X seconds:
db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 3600 }  // 1 hour
);

// Documents with createdAt older
// than 1h are removed
// by a background process

// Useful for: sessions, logs,
// tokens, temporary caches

// The field MUST be a Date

TTL (time-to-live) indexes delete documents automatically after expireAfterSeconds. Ideal for sessions, logs and tokens. The indexed field must be a Date; the cleanup runs in the background.

Compound index
db.customers.createIndex({ city: 1, age: -1 });

// Order matters! This index works for:
db.customers.find({ city: "Lisbon" }).sort({ age: -1 });

// But it does NOT work for:
db.customers.find().sort({ age: -1 }); // without city

Compound indexes cover multiple fields. The order follows the ESR rule: Equality → Sort → Range. Queries must use the index prefix to take advantage of it.

explain (query analysis)
db.customers.find({ city: "Porto" })
  .explain("executionStats");

// Important fields:
// totalDocsExamined: 0 → uses index ✅
// totalDocsExamined: 50000 → collection scan ❌
// executionTimeMillis: time in ms
// indexName: which index was used

explain("executionStats") shows how the query is executed. If totalDocsExamined is much larger than nReturned, an index is missing. An essential optimization tool.

Text index (text)
db.articles.createIndex(
  { title: "text", content: "text" }
);

// Search
db.articles.find(
  { $text: { $search: "mongodb tutorial" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });

text indexes allow full-text search. $text + $search runs the query. $meta: "textScore" gives relevance. Maximum 1 text index per collection.

ESR rule (index order)
// Query:
db.orders.find({ status: "shipped", total: { $gt: 100 } })
  .sort({ date: -1 });

// Ideal index (ESR):
db.orders.createIndex({
  status: 1,   // Equality (first)
  date: -1,    // Sort (second)
  total: 1     // Range (last)
});

ESR rule: equality fields first, sort next, range last. It maximizes index usage. It is not always possible to follow 100% — use explain() to validate.

Tips and Good Practices


10 cards
mongosh (CLI)
// Connect
mongosh
mongosh "mongodb://localhost:27017/store"
mongosh "mongodb+srv://user:pass@cluster.mongodb.net/db"

// Run a file
mongosh script.js
mongosh --eval "db.customers.countDocuments()"

mongosh is MongoDB's modern shell. It supports full JavaScript, autocomplete and --eval for quick commands. It replaces the old mongo shell.

Multi-document transactions
const session = client.startSession();
session.startTransaction();

try {
  await db.collection("accounts").updateOne(
    { _id: "A" }, { $inc: { balance: -50 } }, { session }
  );
  await db.collection("accounts").updateOne(
    { _id: "B" }, { $inc: { balance: 50 } }, { session }
  );
  await session.commitTransaction();
} catch (e) {
  await session.abortTransaction();
}

Transactions guarantee atomicity across multiple documents/collections. They require a replica set or sharded cluster. Use session in all operations. Slower than simple operations.

Backup and restore
# Full backup (binary):
mongodump --uri="mongodb://localhost:27017"

# Only one database:
mongodump --db=store --out=/backups

# Restore:
mongorestore --uri="mongodb://localhost:27017" /backups

# Export/import JSON:
mongoexport --db=store --collection=customers \
  --out=customers.json
mongoimport --db=store --collection=customers \
  --file=customers.json

# mongodump = binary (fast)
# mongoexport = JSON (readable)

mongodump/mongorestore do a binary backup (fast and complete). mongoexport/mongoimport use JSON (readable, for migrations). Do regular dumps and test the restore.

ObjectId
// Generate new
new ObjectId()

// Extract creation date
ObjectId("65f3a...").getTimestamp()
// ISODate("2024-03-14T...")

// Compare
ObjectId("65f...").equals(otherId)

ObjectId has 12 bytes: timestamp (4) + machine (3) + PID (2) + counter (3). getTimestamp() extracts the creation date without an extra field. Sortable chronologically.

Performance (tips)
// ✅ Index filter and sort fields
db.c.createIndex({ field: 1 });

// ✅ Projection: fetch only the needed fields
db.c.find({}, { name: 1, _id: 0 });

// ✅ limit() to avoid loading everything
db.c.find().limit(20);

// ❌ Avoid regex without an index
// ❌ Avoid skip() on large datasets
// ❌ Avoid documents > 1 MB

Index find() and sort() fields. Use a projection to reduce transferred data. limit() avoids loading whole collections. Monitor with explain().

Replica set and sharding
// Replica set = copies of the data
// (high availability):
//   1 primary + N secondaries
//   automatic failover

// Start replica set:
rs.initiate()
rs.status()
rs.add("host2:27017")

// Sharding = split data across
// machines (horizontal scale):
//   shard key defines the split

sh.enableSharding("store")
sh.shardCollection(
  "store.orders", { customer_id: 1 }
)

// Replica = availability
// Shard = capacity/scale

Replica set replicates data across several nodes (automatic failover, reads on secondaries). Sharding splits data across machines using a shard key. Replica set for availability, sharding for horizontal scale.

Embed vs Reference
// EMBED (1:few, accessed together)
{
  name: "Anna",
  address: { street: "X", city: "Porto" }
}

// REFERENCE (1:many, accessed separately)
{
  name: "Anna",
  orders: [ObjectId("..."), ObjectId("...")]
}

Embed for small data accessed together (1:1, 1:few). Reference for large relations or accessed separately (1:many). Limit: a document up to 16 MB.

Replica Set (concepts)
// Start replica set (3 nodes)
mongod --replSet rs0 --port 27017
mongod --replSet rs0 --port 27018
mongod --replSet rs0 --port 27019

// Initialize
rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "localhost:27017" },
    { _id: 1, host: "localhost:27018" },
    { _id: 2, host: "localhost:27019" }
  ]
});

A Replica Set has one primary (writes) and secondaries (reads/failover). It ensures high availability. Required for transactions and recommended in production.

Backup and restore
// Full backup
mongodump --db store --out /backup/

// Restore
mongorestore --db store /backup/store

// Export/Import (JSON)
mongoexport --db store --collection customers --out customers.json
mongoimport --db store --collection customers --file customers.json

mongodump/mongorestore do a binary backup (BSON). mongoexport/mongoimport use JSON/CSV — useful for migrations and debugging. Schedule regular backups.

Administration commands
// Server status
db.serverStatus()

// Operations in progress
db.currentOp()

// Kill a slow operation
db.killOp(opId)

// Profiling (record slow queries)
db.setProfilingLevel(1, { slowms: 100 });
db.getProfilingLevel();

serverStatus() gives server metrics. currentOp() shows active operations. setProfilingLevel(1) records queries above X ms in the system.profile collection.