deleteOne / deleteMany
deleteOne / deleteMany remove documents. Always include a precise filter; without one, deleteMany({}) wipes the collection. Use transactions for multi-document deletes, soft-delete for audit trails.
deleteOne, deleteMany, bulk, soft delete
EXAMPLE
// 1) Delete one — by _id (most common)
const { deletedCount } = await db.users.deleteOne({ _id: userId });
// deletedCount is 0 if no match, 1 if matched
// 2) Delete one — by filter (deletes first match)
await db.tasks.deleteOne({ status: 'done', priority: 'low' });
// 3) Delete many
const { deletedCount } = await db.logs.deleteMany({
createdAt: { $lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
});
console.log(`deleted ${deletedCount} old logs`);
// 4) Delete + return the deleted document
const deleted = await db.orders.findOneAndDelete(
{ _id: orderId, userId: req.user.id }, // scoped — own orders only
);
if (!deleted) return res.status(404).end();
await audit.log({ event: 'order.delete', payload: deleted });
// 5) Bulk write — mix deletes with other ops
await db.notifications.bulkWrite([
{ deleteMany: { filter: { read: true, createdAt: { $lt: oneMonthAgo } } } },
{ deleteOne: { filter: { _id: oneOff } } },
{ updateOne: { filter: { _id: keep }, update: { $set: { archived: true } } } },
], { ordered: false });
// 6) Cascade — Mongo doesn't have FK cascades; do it in app code or use a hook
async function deleteUser(userId) {
const session = client.startSession();
try {
await session.withTransaction(async () => {
await db.users.deleteOne({ _id: userId }, { session });
await db.posts.deleteMany({ userId }, { session });
await db.comments.deleteMany({ userId }, { session });
await db.sessions.deleteMany({ userId }, { session });
});
} finally {
await session.endSession();
}
}
// 7) Soft delete — usually safer than hard delete
await db.users.updateOne(
{ _id: userId },
{ $set: { deletedAt: new Date(), deletedBy: req.user.id } },
);
// In every read query, exclude soft-deleted:
await db.users.find({ deletedAt: { $exists: false } }).toArray();
// Or add a partial index for performance:
await db.users.createIndex({ email: 1 }, { unique: true, partialFilterExpression: { deletedAt: { $exists: false } } });
// 8) TTL index — auto-delete old documents
await db.sessions.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 },
);
// Mongo's TTL monitor sweeps every ~60s and deletes expired docs.
// Great for sessions, OTP codes, temporary tokens, cache entries.
await db.sessions.insertOne({
sid: 'abc',
userId: u,
expiresAt: new Date(Date.now() + 30 * 60_000), // 30 min from now
});
// 9) Drop a collection (destructive — entire collection gone)
await db.staging.drop();
// Drop the whole database (very destructive)
await client.db('temp').dropDatabase();
// 10) Verify before deleting — read first, log, then delete
async function safeDelete(filter) {
const candidates = await db.orders.find(filter).toArray();
if (candidates.length > 100) {
throw new Error(`safety stop: ${candidates.length} matches; refine filter`);
}
await audit.log({ event: 'pre_delete', candidates: candidates.map(c => c._id) });
return db.orders.deleteMany(filter);
}
// 11) Hard delete with ownership check (multi-tenant)
await db.orders.deleteOne({ _id: orderId, userId: req.user.id });
// Filter ALWAYS includes the owner — even if attacker guesses an orderId, they can't delete others'.
// 12) Transactional delete + write to audit log
await session.withTransaction(async () => {
const doc = await db.orders.findOneAndDelete({ _id }, { session });
if (!doc) throw new Error('not found');
await db.deletedOrders.insertOne(doc, { session });
});
// 13) Watch for these bugs
// • db.collection.deleteMany({}) → empties the collection
// • Forgetting to scope by owner → user A can delete user B's data
// • Cascade deletes without a transaction → partial deletes on crash
// • Soft delete without an index excluding deleted → queries scan everything
// • Hard delete without an audit trail → can't answer 'who deleted that?'
// • Deleting in a loop with no batching → many round trips
// 14) Performance
// • DeleteOne / deleteMany are O(documents deleted) plus index updates
// • Bulk deletes hold a write lock per batch; long deletes can affect concurrency
// • For huge cleanups, batch + sleep:
// while ((await db.logs.deleteMany({ createdAt: { $lt: cutoff } }, { limit: 10_000 })).deletedCount === 10_000) {
// await new Promise(r => setTimeout(r, 100));
// }
// • Or use a window: deleteMany matching a 1-day slice; advance the slice
// 15) When to choose what
// Hard delete — compliance / GDPR removal
// Soft delete — typical for user-facing data; preserves audit
// TTL index — anything with a clear expiry (sessions, OTPs, caches)
// Move-to-archive — large compliance windows; archive table outside hot path
Why it matters
deleteMany({}) with an empty filter empties the collection — always include a precise filter scoped to the owner. For most user-facing data, soft delete + a partial index is safer than hard delete; TTL indexes auto-clean ephemeral data without app code.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
db.customers.deleteOne({ _id: id });
db.customers.deleteMany({ active: false });
Try it Yourself »
Discussion
Loading…