Complete MongoDB Tutorial: NoSQL Database for Modern Applications
MongoDB is a highly popular document-oriented NoSQL database for modern applications. With flexible schema and horizontal scaling capabilities, MongoDB is suitable for various use cases from web applications to big data analytics.
What is MongoDB?
MongoDB stores data in JSON-like documents (BSON) providing:
- Schema flexibility: No need to define data structure beforehand
- Horizontal scaling: Easy to scale with sharding
- High performance: Optimized for read/write operations
- Rich query language: Support for complex queries and aggregations
- Native replication: Built-in high availability
- Content management systems
- Real-time analytics
- Mobile applications
- IoT data storage
- E-commerce platforms
- Social media applications
Installing MongoDB
1. Install on Ubuntu
# Import MongoDB public GPG key
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
Add MongoDB repository
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | \
sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
Update and install
sudo apt update
sudo apt install -y mongodb-org
Start MongoDB
sudo systemctl start mongod
sudo systemctl enable mongod
Verify installation
mongosh --eval 'db.runCommand({ connectionStatus: 1 })'
2. Install via Docker
# Pull MongoDB image
docker pull mongo:7.0
Run MongoDB container
docker run -d \
--name mongodb \
-p 27017:27017 \
-e MONGOINITDBROOTUSERNAME=admin \
-e MONGOINITDBROOTPASSWORD=password123 \
-v mongodbdata:/data/db \
mongo:7.0
Connect with mongosh
docker exec -it mongodb mongosh -u admin -p password123
3. MongoDB Atlas (Cloud)
For production, MongoDB Atlas provides a managed service:
# Connection string format
connectionstring = "mongodb+srv://username:password@cluster.mongodb.net/dbname"
MongoDB Shell (mongosh)
1. Basic Commands
// Connect to MongoDB
mongosh
// Show databases
show dbs
// Use/create database
use myapp
// Show collections
show collections
// Current database
db
// Database stats
db.stats()
// Drop database
db.dropDatabase()
2. CRUD Operations
Create (Insert):// Insert single document
db.users.insertOne({
name: "Ruby Abdullah",
email: "ruby@example.com",
age: 30,
skills: ["Python", "JavaScript", "MongoDB"],
address: {
city: "Jakarta",
country: "Indonesia"
},
createdAt: new Date()
})
// Insert multiple documents
db.users.insertMany([
{ name: "Alice", email: "alice@example.com", age: 25 },
{ name: "Bob", email: "bob@example.com", age: 35 },
{ name: "Charlie", email: "charlie@example.com", age: 28 }
])
Read (Query):
// Find all documents
db.users.find()
// Find with filter
db.users.find({ age: { $gt: 25 } })
// Find one document
db.users.findOne({ email: "ruby@example.com" })
// Projection (select fields)
db.users.find({}, { name: 1, email: 1, id: 0 })
// Sort
db.users.find().sort({ age: -1 }) // Descending
// Limit and skip (pagination)
db.users.find().skip(10).limit(5)
// Count
db.users.countDocuments({ age: { $gt: 25 } })
Update:
// Update one document
db.users.updateOne(
{ email: "ruby@example.com" },
{ $set: { age: 31 } }
)
// Update many documents
db.users.updateMany(
{ age: { $lt: 30 } },
{ $set: { status: "young" } }
)
// Replace entire document
db.users.replaceOne(
{ email: "bob@example.com" },
{ name: "Bob Smith", email: "bob@example.com", age: 36 }
)
// Upsert (update or insert)
db.users.updateOne(
{ email: "new@example.com" },
{ $set: { name: "New User", age: 20 } },
{ upsert: true }
)
Delete:
// Delete one document
db.users.deleteOne({ email: "alice@example.com" })
// Delete many documents
db.users.deleteMany({ age: { $lt: 25 } })
// Delete all documents in collection
db.users.deleteMany({})
3. Query Operators
Comparison:// Equal
db.users.find({ age: 30 })
// Not equal
db.users.find({ age: { $ne: 30 } })
// Greater than / less than
db.users.find({ age: { $gt: 25, $lt: 40 } })
// Greater/less than or equal
db.users.find({ age: { $gte: 25, $lte: 40 } })
// In array of values
db.users.find({ age: { $in: [25, 30, 35] } })
// Not in array
db.users.find({ age: { $nin: [25, 30] } })
Logical:
// AND (implicit)
db.users.find({ age: { $gt: 25 }, status: "active" })
// AND (explicit)
db.users.find({
$and: [
{ age: { $gt: 25 } },
{ status: "active" }
]
})
// OR
db.users.find({
$or: [
{ age: { $lt: 25 } },
{ status: "premium" }
]
})
// NOT
db.users.find({
age: { $not: { $gt: 30 } }
})
// NOR (neither)
db.users.find({
$nor: [
{ age: { $lt: 25 } },
{ status: "inactive" }
]
})
Array:
// Match array element
db.users.find({ skills: "Python" })
// Match all elements
db.users.find({ skills: { $all: ["Python", "MongoDB"] } })
// Array size
db.users.find({ skills: { $size: 3 } })
// Element match (for array of objects)
db.orders.find({
items: {
$elemMatch: {
product: "Laptop",
quantity: { $gt: 1 }
}
}
})
Element:
// Field exists
db.users.find({ phone: { $exists: true } })
// Field type
db.users.find({ age: { $type: "int" } })
Text Search:
// Create text index first
db.articles.createIndex({ title: "text", content: "text" })
// Text search
db.articles.find({ $text: { $search: "mongodb tutorial" } })
// Text search with score
db.articles.find(
{ $text: { $search: "mongodb" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })
Indexes
1. Index Types
// Single field index
db.users.createIndex({ email: 1 }) // 1 = ascending, -1 = descending
// Compound index
db.users.createIndex({ lastName: 1, firstName: 1 })
// Unique index
db.users.createIndex({ email: 1 }, { unique: true })
// Text index
db.articles.createIndex({ content: "text" })
// Geospatial index
db.places.createIndex({ location: "2dsphere" })
// TTL index (auto-delete after time)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
2. Index Management
// List indexes
db.users.getIndexes()
// Drop index
db.users.dropIndex("email1")
// Drop all indexes (except id)
db.users.dropIndexes()
// Explain query (check index usage)
db.users.find({ email: "ruby@example.com" }).explain("executionStats")
Aggregation Framework
1. Basic Aggregation
// Count by field
db.users.aggregate([
{ $group: { id: "$status", count: { $sum: 1 } } }
])
// Average
db.users.aggregate([
{ $group: { id: null, avgAge: { $avg: "$age" } } }
])
// Multiple aggregations
db.orders.aggregate([
{ $match: { status: "completed" } },
{
$group: {
id: "$customerId",
totalOrders: { $sum: 1 },
totalSpent: { $sum: "$total" },
avgOrderValue: { $avg: "$total" }
}
},
{ $sort: { totalSpent: -1 } },
{ $limit: 10 }
])
2. Aggregation Stages
// $match - Filter documents
{ $match: { status: "active" } }
// $group - Group and aggregate
{
$group: {
id: "$category",
total: { $sum: "$price" },
avg: { $avg: "$price" },
min: { $min: "$price" },
max: { $max: "$price" },
count: { $sum: 1 }
}
}
// $project - Reshape documents
{
$project: {
name: 1,
fullName: { $concat: ["$firstName", " ", "$lastName"] },
yearJoined: { $year: "$createdAt" }
}
}
// $sort - Sort results
{ $sort: { total: -1, name: 1 } }
// $limit and $skip
{ $skip: 10 }
{ $limit: 5 }
// $unwind - Deconstruct array
{ $unwind: "$items" }
// $lookup - Join collections
{
$lookup: {
from: "products",
localField: "productId",
foreignField: "id",
as: "productDetails"
}
}
// $addFields - Add new fields
{
$addFields: {
totalWithTax: { $multiply: ["$total", 1.1] }
}
}
// $facet - Multiple pipelines
{
$facet: {
byCategory: [
{ $group: { id: "$category", count: { $sum: 1 } } }
],
byStatus: [
{ $group: { id: "$status", count: { $sum: 1 } } }
],
totalCount: [
{ $count: "total" }
]
}
}
3. Real-world Aggregation Examples
// E-commerce: Top selling products
db.orders.aggregate([
{ $unwind: "$items" },
{
$group: {
id: "$items.productId",
totalSold: { $sum: "$items.quantity" },
revenue: { $sum: { $multiply: ["$items.price", "$items.quantity"] } }
}
},
{
$lookup: {
from: "products",
localField: "id",
foreignField: "id",
as: "product"
}
},
{ $unwind: "$product" },
{
$project: {
productName: "$product.name",
totalSold: 1,
revenue: 1
}
},
{ $sort: { revenue: -1 } },
{ $limit: 10 }
])
// User activity report
db.events.aggregate([
{
$match: {
timestamp: {
$gte: ISODate("2024-01-01"),
$lt: ISODate("2024-02-01")
}
}
},
{
$group: {
id: {
userId: "$userId",
day: { $dateToString: { format: "%Y-%m-%d", date: "$timestamp" } }
},
eventCount: { $sum: 1 },
uniqueActions: { $addToSet: "$action" }
}
},
{
$group: {
id: "$id.userId",
activeDays: { $sum: 1 },
totalEvents: { $sum: "$eventCount" },
avgEventsPerDay: { $avg: "$eventCount" }
}
},
{ $sort: { totalEvents: -1 } }
])
Python with PyMongo
1. Setup and Connection
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure
from bson.objectid import ObjectId
from datetime import datetime
Basic connection
client = MongoClient("mongodb://localhost:27017/")
Connection with authentication
client = MongoClient(
"mongodb://admin:password123@localhost:27017/",
authSource="admin"
)
MongoDB Atlas connection
client = MongoClient(
"mongodb+srv://user:password@cluster.mongodb.net/?retryWrites=true&w=majority"
)
Test connection
try:
client.admin.command('ping')
print("Connected successfully!")
except ConnectionFailure:
print("Connection failed!")
Select database and collection
db = client["myapp"]
users = db["users"]
2. CRUD with PyMongo
from pymongo import MongoClient
from bson.objectid import ObjectId
from datetime import datetime
client = MongoClient("mongodb://localhost:27017/")
db = client["myapp"]
users = db["users"]
Insert
def createuser(name, email, age):
user = {
"name": name,
"email": email,
"age": age,
"createdat": datetime.utcnow()
}
result = users.insertone(user)
return result.insertedid
Insert many
def createusers(userlist):
result = users.insertmany(userlist)
return result.insertedids
Read
def getuserbyemail(email):
return users.findone({"email": email})
def getusers(filters=None, limit=10, skip=0):
query = filters or {}
return list(users.find(query).skip(skip).limit(limit))
def getuserbyid(userid):
return users.findone({"id": ObjectId(userid)})
Update
def updateuser(userid, updates):
result = users.updateone(
{"id": ObjectId(userid)},
{"$set": updates}
)
return result.modifiedcount
def incrementage(userid):
result = users.updateone(
{"id": ObjectId(userid)},
{"$inc": {"age": 1}}
)
return result.modifiedcount
Delete
def deleteuser(userid):
result = users.deleteone({"id": ObjectId(userid)})
return result.deletedcount
Usage
userid = createuser("Ruby", "ruby@example.com", 30)
print(f"Created user: {userid}")
user = getuserbyemail("ruby@example.com")
print(f"Found user: {user}")
updateuser(userid, {"age": 31, "status": "active"})
3. Aggregation with PyMongo
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["ecommerce"]
orders = db["orders"]
Aggregation pipeline
def getsalesbycategory():
pipeline = [
{"$unwind": "$items"},
{
"$group": {
"id": "$items.category",
"totalsales": {"$sum": {"$multiply": ["$items.price", "$items.quantity"]}},
"totalitems": {"$sum": "$items.quantity"},
"ordercount": {"$sum": 1}
}
},
{"$sort": {"totalsales": -1}}
]
return list(orders.aggregate(pipeline))
Customer analytics
def gettopcustomers(limit=10):
pipeline = [
{"$match": {"status": "completed"}},
{
"$group": {
"id": "$customerid",
"totalspent": {"$sum": "$total"},
"ordercount": {"$sum": 1},
"avgordervalue": {"$avg": "$total"}
}
},
{"$sort": {"totalspent": -1}},
{"$limit": limit},
{
"$lookup": {
"from": "customers",
"localField": "id",
"foreignField": "id",
"as": "customerinfo"
}
},
{"$unwind": "$customerinfo"},
{
"$project": {
"customername": "$customerinfo.name",
"email": "$customerinfo.email",
"totalspent": 1,
"ordercount": 1,
"avgordervalue": {"$round": ["$avgordervalue", 2]}
}
}
]
return list(orders.aggregate(pipeline))
Time series analysis
def getdailyrevenue(startdate, enddate):
pipeline = [
{
"$match": {
"createdat": {"$gte": startdate, "$lt": enddate},
"status": "completed"
}
},
{
"$group": {
"id": {
"$dateToString": {"format": "%Y-%m-%d", "date": "$createdat"}
},
"revenue": {"$sum": "$total"},
"orders": {"$sum": 1}
}
},
{"$sort": {"id": 1}}
]
return list(orders.aggregate(pipeline))
4. Index Management
from pymongo import MongoClient, ASCENDING, DESCENDING, TEXT
client = MongoClient("mongodb://localhost:27017/")
db = client["myapp"]
users = db["users"]
Create indexes
users.createindex("email", unique=True)
users.createindex([("lastname", ASCENDING), ("firstname", ASCENDING)])
users.createindex([("location", "2dsphere")])
users.createindex([("bio", TEXT), ("interests", TEXT)])
Create TTL index
sessions = db["sessions"]
sessions.createindex("createdat", expireAfterSeconds=3600)
List indexes
for index in users.listindexes():
print(index)
Drop index
users.dropindex("email1")
Motor: Async MongoDB Driver
import asyncio
from motor.motorasyncio import AsyncIOMotorClient
async def main():
# Connect
client = AsyncIOMotorClient("mongodb://localhost:27017/")
db = client["myapp"]
users = db["users"]
# Insert
result = await users.insertone({
"name": "Async User",
"email": "async@example.com"
})
print(f"Inserted: {result.insertedid}")
# Find
user = await users.findone({"email": "async@example.com"})
print(f"Found: {user}")
# Find many
cursor = users.find({"status": "active"})
async for doc in cursor:
print(doc)
# Aggregation
pipeline = [
{"$group": {"id": "$status", "count": {"$sum": 1}}}
]
cursor = users.aggregate(pipeline)
async for doc in cursor:
print(doc)
asyncio.run(main())
FastAPI with MongoDB
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional, List
from motor.motorasyncio import AsyncIOMotorClient
from bson import ObjectId
from datetime import datetime
app = FastAPI()
MongoDB connection
client = AsyncIOMotorClient("mongodb://localhost:27017/")
db = client["myapp"]
Pydantic models
class PyObjectId(ObjectId):
@classmethod
def _getvalidators_(cls):
yield cls.validate
@classmethod
def validate(cls, v, handler):
if not ObjectId.isvalid(v):
raise ValueError("Invalid ObjectId")
return ObjectId(v)
class UserCreate(BaseModel):
name: str
email: str
age: int
class UserResponse(BaseModel):
id: str = Field(alias="id")
name: str
email: str
age: int
createdat: datetime
class Config:
populatebyname = True
jsonencoders = {ObjectId: str}
Routes
@app.post("/users", responsemodel=UserResponse)
async def createuser(user: UserCreate):
userdict = user.modeldump()
userdict["createdat"] = datetime.utcnow()
result = await db.users.insertone(userdict)
created = await db.users.findone({"id": result.insertedid})
created["id"] = str(created["id"])
return created
@app.get("/users", responsemodel=List[UserResponse])
async def getusers(skip: int = 0, limit: int = 10):
cursor = db.users.find().skip(skip).limit(limit)
users = await cursor.tolist(length=limit)
for user in users:
user["id"] = str(user["id"])
return users
@app.get("/users/{userid}", responsemodel=UserResponse)
async def getuser(userid: str):
user = await db.users.findone({"id": ObjectId(userid)})
if not user:
raise HTTPException(statuscode=404, detail="User not found")
user["id"] = str(user["id"])
return user
@app.put("/users/{userid}", responsemodel=UserResponse)
async def updateuser(userid: str, user: UserCreate):
result = await db.users.updateone(
{"id": ObjectId(userid)},
{"$set": user.modeldump()}
)
if result.modifiedcount == 0:
raise HTTPException(statuscode=404, detail="User not found")
updated = await db.users.findone({"id": ObjectId(userid)})
updated["id"] = str(updated["id"])
return updated
@app.delete("/users/{userid}")
async def deleteuser(userid: str):
result = await db.users.deleteone({"id": ObjectId(userid)})
if result.deletedcount == 0:
raise HTTPException(statuscode=404, detail="User not found")
return {"message": "User deleted"}
Run: uvicorn main:app --reload
Best Practices
1. Schema Design
# Embedding vs Referencing
Embedding (for data always accessed together)
userwithaddress = {
"name": "Ruby",
"email": "ruby@example.com",
"addresses": [
{"type": "home", "city": "Jakarta", "country": "Indonesia"},
{"type": "work", "city": "Bandung", "country": "Indonesia"}
]
}
Referencing (for large or frequently updated data)
user = {
"name": "Ruby",
"email": "ruby@example.com"
}
order = {
"userid": ObjectId("..."), # Reference to user
"items": [...],
"total": 100
}
2. Connection Management
from pymongo import MongoClient
from contextlib import contextmanager
Connection pooling
client = MongoClient(
"mongodb://localhost:27017/",
maxPoolSize=50,
minPoolSize=10,
maxIdleTimeMS=30000
)
Context manager for transactions
@contextmanager
def getsession():
with client.startsession() as session:
yield session
Usage with transaction
with getsession() as session:
with session.starttransaction():
db.users.insertone({"name": "Test"}, session=session)
db.logs.insertone({"action": "usercreated"}, session=session)
3. Error Handling
from pymongo.errors import (
ConnectionFailure,
DuplicateKeyError,
WriteError
)
def safeinsert(collection, document):
try:
result = collection.insertone(document)
return result.insertedid
except DuplicateKeyError:
print("Document already exists")
return None
except WriteError as e:
print(f"Write error: {e}")
return None
except ConnectionFailure:
print("Connection failed")
return None
4. Performance Tips
# Use projection to reduce data transfer
users.find({}, {"name": 1, "email": 1})
Batch operations
from pymongo import InsertOne, UpdateOne
operations = [
InsertOne({"name": "User 1"}),
InsertOne({"name": "User 2"}),
UpdateOne({"name": "User 1"}, {"$set": {"status": "active"}})
]
result = users.bulkwrite(operations)
Create appropriate indexes
users.createindex([("email", 1)], unique=True)
users.createindex([("status", 1), ("created_at", -1)])
Conclusion
MongoDB is a powerful and flexible NoSQL database for modern applications. Key takeaways:
Usage tips:
- Design schema based on access patterns
- Use indexes for frequently used queries
- Leverage aggregation framework for analytics
- Implement proper connection pooling
- Handle errors gracefully