mongodb-development
Design, model, index, and query production document databases using MongoDB. Use when deciding between embedding and referencing relationships (1:1, 1:N, N:N), applying the Equality-Sort-Range (ESR) r
Files1
View on GitHubSKILL.mdskills/mongodb-development/SKILL.md
GitHubMongoDB Document Database Engineering
MongoDB is a document-oriented database that stores records as flexible, binary JSON (BSON) structures. Designing performant, resilient MongoDB systems requires choosing appropriate document relationships (embedding vs referencing), engineering compound indexes under the Equality-Sort-Range (ESR) rule, optimizing multi-stage aggregation pipelines, and configuring cluster write/read concerns for data safety.
Purpose & Overview
This skill guides the schema design, query optimization, and transaction management of MongoDB databases. It covers document modeling trade-offs (bounded embedding vs referencing for 1:N and N:N relations), compound index creation following the ESR rule, TTL and partial indexes, aggregation pipeline execution plans, JSON Schema collection validation, replica set write and read concerns (w: "majority"), multi-document ACID transactions, and query diagnostics with explain("executionStats").
Core Principles & Rules
- Follow the Equality-Sort-Range (ESR) rule for compound indexes: When querying with equality filters, sorting, and range conditions, always order compound index fields as:
- First: Equality fields (exact match:
{ status: "active" }) - Second: Sort fields (ordering:
{ createdAt: -1 }) - Third: Range fields (inequality operators:
{ age: { $gte: 21 } })
- First: Equality fields (exact match:
- Never embed unbounded arrays (Anti-Pattern: 1-to-Squillions): Documents have a hard 16MB BSON size limit. Never embed arrays that can grow indefinitely (e.g., logging events, comments, sensor readings) inside a parent document. Reference the parent ID from child documents instead.
- Embed for atomic updates and co-located access: Embed subdocuments when data is accessed together in 90%+ of queries, updated together atomically, and has a bounded size (e.g., user addresses, order line items).
- Always use
explain("executionStats")to verify index coverage: Never deploy a query without inspecting execution plans. Verify thatstageisIXSCAN(Index Scan) rather thanCOLLSCAN(Collection Scan), and confirmtotalDocsExaminedis close or equal tonReturned. - Use
w: "majority"write concern for durable writes: Ensure writes are acknowledged by a majority of replica set voting members before returning to prevent data rollback during primary failover. - Place
$matchand$projectat the earliest aggregation stages: Filter out unwanted documents and project only needed fields as early in the pipeline as possible to minimize RAM usage before memory-intensive$sortor$groupstages. - Multi-document transactions must be short-lived: Multi-document ACID transactions hold locks and pin the WiredTiger cache. Keep transactions under 60 seconds, touch minimal documents, and avoid long external network calls inside a transaction block.
Standard Workflows & Procedures
Workflow 1: Schema Design & Relationship Selection
- Identify application query patterns: read-to-write ratio, joined queries, and write atomicity requirements.
- For 1:1 relations: Embed by default unless fields are rarely queried or large (use subset pattern).
- For 1:Few (Bounded 1:N): Embed as an array of subdocuments (e.g., max 10 shipping addresses).
- For 1:Many (Unbounded 1:N): Reference parent in child documents (
parent_idforeign key). - For N:N: Reference two-way or one-way array of IDs, or use a junction collection with metadata.
- Attach a JSON Schema validator to the collection via
createCollectionorcollMod.
Workflow 2: Compound Index Creation with ESR
- Analyze query shape:
db.orders.find({ tenantId: "tenant_1", status: "completed", amount: { $gte: 100 } }) .sort({ createdAt: -1 }) - Break down query components:
- Equality:
tenantId,status - Sort:
createdAt - Range:
amount
- Equality:
- Formulate the index conforming strictly to ESR:
db.orders.createIndex({ tenantId: 1, status: 1, createdAt: -1, amount: 1 })
Workflow 3: Diagnosing Slow Queries with explain()
- Run explain with full execution statistics:
db.orders.find(query).sort(sort).explain("executionStats") - Evaluate execution metrics:
executionStages.stage: Must beIXSCAN+FETCHor pureIXSCAN(covered query).totalDocsExamined: Should closely matchnReturned. IftotalDocsExamined >> nReturned, the index is inefficient.totalKeysExamined: Check if excess index keys were evaluated.executionStages.sortStage: IfSORTappears, the sort was done in RAM instead of via index.
Real-World Examples & Patterns
1. JSON Schema Collection Validation & ESR Compound Index
// 1. Create collection with strict schema validation
db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["tenantId", "customerId", "status", "totalAmount", "items", "createdAt"],
properties: {
tenantId: { bsonType: "string" },
customerId: { bsonType: "string" },
status: {
enum: ["PENDING", "PROCESSING", "COMPLETED", "CANCELLED"]
},
totalAmount: { bsonType: "number", minimum: 0 },
items: {
bsonType: "array",
minItems: 1,
items: {
bsonType: "object",
required: ["sku", "quantity", "price"],
properties: {
sku: { bsonType: "string" },
quantity: { bsonType: "int", minimum: 1 },
price: { bsonType: "number", minimum: 0 }
}
}
},
createdAt: { bsonType: "date" }
}
}
},
validationAction: "error"
});
// 2. Create compound index applying ESR (Equality, Sort, Range)
// Query: find orders for tenantId with status, ordered by createdAt, filtering totalAmount > X
db.orders.createIndex(
{ tenantId: 1, status: 1, createdAt: -1, totalAmount: 1 },
{ name: "idx_orders_tenant_status_date_amount" }
);
// 3. Create partial unique index (e.g. unique idempotencyKey for pending orders only)
db.orders.createIndex(
{ idempotencyKey: 1 },
{
unique: true,
partialFilterExpression: { idempotencyKey: { $exists: true } }
}
);
// 4. Create TTL index for transient session tokens (expires 24 hours after createdAt)
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 86400 }
);
2. High-Performance Aggregation Pipeline
Optimized pipeline performing early $match, pushing sorting to index, and joining with $lookup:
db.orders.aggregate([
// Stage 1: Filter early using index (Equality & Range)
{
$match: {
tenantId: "tenant_99",
status: "COMPLETED",
createdAt: {
$gte: ISODate("2026-01-01T00:00:00Z"),
$lt: ISODate("2026-02-01T00:00:00Z")
}
}
},
// Stage 2: Prune unnecessary fields to minimize RAM footprint
{
$project: {
customerId: 1,
totalAmount: 1,
createdAt: 1
}
},
// Stage 3: Aggregate totals per customer
{
$group: {
_id: "$customerId",
totalSpent: { $sum: "$totalAmount" },
orderCount: { $sum: 1 }
}
},
// Stage 4: Filter aggregated results
{
$match: {
totalSpent: { $gte: 500 }
}
},
// Stage 5: Lookup customer profile using sub-pipeline to avoid full joins
{
$lookup: {
from: "customers",
let: { custId: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$customerId", "$$custId"] } } },
{ $project: { name: 1, email: 1, tier: 1 } }
],
as: "customer"
}
},
// Stage 6: Unwind single customer record
{
$unwind: "$customer"
},
// Stage 7: Sort top spenders (allowDiskUse if > 100MB)
{
$sort: { totalSpent: -1 }
},
{
$limit: 20
}
]);
3. Multi-Document ACID Transaction with Session (Python / PyMongo)
from pymongo import MongoClient
from pymongo.read_concern import ReadConcern
from pymongo.write_concern import WriteConcern
client = MongoClient("mongodb://mongo1:27017,mongo2:27017/?replicaSet=rs0")
def transfer_credits(client: MongoClient, from_user_id: str, to_user_id: str, amount: float):
# Enforce majority write concern and read concern for the transaction
wc_majority = WriteConcern("majority", wtimeout=5000)
rc_majority = ReadConcern("majority")
with client.start_session() as session:
def callback(s):
users = client.app_db.get_collection(
"users",
write_concern=wc_majority,
read_concern=rc_majority
)
# 1. Deduct credits from sender if sufficient balance exists
sender = users.find_one_and_update(
{"_id": from_user_id, "balance": {"$gte": amount}},
{"$inc": {"balance": -amount}},
session=s
)
if not sender:
raise ValueError("Insufficient balance or sender not found")
# 2. Add credits to recipient
recipient = users.find_one_and_update(
{"_id": to_user_id},
{"$inc": {"balance": amount}},
session=s
)
if not recipient:
raise ValueError("Recipient not found")
# 3. Insert audit log
client.app_db.audit_logs.insert_one(
{
"from": from_user_id,
"to": to_user_id,
"amount": amount,
"timestamp": datetime.utcnow()
},
session=s
)
# with_transaction automatically handles transient transaction errors and retries
session.with_transaction(
callback,
read_concern=rc_majority,
write_concern=wc_majority
)
Common Anti-Patterns & Pitfalls
| Anti-Pattern | Why It Fails | Remediated Practice |
|---|---|---|
| Unbounded array embedding (1:Squillions) | Documents grow until hitting the 16MB limit, degrading WiredTiger performance and causing write failures. | Reference parent documents using foreign keys (parent_id) in child documents. |
| Violating the ESR Rule in Compound Indexes | Placing range fields before sort fields forces in-memory sort stages (SORT stage in explain()), wasting memory and CPU. |
Always order index fields as: Equality, then Sort, then Range. |
Omitting Write Concern majority |
With default w: 1, writes acknowledged by only the primary can be rolled back if the primary crashes before replicating. |
Set w: "majority" on critical write operations. |
Full Collection Scans (COLLSCAN) |
Unindexed queries inspect every document in the collection, creating high CPU load and disk I/O bottlenecks. | Verify query coverage with explain("executionStats") and add targeted indexes. |
Unconstrained $lookup in Aggregations |
Unfiltered $lookup joins entire collections into memory, causing aggregation stages to exceed the 100MB RAM limit. |
Filter early with $match and use $lookup sub-pipelines with targeted $project. |
| Long-running ACID transactions | Long transactions pin WiredTiger cache history, causing write stalls and transaction aborts (NoSuchTransaction). |
Keep transactions under 5 seconds, touching only minimal documents. |
Verification Checklist
- All compound indexes strictly respect the Equality-Sort-Range (ESR) rule.
- No collections store unbounded arrays inside documents.
- Collections enforce schema structure and types using
$jsonSchema. - Every slow or critical query has been analyzed with
explain("executionStats"). -
totalDocsExaminedequalsnReturnedon key query paths (minimal over-fetching). - Critical updates use write concern
{ w: "majority" }. - Aggregation pipelines execute
$matchand$projectat the initial stages. - Multi-document transactions use
with_transactionwith retry logic for transient errors. - In-memory sort limits (100MB) are avoided through index-assisted sorting.
Tooling & Reference Guides
- Official Drivers:
mongodb(Node.js/TypeScript),pymongo/motor(Python),mongo-go-driver(Go). - Diagnostics:
mongoshCLI, MongoDB Compass,db.collection.explain("executionStats"), MongoDB Atlas Performance Advisor. - Data Modeling Tools: MongoDB Relational Migrator, Hackolade.
- Documentation: MongoDB Indexing Strategies, Equality Sort Range Rule, MongoDB Aggregation Pipeline Optimization.