Using a Dynamic Value in Aggregation

て烟熏妆下的殇ゞ 提交于 2019-12-24 08:52:35

问题


I have a document structure like this:

{
    "_id" : ObjectId("59d7cd63dc2c91e740afcdb"),
    "dateJoined": ISODate("2014-12-28T16:37:17.984Z"),
    "dateActivated": ISODate("2015-02-28T16:37:17.984Z"), 
    "enrolled" : [
        { "month":-10, "enrolled":'00'},
        { "month":-9, "enrolled":'00'},
        { "month":-8, "enrolled":'01'},
        //other months
        { "month":8, "enrolled":'11'},
        { "month":9, "enrolled":'11'},
        { "month":10, "enrolled":'00'}
    ]
}

"month" value in enrolled is relative to dateJoined that range from -X to +X that is pre-populated.

I would like to count number of document with enrolled value of '01' for every sub document that satisfies condition - like "5 months before activating and 2 months after activating". All sub document items must match the condition to count as 1. [Yes, it is possible to enroll before activating :)]

As the month value is not based on dateActivated, I should be able to dynamically calculate this for every document.

I am trying to use MongoDB aggregation framework but not sure how to dynamically.

db.getCollection("enrollments").aggregate(
    { $match:{ //matching condition }},
    { $project: {
            enrollments: {
                $filter: {
                    input: "$enrolled",
                    as: "enrollment",
                    cond: {
                        $eq: ['$$enrolled.enroll', '01']
                        //how can I check for month value here?
                    }
                }
            }
    }}
)

回答1:


The general ask here is to include the range for the "month" values in consideration where it is "greater than" the -5 months "before" and "less than" the +2 months "after" as recorded within the "enrolled" array entries.

The problem is that since these values are based on "dateJoined", they need to be adjusted by the correct interval between the "dateJoined" and the "dateActivated". This makes the expression effectively:

monthsDiff = (yearActivated - yearJoined)*12 + (monthActivated - monthJoined)

where month >= ( startRange + monthsDiff ) and month <= ( endRange + monthsDiff )
and enrolled = "01"

Or logically expressed "The months between the expressed range adjusted by the number of months difference between joining and activating".

As stated in comment, the very first thing you need to to here is to store those date values as a BSON Date as opposed to their present apparent "string" values. Once that is done, you can then apply the following aggregation to calculate the difference from the supplied dates and filter the adjusted range accordingly from the array before counting:

var rangeStart = -5,
    rangeEnd = 2;

db.getCollection('enrollments').aggregate([
  { "$project": {
    "enrollments": {
      "$size": {
        "$filter": {
          "input": "$enrolled",
          "as": "e",
          "cond": {
            "$let": {
              "vars": {
                "monthsDiff": {
                  "$add": [
                    { "$multiply": [
                      { "$subtract": [
                        { "$year": "$dateActivated" },
                        { "$year": "$dateJoined" }
                      ]},
                      12
                    }},
                    { "$subtract": [
                      { "$month": "$dateActivated" },
                      { "$month": "$dateJoined" }
                    ]}
                  ]
                }
              },
              "in": {
                "$and": [
                  { "$gte": [ { "$add": [ rangeStart, "$$monthsDiff" ] }, "$$e.month" ] },
                  { "$lte": [ { "$add": [ rangeEnd, "$$monthsDiff" ] }, "$$e.month" ] },
                  { "$eq": [ "$$e.enrolled", "01" ] }
                ]
              }
            }
          } 
        }
      }
    }
  }}
])

So this applies the same $filter to the array which you were attempting, but now takes into account the adjusted values on the range of months to filter by as well.

To make this easier to read we apply $let which allows calculation of the common value obtained for $$monthsDiff as implemented in a variable. Here is where the expression explained originally is applied, using $year and $month to extract those numeric values from the dates as stored.

Using the additional mathematical operators $add, $subtract and $multiply you can calculate both the difference in months and also later apply to adjust the "range" values in the logical conditions with $gte and $lte.

Finally, because $filter emits an array of only the entries matching the conditions, in order to "count" we apply $size which returns the length of the "filtered" array, which is the "count" of matches.

Depending on your intended purpose the whole expression can also be provided in argument to $sum as a $group accumulator, if then was indeed the intention.




回答2:


You can try the below aggregation provided you store days instead of months.

Days diff to calculate the days between dateActivated and dateJoined offsetting the days to get the enrollement days relative to dateActivated.

Compare daysdiff against the following values.

-120-0 days when enrollment is after dateActivated

0-150 days when enrollment is before dateActivated

$or the above conditions & $and with enrolled value.

db.getCollection("enrollments").aggregate(
 {
  "$project": {
    "enrollments": {
      "$filter": {
        "input": "$enrolled",
        "as": "enrollment",
        "cond": {
          "$and": [
            {
              "$eq": [
                "$$enrollment.enrolled",
                "01"
              ]
            },
            {
              "$let": {
                "vars": {
                  "daysdiff": {
                    "$divide": [
                      {
                        "$subtract": [
                          "$dateActivated",
                          {
                            "$add": [
                              "$dateJoined",
                              {
                                "$multiply": [
                                  "$$enrollment.day",
                                  86400 * 1000
                                ]
                              }
                            ]
                          }
                        ]
                      },
                      86400 * 1000
                    ]
                  }
                },
                "in": {
                  "$or": [
                    {
                      "$and": [
                        {
                          "$lt": [
                            "$$daysdiff",
                            150
                          ]
                        },
                        {
                          "$gt": [
                            "$$daysdiff",
                            0
                          ]
                        }
                      ]
                    },
                    {
                      "$and": [
                        {
                          "$lt": [
                            "$$daysdiff",
                            0
                          ]
                        },
                        {
                          "$gt": [
                            "$$daysdiff",
                            -120
                          ]
                        }
                      ]
                    }
                  ]
                }
              }
            }
          ]
        }
      }
    }
  }
})


来源:https://stackoverflow.com/questions/46697878/using-a-dynamic-value-in-aggregation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!