MongoDB Patterns
Aggregation Pipeline
// Sales report: group by month, filter, sort
db.orders.aggregate([
{ $match: { status: "completed", createdAt: { $gte: new Date("2024-01-01") } } },
{ $group: {
_id: { $dateToString: { format: "%Y-%m", date: "$createdAt" } },
total: { $sum: "$amount" },
count: { $sum: 1 },
avg: { $avg: "$amount" }
}},
{ $sort: { _id: -1 } },
{ $limit: 12 }
]);
// $lookup (left join)
db.orders.aggregate([
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}},
{ $unwind: "$user" },
{ $project: { amount: 1, "user.name": 1, "user.email": 1 } }
]);
// $facet — multiple aggregations in one pass
db.products.aggregate([
{ $facet: {
byCategory: [
{ $group: { _id: "$category", count: { $sum: 1 } } }
],
priceStats: [
{ $group: { _id: null, min: { $min: "$price" }, max: { $max: "$price" } } }
],
total: [{ $count: "n" }]
}}
]);
Indexes
// Single field
db.users.createIndex({ email: 1 }, { unique: true });
// Compound — order matters (equality → sort → range)
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });
// Partial index
db.orders.createIndex({ userId: 1 }, { partialFilterExpression: { status: "pending" } });
// TTL — auto-expire documents
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 86400 });
// Text search
db.articles.createIndex({ title: "text", body: "text" }, { weights: { title: 10 } });
db.articles.find({ $text: { $search: "mongodb patterns" } },
{ score: { $meta: "textScore" } })
.sort({ score: { $meta: "textScore" } });
// Check index usage
db.orders.explain("executionStats").find({ userId: ObjectId("...") });
Schema Design Patterns
// Embed (1-to-few, read together)
{
_id: ObjectId("..."),
name: "Alice",
addresses: [
{ type: "home", street: "123 Main St", city: "NYC" }
]
}
// Reference (1-to-many, independent access)
// order document
{ _id: ObjectId("..."), userId: ObjectId("..."), items: [...] }
// user document
{ _id: ObjectId("..."), name: "Alice" }
// Bucket pattern (time-series)
{
sensorId: "temp-01",
hour: ISODate("2024-01-15T12:00:00Z"),
readings: [22.1, 22.3, 22.0, ...], // up to 60 readings
count: 60,
min: 21.8, max: 22.5, avg: 22.1
}
// Outlier pattern — handle large arrays
{
_id: ObjectId("..."),
productId: ObjectId("..."),
reviews: [...], // first 1000
hasMore: true // flag for overflow documents
}
Transactions (Multi-document)
const session = client.startSession();
try {
await session.withTransaction(async () => {
await db.accounts.updateOne(
{ _id: fromId },
{ $inc: { balance: -amount } },
{ session }
);
await db.accounts.updateOne(
{ _id: toId },
{ $inc: { balance: amount } },
{ session }
);
});
} finally {
await session.endSession();
}
Change Streams
// Watch collection changes
const stream = db.orders.watch([
{ $match: { "fullDocument.status": "completed" } }
], { fullDocument: "updateLookup" });
stream.on("change", async (change) => {
console.log(change.operationType, change.fullDocument);
await notifyFulfillment(change.fullDocument);
});
// Resume after restart
const token = await redis.get("resume_token");
const stream = db.orders.watch([], { resumeAfter: token });
stream.on("change", async (change) => {
await redis.set("resume_token", JSON.stringify(change._id));
process(change);
});
Performance Tips
- Analyze queries:
db.collection.explain("executionStats")
- Index selectivity: avoid low-cardinality fields as leading index key
- Projection always — never fetch full document when a subset suffices
- Use
allowDiskUse: true for large aggregations
- Atlas Search for full-text (Lucene-backed, much faster than
$text)
- Increase
wiredTigerCacheSizeGB to 50–60% of RAM