MongoDB is a document-oriented NoSQL database used for high volume data storage. Instead of using tables and rows as in traditional relational databases, MongoDB uses collections and documents with a JSON-like structure.
Use this file to discover all available pages before exploring further.
MongoDB Anti-Patterns Overview
MongoDB, despite its flexibility and scalability, has several common anti-patterns that can lead to performance issues, maintenance problems, and data inconsistency. Here are the most important anti-patterns to avoid when working with MongoDB.
Not Using Proper Indexing
// Anti-pattern: Querying without indexesdb.users.find({ email: "user@example.com" });// This query will perform a full collection scan if there's no index on the email field// Better approach: Create appropriate indexesdb.users.createIndex({ email: 1 }, { unique: true });// Now the query will use the indexdb.users.find({ email: "user@example.com" });
// Anti-pattern: Not analyzing query performancedb.orders.find({ status: "processing", createdAt: { $gt: new Date('2023-01-01') } }).sort({ createdAt: -1 });// Better approach: Use explain() to analyze query performancedb.orders.find({ status: "processing", createdAt: { $gt: new Date('2023-01-01') } }).sort({ createdAt: -1 }).explain("executionStats");// Create a compound index based on the query patterndb.orders.createIndex({ status: 1, createdAt: -1 });
Not using proper indexes leads to full collection scans and poor query performance. Create indexes based on your query patterns and use explain() to verify that your queries are using indexes efficiently.
Deeply nested documents are difficult to query and update. Limit nesting to 2-3 levels and consider flattening your document structure for better performance and maintainability.
Using Massive Arrays
// Anti-pattern: Document with a massive arraydb.products.insertOne({ name: "Popular Product", sku: "PP-123", // Thousands of reviews in a single document reviews: [ { user: "user1", rating: 5, comment: "Great product!" }, { user: "user2", rating: 4, comment: "Good product." }, // ... thousands more reviews ]});// Better approach: Use a separate collection for reviewsdb.products.insertOne({ name: "Popular Product", sku: "PP-123"});db.reviews.insertMany([ { productSku: "PP-123", user: "user1", rating: 5, comment: "Great product!" }, { productSku: "PP-123", user: "user2", rating: 4, comment: "Good product." }, // ... thousands more reviews as separate documents]);// Create an index on productSku for efficient queriesdb.reviews.createIndex({ productSku: 1 });// Query for a product's reviewsdb.reviews.find({ productSku: "PP-123" }).sort({ rating: -1 });
Massive arrays in documents can lead to performance issues and hit the 16MB document size limit. Use separate collections for one-to-many relationships with high cardinality.
Not Using Aggregation Framework
// Anti-pattern: Multiple queries and client-side processing// Step 1: Get all orders for a customerconst orders = db.orders.find({ customerId: "12345" }).toArray();// Step 2: Calculate total spent in application codelet totalSpent = 0;for (const order of orders) { totalSpent += order.total;}// Step 3: Get customer detailsconst customer = db.customers.findOne({ _id: "12345" });// Step 4: Combine data in application codeconst result = { customer: customer.name, email: customer.email, totalOrders: orders.length, totalSpent: totalSpent, averageOrderValue: totalSpent / orders.length};// Better approach: Use the aggregation frameworkconst result = db.orders.aggregate([ { $match: { customerId: "12345" } }, { $group: { _id: "$customerId", totalOrders: { $sum: 1 }, totalSpent: { $sum: "$total" }, averageOrderValue: { $avg: "$total" } } }, { $lookup: { from: "customers", localField: "_id", foreignField: "_id", as: "customerDetails" } }, { $unwind: "$customerDetails" }, { $project: { _id: 0, customer: "$customerDetails.name", email: "$customerDetails.email", totalOrders: 1, totalSpent: 1, averageOrderValue: 1 } }]).toArray();
Using multiple queries and processing data in application code is inefficient. Use MongoDB’s aggregation framework to process data on the server side, reducing network traffic and improving performance.
Not Using Transactions When Needed
// Anti-pattern: No transactions for operations that should be atomic// Transfer funds between accountsdb.accounts.updateOne( { _id: sourceAccountId }, { $inc: { balance: -amount } });// If an error occurs here, the system is in an inconsistent statedb.accounts.updateOne( { _id: destinationAccountId }, { $inc: { balance: amount } });// Better approach: Use transactions for atomicityconst session = client.startSession();session.startTransaction();try { await db.accounts.updateOne( { _id: sourceAccountId, balance: { $gte: amount } }, { $inc: { balance: -amount } }, { session } ); await db.accounts.updateOne( { _id: destinationAccountId }, { $inc: { balance: amount } }, { session } ); await session.commitTransaction();} catch (error) { await session.abortTransaction(); throw error;} finally { await session.endSession();}
Not using transactions for operations that need to be atomic can lead to data inconsistency. Use transactions when you need to ensure that multiple operations succeed or fail as a unit.
Not Using Schema Validation
// Anti-pattern: No schema validationdb.users.insertOne({ name: "John Doe", email: "not-an-email", // Invalid email format age: "thirty" // Should be a number});// Better approach: Use schema validationdb.createCollection("users", { validator: { $jsonSchema: { bsonType: "object", required: ["name", "email", "age"], properties: { name: { bsonType: "string", description: "must be a string and is required" }, email: { bsonType: "string", pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", description: "must be a valid email address and is required" }, age: { bsonType: "int", minimum: 0, maximum: 120, description: "must be an integer between 0 and 120 and is required" } } } }, validationLevel: "strict", validationAction: "error"});
Not using schema validation can lead to inconsistent data. Use MongoDB’s schema validation to enforce data integrity and structure.
Using $where queries is slow and can be a security risk (potential for injection attacks). Use standard MongoDB query operators instead.
Not Using Proper Connection Pooling
// Anti-pattern: Creating new connections for each operationasync function getUserById(id) { const client = new MongoClient(uri); try { await client.connect(); const db = client.db("mydb"); return await db.collection("users").findOne({ _id: id }); } finally { await client.close(); }}// Better approach: Use connection pooling// Initialize once at application startupconst client = new MongoClient(uri, { useUnifiedTopology: true, maxPoolSize: 50});await client.connect();const db = client.db("mydb");// Reuse the connection for operationsasync function getUserById(id) { return await db.collection("users").findOne({ _id: id });}// Close the connection when the application shuts downprocess.on("SIGINT", async () => { await client.close(); process.exit();});
Creating new connections for each operation is inefficient. Use connection pooling to reuse connections and improve performance.
Not specifying write concerns can lead to data loss in case of failures. Use appropriate write concerns based on your application’s durability requirements.
Not Using Projection in Queries
// Anti-pattern: Retrieving entire documents when only a few fields are neededconst users = await db.users.find({ age: { $gt: 18 } }).toArray();// Client only uses name and emailconst result = users.map(user => ({ name: user.name, email: user.email}));// Better approach: Use projection to retrieve only needed fieldsconst result = await db.users.find( { age: { $gt: 18 } }, { projection: { name: 1, email: 1, _id: 0 } }).toArray();
Retrieving entire documents when only a few fields are needed wastes bandwidth and memory. Use projection to retrieve only the fields you need.
Not Using Change Streams for Real-time Updates
// Anti-pattern: Polling for changesasync function pollForChanges() { const lastCheckTime = new Date(Date.now() - 60000); // Last minute const changes = await db.orders.find({ updatedAt: { $gt: lastCheckTime } }).toArray(); // Process changes // Poll again after delay setTimeout(pollForChanges, 5000);}// Better approach: Use change streamsconst changeStream = db.orders.watch();changeStream.on("change", change => { // Process change in real-time console.log("Received change:", change); if (change.operationType === "insert") { // Handle new order } else if (change.operationType === "update") { // Handle order update } else if (change.operationType === "delete") { // Handle order deletion }});// Handle errorschangeStream.on("error", error => { console.error("Change stream error:", error); // Reconnect logic});
Polling for changes is inefficient and can miss updates. Use change streams for real-time notifications of database changes.
Not Using Appropriate Data Types
// Anti-pattern: Using strings for IDs, dates, and numeric valuesdb.orders.insertOne({ orderId: "12345", // Should be ObjectId or a numeric ID customerId: "67890", // Should be ObjectId or a numeric ID orderDate: "2023-05-15", // Should be a Date object total: "99.99", // Should be a numeric type items: [/* ... */]});// Better approach: Use appropriate data typesdb.orders.insertOne({ _id: new ObjectId(), // MongoDB's ObjectId customerId: new ObjectId("507f1f77bcf86cd799439011"), orderDate: new Date("2023-05-15"), total: 99.99, // Numeric value items: [/* ... */]});
Using inappropriate data types makes queries and aggregations more complex and less efficient. Use the appropriate BSON data types for your data.