MongoDB PHP聚合数据+计数+

时间:2016-03-28 20:26:27

标签: php mongodb mongodb-query aggregation-framework

我在PHP / MYsql中创建了一个平台,现在我正在迁移到mongo

我对mysql的旧查询:

select sum(game_won) as game_won,count(id) as total,position
from games_player_stats 
where position < 6 and position > 0 and user_id = :pa_id 
group by position 
order by total desc

新的json格式如下所示:

{
 "region" : "EUW",
 "players" : [
         {
            "position" : 2,
            "summoner_id" : 123456,
            "game_won": 1
         },
         {
            "position" : 1,
            "summoner_id" : 123459,
            "game_won": 0
         },
         {
            "position" : 3,
            "summoner_id" : 123458,
            "game_won": 1
         },
         {
            "position" : 4,
            "summoner_id" : 123457,
            "game_won": 0
         }
             ]
}

有了这样的多个文件,我需要找到多少次summoner_id 123456有位置2或任何其他位置1-6以及他在那个位置赢了多少次

索引需要在region和summoner_id上查询

结果看起来像

 {
   "positions" : 
         [
           { "position" : 1,
             "total" : 123,
             "won" : 65
           },
           { "position" : 2,
             "total" : 37,
             "won" : 10
           }
         ]
   }

我需要使用Map / Reduce吗?

1 个答案:

答案 0 :(得分:2)

最好的结果是通过MongoDB的聚合框架获得的。它与mapReduce的不同之处在于,所有操作都是使用&#34;本机编码的运算符&#34;而不是mapReduce使用的JavaScript评估。

这意味着&#34;更快&#34;,并且显着如此。更不用说还有某些部分你正在寻找的结果实际上有利于&#34;多个组&#34;管道本身可用的概念&#34;操作,否则使用mapReduce会相当丑陋。

聚合管道格式

根据MongoDB&#34;服务器&#34;最佳方法会有所不同。你有的版本。

理想情况下,使用 MongoDB 3.2 ,您可以使用$filter来预先过滤&#34;使用$unwind处理前的数组内容:

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 版本,仍然是&#34;预过滤&#34;但使用$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聚合框架的早期版本,&#34; post filter&#34;与$match&#34;&#34; $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完成的,因为您正在寻找&#34;多个&#34;数组元素中的条件。使用&#34;单身&#34;条件在数组元素上使用"dot notation"

    "players.summoner_id": 12345
    

    但对于任何超过&#34;一个&#34;条件你需要使用$elemMatch,否则所有的陈述都是&#34;这是否匹配数组中的某些东西?&#34; ,并且不包括&#34;所有&#34;在元素内。因此,即使仅$gte$lte组合实际上也是&#34;两个&#34;条件,因此需要$elemMatch

    "players": {
        "$elemMatch": {
            "position": { "$gte": 1, "$lte": 6 }
        }
    }
    

    此处还注意到&#34; 1到6包含&#34; 表示&#34;大于或等于&#34; ,反之亦然&#34;小于&#34; 条件。

-

  • &#34;预过滤&#34; :请注意,最终的目标是&#34; group&#34;由数组中的元素组成,"position"。这意味着最终您需要$unwind内容来执行此操作。

    然而,$unwind管道操作的成本非常高,考虑到它会分开&#34;数组并创建一个新文档来处理每个数组成员。因为你只想要&#34;一些&#34;实际上符合条件的成员,他们希望&#34;删除&#34;来自阵列的任何未匹配的内容&#34;之前&#34;你取消了这个内容的标准化。

    使用$filter运算符,MongoDB 3.2有一个很好的方法。它完全按照&#34;过滤&#34;的命名执行。数组的内容仅限于与特定条件集匹配的元素。

    在汇总管道阶段,我们使用它的逻辑变体&#34; $gte$lte等运算符。这些值将根据条件匹配的位置返回true/false值。同样在数组中,实际上可以使用&#34;点符号&#34;使用成员字段来引用它们。到"as"中指向当前已处理成员的别名参数。

    $and这里也是另一个&#34;逻辑运算符&#34;它做了相同的true/false响应。所以这意味着&#34;所有&#34;必须满足其中的参数才能返回true。对于$filter本身,true/false中评估的"cond"确定是否返回数组元素。

    对于没有$filter运算符的MongoDB 2.6,同样用$map$setDifference的组合表示。只需将$map看一下每个元素即可在"in"中应用表达式。在这种情况下,我们使用$cond作为&#34;三元&#34;运营商评估&#39; if / then / else`表格。

    所以这里"if"返回true"then"中的表达式将作为当前数组成员返回。在false的位置,else中的表达式返回,在这种情况下,我们返回false的值(PHP False)。

    由于所有成员实际上都是由$map的结果返回的,因此我们通过应用$filter运算符来模拟$setDifference。这与数组成员进行了比较,并有效地&#34;删除&#34;从结果中返回元素false的所有成员。因此,对于您所拥有的不同阵列成员,所得到的&#34; set&#34; (作为&#34;&#34;&#34;唯一&#34;元素)只包含那些条件为true且返回非假值的元素。

  • <强>&#34;邮政&#34;过滤:MongoDB 2.6下面的服务器版本必须采用的替代方法是&#34; post&#34;过滤数组内容。由于这些版本中没有运算符允许在$unwind之前对数组内容执行此类操作,因此此处的简单过程是将另一个$match应用于内容&#34;&#34;之后已处理$unwind

    { "$match": {
        "players.summoner_id": 123456,
        "players.position": { "$gte": 1, "$lte": 6 }
    }}
    

    在这里你使用&#34;点符号&#34;因为现在每个数组元素实际上都是它自己的文档,除了查看指定路径上的条件之外,没有别的东西可以比较。

    这是理想,因为当您处理$unwind时,实际上不符合条件的所有元素仍然存在。这最终意味着需要处理的更多文件&#34;并且具有以下双重成本:

    1. 必须为每个成员创建一个新文档,尽管它与条件不匹配

    2. 现在你必须在每个&#34;文件中应用条件&#34;由于$unwind

    3. 而发出

      这对性能有潜在的巨大影响,因此现代MongoDB版本引入了对数组进行操作的方法,而无需借助$unwind来处理。你仍然需要它用于剩余的处理,因为你是&#34;分组&#34;在数组中包含的属性上。但是,当然希望首先摆脱不匹配的元素&#34;

    4. 正在进行分组:现在,元素已经过滤和反规范化,只剩下实际的$group条件,这将导致"position"在每个元素内。这是向"_id"提供分组键并使用适当的数据累积的简单问题。

      在这种情况下,您有两个结构,即:

          "total": { "$sum": 1 },
          "won": { "$sum": "$players.won" }
      

      基本的{ "$sum": 1 }只是&#34;计算&#34;每个组匹配的元素和{ "$sum": "$players.won" }实际上使用"won"值来累计总数。这是$sum累加器的标准用法。

      当然你的输出显示了一个&#34;数组&#34;中的内容,所以以下几个阶段实际上是&#34;可选&#34;因为真正的工作实际上是&#34;分组&#34;已经完成了。因此,您实际上只能使用第一个$group之前提供的表单中的结果,剩下的只是将所有内容放入单个文档响应中,而不是“#34;每个位置的文档”。价值&#34;,这将是此时的回报。

      第一个音符是从$group输出的不是。因此,如果您想要特定的结果顺序(即按位置升序),则必须在$sort阶段之后$group。这将对管道的结果文档进行排序,直到应用它为止。

      在你的情况下,你实际上是要求"total"进行排序,所以你当然会将其应用于-1含义&#34;降序&#34;在这种情况下。但无论如何,你仍然不应该假设$group的输出是以任何方式排序的。

      &#34;第二&#34; $group这里基本上是装饰性的,因为这就是制作一个单一文件的原因。响应。在分组键中使用null(PHP NULL)基本上表示&#34;对所有内容进行分组&#34;并将生成一份文件作为回应。这里的$push累加器实际上是&#34;数组&#34;来自此前的管道中的文件。

层的总结

这就是积累数据的一般过程:

  1. 匹配条件所需的文档,因为如果它们甚至不包含与您最终想要的条件匹配的数组元素,那么稍后将条件应用于每个文档将是一种浪费。< / p>

  2. 过滤数组内容并取消规范化。理想情况下,作为&#34;预过滤器&#34;在可能的情况。这将文档转换为一个表格,用于分组,从原始数组形式。

  3. 使用适当的运算符累计内容,$sum$avg$push或根据需要提供的任何其他运算符。根据结构和条件,您可以随时使用&#34;不止一个&#34; $group管道阶段。

  4. 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示例相同。这是一个有用的提示,这样你就不会出错。如果它看起来不同那么你就不会做同样的事情。的事情。