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.
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.
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).
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.
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.
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.
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.
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.
$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.
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.
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.
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.
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.
Bulk Write Operations: Performance Optimization
When performing multiple write operations (inserts, updates, deletes) in MongoDB, individual operations can be slow due to network round trips. Bulk write operations allow you to batch these operations into a single command, significantly improving performance. MongoDB provides two types of bulk operations: ordered and unordered. Ordered operations execute sequentially and stop on the first error, while unordered operations execute in parallel and continue even if some operations fail. This is analogous to SQL batch inserts or updates, but MongoDB's bulk operations are more flexible. For example, in SQL you might write: INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com'); In MongoDB, you can use db.collection.bulkWrite() with an array of operations. This reduces latency and improves throughput, especially in high-volume applications. However, be cautious with large batches as they can impact memory and lock contention. MongoDB recommends batch sizes of 100-1000 operations. Also, note that bulk operations are not atomic; if you need atomicity across multiple documents, consider transactions.
MongoDB Transactions: Multi-Document ACID in Practice
MongoDB supports multi-document ACID transactions since version 4.0, allowing you to perform multiple read and write operations across documents and collections atomically. This is similar to SQL transactions where you can commit or rollback a set of operations. For example, in SQL: BEGIN TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; In MongoDB, you use session.startTransaction() and session.commitTransaction(). Transactions are essential for use cases like financial transfers, inventory management, or any scenario where data consistency across documents is critical. However, transactions come with overhead: they require replica sets (or sharded clusters with MongoDB 4.2+), and they can impact performance if used excessively. Best practices include keeping transactions short, avoiding unnecessary operations inside them, and handling retry logic for transient errors. Also, note that transactions are not supported on standalone servers. Use them judiciously for operations that truly require atomicity.
Change Streams: Reactive Data Processing
Change streams allow applications to watch for real-time changes (inserts, updates, deletes) in collections, databases, or entire deployments. This is similar to SQL triggers or CDC (Change Data Capture) mechanisms, but MongoDB change streams are native and scalable. For example, in SQL you might use a trigger to log changes: CREATE TRIGGER log_changes AFTER INSERT ON users FOR EACH ROW INSERT INTO audit_log (action, user_id) VALUES ('INSERT', NEW.id); In MongoDB, you open a change stream cursor and process events as they occur. Change streams are built on the oplog (operations log) and provide a reliable, ordered stream of events. They are ideal for reactive applications, real-time analytics, caching invalidation, or synchronizing data with external systems. You can filter events using pipelines, resume from a specific point in time, and handle errors gracefully. However, change streams require a replica set (or sharded cluster) and may have latency depending on the deployment. Also, be aware of the oplog size; if the oplog is too small, change stream events may be lost before they are consumed.
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)| 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 |
| bulk_write_example.js | const { MongoClient } = require('mongodb'); | Bulk Write Operations |
| transaction_example.js | const { MongoClient } = require('mongodb'); | MongoDB Transactions |
| change_stream_example.js | const { MongoClient } = require('mongodb'); | Change Streams |
Key takeaways
Interview Questions on This Topic
What's the difference between updateOne() and replaceOne() in MongoDB, and when would you deliberately choose replaceOne()?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Drawn from code that ran under real load.
That's NoSQL. Mark it forged?
11 min read · try the examples if you haven't