【问题标题】:How to calculate the running total using aggregate?如何使用聚合计算运行总数?
【发布时间】:2015-03-15 17:09:01
【问题描述】:

我正在开发一个简单的财务应用程序来跟踪收入和结果。

为了简单起见,假设这些是我的一些文档:

{ description: "test1", amount: 100, dateEntry: ISODate("2015-01-07T23:00:00Z") }
{ description: "test2", amount: 50,  dateEntry: ISODate("2015-01-06T23:00:00Z") }
{ description: "test3", amount: 11,  dateEntry: ISODate("2015-01-09T23:00:00Z") }
{ description: "test4", amount: 2,   dateEntry: ISODate("2015-01-09T23:00:00Z") }
{ description: "test5", amount: 12,  dateEntry: ISODate("2015-01-09T23:00:00Z") }
{ description: "test6", amount: 4,   dateEntry: ISODate("2015-01-09T23:00:00Z") }

我现在想要的是根据这些数据绘制一个“余额”图表:

{ day: "2015-01-06", amount: 50  }
{ day: "2015-01-07", amount: 150 }
{ day: "2015-01-09", amount: 179 }

换句话说,我需要按天对我的所有交易进行分组,并且每天我需要汇总我之前的所有交易(从世界之初开始)。

我已经知道如何按天分组了:

$group: {
   _id: { 
      y: {$year:"$dateEntry"}, 
      m: {$month:"$dateEntry"}, 
      d: {$dayOfMonth:"$dateEntry"} 
   }, 
   sum: ???
}

但我不知道如何返回并汇总所有金额。

假设我需要显示每月余额报告:我是否应该运行 31 次查询,每天一次,将除后几天之外的所有交易金额相加?当然可以,但不要认为这是最好的解决方案。

【问题讨论】:

  • 我不知道如何准确地编写查询,尼尔。如果我只是做了 {$sum:"$amount"} 只有当前的组交易将包含在我的结果中,我不希望这样。请仔细阅读问题;)
  • 对不起。也许我无法解释我到底想要什么,说真的。我不想要每天所有金额的简单总和。我想总结每天前几天的所有金额。在我的示例中,2015-01-07 是 1 月 6 日和 1 月 7 日金额的总和,2015-01-09 是 1 月 6、7、9 日金额的总和,依此类推。
  • 为什么我的问题被否决了?我认为这将是一个有趣的...
  • 我真的无法理解......我认为按天分组和求和会给我每天交易的总和,而不是你所说的“加起来”。我的意思是,在 6 时,我将仅对 6 笔交易求和,7 笔仅对 7 笔进行求和,以此类推。

标签: mongodb mongodb-query mapreduce aggregation-framework cumulative-sum


【解决方案1】:

实际上比聚合框架更适合mapReduce,至少在最初的问题解决中。聚合框架没有先前文档的值的概念,或者文档的先前“分组”值的概念,所以这就是它不能这样做的原因。

另一方面,mapReduce 有一个“全局范围”,可以在处理阶段和文档时在它们之间共享。这将在您需要的一天结束时为您提供当前余额的“运行总计”。

db.collection.mapReduce(
  function () {
    var date = new Date(this.dateEntry.valueOf() -
      ( this.dateEntry.valueOf() % ( 1000 * 60 * 60 * 24 ) )
    );

    emit( date, this.amount );
  },
  function(key,values) {
      return Array.sum( values );
  },
  { 
      "scope": { "total": 0 },
      "finalize": function(key,value) {
          total += value;
          return total;
      },
      "out": { "inline": 1 }
  }
)      

这将按日期分组求和,然后在“最终确定”部分中计算每天的累积总和。

   "results" : [
            {
                    "_id" : ISODate("2015-01-06T00:00:00Z"),
                    "value" : 50
            },
            {
                    "_id" : ISODate("2015-01-07T00:00:00Z"),
                    "value" : 150
            },
            {
                    "_id" : ISODate("2015-01-09T00:00:00Z"),
                    "value" : 179
            }
    ],

从长远来看,您最好有一个单独的集合,每天都有一个条目,并在更新中使用$inc 更改余额。只需在每天开始的时候做一个$incupsert 来创建一个新的文档来结转前一天的余额:

// increase balance
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": amount } },
    { "upsert": true }
);

// decrease balance
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": -amount } },
    { "upsert": true }
);

// Each day
var lastDay = db.daily.findOne({ "dateEntry": lastDate });
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": lastDay.balance } },
    { "upsert": true }
);

如何不这样做

虽然从原文开始,聚合框架中引入了更多的运算符,但这里提出的问题在聚合语句中仍然不实际

同样的基本规则适用于聚合框架不能从以前的“文档”引用值,也不能存储“全局变量”。 “黑客” 通过将所有结果强制转换为数组:

db.collection.aggregate([
  { "$group": {
    "_id": { 
      "y": { "$year": "$dateEntry" }, 
      "m": { "$month": "$dateEntry" }, 
      "d": { "$dayOfMonth": "$dateEntry" } 
    }, 
    "amount": { "$sum": "$amount" }
  }},
  { "$sort": { "_id": 1 } },
  { "$group": {
    "_id": null,
    "docs": { "$push": "$$ROOT" }
  }},
  { "$addFields": {
    "docs": {
      "$map": {
        "input": { "$range": [ 0, { "$size": "$docs" } ] },
        "in": {
          "$mergeObjects": [
            { "$arrayElemAt": [ "$docs", "$$this" ] },
            { "amount": { 
              "$sum": { 
                "$slice": [ "$docs.amount", 0, { "$add": [ "$$this", 1 ] } ]
              }
            }}
          ]
        }
      }
    }
  }},
  { "$unwind": "$docs" },
  { "$replaceRoot": { "newRoot": "$docs" } }
])

这既不是一个高性能的解决方案,也不是“安全”,考虑到更大的结果集运行违反 16MB BSON 限制的非常真实的可能性。作为“黄金法则”,任何建议将所有内容放入单个文档的数组中的东西:

{ "$group": {
  "_id": null,
  "docs": { "$push": "$$ROOT" }
}}

那么这是一个基本缺陷,因此不是解决方案


结论

解决这个问题的更有说服力的方法通常是对结果的运行光标进行后处理:

var globalAmount = 0;

db.collection.aggregate([
  { $group: {
    "_id": { 
      y: { $year:"$dateEntry"}, 
      m: { $month:"$dateEntry"}, 
      d: { $dayOfMonth:"$dateEntry"} 
    }, 
    amount: { "$sum": "$amount" }
  }},
  { "$sort": { "_id": 1 } }
]).map(doc => {
  globalAmount += doc.amount;
  return Object.assign(doc, { amount: globalAmount });
})

所以总的来说,最好:

  • 使用游标迭代和跟踪变量进行总计。 mapReduce 示例是上述简化过程的人为示例。

  • 使用预先汇总的总计。可能与游标迭代一致,具体取决于您的预聚合过程,无论是间隔总计还是“结转”运行总计。

聚合框架应该真正用于“聚合”,仅此而已。通过诸如操作到数组之类的过程来强制对数据进行强制转换只是为了处理您想要的方式既不明智也不安全,最重要的是,客户端操作代码更加干净和高效。

让数据库做它们擅长的事情,因为你的“操作”在代码中处理得更好。

【讨论】:

  • 谢谢尼尔,答案的更新正是我所做的。作为 MongoDB 的新手(仅在本周五开始),我一直在寻找“现代”解决方案。我只有一个问题。我正在对集合进行批量插入,因此聚合仅适用于增量(较新)数据。是否有我应该注意的性能影响。
【解决方案2】:

Mongo 5 开始,这是新的$setWindowFields 聚合运算符的完美用例:

// { day: "2015-01-06", "amount": 50 }
// { day: "2015-01-07", "amount": 100 }
// { day: "2015-01-09", "amount": 11 }
db.collection.aggregate([
  { $setWindowFields: {
    sortBy: { day: 1 },
    output: {
      cumulative: {
        $sum: "$amount",
        window: { documents: [ "unbounded", "current" ] }
      }
    }
  }}
])
// { day: "2015-01-06", amount: 50,  cumulative: 50 }
// { day: "2015-01-07", amount: 100, cumulative: 150 }
// { day: "2015-01-09", amount: 11,  cumulative: 161 }

这个:

  • 在每个文档中添加cumulative 字段 (output: { cumulative: { ... }})
  • 这是amounts ($sum: "$amount")的$sum
  • 在指定的文档范围内 (window)
    • 在我们的例子中是任何以前的文档:集合中的window: { documents: [ "unbounded", "current" ] } }
    • [ "unbounded", "current" ] 定义,表示该窗口是在第一个文档 (unbounded) 和当前文档 (current) 之间看到的所有文档。
  • 另请注意,我们已确保按天对文档进行排序 (sortBy: { day: 1 })。

这是针对您的确切问题的完整查询(使用初始的$group 将您的文档按天分组,并加上它们的金额总和):

// { date: ISODate("2015-01-06T23:00:00Z"), "amount": 50 },
// { date: ISODate("2015-01-07T23:00:00Z"), "amount": 100 },
// { date: ISODate("2015-01-09T23:00:00Z"), "amount": 11 },
// { date: ISODate("2015-01-09T23:00:00Z"), "amount": 2 }
db.collection.aggregate([
  { $group: {
    _id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
    "amount": { "$sum": "$amount" } }
  },
  { $setWindowFields: {
    sortBy: { _id: 1 },
    output: {
      cumulative: {
        $sum: "$amount",
        window: { documents: [ "unbounded", "current" ] }
      }
    }
  }}
])
// { _id: "2015-01-06", amount: 50,  cumulative: 50 }
// { _id: "2015-01-07", amount: 100, cumulative: 150 }
// { _id: "2015-01-09", amount: 13,  cumulative: 163 }

【讨论】:

  • 谢谢,我更喜欢这个,因为我可以在 Grafana 中运行它
猜你喜欢
  • 1970-01-01
  • 2013-04-17
  • 1970-01-01
  • 1970-01-01
  • 2015-06-29
  • 1970-01-01
  • 2013-06-10
  • 2020-10-02
  • 2016-02-06
相关资源
最近更新 更多