MongoDB CRUD — The updateOne Without $Set Data Wipe
Production failure: a plain document in updateOne wiped 10,000 order fields.
20+ years shipping high-throughput database systems. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- MongoDB CRUD uses documents in collections: InsertOne/InsertMany (create), find/findOne (read), updateOne/updateMany with operators (update), deleteOne/deleteMany (delete).
- The _id field is auto-generated as an ObjectId — capture it immediately after insert to avoid extra queries.
- Update operators ($set, $inc, $push) prevent accidental document replacement; never pass a plain object as the update argument.
- find() returns a cursor, not an array — always chain .toArray() or iterate; use .limit() on open-ended queries to protect production.
- Soft-delete (isDeleted flag + deletedAt timestamp) is the production standard; physical deletion reserved for cleanup or GDPR erasure.
MongoDB CRUD operations are the four fundamental actions you perform against a MongoDB database: Create, Read, Update, and Delete. They map directly to HTTP verbs (POST, GET, PUT/PATCH, DELETE) and are the building blocks of any application that persists data.
Unlike SQL databases where you write declarative queries in a separate language, MongoDB CRUD operations are executed via driver methods in your application code—insertOne, find, updateOne, deleteMany, etc.—and operate on BSON documents. The critical distinction is that MongoDB's update operations are not atomic field replacements by default; if you call updateOne without an update operator like $set, you will replace the entire document with whatever object you pass, wiping all other fields.
This is a common footgun that has caused production data loss at scale, and understanding it is the difference between a safe mutation and a silent disaster.
In the broader ecosystem, MongoDB CRUD sits between raw database drivers and ORM/ODM layers like Mongoose or Prisma. You should use raw CRUD operations when you need fine-grained control over query performance, write concerns, or retry logic—especially in high-throughput systems where an ORM's abstraction overhead or unexpected behavior (like Mongoose's vs save()updateOne semantics) can introduce latency or bugs.
Avoid raw CRUD when you need complex transactional guarantees across multiple documents (use the session API with retryable writes) or when your team benefits from schema validation and type safety that an ODM provides. MongoDB's CRUD methods are also the foundation for aggregation pipelines and change streams, so mastering them directly—including the $set vs replacement distinction—is non-negotiable for any engineer working with MongoDB at scale.
Real-world impact: a single updateOne({_id: id}, {name: 'new'}) without $set will drop every other field in that document—embedded arrays, timestamps, nested objects, all gone. MongoDB's write concern defaults to {w: 1} (acknowledged by the primary), but without {j: true} (journaled), a crash after acknowledgment can still lose the write.
For critical data, you pair {w: 'majority', j: true} with retry logic using RetryableWrites and ReadConcern.majority to prevent phantom reads. These aren't academic concerns; they're the difference between a resilient system and a pager at 3 AM.
Imagine MongoDB is a giant filing cabinet where each drawer is a 'collection' and each folder inside is a 'document'. CRUD is just the four things you'd ever do to that cabinet: drop in a new folder (Create), read what's inside one (Read), scribble changes on it (Update), or shred it (Delete). That's it — every database operation in existence boils down to these four actions.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Every application that stores data — whether it's a social media feed, an e-commerce cart, or a hospital records system — needs to talk to a database. MongoDB has become the go-to choice for teams building flexible, fast-moving products because it stores data as JSON-like documents instead of rigid rows and columns. Knowing how to speak its language isn't optional; it's the difference between an app that ships and one that stalls.
What MongoDB CRUD Operations Actually Do
CRUD stands for Create, Read, Update, Delete — the four fundamental operations for persisting and retrieving data. In MongoDB, these map to insertOne/insertMany, find, updateOne/updateMany/replaceOne, and deleteOne/deleteMany. The core mechanic: each operation targets a single document or a batch, using a filter to select documents and a modifier to specify changes. Unlike SQL, MongoDB updates are atomic at the document level — no multi-document transactions by default.
Key properties: updateOne without $set replaces the entire matched document with the new document you pass. This is a common trap — if you only want to change a single field, omitting $set wipes all other fields. The operation is O(log n) per document due to B-tree index traversal on the filter, but the write itself is O(1) for the document size. MongoDB uses a write-ahead journal for durability, and by default, write concern is "acknowledged" — the driver waits for the primary to confirm.
Use updateOne when you need to modify a single document by a unique identifier (e.g., _id). It's ideal for real-time updates like incrementing a counter, setting a status, or patching a field. In production, always pair updateOne with $set unless you explicitly intend a full replacement. The difference between a partial update and a full document wipe is just two characters — and that mistake has taken down production systems.
Create — Inserting Documents the Right Way
Inserting data sounds trivial until you're doing it wrong in production. MongoDB gives you two insertion methods: insertOne() for a single document and insertMany() for a batch. The key insight most tutorials skip is what MongoDB hands back after an insert — an acknowledgement object containing the auto-generated _id. That _id is a 12-byte ObjectId, globally unique by design, and you should be capturing it in your application logic rather than ignoring it.
Why does this matter? Because in a typical e-commerce flow you insert an order document, then immediately need that order's _id to create a shipment record that references it. Ignoring the return value forces a second round-trip to the database just to find what MongoDB already told you.
insertMany() is more nuanced. By default it's ordered, meaning if document number 3 of 10 fails validation, documents 1 and 2 are already committed and 4-10 are abandoned. Passing the option { ordered: false } lets MongoDB push through all valid documents and collect errors at the end — a much better pattern for bulk imports where one bad record shouldn't kill the whole batch.
// Connect to a local MongoDB instance using the official Node.js driver const { MongoClient, ObjectId } = require('mongodb'); async function insertOrders() { const client = new MongoClient('mongodb://localhost:27017'); try { await client.connect(); const db = client.db('storefront'); // target database const ordersCollection = db.collection('orders'); // target collection // --- insertOne: single document --- // MongoDB auto-generates _id if you don't provide one const singleInsertResult = await ordersCollection.insertOne({ customerId: 'cust_8821', items: [ { productSku: 'SHOE-RED-42', quantity: 1, unitPrice: 79.99 }, { productSku: 'LACE-BLK', quantity: 2, unitPrice: 3.50 } ], orderStatus: 'pending', createdAt: new Date() // always store dates as Date objects, not strings }); // insertedId is the ObjectId MongoDB assigned — save this, don't query for it again console.log('New order ID:', singleInsertResult.insertedId); // --- insertMany: bulk insert with ordered:false so one bad doc doesn't abort the rest --- const bulkOrders = [ { customerId: 'cust_1103', items: [{ productSku: 'HAT-GRN-M', quantity: 1, unitPrice: 24.00 }], orderStatus: 'pending', createdAt: new Date() }, { customerId: 'cust_4477', items: [{ productSku: 'BELT-BRN-L', quantity: 1, unitPrice: 34.50 }], orderStatus: 'pending', createdAt: new Date() } ]; const bulkInsertResult = await ordersCollection.insertMany(bulkOrders, { ordered: false // continue inserting even if one document fails validation }); // insertedCount tells you how many actually landed console.log('Orders inserted:', bulkInsertResult.insertedCount); console.log('Inserted IDs:', bulkInsertResult.insertedIds); } finally { await client.close(); // always close the connection } } insertOrders().catch(console.error);
Read — Querying Documents Without Killing Your Database
Reading data is where most MongoDB performance problems are born. find() returns a cursor, not an array — meaning MongoDB streams results lazily rather than loading everything into memory at once. This is a feature, not a quirk, and it matters the moment your collection grows past a few thousand documents.
The filter argument is where the real power lives. MongoDB's query language is composable: you can filter by exact match, range ($gte, $lte), array membership ($in), logical operators ($and, $or), and even run regex searches — all within a single query object. But power without discipline is dangerous. Running find({}) on a million-document collection with no limit() is how you bring a production server to its knees.
Projection is the query-level equivalent of SELECT in SQL — it tells MongoDB which fields to return. Always use it. Fetching a 40-field customer document when your UI only needs name and email wastes bandwidth, serialization time, and memory on both sides of the wire.
Indexes are what make reads fast, but that's a separate topic. The habit to build now is: every field you filter or sort on should eventually have an index behind it. Use explain('executionStats') on any query you care about to see whether MongoDB is doing a full collection scan (bad) or an index scan (good).
const { MongoClient } = require('mongodb'); async function queryOrders() { const client = new MongoClient('mongodb://localhost:27017'); try { await client.connect(); const ordersCollection = client.db('storefront').collection('orders'); // --- findOne: grab the first match, returns a plain object (not a cursor) --- const latestPendingOrder = await ordersCollection.findOne( { orderStatus: 'pending' }, // filter: only pending orders { projection: { customerId: 1, items: 1, createdAt: 1, _id: 0 }, // only return these fields sort: { createdAt: -1 } // newest first } ); console.log('Latest pending order:', latestPendingOrder); // --- find with range filter: orders created in the last 7 days --- const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const recentOrders = await ordersCollection .find( { createdAt: { $gte: sevenDaysAgo }, // $gte = greater than or equal to orderStatus: { $in: ['pending', 'processing'] } // $in matches any value in the array }, { projection: { customerId: 1, orderStatus: 1, createdAt: 1 } } ) .sort({ createdAt: -1 }) .limit(25) // ALWAYS limit open-ended queries in production .toArray(); // materialise the cursor into an array console.log(`Found ${recentOrders.length} recent orders`); recentOrders.forEach(order => { console.log(` ${order.customerId} — ${order.orderStatus} — ${order.createdAt.toISOString()}`); }); // --- countDocuments: how many total pending orders exist? --- // Use countDocuments() NOT count() — count() is deprecated and ignores filters in some edge cases const pendingTotal = await ordersCollection.countDocuments({ orderStatus: 'pending' }); console.log('Total pending orders:', pendingTotal); } finally { await client.close(); } } queryOrders().catch(console.error);
find() with no .limit() can kill your application and database under load. Always paginate or limit.explain() to verify.Update — Changing Data Without Replacing It
Updating is where MongoDB beginners most often shoot themselves. The critical rule: always use an update operator like $set, $inc, or $push. If you pass a plain document as the second argument to updateOne(), MongoDB treats it as a full replacement and wipes every field not in your update object. That's a legal operation, but it's almost never what you want.
MongoDB's update operators are surgical. $set modifies only the fields you name, leaving everything else untouched. $inc atomically increments a number — perfect for view counters or inventory tracking without a read-modify-write cycle. $push appends to an array, and $pull removes from one. $unset deletes a field entirely.
The upsert option is powerful but underused. Setting { upsert: true } tells MongoDB: if the filter matches something, update it; if nothing matches, create a new document. This collapses a common 'find-then-insert-or-update' pattern into a single atomic operation — no race conditions, no extra round-trips.
For bulk changes, updateMany() applies your update to every document matching the filter. Just make sure your filter is tight. Running updateMany({}, { $set: { archived: true } }) marks every single document in the collection as archived — no confirmation prompt, no undo.
const { MongoClient, ObjectId } = require('mongodb'); async function updateOrders() { const client = new MongoClient('mongodb://localhost:27017'); try { await client.connect(); const ordersCollection = client.db('storefront').collection('orders'); // --- updateOne with $set: change a single order's status --- // ALWAYS use $set — without it, your whole document gets replaced const targetOrderId = new ObjectId('64f3a2b1c9e77f001a3d8e12'); const statusUpdateResult = await ordersCollection.updateOne( { _id: targetOrderId }, // filter: match by exact _id { $set: { orderStatus: 'dispatched', dispatchedAt: new Date() // add a new field on the fly — no schema migration needed } } ); console.log('Documents matched:', statusUpdateResult.matchedCount); // how many matched the filter console.log('Documents modified:', statusUpdateResult.modifiedCount); // how many were actually changed // --- $inc: atomically decrement stock without a read-modify-write --- // This is safe under concurrent writes; plain read+write is NOT const inventoryCollection = client.db('storefront').collection('inventory'); await inventoryCollection.updateOne( { productSku: 'SHOE-RED-42' }, { $inc: { stockCount: -1 } } // subtract 1 from stockCount atomically ); // --- upsert: create a shipment record if it doesn't exist, update if it does --- const shipmentResult = await client.db('storefront').collection('shipments').updateOne( { orderId: targetOrderId }, // filter: does a shipment for this order exist? { $set: { orderId: targetOrderId, carrier: 'FastFreight', trackingNumber: 'FF-993821-XZ', estimatedDelivery: new Date('2024-09-05') } }, { upsert: true } // create it if it doesn't exist ); // upsertedId is non-null only when a new document was created if (shipmentResult.upsertedId) { console.log('Shipment record created with ID:', shipmentResult.upsertedId); } else { console.log('Existing shipment record updated'); } // --- updateMany: mark all orders older than 30 days as 'archived' --- const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); const archiveResult = await ordersCollection.updateMany( { createdAt: { $lt: thirtyDaysAgo }, orderStatus: 'dispatched' }, // tight filter! { $set: { orderStatus: 'archived', archivedAt: new Date() } } ); console.log(`Archived ${archiveResult.modifiedCount} old orders`); } finally { await client.close(); } } updateOrders().catch(console.error);
Delete — Removing Data Safely and Intentionally
Deletion in MongoDB is permanent and instantaneous. There's no recycle bin, no soft-delete built in, and no ROLLBACK. This is why production teams almost universally implement soft-deletes — adding a deletedAt timestamp field and filtering it out of queries — rather than physically removing documents. Physical deletion is reserved for true cleanup jobs like purging GDPR-expired data or clearing test fixtures.
MongoDB gives you deleteOne() for surgical removal of a single document and deleteMany() for bulk removal. The same golden rule from updates applies: if your filter is too broad, you will delete more than you intended. Always test your filter with a find() call first — confirm the count and spot-check a few returned documents before converting it to a deleteMany().
findOneAndDelete() is the atomic 'grab it and kill it' operation. It deletes the document and returns it to your application in a single server-side operation. This is exactly what you need for job queue patterns where a worker claims a task — using separate find() then deleteOne() calls creates a race condition where two workers could claim the same job.
Never run deleteMany({}) in production without a filter. There's no faster way to have a very bad day.
const { MongoClient, ObjectId } = require('mongodb'); async function deleteOrders() { const client = new MongoClient('mongodb://localhost:27017'); try { await client.connect(); const db = client.db('storefront'); const ordersCollection = db.collection('orders'); // --- PATTERN 1: Soft delete (recommended for business data) --- // Don't physically remove — mark as deleted so you keep the audit trail const orderToSoftDelete = new ObjectId('64f3a2b1c9e77f001a3d8e13'); await ordersCollection.updateOne( { _id: orderToSoftDelete }, { $set: { isDeleted: true, deletedAt: new Date() // lets you audit WHEN it was deleted and query "deleted in last 30 days" } } ); console.log('Order soft-deleted (record preserved for audit)'); // --- PATTERN 2: Hard delete with deleteOne --- // Appropriate for test data, temp records, or GDPR erasure requests const tempOrderId = new ObjectId('64f3a2b1c9e77f001a3d8e14'); const hardDeleteResult = await ordersCollection.deleteOne({ _id: tempOrderId }); console.log('Hard-deleted document count:', hardDeleteResult.deletedCount); // deletedCount will be 0 if the _id didn't exist — not an error, just a miss // --- PATTERN 3: findOneAndDelete — atomic claim-and-remove for job queues --- const jobsCollection = db.collection('pendingEmailJobs'); // Grab the highest-priority job AND remove it in one atomic step // If two workers call this simultaneously, only one gets the document const claimedJob = await jobsCollection.findOneAndDelete( { status: 'queued' }, { sort: { priority: -1, queuedAt: 1 }, // highest priority, then FIFO returnDocument: 'before' // return the document as it was before deletion } ); if (claimedJob) { console.log('Worker claimed job:', claimedJob.jobId, '| Recipient:', claimedJob.recipientEmail); } else { console.log('No jobs in queue right now'); } // --- PATTERN 4: deleteMany for bulk cleanup (always preview first!) --- // Step 1: preview — what would get deleted? const toDeleteCount = await ordersCollection.countDocuments({ isDeleted: true, deletedAt: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) } // older than 90 days }); console.log(`About to permanently purge ${toDeleteCount} soft-deleted orders...`); // Step 2: execute only when you're sure const purgeResult = await ordersCollection.deleteMany({ isDeleted: true, deletedAt: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) } }); console.log('Purged:', purgeResult.deletedCount, 'old orders'); } finally { await client.close(); } } deleteOrders().catch(console.error);
Write Concerns, Error Handling, and Retry Logic
Production applications need to decide how durable their writes are. MongoDB's writeConcern setting controls the level of acknowledgment: w:1 (acknowledge from primary only), w:majority (ack from replica set majority), or j:true (journaled). Each level trades latency for durability. If you absolutely cannot lose a write — say a payment confirmation — use w:majority and j:true. For logs or transient data, w:1 is fine.
Errors happen. Duplicate key errors (E11000) are thrown when a unique index constraint is violated. Your code must catch them. Network timeouts and writeConcern timeouts are distinct: the former means the driver couldn't reach the server, the latter means the server couldn't gather enough acknowledgments in time. Both require retry logic. The MongoDB driver provides built-in retryable writes for network errors, but not for writeConcern errors. You'll need to implement exponential backoff yourself.
A robust write pattern: attempt the write, catch errors, inspect the error label to distinguish transient from permanent, retry transient errors with backoff, and log permanent errors for later investigation. Never swallow errors — a failed write that goes unnoticed becomes a silent data loss.
const { MongoClient } = require('mongodb'); async function insertWithRetry(collection, doc, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { // Set high writeConcern for critical data const result = await collection.insertOne(doc, { writeConcern: { w: 'majority', j: true } }); return result; } catch (err) { // Differentiate transient vs permanent if (err.code === 11000) { // Duplicate key — permanent, log and abort console.error('Duplicate key for doc:', JSON.stringify(doc)); throw err; // Re-throw for caller to handle } if (err.hasErrorLabel('TransientTransactionError') || err.message.includes('timeout')) { // Transient — retry with backoff if (attempt === maxRetries) { console.error('Max retries reached, write failed'); throw err; } const delay = Math.pow(2, attempt) * 100; // 200ms, 400ms, 800ms console.log(`Retrying write, attempt ${attempt + 1} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } else { // Other permanent error throw err; } } } }
Indexing Strategies That Actually Matter For Read Performance
You've written a find() query. It works. Great. Now run it against a collection with 10 million documents and watch your application fall over. This isn't a bug — it's physics. MongoDB scans every document without an index. That's a collection scan. In production, that means timeout errors and angry users.
The WHY: Indexes are B-tree data structures that map field values to document locations. Without them, MongoDB has no shortcut to find your data. It reads every document, checks the filter, and discards what doesn't match. Slow reads are almost always missing indexes.
The HOW: Create indexes on fields you filter, sort, or join on. Use createIndex({ status: 1 }) for equality filters. For range queries or sorts, compound indexes like { status: 1, created_at: -1 } serve both conditions. Use to verify query plans. Never guess — measure.explain()
Senior shortcut: Drop indexes on high-write collections if they slow writes too much. Write-heavy systems need fewer indexes. Read-heavy systems need more. Balance is everything.
// io.thecodeforge — database tutorial // Check query execution plan — spot collection scans // This finds orders for user 8472 placed after Jan 1 2024 db.orders.find({ user_id: 8472, created_at: { $gte: ISODate("2024-01-01") } }).explain("executionStats"); // Output will show stage: "COLLSCAN" if no index — add one: db.orders.createIndex({ user_id: 1, created_at: -1 }); // Verify again — stage becomes "IXSCAN", totalDocsExamined drops from 2M to 50
createIndex({...}, { background: true }) in older versions. In MongoDB 4.2+, background builds are default — but still test first.explain() before you deploy.Transactions — When CRUD Alone Isn't Enough
Your bank transfer updates account A, then account B. Power failure halfway through. Account A is empty, account B never got the money. Congratulations — you've lost customer trust and violated atomicity. Single-document operations in MongoDB are atomic by default. Multi-document operations are not. That's where transactions come in.
The WHY: Transactions give you ACID guarantees across multiple documents or collections. They're critical for financial systems, inventory management, or any operation where partial updates cause corruption. MongoDB supports multi-document transactions since version 4.0.
The HOW: Use startSession() and withTransaction(). Keep transactions short — they hold locks and impact performance. Never do heavy writes or network calls inside a transaction. If a transaction fails, catch the error and implement retry logic. Your callback should be idempotent — running it twice should be safe.
Real talk: Don't use transactions as a crutch for bad schema design. If you need frequent transactions, you might have a relational data model in a document database. Consider embedding related data or rethinking your schema first.
// io.thecodeforge — database tutorial // Atomic transfer between two accounts // Without transaction: account_a debits, account_b never credits const session = db.getMongo().startSession(); session.startTransaction(); try { const accounts = session.getDatabase("banking").accounts; accounts.updateOne( { account_id: "A100", balance: { $gte: 500 } }, { $inc: { balance: -500 } }, { session } ); accounts.updateOne( { account_id: "B200" }, { $inc: { balance: 500 } }, { session } ); session.commitTransaction(); print("Transfer successful"); } catch (e) { session.abortTransaction(); print("Transfer failed: " + e); } finally { session.endSession(); }
$gte condition inside the transfer update. If balance is insufficient, the update matches zero documents and you abort — no race condition possible.Best Practices for Beginners — Stop Writing Naive Queries
Most CRUD failures come from ignoring the database's temperament. MongoDB is not MySQL with JSON. It's a document store that punishes you for treating it like a relational database. The first rule: design your schema for the read patterns, not the write convenience. A denormalized document that saves one join is worth a thousand normalized ones that require $lookup.
Always use write concerns. The default acknowledges the primary only. That's fine for logs, suicidal for billing. Set w:majority on anything that matters. For reads, avoid without filters on large collections. That's a full collection scan masquerading as a query. Index your filter fields before you write the second document.find()
Error handling is not optional. Every write can fail — network blips, duplicate keys, document size limits. Wrap your operations with retry logic using a bounded exponential backoff. MongoDB drivers handle transient errors for you, but only if you bother reading the docs. Three retries, cap at 5 seconds, log every failure.
// io.thecodeforge — database tutorial -- Safe insert with write concern and error handling CREATE PROCEDURE InsertOrder( IN orderId VARCHAR(36), IN customerId VARCHAR(36), IN total DECIMAL(10,2) ) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN -- Log failure, retry logic lives in app layer INSERT INTO failed_operations (operation, data, attempted_at) VALUES ('InsertOrder', CONCAT(orderId, ',', customerId, ',', total), NOW()); RESIGNAL; END; INSERT INTO orders (_id, customer_id, total, status) VALUES (orderId, customerId, total, 'pending'); -- Force majority acknowledged write SET SESSION wsrep_sync_wait = 1; END;
Common Challenges and Solutions — The Stuff That Actually Breaks
The most frequent failure in production: duplicate key errors on upserts. You run an updateOne with upsert: true, two app instances fire at the same millisecond, and MongoDB throws E11000. The fix? Use a unique compound index on the fields that define the document identity. If you still hit conflicts, move to a deterministic _id generation — UUIDs aren't just for primary keys.
Second place: document size limits. MongoDB maxes out at 16MB per document. That's generous until you embed an array of comments that grows unbounded. The solution is bucketing. Store time-series data in pre-defined chunks (one document per hour, per user). When a bucket hits 5000 entries, split or archive. Check Object.bsonsize() in your application code before writes.
Third: reading stale data after a write. Replica sets replicate asynchronously. If your app reads from a secondary, it might see an older version. Primary reads are consistent. Secondary reads are fast but eventually consistent. Know the trade-off. For financial data, always read from the primary. For analytics, hit the secondaries.
// io.thecodeforge — database tutorial -- Handle duplicate key gracefully with retry CREATE PROCEDURE InsertOrUpdateOrder( IN orderId VARCHAR(36), IN customerId VARCHAR(36), IN total DECIMAL(10,2) ) BEGIN DECLARE EXIT HANDLER FOR 1062 -- Duplicate key error BEGIN -- Update existing record UPDATE orders SET total = total + VALUES(total) WHERE _id = orderId; END; INSERT INTO orders (_id, customer_id, total, status) VALUES (orderId, customerId, total, 'pending') ON DUPLICATE KEY UPDATE total = total + VALUES(total); END;
db.collection.stats() on collections that might hit 16MB. It shows average document size. If any document exceeds 8MB, refactor your schema before it breaks in production.Installation — Stop Guessing Which Driver and Version to Use
MongoDB CRUD doesn't work without a properly installed driver. The wrong driver version silently breaks queries, timeouts, and connection pools. For Node.js, install the official MongoDB driver, not the deprecated mongodb wrapper. Use npm install mongodb@6 — version 6 drops callback hell for native promises and unified topology. Python users need pymongo with dnspython for SRV connections — pip install pymongo[srv]. Java demands the synchronous driver (mongodb-driver-sync), not the old async. Always verify connectivity with a ping command after installation. Connection strings must escape special characters in passwords. MongoDB drivers don't warn you about hostname resolution failures—they just hang. Install once, test twice. A failed installation means every CRUD operation silently fails later.
// io.thecodeforge — database tutorial // MongoDB Node.js driver v6 install // npm install mongodb@6 const { MongoClient } = require('mongodb'); async function verifyConnection() { const uri = 'mongodb://localhost:27017'; const client = new MongoClient(uri); try { await client.connect(); const db = client.db('admin'); // Ping — fails fast if install wrong await db.command({ ping: 1 }); console.log('Connected. Driver OK.'); } finally { await client.close(); } } verifyConnection().catch(console.dir);
Alternative: MongoDB Compass — When You Need to See Your Data Fast
Compass is the GUI that strips away query guesswork. Instead of writing a find filter blind, you visually inspect documents, build aggregation pipelines with drag-and-drop, and test indexes by running explain plans on real data. Compass excels at debugging: open a collection, sort by size, spot an unexpectedly large field, and kill it with a targeted delete. It also validates your connection strings without writing one line of code. But Compass is read-heavy — never use it to bulk update or delete in production. Its real power is schema analysis: the Schema tab shows field types, missing fields, and value distributions. Use Compass to profile before writing CRUD, then write your code. The tool is free and ships with MongoDB Community.
// io.thecodeforge — database tutorial // Example aggregation built in Compass (then exported) [{ $match: { status: "active" } }, { $group: { _id: "$category", count: { $sum: 1 }, avgPrice: { $avg: "$price" } } }, { $sort: { count: -1 } }] // Paste into Compass Aggregation tab // Click Export to Language -> Node.js // No syntax errors — Compass validated it live
MongoDB Atlas Setup — From Zero to First Collection Without a Local Install
You want to write CRUD queries, not wrestle with daemons, config files, or missing dependencies. MongoDB Atlas lets you skip all that. It's a cloud-hosted cluster you can spin up in five minutes, free tier included. No local install, no brew, no apt-get. Why run a database on your laptop when you can hit a fully managed endpoint from your code? The payoff: you test against a production-like environment immediately, and your first collection is waiting after one API call. Here's the exact path: sign up at atlas.mongodb.com, click "Build a Database" (free M0 sandbox is fine), pick a cloud provider and region, create a database user with a password, whitelist your IP (or 0.0.0.0/0 for dev), get your connection string, connect via MongoDB Shell or Compass, then create your first database and collection with a single insert. Done.
// io.thecodeforge — database tutorial // Step 1: Connect from shell using your Atlas URI mongosh "mongodb+srv://cluster0.xxxxx.mongodb.net/" --username <user> // Step 2: Create db and collection in one insert testdb.users.insertOne({ name: "Jane", role: "dev" }) // Step 3: Verify show dbs testdb.users.find() // Output: // { _id: ObjectId("..."), name: "Jane", role: "dev" }
Accidental Bulk Update Wipes Fields on 10k Orders
- Always use $set for partial updates; a single-line omit can cost hours of data recovery.
- Write a small script to validate the update with findOne before executing on production.
ObjectId().find() without materialisation prints cursor info, not data.typeof filter._idIf string, convert: new ObjectId(filter._id)console.log('Update argument:', update);If it's a plain object, wrap in { $set: update }db.collection.insertMany(docs, { ordered: false })Check result.writeErrors array| Operation | Single Document Method | Multiple Documents Method | Atomic Grab-and-Act |
|---|---|---|---|
| Create | insertOne(doc) | insertMany([docs], {ordered:false}) | N/A |
| Read | findOne(filter, options) | find(filter).limit(n).toArray() | N/A |
| Update | updateOne(filter, {$set:{...}}) | updateMany(filter, {$set:{...}}) | findOneAndUpdate() |
| Delete | deleteOne(filter) | deleteMany(filter) | findOneAndDelete() |
| Upsert support | Yes — {upsert:true} option | Yes — {upsert:true} option | Yes — {upsert:true} option |
| Returns modified doc | No — returns result metadata | No — returns result metadata | Yes — returns document |
| Race-condition safe | Not inherently | Not inherently | Yes — single atomic op |
| File | Command / Code | Purpose |
|---|---|---|
| insertOrders.js | const { MongoClient, ObjectId } = require('mongodb'); | Create |
| queryOrders.js | const { MongoClient } = require('mongodb'); | Read |
| updateOrders.js | const { MongoClient, ObjectId } = require('mongodb'); | Update |
| deleteOrders.js | const { MongoClient, ObjectId } = require('mongodb'); | Delete |
| writeWithRetry.js | const { MongoClient } = require('mongodb'); | Write Concerns, Error Handling, and Retry Logic |
| IndexAudit.sql | db.orders.find({ user_id: 8472, created_at: { $gte: ISODate("2024-01-01") } }).e... | Indexing Strategies That Actually Matter For Read Performanc |
| TransferFunds.sql | const session = db.getMongo().startSession(); | Transactions |
| BestPracticesExample.sql | CREATE PROCEDURE InsertOrder( | Best Practices for Beginners |
| CommonChallengesSolution.sql | CREATE PROCEDURE InsertOrUpdateOrder( | Common Challenges and Solutions |
| MongoInstallVerify.sql | const { MongoClient } = require('mongodb'); | Installation |
| CompassAggregationTest.sql | [{ | Alternative: MongoDB Compass |
| ConnectAndInsert.sql | mongosh "mongodb+srv://cluster0.xxxxx.mongodb.net/" --username | MongoDB Atlas Setup |
Key takeaways
Common mistakes to avoid
4 patternsUpdating without $set
Comparing string IDs to ObjectId
Using deprecated count() instead of countDocuments()
count() entirely.Not using projection in reads
find() and findOne() options. Pro tip: exclude _id if not needed.Interview Questions on This Topic
What's the difference between updateOne() and replaceOne() in MongoDB, and when would you deliberately choose replaceOne()?
How would you implement an atomic 'claim a task from a queue' operation in MongoDB without creating a race condition between two simultaneous workers?
If countDocuments() and estimatedDocumentCount() both count documents, why do they exist separately — and which would you use on a 50-million-document collection for a real-time dashboard?
Frequently Asked Questions
insertOne() adds a single document and returns a result with the new document's _id. insertMany() adds an array of documents in one network round-trip, which is dramatically faster for bulk loads. The key option to know is { ordered: false }, which tells MongoDB to continue inserting valid documents even if some fail, rather than aborting the entire batch on the first error.
You're passing a plain object as the second argument instead of using an update operator. updateOne({ _id: id }, { status: 'active' }) replaces the document entirely. You need updateOne({ _id: id }, { $set: { status: 'active' } }) — the $set operator tells MongoDB to modify only the named fields and leave everything else alone.
Yes — since version 4.0, MongoDB supports multi-document ACID transactions. For single-document operations, MongoDB has always been atomic. Multi-document transactions are available on replica sets and sharded clusters, but they come with a performance cost. The best practice is to model your data so that most operations only need to touch one document, reserving transactions for the rare cases where you genuinely need cross-collection atomicity.
writeConcern controls how many nodes acknowledge a write. w:1 acknowledges primary only; w:majority waits for replica set majority. Higher writeConcern increases durability but adds latency. Set to majority for critical data, 1 for logs or non-critical.
20+ years shipping high-throughput database systems. Drawn from code that ran under real load.
That's NoSQL. Mark it forged?
9 min read · try the examples if you haven't