The goal
You've been asked to look after a MongoDB database, and you've only worked with SQL so far. This post gets you connected and doing useful work, and covers the mistakes that matter most.
| SQL term | MongoDB term |
|---|---|
| Database | Database |
| Table | Collection |
| Row | Document (JSON-like) |
| Column | Field |
| Primary key | _id |
JOIN | Embedded documents or $lookup |
Step 1: get a test server
Use a throwaway server, not production, to practice on:
docker run -d --name mongo-dev -p 27017:27017 mongo:7
docker exec -it mongo-dev mongosh
If you already have a server, connect like this:
mongosh "mongodb://appuser@db01.contoso.com:27017/shop?authSource=admin"
mongosh prompts for the password, so it won't end up in your shell history. For a replica set, list the hosts, or use the mongodb+srv:// address that Atlas gives you:
mongosh "mongodb://db01:27017,db02:27017,db03:27017/shop?replicaSet=rs0&authSource=admin"
Step 2: look around
show dbs
use shop
show collections
db.getName()
db.stats()
db.version()
use shop doesn't create anything by itself. A database appears when you first write to it.
Step 3: insert documents
db.orders.insertOne({
orderId: 1001,
customer: { name: "Asha", city: "Pune" },
items: [ { sku: "A1", qty: 2 }, { sku: "B7", qty: 1 } ],
total: 450.50,
status: "new",
createdAt: new Date()
})
db.orders.insertMany([
{ orderId: 1002, customer: { name: "Ravi", city: "Delhi" }, total: 120, status: "paid", createdAt: new Date() },
{ orderId: 1003, customer: { name: "Meera", city: "Pune" }, total: 980, status: "new", createdAt: new Date() }
])
There's no table definition to create. Documents in a collection can have different fields. That's flexible, and it also means typos can create new field names without any error.
Step 4: query
// One document
db.orders.findOne({ orderId: 1001 })
// Filter, and return selected fields
db.orders.find(
{ status: "new", total: { $gt: 100 } },
{ orderId: 1, total: 1, _id: 0 }
)
// Search inside a nested field; note the quotes
db.orders.find({ "customer.city": "Pune" })
// Sort and limit
db.orders.find().sort({ createdAt: -1 }).limit(5)
// Count
db.orders.countDocuments({ status: "new" })
Common operators:
| Operator | Meaning |
|---|---|
$gt, $gte, $lt, $lte | Greater or less than |
$ne | Not equal |
$in | Matches any in a list |
$exists | The field is present |
$or | Either condition |
A simple group-by, with an aggregation:
db.orders.aggregate([
{ $match: { status: "new" } },
{ $group: { _id: "$customer.city", orders: { $sum: 1 }, total: { $sum: "$total" } } },
{ $sort: { total: -1 } }
])
Step 5: update and delete safely
db.orders.updateOne(
{ orderId: 1001 },
{ $set: { status: "paid" }, $currentDate: { updatedAt: true } }
)
Always use an operator such as $set. replaceOne replaces the entire document, and you lose every field you didn't include.
Before you change anything, run the same filter with find and check what it matches:
db.orders.countDocuments({ status: "cancelled" })
db.orders.deleteMany({ status: "cancelled" })
Caution
db.orders.deleteMany({})anddb.orders.updateMany({}, ...)affect every document, because an empty filter matches everything.db.orders.drop()removes the whole collection and its indexes. There's no undo, so check that you're in the right database (db.getName()) first, and have a backup.
Step 6: add your first index
Without an index, MongoDB reads every document to answer a query. See this with explain:
db.orders.find({ orderId: 1001 }).explain("executionStats")
Check these fields:
| Field | What to look for |
|---|---|
winningPlan.stage | COLLSCAN reads everything; IXSCAN uses an index |
totalDocsExamined | Should be close to nReturned |
executionTimeMillis | How long it took |
Create an index and try again:
db.orders.createIndex({ orderId: 1 }, { unique: true })
db.orders.createIndex({ status: 1, createdAt: -1 })
db.orders.getIndexes()
For a compound index, put fields you test for equality first, followed by your sort field. Indexes speed up reads, but they use memory and disk and slow down writes, so add only those you need. See slow queries and indexes.
Warning
Index builds on a large production collection use resources and take time. Check the impact first, and run them outside busy periods.
Step 7: create a user
use admin
db.createUser({
user: "shop_app",
pwd: passwordPrompt(),
roles: [ { role: "readWrite", db: "shop" } ]
})
Give each application its own user, with access to only the databases it needs. Turn on authentication on any real server, and don't expose port 27017 to the internet.
Common errors
| Error | Likely cause | Fix |
|---|---|---|
Authentication failed | Wrong password, or wrong authSource | The user usually lives in admin; add ?authSource=admin |
ECONNREFUSED | Service down, or bindIp limits it to localhost | Check systemctl status mongod and net.bindIp |
| Server selection timeout | Network, firewall or wrong replica set name | Test the port, and check rs.status() |
not primary | You're writing to a secondary | Connect using the replica set string |
E11000 duplicate key | A value breaks a unique index | Find the existing document |
not authorized on shop | Role is missing | Grant the role for that database |
If you're on a replica set, check its health:
rs.status().members.map(m => ({ name: m.name, state: m.stateStr, health: m.health }))
db.hello().isWritablePrimary
Slow operations running now:
db.currentOp({ active: true, secs_running: { $gt: 5 } })
Prevent it next time
- Test filters with
findorcountDocumentsbefore running an update or delete. - Use a replica set for anything that matters, even if it's small.
- Keep the DBA cheat sheet nearby.
- When you're designing collections, read embed vs reference.