MongoDB 的聚合框架获得了最好的结果。它与 mapReduce 的不同之处在于,所有操作都是使用“本机编码运算符”执行的,而不是 mapReduce 使用的 JavaScript 评估。
这意味着“更快”,而且意义重大。更不用说您在结果中寻找的某些部分实际上支持“多组”概念,该概念本质上可用于操作“管道”,否则使用 mapReduce 将是一个相当丑陋的累加器。
聚合管道格式
最佳方法会因您可用的 MongoDB“服务器”版本而异。
理想情况下,使用 MongoDB 3.2 在使用 $unwind 处理之前使用 $filter 来“预过滤”数组内容:
var pipeline = [
// Match documents with array members matching conditions
{ "$match": {
"players": {
"$elemMatch": {
"summoner_id": 123456,
"position": { "$gte": 1, "$lte": 6 }
}
}
}},
// Filter the array content for matched conditions
{ "$project": {
"players": {
"$filter": {
"input": "$players",
"as": "player"
"cond": {
"$and": [
{ "$eq": [ "$$player.summoner_id", 123456 ] },
{ "$gte": [ "$$player.position", 1 ] },
{ "$lte": [ "$$player.position", 6 ] }
]
}
}
}
}},
// Unwind the array contents to de-normalize
{ "$unwind": "$players" },
// Group on the inner "position"
{ "$group": {
"_id": "$players.position",
"total": { "$sum": 1 },
"won": { "$sum": "$players.won" }
}},
// Optionally Sort by position since $group is not ordered
{ "$sort": { "total": -1 } },
// Optionally $group to a single document response with an array
{ "$group": {
"_id": null,
"positions": {
"$push": {
"position": "$_id",
"total": "$total",
"won": "$won"
}
}
}}
];
db.collection.aggregate(pipeline);
对于 MongoDB 2.6.x 版本,仍然是“预过滤器”,但使用 $map 和 $setDifference:
var pipeline = [
// Match documents with array members matching conditions
{ "$match": {
"players": {
"$elemMatch": {
"summoner_id": 123456,
"position": { "$gte": 1, "$lte": 6 }
}
}
}},
// Filter the array content for matched conditions
{ "$project": {
"players": {
"$setDifference": [
{ "$map": {
"input": "$players",
"as": "player",
"in": {
"$cond": {
"if": {
"$and": [
{ "$eq": [ "$$player.summoner_id", 123456 ] },
{ "$gte": [ "$$player.position", 1 ] },
{ "$lte": [ "$$player.position", 6 ] }
]
},
"then": "$$player",
"else": false
}
}
}},
[false]
]
}
}},
// Unwind the array contents to de-normalize
{ "$unwind": "$players" },
// Group on the inner "position"
{ "$group": {
"_id": "$players.position",
"total": { "$sum": 1 },
"won": { "$sum": "$players.won" }
}},
// Optionally Sort by position since $group is not ordered
{ "$sort": { "total": -1 } },
// Optionally $group to a single document response with an array
{ "$group": {
"_id": null,
"positions": {
"$push": {
"position": "$_id",
"total": "$total",
"won": "$won"
}
}
}}
];
对于使用 MongoDB 2.2 中的聚合框架的早期版本,使用$match“后过滤器”$unwind:
var pipeline = [
// Match documents with array members matching conditions
{ "$match": {
"players": {
"$elemMatch": {
"summoner_id": 123456,
"position": { "$gte": 1, "$lte": 6 }
}
}
}},
{ "$unwind": "$players" },
// Post filter the denormalized content
{ "$match": {
"players.summoner_id": 123456,
"players.position": { "$gte": 1, "$lte": 6 }
}},
// Group on the inner "position"
{ "$group": {
"_id": "$players.position",
"total": { "$sum": 1 },
"won": { "$sum": "$players.won" }
}},
// Optionally Sort by position since $group is not ordered
{ "$sort": { "total": -1 } },
// Optionally $group to a single document response with an array
{ "$group": {
"_id": null,
"positions": {
"$push": {
"position": "$_id",
"total": "$total",
"won": "$won"
}
}
}}
];
演练
-
匹配文档:这主要是使用$elemMatch 完成的,因为您正在寻找数组元素中的“多个”条件。在数组元素上使用“单一”条件时,可以使用 "dot notation":
"players.summoner_id": 12345
但是对于“一个”以外的条件,您需要使用$elemMatch,否则所有语句真正要问的是“这是否匹配数组中的something?” em>,并且不包含到元素内的“全部”。所以即使是$gte 和$lte 单独组合实际上也是“两个”条件,因此需要$elemMatch:
"players": {
"$elemMatch": {
"position": { "$gte": 1, "$lte": 6 }
}
}
这里还要注意,“1到6包括”表示“大于或等于”,反之“小于” em> 条件。
-
-
“预过滤”:请注意,最终目标是按数组中的元素“分组”,即"position"。这意味着最终您将需要 $unwind 内容来执行此操作。
但是,$unwind 管道操作将非常昂贵,因为它会“拆分”数组并为每个数组成员创建一个新文档来处理。由于您只想要“一些”实际匹配条件的成员,因此最好在“取消”对这些内容进行反规范化之前从数组中“删除”任何不匹配的内容。
MongoDB 3.2 使用 $filter 运算符对此提供了很好的方法。它通过将数组的内容“过滤”为仅匹配特定条件集的元素来执行命名。
在聚合管道阶段,我们使用运算符的“逻辑变体”,例如 $gte 和 $lte。这些返回一个true/false 值,具体取决于条件匹配的位置。同样在数组中,这些实际上可以通过使用“点符号”的成员字段引用到 "as" 中的别名参数,该参数指向当前处理的成员。
这里的$and 也是另一个“逻辑运算符”,它执行相同的true/false 响应。所以这意味着必须满足其参数数组中的“所有”参数才能返回true。对于$filter 本身,在"cond" 中求值的true/false 决定是否返回数组元素。
对于没有 $filter 运算符的 MongoDB 2.6,同样可以用 $map 和 $setDifference 的组合来表示,只需将 $map 查看每个元素并在 "in" 中应用一个表达式。在这种情况下,我们使用$cond,它作为“三元”运算符计算“if/then/else”形式。
所以这里"if" 返回true 中的表达式"then" 作为当前数组成员返回。如果是false,则返回else 中的表达式,在这种情况下,我们将返回false 的值(PHP False)。
由于所有成员实际上都是由$map 的结果返回的,因此我们通过应用$setDifference 运算符来模拟$filter。这将与数组的成员进行比较,并有效地从结果中“删除”元素返回为 false 的任何成员。因此,对于您拥有的不同数组成员,生成的“集合”(作为“唯一”元素的“集合”)只包含条件为 true 且返回非假值的元素。
-
“发布”过滤:对于 MongoDB 2.6 以下的服务器版本,强制的替代方法是“发布”过滤数组内容。由于在这些版本中没有运算符允许对$unwind 之前的数组内容执行此类操作,因此此处处理将另一个$match 应用于“$unwind”“之后”的内容的简单过程:
{ "$match": {
"players.summoner_id": 123456,
"players.position": { "$gte": 1, "$lte": 6 }
}}
这里你使用“点表示法”,因为每个数组元素现在实际上都是它自己的文档,除了查看指定路径上的条件之外,没有其他可以比较的。
这不是理想的,因为当您处理$unwind 时,实际上不符合条件的所有元素仍然存在。这最终意味着“需要处理更多的文件”并且具有以下双重成本:
尽管不符合条件,但必须为每个成员创建一个新文档
现在您必须将条件应用于由于$unwind而发出的每个“文档”
这可能会对性能产生巨大影响,因此,现代 MongoDB 版本引入了对数组进行操作的方法,而无需借助 $unwind 进行处理。您仍然需要它来进行剩余的处理,因为您正在对数组中包含的属性进行“分组”。但当然最好是“先去掉不匹配的元素”。
-
剩余分组:现在元素被过滤和反规范化,它只剩下执行实际的$group 条件,将每个元素内的"position" 汇总。这很简单,只需将分组密钥提供给"_id" 并使用适当的数据累积。
在这种情况下,您有两个构造,分别是:
"total": { "$sum": 1 },
"won": { "$sum": "$players.won" }
基本的{ "$sum": 1 } 只是“计算”为每个组匹配的元素,{ "$sum": "$players.won" } 实际上使用"won" 值来累积总数。这是$sum 累加器的标准用法。
当然,您的输出显示了“数组”中的内容,因此以下阶段实际上是“可选的”,因为实际“分组”的实际工作已经完成。因此,您实际上可以只使用提供给第一个$group 的表单中的结果,剩下的只是将所有内容放入单个文档响应中,而不是“每个'位置'值一个文档”,这将是返回点。
$group 输出的第一个注释是未排序的。因此,如果您想要特定的结果顺序(即按位置升序),那么您必须在 $group 阶段之后 $sort。这将对管道的结果文档进行排序,从应用它的点开始。
在您的情况下,您实际上是在要求对"total" 进行排序,因此您当然可以将其与-1 一起应用,在这种情况下表示“降序”。但无论如何,您仍然不应假定$group 的输出是按任何方式排序的。
这里的“第二个”$group 基本上是装饰性的,因为这就是“单个文档”响应的原因。在分组键中使用 null ( PHP NULL ) 基本上表示“将所有内容分组”,并将生成单个文档作为响应。这里的$push 累加器实际上是从前面的管道中的文档中生成“数组”的。
总结
这就是像这样积累数据的一般过程:
将所需的文档与条件相匹配,因为毕竟在每个文档甚至不包含与您最终想要的条件相匹配的数组元素时,再将条件应用于每个文档都是一种浪费。
过滤数组内容并去规范化。理想情况下,尽可能作为“预过滤器”。这会将文档从原始数组形式中提取为用于分组的形式。
使用适合任务的运算符来累积内容,$sum 或 $avg 或 $push 或根据需要可用的任何其他运算符。根据结构和条件,您始终可以使用“多个”$group 管道阶段。
PHP 翻译
PHP 表示法的初始示例:
pipeline = array(
array(
'$match' => array(
'players' => array(
'$elemMatch' => array(
'summoner_id' => 123456,
'position' => array( '$gte' => 0, '$lte' => 6 )
)
)
)
),
array(
'$project' => array(
'$filter' => array(
'input' => '$players',
'as' => 'player',
'cond' => (
'$and' => array(
array( '$eq' => array( '$$player.summoner_id' => 123456 ) ),
array( '$gte' => array( '$$player.position' => 1 ) ),
array( '$lte' => array( '$$player.position' => 6 ) )
)
)
)
)
),
array( '$unwind' => '$players' ),
array(
'$group' => array(
'_id' => '$players.position',
'total' => array( '$sum' => 1 ),
'won' => array( '$sum' => '$players.won' )
)
),
array( '$sort' => array( 'total' => -1 ) ),
array(
'$group' => array(
'_id' => NULL,
'positions' => array(
'$push' => array(
'position' => '$_id',
'total' => '$total',
'won' => '$won'
)
)
)
)
)
$result = $collection->aggregate($pipeline);
当您在 PHP 中创建与 JSON 进行比较的数据结构时,使用以下内容检查您的结构通常很有用:
echo json_encode($pipeline, JSON_PRETTY_PRINT)
然后您可以看到您在 PHP 表示法中所做的与您正在遵循的 JSON 示例相同。这是一个有用的提示,因此您不会真的出错。如果它看起来不同,那么您就没有做“相同”的事情。