Getting Started
Connect to MongoDB
mongosh is the modern MongoDB Shell (replacing the legacy mongo shell). A connection string follows the URI format mongodb://[user:pass@]host[:port]/[db][?options]. The /admin database is the default auth database. You can connect to a specific database directly by appending it to the URI.
# connect to a local server on default port
mongosh
# connect to a specific host and port
mongosh "mongodb://localhost:27017"
# connect with authentication
mongosh "mongodb://user:pass@localhost:27017/admin"
# connect to a specific database directly
mongosh "mongodb://localhost:27017/mydb"
# show connection info
db.getMongo().getDBNames()Database Basics
MongoDB creates databases and collections lazily — they only appear after you insert the first document. 'use mydb' switches context but does not create the database until data is written. show dbs does not list empty databases. dropDatabase permanently deletes all collections and their data.
# show all databases
show dbs
# switch to (or create) a database
use mydb
# show the current database
db
# create is implicit: inserting into a
# non-existent collection creates both the
# collection and the database
# drop the current database
db.dropDatabase()Collections
Collections are analogous to SQL tables and hold documents. They are created automatically on first insert, but createCollection lets you set options like capped (fixed-size collections that overwrite old documents — useful for logs). stats() shows storage size, document count, and index details.
# list collections in current database
show collections
db.getCollectionNames()
# create a collection explicitly with options
db.createCollection("users", {
capped: true,
size: 5242880,
max: 5000
})
# rename a collection
db.users.renameCollection("accounts")
# drop a collection
db.users.drop()
# get collection stats
db.users.stats()BSON Data Types
BSON extends JSON with additional types essential for applications: ObjectId (12-byte unique IDs), ISODate, NumberDecimal (exact decimal for money — avoids float rounding), NumberLong (64-bit ints), BinData (binary), and more. Using NumberDecimal for currency avoids classic floating-point errors.
# MongoDB stores BSON (binary JSON) with rich types
{
_id: ObjectId("65a1b2c3d4e5f6a7b8c9d0e1"),
name: "Alice", // String
age: 30, // Int32
score: NumberLong(9007199254740992), // Int64
price: NumberDecimal("19.99"), // Decimal128 (exact money)
active: true, // Boolean
birthday: ISODate("1995-08-15"), // Date
tags: ["red", "blue"], // Array
meta: { views: 100 }, // Embedded document
data: BinData(0, "aGVsbG8="), // Binary
id: UUID("...") // UUID
}Shell Helpers & Help
mongosh supports full JavaScript, so you can write loops, functions, and variables in the shell. .pretty() formats output for readability (default in mongosh). load() runs a .js file in the shell context — useful for repetitive admin scripts. db.collection.help() lists all available methods.
# get help on database methods
db.help()
# get help on collection methods
db.users.help()
# list common shell commands
help
# pretty-print query results
db.users.find().pretty()
# print JSON in a compact form
db.users.find().toArray()
# execute a JavaScript file
mongosh --file script.js
load("script.js")CRUD Operations
Insert Documents
MongoDB auto-generates an _id (ObjectId) if you don't provide one. The _id must be unique within a collection — duplicate _id raises a write error. ordered:false lets insertMany continue inserting remaining documents after an error, improving throughput for bulk loads.
# insert a single document
db.users.insertOne({
name: "Alice",
email: "[email protected]",
age: 30
})
# insert multiple documents
db.users.insertMany([
{ name: "Bob", age: 25 },
{ name: "Carol", age: 28 }
])
# insert with a custom _id
db.users.insertOne({ _id: 1, name: "Dave" })
# ordered insert (stops on error) vs unordered
db.users.insertMany(docs, { ordered: false })Find / Query Documents
find() returns a cursor (lazily evaluated); findOne() returns a single document or null. Projection controls which fields are returned — you cannot mix inclusion and exclusion (except _id). Use 1 to include, 0 to exclude. countDocuments is accurate; the older count() is deprecated for filtered counts.
# find all documents
db.users.find()
# find with a filter
db.users.find({ age: 30 })
db.users.find({ "address.city": "NYC" })
# find one matching document
db.users.findOne({ name: "Alice" })
# projection: include/exclude fields
db.users.find({}, { name: 1, age: 1, _id: 0 })
db.users.find({}, { email: 0 })
# count matching documents
db.users.countDocuments({ age: { $gte: 18 } })Update Documents
updateOne/updateMany use update operators ($set, $inc, etc.) to modify specific fields. replaceOne replaces the whole document except _id. upsert:true creates a new document when no match exists — useful for 'create or update' patterns. Always use $set to update fields; a bare object replaces the document (legacy behavior).
# update one document
db.users.updateOne(
{ name: "Alice" },
{ $set: { age: 31, status: "active" } }
)
# update many documents
db.users.updateMany(
{ status: "pending" },
{ $set: { status: "active" } }
)
# replace an entire document (keeps _id)
db.users.replaceOne(
{ name: "Alice" },
{ name: "Alice", age: 32, city: "NYC" }
)
# upsert: insert if no match found
db.users.updateOne(
{ name: "Eve" },
{ $set: { age: 22 } },
{ upsert: true }
)Update Operators
Update operators modify fields atomically at the document level. $inc is concurrency-safe (no read-modify-write race). $min/$max only update if the new value is smaller/larger. $currentDate is handy for 'last modified' timestamps. Multiple operators can be combined in one update.
# $set and $unset
db.users.updateOne({n:"A"}, { $set: {age:30}, $unset: {temp:""} })
# $inc increments a numeric field
db.users.updateOne({n:"A"}, { $inc: {views: 1} })
db.users.updateOne({n:"A"}, { $inc: {balance: -50} })
# $rename a field
db.users.updateOne({n:"A"}, { $rename: {n: "name"} })
# $min / $max keep the smaller/larger value
db.users.updateOne({n:"A"}, { $min: {lowScore: 80} })
# $mul multiply the field value
db.users.updateOne({n:"A"}, { $mul: {price: 1.1} })
# $currentDate sets to current date
db.users.updateOne({n:"A"}, { $currentDate: {updatedAt: true} })Delete Documents
deleteOne removes the first match; deleteMany removes all matches. deleteMany({}) empties the collection but keeps indexes (faster than drop+recreate if you want to preserve index definitions). findOneAndDelete returns the deleted document atomically. Deletions are irreversible — always filter carefully.
# delete one matching document
db.users.deleteOne({ name: "Alice" })
# delete all matching documents
db.users.deleteMany({ status: "inactive" })
# delete all documents in a collection
db.users.deleteMany({})
# findAndDelete: return the deleted document
db.users.findOneAndDelete({ name: "Bob" })
# remove is deprecated, prefer deleteOne/deleteMany
db.users.remove({ name: "Bob" }, { justOne: true })Array Update Operators
$push appends (allows duplicates); $addToSet deduplicates. $pull removes elements matching a condition. The positional operator ($) refers to the first array element matched by the query — essential for updating specific array elements without knowing their index. $[] updates all array elements.
# $push append to an array
db.users.updateOne({n:"A"}, { $push: {tags: "new"} })
# $push with $each for multiple values
db.users.updateOne({n:"A"}, { $push: {tags: { $each: ["x","y"] } } })
# $addToSet adds only if not already present
db.users.updateOne({n:"A"}, { $addToSet: {tags: "x"} })
# $pull remove all matching array elements
db.users.updateOne({n:"A"}, { $pull: {tags: "x"} })
# $pop remove first (-1) or last (1) element
db.users.updateOne({n:"A"}, { $pop: {tags: 1} })
# update an array element by position
db.users.updateOne({}, { $set: {"tags.0": "first"} })
db.users.updateOne({"tags":"old"}, { $set: {"tags.$":"new"} })Querying Documents
Comparison Operators
Comparison operators are the foundation of queries. $in is far more efficient than multiple $or conditions on the same field. You can combine multiple operators on one field (e.g. $gte and $lte for a range). $exists:true finds documents with the field, $exists:false finds those without it.
# $eq equal (same as direct value)
db.products.find({ price: { $eq: 100 } })
# $ne not equal
db.products.find({ price: { $ne: 100 } })
# $gt, $gte, $lt, $lte
db.products.find({ price: { $gte: 50, $lte: 200 } })
# $in matches any value in an array
db.products.find({ category: { $in: ["books", "music"] } })
# $nin not in array
db.products.find({ category: { $nin: ["books"] } })
# $exists check field presence
db.products.find({ discount: { $exists: true } })Logical Operators
Implicit AND (comma in a filter) is the most common and efficient. Use explicit $and only when you need multiple conditions on the same field (implicit AND on the same field overrides earlier conditions). $or uses separate index scans and merges results — ensure fields in $or are indexed for performance.
# $and (explicit, useful for same-field conditions)
db.users.find({ $and: [
{ age: { $gte: 18 } },
{ age: { $lte: 65 } }
]})
# $or matches if ANY condition is true
db.users.find({ $or: [
{ status: "active" },
{ vip: true }
]})
# $nor matches if NO condition is true
db.users.find({ $nor: [{ status: "active" }]})
# $not inverts a condition
db.users.find({ age: { $not: { $gt: 18 } } })
# implicit AND (comma-separated)
db.users.find({ age: { $gte: 18 }, status: "active" })Element & Evaluation Operators
$type filters by BSON type (useful for mixed-type fields). $regex supports options like 'i' (case-insensitive) — but regex without a prefix anchor cannot use indexes efficiently. $expr enables comparisons between document fields, which normal operators can't do. Use $expr sparingly as it can bypass indexes.
# $type filter by BSON data type
db.users.find({ age: { $type: "int" } })
db.users.find({ age: { $type: ["int", "double"] } })
# $regex regular expression match
db.users.find({ name: { $regex: "^Al", $options: "i" } })
# $mod modulo operation
db.users.find({ age: { $mod: [2, 0] } }) # even ages
# $expr compare fields within a document
db.orders.find({ $expr: { $gt: ["$total", "$budget"] } })
# $jsonSchema validate against a schema
db.users.find({ $jsonSchema: {
required: ["name", "email"]
}})Array Query Operators
$all finds arrays containing all specified values (order-independent). $elemMatch is essential when an array element must satisfy multiple conditions simultaneously (a plain query would match elements individually). $size matches exact lengths only — there's no $size with a range; pre-compute and store the length as a separate field instead.
# $all: array contains ALL specified values
db.posts.find({ tags: { $all: ["mongo", "db"] } })
# $elemMatch: at least one element matches all conditions
db.students.find({ scores: { $elemMatch: {
subject: "math", score: { $gte: 90 }
}}})
# $size: array of exact length
db.users.find({ tags: { $size: 3 } })
# match if array contains the value
db.posts.find({ tags: "mongo" })
# match a specific array index
db.users.find({ "scores.0": 95 }) # first score is 95Cursor Methods & Sorting
skip+limit implements pagination but skip becomes slow for large offsets (it scans all skipped docs). For deep pagination, use keyset pagination: query with a filter on the sort field greater than the last seen value. sort() can use an index to avoid in-memory sorting — always index your sort fields for large collections.
# limit and skip (pagination)
db.users.find().limit(10)
db.users.find().skip(20).limit(10) # page 3
# sort: 1 ascending, -1 descending
db.users.find().sort({ age: 1, name: -1 })
# count results
db.users.find({active:true}).count()
# iterate the cursor in the shell
db.users.find().forEach(doc => print(doc.name))
# get a specific field's distinct values
db.users.distinct("city")Indexing
Create Indexes
Indexes dramatically speed up queries but slow down writes and consume disk. Single-field indexes support queries on that field in either sort direction. Compound indexes follow the ESR (Equality, Sort, Range) rule for optimal ordering. TTL indexes automatically delete documents after a duration — perfect for sessions and logs.
# create a single-field index
db.users.createIndex({ email: 1 }) # ascending
db.users.createIndex({ age: -1 }) # descending
# create a unique index
db.users.createIndex({ email: 1 }, { unique: true })
# create a compound index
db.users.createIndex({ lastName: 1, firstName: 1 })
# create a text index
db.posts.createIndex({ title: "text", body: "text" })
# create a TTL (time-to-live) index
db.sessions.createIndex({ createdAt: 1 },
{ expireAfterSeconds: 3600 })
# name an index explicitly
db.users.createIndex({ email: 1 }, { name: "email_idx" })View & Manage Indexes
getIndexes lists all indexes with their keys and options. hideIndex/unhideIndex lets you test the impact of removing an index without actually dropping it — the query planner ignores hidden indexes. If performance stays the same, it's safe to drop. Always run explain() before and after index changes.
# list all indexes on a collection
db.users.getIndexes()
# total index size in bytes
db.users.totalIndexSize()
# drop an index by name
db.users.dropIndex("email_1")
# drop all indexes except _id
db.users.dropIndexes()
# hide an index (test impact before dropping)
db.users.hideIndex("email_1")
db.users.unhideIndex("email_1")
# check if queries use indexes
db.users.find({email:"[email protected]"}).explain("executionStats")Special Index Types
2dsphere indexes power geo queries ($near, $geoWithin). Hashed indexes support hash-based sharding for even data distribution. Wildcard indexes ($**) cover unpredictable/variable field names in documents — useful for polymorphic data. Partial indexes only index matching documents, saving space when you only query a subset.
# geospatial 2dsphere index (GeoJSON points)
db.places.createIndex({ location: "2dsphere" })
db.places.find({
location: { $near: {
$geometry: { type: "Point", coordinates: [-73.99, 40.73] },
$maxDistance: 1000
}}
})
# hashed index (for sharding)
db.users.createIndex({ user_id: "hashed" })
# wildcard index (index arbitrary field names)
db.products.createIndex({ "$**": 1 })
# partial index (only matching documents)
db.users.createIndex({ email: 1 },
{ partialFilterExpression: { active: true } })Explain & Query Plans
explain() is the primary performance tool. COLLSCAN (collection scan) means no index was used — a red flag for large collections. IXSCAN means an index was used. totalDocsExamined >> nReturned indicates a poor index (examining many docs to return few). hint() forces a specific index for testing the planner's choice.
# explain a query (basic)
db.users.find({age:30}).explain()
# explain with execution stats (timing)
db.users.find({age:30}).explain("executionStats")
# key fields in the output:
# winningPlan.stage -> COLLSCAN (bad) or IXSCAN (good)
# totalDocsExamined -> should be close to nReturned
# executionTimeMillis -> query duration
# indexUsed -> which index was chosen
# force a specific index (testing)
db.users.find({age:30}).hint("age_1")
db.users.find({age:30}).hint({age:1})Indexing Best Practices
The ESR (Equality, Sort, Range) rule is the single most important compound-index design principle: put equality-filtered fields first, then sort fields, then range fields. A covered query (all fields are in the index) never fetches the document — the fastest possible query. Avoid indexes you don't use; each one adds write overhead.