对查询中的多个日期范围进行计数

fpe*_*a06 3 mongodb mongodb-query aggregation-framework

我有以下聚合查询,它给出给定日期范围内的计数 (countA)。在本例中为 01/01/2016-03/31/2016。是否可以添加第二个日期愤怒期(例如 04/01/2016-07/31/2016)并将其计为 countB?

db.getCollection('customers').aggregate(
    {$match: {"status": "Closed"}},
    {$unwind: "$lines"},
    {$match: {"lines.status": "Closed"}},
    {$match: {"lines.deliveryMethod": "Tech Delivers"}},
    {$match: {"date": {$gte: new Date('01/01/2016'), $lte: new Date('03/31/2016')}}},
    {$group:{_id:"$lines.productLine",countA: {$sum: 1}}}
)
Run Code Online (Sandbox Code Playgroud)

提前致谢

Bla*_*ven 5

当然,您还可以大大简化管道阶段,主要是因为连续$match阶段实际上是单个阶段,并且您应该始终在任何聚合管道的开头使用匹配条件。即使它实际上没有“过滤”数组内容,它至少只是选择包含实际匹配的条目的文档。这极大地加快了速度,尤其是对于大型数据集。

对于两个日期范围,这只是一个$or查询参数。此外,它还会在数组过滤完成“之前”应用,因为毕竟它首先是文档级别匹配。再说一遍,在第一个管道中$match

db.getCollection('customers').aggregate([
    // Filter all document conditions first. Reduces things to process.
    { "$match": {
       "status": "Closed",
       "lines": { "$elemMatch": {
           "status": "Closed",
           "deliveryMethod": "Tech Delivers"
       }},
       "$or": [
           { "date": {
               "$gte": new Date("2016-01-01"), 
               "$lt": new Date("2016-04-01")
           }},
           { "date": {
               "$gte": new Date("2016-04-01"), 
               "$lt": new Date("2016-08-01")
           }}
       ]
    }},
    // Unwind the array
    { "$unwind": "$lines" },

    // Filter just the matching elements
    // Successive $match is really just one pipeline stage
    { "$match": {
        "lines.status": "Closed",
        "lines.deliveryMethod": "Tech Delivers"
    }},

    // Then group on the productline values within the array
    { "$group":{ 
        "_id": "$lines.productLine",
        "countA": { 
            "$sum": {
                "$cond": [
                    { "$and": [
                        { "$gte": [ "$date", new Date("2016-01-01") ] },
                        { "$lt": [ "$date", new Date("2016-04-01") ] }
                    ]},
                    1,
                    0
                ]
            }
        },
        "countB": {
            "$sum": {
                "$cond": [
                    { "$and": [
                        { "$gte":  [ "$date", new Date("2016-04-01") ] },
                        { "$lt": [ "$date", new Date("2016-08-01") ] }
                    ]},
                    1,
                    0
                ]
            }
        }
    }}
])
Run Code Online (Sandbox Code Playgroud)

基本上$or“连接”两个结果集,因为它寻找要应用的“任一”范围标准。由于这是除了其他参数之外给出的,因此逻辑是“AND”条件,就像其他参数满足任一$or参数的标准一样。请注意,$gteand$lt组合也是在同一键上表达“AND”条件的另一种形式。

$elemMatch由于数组元素需要“两个”条件,因此应用了 。如果您只是直接使用“点符号”应用它们,那么真正要求的是“至少一个数组元素”匹配每个条件,而不是匹配“两个”条件的数组元素。

稍后的过滤$unwind可以使用“点表示法”,因为数组元素现在已“非规范化”到单独的文档中。因此,现在每个文档只有一个元素可以匹配条件。

当您应用 时$group,而不是仅仅使用{ "$sum": 1 }您,而是通过使用“有条件地评估是否计算它” $cond。由于两个日期范围都在结果内,因此您只需要确定当前“汇总”的文档是否属于一个日期作为“三元”(if/then/else)运算符,这就是所$cond提供的。

它查看"date"文档中的值,如果它与条件集(第一个参数 - if )匹配,则返回1(第二个参数 - then ),否则返回0,实际上不添加到当前计数。

由于这些是“逻辑”条件,因此“AND”用逻辑运算符表示$and,该运算符本身返回trueor false,要求两个包含的条件均为true

另请注意对象构造函数中的更正Date,因为如果您不使用该表示形式的字符串进行实例化,则结果Date将采用“本地时间”格式,而不是 MongoDB 存储日期的“UTC”格式。仅当您确实这么想时才使用“本地”构造函数,但人们通常并不这么认为。

另一个注意事项是$lt日期更改,该日期应始终比您要查找的最后日期大“一天”。请记住,这些是“一天的开始”日期,因此您通常需要该日期内的所有可能时间,而不仅仅是开始时间。所以正确的条件是“小于第二天”。


根据记录,对于 MongoDB 2.6 版本,最好在“之前”“预过滤”数组内容$unwind。这消除了在“反规范化”过程中生成新文档的开销,该过程与您想要应用于数组元素的条件不匹配。

对于 MongoDB 3.2 及更高版本,请使用$filter

db.getCollection('customers').aggregate([
    // Filter all document conditions first. Reduces things to process.
    { "$match": {
       "status": "Closed",
       "lines": { "$elemMatch": {
           "status": "Closed",
           "deliveryMethod": "Tech Delivers"
       }},
       "$or": [
           { "date": {
               "$gte": new Date("2016-01-01"), 
               "$lt": new Date("2016-04-01")
           }},
           { "date": {
               "$gte": new Date("2016-04-01"), 
               "$lt": new Date("2016-08-01")
           }}
       ]
    }},

    // Pre-filter the array content to matching elements
    { "$project": {
        "lines": {
            "$filter": {
                "input": "$lines",
                "as": "line",
                "cond": {
                    "$and": [
                        { "$eq": [ "$$line.status", "Closed" ] },
                        { "$eq": [ "$$line.deliveryMethod", "Tech Delivers" ] }
                    ]
                }
            }
        }
    }},

    // Unwind the array
    { "$unwind": "$lines" },

    // Then group on the productline values within the array
    { "$group":{ 
        "_id": "$lines.productLine",
        "countA": { 
            "$sum": {
                "$cond": [
                    { "$and": [
                        { "$gte": [ "$date": new Date("2016-01-01") ] },
                        { "$lt": [ "$date", new Date("2016-04-01") ] }
                    ]},
                    1,
                    0
                ]
            }
        },
        "countB": {
            "$sum": {
                "$cond": [
                    { "$and": [
                        { "$gte":  [ "$date", new Date("2016-04-01") ] },
                        { "$lt": [ "$date", new Date("2016-08-01") ] }
                    ]},
                    1,
                    0
                ]
            }
        }
    }}
])
Run Code Online (Sandbox Code Playgroud)

或者至少对于 MongoDB 2.6,则$redact改为应用:

db.getCollection('customers').aggregate([
    // Filter all document conditions first. Reduces things to process.
    { "$match": {
       "status": "Closed",
       "lines": { "$elemMatch": {
           "status": "Closed",
           "deliveryMethod": "Tech Delivers"
       }},
       "$or": [
           { "date": {
               "$gte": new Date("2016-01-01"), 
               "$lt": new Date("2016-04-01")
           }},
           { "date": {
               "$gte": new Date("2016-04-01"), 
               "$lt": new Date("2016-08-01")
           }}
       ]
    }},

    // Pre-filter the array content to matching elements
    { "$redact": {
        "$cond": {
            "if": {
                "$and": [
                    { "$eq": [ "$status", "Closed" ] },
                    { "$eq": [ 
                        { "$ifNull": ["$deliveryMethod", "Tech Delivers" ] },
                        "Tech Delivers" 
                    ]

            },
            "then": "$$DESCEND",
            "else": "$$PRUNE"
        }
    }},

    // Unwind the array
    { "$unwind": "$lines" },

    // Then group on the productline values within the array
    { "$group":{ 
        "_id": "$lines.productLine",
        "countA": { 
            "$sum": {
                "$cond": [
                    { "$and": [
                        { "$gte": [ "$date": new Date("2016-01-01") ] },
                        { "$lt": [ "$date", new Date("2016-04-01") ] }
                    ]},
                    1,
                    0
                ]
            }
        },
        "countB": {
            "$sum": {
                "$cond": [
                    { "$and": [
                        { "$gte":  [ "$date", new Date("2016-04-01") ] },
                        { "$lt": [ "$date", new Date("2016-08-01") ] }
                    ]},
                    1,
                    0
                ]
            }
        }
    }}
])
Run Code Online (Sandbox Code Playgroud)

注意到$ifNull由于 的递归性质,其中有一些有趣的东西是必要的$$DESCEND,因为文档的所有级别都被检查,包括“顶级”文档,然后“下降”到后续数组和成员甚至嵌套对象。由于顶级字段的早期查询选择标准,“status”字段存在并且具有“Closed”值,但当然不存在名为“deliveryMethod”的“顶级”元素,因为它仅位于数组内元素。

这基本上就是像这样使用时需要采取的“小心” $redact,如果文档的结构不允许这样的条件,那么它实际上不是一个选项,所以改为恢复$unwind处理$match

但如果可能,请优先使用这些方法而不是 then$unwind处理$match,因为通过使用更新的技术将节省大量时间并使用更少的资源。