iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…