如何使用聚合计算运行总计

Fab*_* B. 7 mapreduce mongodb mongodb-query aggregation-framework

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

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

{ "_id" : ObjectId("54adc0659413535e02fba115"), "description" : "test1", "amount" : 100, "dateEntry" : ISODate("2015-01-07T23:00:00Z") }
{ "_id" : ObjectId("54adc21a0d150c760270f99c"), "description" : "test2", "amount" : 50, "dateEntry" : ISODate("2015-01-06T23:00:00Z") }
{ "_id" : ObjectId("54b05da766341e4802b785c0"), "description" : "test3", "amount" : 11, "dateEntry" : ISODate("2015-01-09T23:00:00Z") }
{ "_id" : ObjectId("54b05db066341e4802b785c1"), "description" : "test4", "amount" : 2, "dateEntry" : ISODate("2015-01-09T23:00:00Z") }
{ "_id" : ObjectId("54b05dbb66341e4802b785c2"), "description" : "test5", "amount" : 12, "dateEntry" : ISODate("2015-01-09T23:00:00Z") }
{ "_id" : ObjectId("54b05f4ee0933a5c02398d55"), "description" : "test6", "amount" : 4, "dateEntry" : ISODate("2015-01-09T23:00:00Z") }
Run Code Online (Sandbox Code Playgroud)

我现在想要的是根据这些数据绘制一个"平衡"图表:

[
   { day:'2015-01-06', amount:50}, 
   { day:'2015-01-07', amount:150}, 
   { day:'2015-01-09', amount:179},
...
]
Run Code Online (Sandbox Code Playgroud)

换句话说,我需要按天分组我的所有交易,并且每天我需要总结我以前的所有交易(从世界的开始).

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

$group: {
   _id: { 
      y: {$year:"$dateEntry"}, 
      m: {$month:"$dateEntry"}, 
      d: {$dayOfMonth:"$dateEntry"} 
   }, 
   sum: ???
}
Run Code Online (Sandbox Code Playgroud)

但我不知道如何回去并总结所有金额.想象一下,我需要显示月度余额报告:我应该运行31个查询,每天一个查询除了下一天之外的所有交易金额吗?当然可以,但不要认为这是最好的解决方案.

提前致谢!

Xav*_*hot 6

从 开始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 }
Run Code Online (Sandbox Code Playgroud)

这:

  • cumulative在每个文档中添加字段 ( output: { cumulative: { ... }})
  • 这是s ( )$sumamount$sum: "$amount"
  • 在指定的文档范围内 (the 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 }
Run Code Online (Sandbox Code Playgroud)


Nei*_*unn 5

实际上,至少在最初的问题解决中,比聚合框架更适合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 }
  }
)      
Run Code Online (Sandbox Code Playgroud)

这将按日期分组求和,然后在“完成”部分中逐日累计。

   "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
            }
    ],
Run Code Online (Sandbox Code Playgroud)

从长远来看,最好是有一个单独的集合,并为每天添加一个条目,以$inc在更新中更改余额。还要在每天开始时进行一次upsert操作,以创建一个新文件,结转前一天的余额:$inc

// 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 }
);
Run Code Online (Sandbox Code Playgroud)

如何不这样做

的确,由于最初的写作有更多的运算符引入聚合框架,因此,在聚合语句中执行此处要求的操作仍然不切实际

相同的基本规则适用于聚合框架不能引用先前“文档”中的值,也不能存储“全局变量”。通过强制将所有结果“破解”到数组中:

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" } }
])
Run Code Online (Sandbox Code Playgroud)

考虑到较大的结果集具有突破16MB BSON限制的真正可能性,这既不是高效的解决方案,也不是“安全”的解决方案。作为“黄金法则”,任何建议将所有内容放入单个文档的数组中的内容:

{ "$group": {
  "_id": null,
  "docs": { "$push": "$$ROOT" }
}}
Run Code Online (Sandbox Code Playgroud)

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


结论

处理此问题的更具说服力的方法通常是在运行的结果游标上进行后处理:

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 });
})
Run Code Online (Sandbox Code Playgroud)

因此,通常最好:

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

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

聚合框架应真正用于“聚合”,仅此而已。通过诸如处理数组之类的过程来强制处理数据,以处理您想要的方式既不明智也不安全,最重要的是,客户端处理代码更加简洁高效。

让数据库做他们擅长的事情,因为您的“操纵”要好得多地用代码来处理。