删除其他人时按组保留N个最新记录

时间:2017-08-30 04:29:41

标签: mongodb mongodb-query

MongoDB中是否有一种方法可以识别(以便删除)除每组最近的N条记录以外的所有记录?

我们有一个MongoDB集合,我们收集了#34; docs"和"记录"。每个日志都保留对docId和日期的引用:

  • doc ({_id: ObjectId})
  • log ({ docId: String, date: Date})

日志数据库变得越来越大。我想删除除以下任何日志之外的所有日志:

  • 不到30天,或
  • 在给定docId的最新12个日志中

我知道如何删除超过30天的所有日志。但我不知道如何保留文档的最新N日志。

1 个答案:

答案 0 :(得分:2)

在单一陈述中无法完成。你能做的就是基本上确定最后的1​​2"对于每个可能的"docId"值,然后在发出删除文档的请求时,通过将列表添加到$nin来排除这些文档被删除。

您没有指定首选编程环境,但这是使用nodejs的一般过程:

const MongoClient = require('mongodb').MongoClient;

const uri = 'mongodb://localhost/test';

(async function() {

  let db;

  // Calculate date cutoff
  let oneDay = 1000 * 60 * 60 * 24,
      thirtyDays = oneDay * 30,
      now = Date.now(),
      cutoff = new Date(
        ( now - ( now % oneDay ) ) - thirtyDays
      );

  try {

    db = await MongoClient.connect(uri);

    let log = db.collection('log');
    let doc = db.collection('doc');

    await new Promise((resolve,reject) => {

      let ops = [];

      let stream = doc.find();

      stream.on('error', reject);
      stream.on('end', async () => {
        if ( ops.length > 0 ) {
          await log.bulkWrite(ops);
          ops = [];
        }
        resolve();
      });

      stream.on('data', async (data) => {

        // Pause processing input stream
        stream.pause();

        // get last 12 for doc
        let last = await (log.find({ docId: data._id })
          .project({ _id: 1 })
          .sort({ date: -1 }).limit(12)).map(d => d._id);

        ops.push({
          deleteMany: {
            filter: {
              _id: { $nin: last },
              docId: data._id,
              date: { $lt: cutoff }
            }
          }
        });

        if ( ops.length >= 1000 ) {
          await log.bulkWrite(ops);
          ops = [];
        }

        // Resume processing input stream
        stream.resume()

      });

    });

  } catch(e) {
    console.error(e);
  } finally {
    db.close();
  }

})();

这里的基本前提是你循环使用" doc"收集然后执行查询" log"收集以返回最近的12个文件。然后,我们列出了每个找到的文档中的_id值(如果有的话)。

这样做的关键是接下来要做的是对数据库发出deleteMany操作。因为会有很多这些,所以我们将使用.bulkWrite()而不是每次迭代源文档时发出请求。这样可以大大减少网络流量并延迟。

然后基本陈述是删除"docId"与光标中的源匹配当前文档的所有文档,以及日期早于30天截止点的位置。

其他条件使用$nin来排除"排除"任何被识别为在最近的12"来自上一个查询。这样可以确保始终保留这些文档,因为它们不会被删除。

        ops.push({
          deleteMany: {
            filter: {
              _id: { $nin: last },
              docId: data._id,
              date: { $lt: cutoff }
            }
          }
        });

这就是它真正存在的一切。剩下的处理是关于积累"批次"当实际请求被发送到服务器以处理并实际删除文档时,直到有1000个条目(合理的大小,但是对于请求的任何16MB BSON限制都是可能的)。

当光标耗尽时,过程完成,任何剩余的"批处理"指示已经提交。

MongoDB 3.6预览

有一件事你可以摆脱目前的"即将到来的" MongoDB的发布是它允许"非相关" $lookup的形式,这意味着我们基本上可以获得前12名"对于单个请求中的每个目标文档,而不是发出多个查询。

这样做是因为这种形式的$lookup需要一个"管道"作为参数而不是基于本地和外键匹配的固定输出。这样,我们就可以$match$sort$limit返回结果。

const MongoClient = require('mongodb').MongoClient;

const uri = 'mongodb://localhost/test';

(async function() {

  let db;

  // Calculate date cutoff
  let oneDay = 1000 * 60 * 60 * 24,
      thirtyDays = oneDay * 30,
      now = Date.now(),
      cutoff = new Date(
        ( now - ( now % oneDay ) ) - thirtyDays
      );

  try {

    db = await MongoClient.connect(uri);

    await new Promise((resolve,reject) => {

      let ops = [];

      let stream = db.collection('doc').aggregate([
        { "$lookup": {
          "from": "log",
          "let": {
            "id": "$_id"
          },
          "pipeline": [
            { "$match": {
              "docId": { "$eq": { "$expr": "$$id" } }
            }},
            { "$sort": { "date": -1 } },
            { "$limit": 12 },
            { "$project": { "_id": 1 } }
          ],
          "as": 'docs'
        }},
      ]);

      stream.on('error', reject);
      stream.on('end', async () => {
        if ( ops.length > 0 ) {
          await db.collection('log').bulkWrite(ops);
          ops = [];
        }
        resolve();
      });

      stream.on('data', async (data) => {
        stream.pause();

        ops.push({
          deleteMany: {
            filter: {
              _id: { $nin: data.docs.map(d => d._id) },
              docId: data._id,
              date: { $lt: cutoff }
            }
          }
        });

        if ( ops.length >= 1000 ) {
          await db.collection('log').bulkWrite(ops);
          ops = [];
        }
        stream.resume();

      });

    });


  } catch(e) {
    console.error(e);
  } finally {
    db.close();
  }

})();

关键是$expr,它仅在3.5.12开发版本中定稿。这允许有效的$match表达式,然后使其成为处理单独查询的可行替代方案。

当然,你真的想等待它为生产做好准备。但了解它是件好事,这样你就可以转换到最终升级基础MongoDB这样的过程。