MongoDB:如何基于另一查询结果高效批量删除文档?
我有一个可能包含数百万文档的集合,示例文档结构如下:
{'_id': '1', 'user_id': 1, 'event_type': 'a', 'name': 'x'} {'_id': '2', 'user_id': 1, 'event_type': 'b', 'name': 'x'} {'_id': '3', 'user_id': 1, 'event_type': 'c', 'name': 'x'} {'_id': '4', 'user_id': 2, 'event_type': 'a', 'name': 'x'} {'_id': '5', 'user_id': 2, 'event_type': 'b', 'name': 'x'} {'_id': '6', 'user_id': 3, 'event_type': 'a', 'name': 'x'} {'_id': '7', 'user_id': 3, 'event_type': 'b', 'name': 'x'} {'_id': '8', 'user_id': 4, 'event_type': 'a', 'name': 'x'} {'_id': '9', 'user_id': 4, 'event_type': 'b', 'name': 'x'} {'_id': '10', 'user_id': 4, 'event_type': 'c', 'name': 'x'}
需要运行每日任务,删除所有满足该文档所属user_id存在event_type为'c'的文档的记录,处理后集合应保留:
{'_id': '4', 'user_id': 2, 'event_type': 'a', 'name': 'x'} {'_id': '5', 'user_id': 2, 'event_type': 'b', 'name': 'x'} {'_id': '6', 'user_id': 3, 'event_type': 'a', 'name': 'x'} {'_id': '7', 'user_id': 3, 'event_type': 'b', 'name': 'x'}
我用Mongo Shell实现了功能,但运行速度极慢,代码如下:
var cur = db.my_collection.find({'event_type': 'c'}) ids = []; while (cur.hasNext()) { ids.push(cur.next()['user_id']); if (ids.length == 5){ print('deleting for user_ids', ids); print(db.my_collection.deleteMany({user_id: {$in: ids}})); ids = []; } } if (ids.length){db.my_collection.deleteMany({user_id: {$in: ids}})}
问题在于每次cur.next()似乎仅获取一个文档,而且批量删除的批次太小。想知道有没有更优的实现方式,比如Elasticsearch中切片滚动并行扫描分片、批量提交deleteByQuery的方案,MongoDB是否支持类似并行处理?
数据规模:集合约1000万文档,其中30万匹配event_type='c',需删除约70万文档。
1. 一次性去重获取目标user_id,避免重复处理
原代码逐个读取event_type='c'的文档,还可能重复获取同一个user_id(如果一个用户有多个c类型事件)。改用聚合查询直接去重获取所有需要删除的user_id,效率更高:
// 一次性获取所有存在event_type='c'的user_id,自动去重 var targetUserIds = db.my_collection.aggregate([ { $match: { event_type: 'c' } }, { $group: { _id: '$user_id' } }, { $project: { user_id: '$_id', _id: 0 } } ]).toArray().map(item => item.user_id);
2. 增大批量删除的批次大小
原代码每5个user_id执行一次删除,请求次数过多。可以把批次调到1000-5000(根据你的MongoDB集群性能调整),减少IO开销:
const batchSize = 1000; for (let i = 0; i < targetUserIds.length; i += batchSize) { const batch = targetUserIds.slice(i, i + batchSize); print(`Deleting batch ${i/batchSize + 1}, user_ids count: ${batch.length}`); db.my_collection.deleteMany({ user_id: { $in: batch } }); }
3. 并行处理:用切片游标实现分片并行扫描
MongoDB支持切片游标,可以将游标拆分成多个分片,在多线程/进程中并行处理,类似ES的切片滚动。适合分片集群或单节点大集合场景:
切片游标核心逻辑示例
比如分成4个切片并行处理(Mongo Shell本身单线程,生产环境建议用Node.js/Python等多线程语言实现):
const sliceCount = 4; // 每个切片单独处理 for (let slice = 0; slice < sliceCount; slice++) { (function(slice) { // 创建切片游标,指定切片ID和总切片数 const cur = db.my_collection.find({ event_type: 'c' }) .batchSize(1000) .addOption({ slice: { id: slice, max: sliceCount } }); let ids = []; while (cur.hasNext()) { ids.push(cur.next().user_id); if (ids.length >= 1000) { db.my_collection.deleteMany({ user_id: { $in: ids } }); ids = []; } } if (ids.length > 0) { db.my_collection.deleteMany({ user_id: { $in: ids } }); } })(slice); }
每个切片游标会扫描集合中一部分数据,多进程/线程同时处理不同切片,能大幅提升处理速度。
4. 一步式聚合管道删除(MongoDB 4.2+)
MongoDB 4.2及以上支持在deleteMany中使用聚合管道,无需先获取user_id再删除,直接完成筛选+删除:
db.my_collection.deleteMany({ $expr: { $ne: [ { $size: { $filter: { input: { $lookup: { from: 'my_collection', localField: 'user_id', foreignField: 'user_id', as: 'user_events' } }, cond: { $eq: ['$$this.event_type', 'c'] } } }, 0 ] } } });
注意:大集合下需确保user_id和event_type有索引,否则性能会受影响。
索引优化(必做)
无论用哪种方案,都要创建以下索引,大幅提升查询和删除效率:
// 针对event_type=c的查询创建索引 db.my_collection.createIndex({ event_type: 1 }); // 针对user_id的删除查询创建索引 db.my_collection.createIndex({ user_id: 1 });
内容的提问来源于stack exchange,提问作者Ron Serruya

