You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 13:40:28