Related
In MongoDB, I have documents with a structure like this:
{
_id: "123456...", // an ObjectId
name: "foobar",
classification: {
class_1: 0.45,
class_2: 0.11,
class_3: 0.44
}
}
Using the aggregation pipeline, is it possible to give me an object that contains the highest classification? So, given the above, I would like something like this as result:
{
_id: "123456...", // an ObjectId
name: "foobar",
classification: "class_1"
}
I thought I could use $unwind but the classification property is not an array.
For what it's worth: I know there will always be three properties in classification, so it's ok to hard-code the keys in the query.
You should probably note here that every technique applied is essentially based on "coercion" of the "key/value" pairs into an "array" format for comparison and extraction. So the real lesson to learn is is that your document "should" in fact store this as an "array" instead. But onto the techniques.
If you have MongoDB 3.4 then you can use $objectToArray to turn the "keys" into an array so you can get the value:
Dynamic
db.collection.aggregate([
{ "$addFields": {
"classification": {
"$arrayElemAt": [
{ "$map": {
"input": {
"$filter": {
"input": { "$objectToArray": "$classification" },
"as": "c",
"cond": {
"$eq": [
"$$c.v",
{ "$max": {
"$map": {
"input": { "$objectToArray": "$classification" },
"as": "c",
"in": "$$c.v"
}
}}
]
}
}
},
"as": "c",
"in": "$$c.k",
}},
0
]
}
}}
])
Otherwise just to the transformation as you iterate the cursor if you do not really need it for further aggregation. As a basic JavaScript example:
db.collection.find().map(d => Object.assign(
d,
{ classification: Object.keys(d.classification)
.filter(k => d.classification[k] === Math.max.apply(null,
Object.keys(d.classification).map(k => d.classification[k])
))[0]
}
));
And that's also the same basic logic that you apply using mapReduce if you were actually aggregating something.
Both produce:
/* 1 */
{
"_id" : "123456...",
"name" : "foobar",
"classification" : "class_1"
}
HardCoding
On the "hardcoding" case which you say is okay. Then you can construct like this with $switch by supplying $max with each of the values:
db.collection.aggregate([
{ "$addFields": {
"classification": {
"$let": {
"vars": {
"max": {
"$max": [
"$classification.class_1",
"$classification.class_2",
"$classification.class_3"
]
}
},
"in": {
"$switch": {
"branches": [
{ "case": { "$eq": [ "$classification.class_1", "$$max" ] }, "then": "class_1" },
{ "case": { "$eq": [ "$classification.class_2", "$$max" ] }, "then": "class_2" },
{ "case": { "$eq": [ "$classification.class_3", "$$max" ] }, "then": "class_3" },
]
}
}
}
}
}}
])
Which gives rise to then actually being able to write that out longer using $cond, and then the only real constraint is the change in $max for MongoDB 3.2, which allowed an array of arguments as opposed to it's previous role as an "accumulator only":
db.collection.aggregate([
{ "$addFields": {
"classification": {
"$let": {
"vars": {
"max": {
"$max": [
"$classification.class_1",
"$classification.class_2",
"$classification.class_3"
]
}
},
"in": {
"$cond": {
"if": { "$eq": [ "$classification.class_1", "$$max" ] },
"then": "class_1",
"else": {
"$cond": {
"if": { "$eq": [ "$classification.class_2", "$$max" ] },
"then": "class_2",
"else": "class_3"
}
}
}
}
}
}
}}
])
If you were "really" constrained then you could "force" the "max" through a separate pipeline stage using $map and $unwind on the array then $group again. This would make the operations compatible with MongoDB 2.6:
db.collection.aggregate([
{ "$project": {
"name": 1,
"classification": 1,
"max": {
"$map": {
"input": [1,2,3],
"as": "e",
"in": {
"$cond": {
"if": { "$eq": [ "$$e", 1 ] },
"then": "$classification.class_1",
"else": {
"$cond": {
"if": { "$eq": [ "$$e", 2 ] },
"then": "$classification.class_2",
"else": "$classification.class_3"
}
}
}
}
}
}
}},
{ "$unwind": "$max" },
{ "$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"classification": { "$first": "$classification" },
"max": { "$max": "$max" }
}},
{ "$project": {
"name": 1,
"classification": {
"$cond": {
"if": { "$eq": [ "$classification.class_1", "$max" ] },
"then": "class_1",
"else": {
"$cond": {
"if": { "$eq": [ "$classification.class_2", "$max" ] },
"then": "class_2",
"else": "class_3"
}
}
}
}
}}
])
And going really ancient, then we can instead $unwind from $const, which was (and still is) a "hidden" and undocumented operator equal in function to $literal (which is technically aliased to it) in modern versions, but also with the alternate syntax to $cond as an "array" ternary operation this then becomes compatible with all versions since the aggregation framework existed:
db.collection.aggregate([
{ "$project": {
"name": 1,
"classification": 1,
"temp": { "$const": [1,2,3] }
}},
{ "$unwind": "$temp" },
{ "$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"classification": { "$first": "$classification" },
"max": {
"$max": {
"$cond": [
{ "$eq": [ "$temp", 1 ] },
"$classification.class_1",
{ "$cond": [
{ "$eq": [ "$temp", 2 ] },
"$classification.class_2",
"$classification.class_3"
]}
]
}
}
}},
{ "$project": {
"name": 1,
"classification": {
"$cond": [
{ "$eq": [ "$max", "$classification.class_1" ] },
"class_1",
{ "$cond": [
{ "$eq": [ "$max", "$classification.class_2" ] },
"class_2",
"class_3"
]}
]
}
}}
])
But it is of course possible, even if extremely messy.
You can use $indexOfArray operator to find the $max value in classification followed by projecting the key. $objectToArray to convert classification embedded doc into array of key value pairs in 3.4.4 version.
db.collection.aggregate([
{
"$addFields": {
"classification": {
"$let": {
"vars": {
"classificationkv": {
"$objectToArray": "$classification"
}
},
"in": {
"$let": {
"vars": {
"classificationmax": {
"$arrayElemAt": [
"$$classificationkv",
{
"$indexOfArray": [
"$$classificationkv.v",
{
"$max": "$$classificationkv.v"
}
]
}
]
}
},
"in": "$$classificationmax.k"
}
}
}
}
}
}
])
In the end, I went with a more simple solution, but not as generic as the other ones posted here. I used this a switch case statement:
{'$project': {'_id': 1, 'name': 1,
'classification': {'$switch': {
'branches': [
{'case': {'$and': [{'$gt': ['$classification.class_1', '$classification.class_2']},
{'$gt': ['$classification.class_1', '$classification.class_3']}]},
'then': "class1"},
{'case': {'$and': [{'$gt': ['$classification.class_2', '$classification.class_1']},
{'$gt': ['$classification.class_2', '$classification.class_3']}]},
'then': "class_2"},
{'case': {'$and': [{'$gt': ['$classification.class_3', '$classification.class_1']},
{'$gt': ['$classification.class_3', '$classification.class_2']}]},
'then': "class_3"}],
'default': ''}}
}}
This works for me, but the other answers might be a better option, YMMV.
I have a collection set with documents like :
{
"_id": ObjectId("57065ee93f0762541749574e"),
"name": "myName",
"results" : [
{
"_id" : ObjectId("570e3e43628ba58c1735009b"),
"color" : "GREEN",
"week" : 17,
"year" : 2016
},
{
"_id" : ObjectId("570e3e43628ba58c1735009d"),
"color" : "RED",
"week" : 19,
"year" : 2016
}
]
}
I am trying to build a query witch alow me to return all documents of my collection but only select the field 'results' with subdocuments if week > X and year > Y.
I can select the documents where week > X and year > Y with the aggregate function and a $match but I miss documents with no match.
So far, here is my function :
query = ModelUser.aggregate(
{$unwind:{path:'$results', preserveNullAndEmptyArrays:true}},
{$match:{
$or: [
{$and:[
{'results.week':{$gte:parseInt(week)}},
{'results.year':{$eq:parseInt(year)}}
]},
{'results.year':{$gt:parseInt(year)}},
{'results.week':{$exists: false}}
{$group:{
_id: {
_id:'$_id',
name: '$name'
},
results: {$push:{
_id:'$results._id',
color: '$results.color',
numSemaine: '$results.numSemaine',
year: '$results.year'
}}
}},
{$project: {
_id: '$_id._id',
name: '$_id.name',
results: '$results'
);
The only thing I miss is : I have to get all 'name' even if there is no result to display.
Any idea how to do this without 2 queries ?
It looks like you actually have MongoDB 3.2, so use $filter on the array. This will just return an "empty" array [] where the conditions supplied did not match anything:
db.collection.aggregate([
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$filter": {
"input": "$results",
"as": "result",
"cond": {
"$and": [
{ "$eq": [ "$$result.year", year ] },
{ "$or": [
{ "$gt": [ "$$result.week", week ] },
{ "$not": { "$ifNull": [ "$$result.week", false ] } }
]}
]
}
}
}
}}
])
Where the $ifNull test in place of $exists as a logical form can actually "compact" the condition since it returns an alternate value where the property is not present, to:
db.collection.aggregate([
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$filter": {
"input": "$results",
"as": "result",
"cond": {
"$and": [
{ "$eq": [ "$$result.year", year ] },
{ "$gt": [
{ "$ifNull": [ "$$result.week", week+1 ] },
week
]}
]
}
}
}
}}
])
In MongoDB 2.6 releases, you can probably get away with using $redact and $$DESCEND, but of course need to fake the match in the top level document. This has similar usage of the $ifNull operator:
db.collection.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$and": [
{ "$eq": [{ "$ifNull": [ "$year", year ] }, year ] },
{ "$gt": [
{ "$ifNull": [ "$week", week+1 ] }
week
]}
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}}
])
If you actually have MongoDB 2.4, then you are probably better off filtering the array content in client code instead. Every language has methods for filtering array content, but as a JavaScript example reproducible in the shell:
db.collection.find().forEach(function(doc) {
doc.results = doc.results.filter(function(result) {
return (
result.year == year &&
( result.hasOwnProperty('week') ? result.week > week : true )
)
]);
printjson(doc);
})
The reason being is that prior to MongoDB 2.6 you need to use $unwind and $group, and various stages in-between. This is a "very costly" operation on the server, considering that all you want to do is remove items from the arrays of documents and not actually "aggregate" from items within the array.
MongoDB releases have gone to great lengths to provide array processing that does not use $unwind, since it's usage for that purpose alone is not a performant option. It should only ever be used in the case where you are removing a "significant" amount of data from arrays as a result.
The whole point is that otherwise the "cost" of the aggregation operation is likely greater than the "cost" of transferring the data over the network to be filtered on the client instead. Use with caution:
db.collection.aggregate([
// Create an array if one does not exist or is already empty
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$cond": [
{ "$ifNull": [ "$results.0", false ] },
"$results",
[false]
]
}
}},
// Unwind the array
{ "$unwind": "$results" },
// Conditionally $push based on match expression and conditionally count
{ "$group": {
"_id": "_id",
"name": { "$first": "$name" },
"user": { "$first": "$user" },
"results": {
"$push": {
"$cond": [
{ "$or": [
{ "$not": "$results" },
{ "$and": [
{ "$eq": [ "$results.year", year ] },
{ "$gt": [
{ "$ifNull": [ "$results.week", week+1 ] },
week
]}
]}
] },
"$results",
false
]
}
},
"count": {
"$sum": {
"$cond": [
{ "$and": [
{ "$eq": [ "$results.year", year ] },
{ "$gt": [
{ "$ifNull": [ "$results.week", week+1 ] },
week
]}
] }
1,
0
]
}
}
}},
// $unwind again
{ "$unwind": "$results" }
// Filter out false items unless count is 0
{ "$match": {
"$or": [
"$results",
{ "count": 0 }
]
}},
// Group again
{ "$group": {
"_id": "_id",
"name": { "$first": "$name" },
"user": { "$first": "$user" },
"results": { "$push": "$results" }
}},
// Now swap [false] for []
{ "$project": {
"name": 1,
"user": 1,
"results": {
"$cond": [
{ "$ne": [ "$results", [false] ] },
"$results",
[]
]
}
}}
])
Now that is a lot of operations and shuffling just to "filter" content from an array compared to all of the other approaches which are really quite simple. And aside from the complexity, it really does "cost" a lot more to execute on the server.
So if your server version actually supports the newer operators that can do this optimally, then it's okay to do so. But if you are stuck with that last process, then you probably should not be doing it and instead do your array filtering in the client.
I have these three MongoDB documents:
{
"_id" : ObjectId("571094afc2bcfe430ddd0815"),
"name" : "Barry",
"surname" : "Allen",
"address" : [
{
"street" : "Red",
"number" : NumberInt(66),
"city" : "Central City"
},
{
"street" : "Yellow",
"number" : NumberInt(7),
"city" : "Gotham City"
}
]
}
{
"_id" : ObjectId("57109504c2bcfe430ddd0816"),
"name" : "Oliver",
"surname" : "Queen",
"address" : {
"street" : "Green",
"number" : NumberInt(66),
"city" : "Star City"
}
}
{
"_id" : ObjectId("5710953ac2bcfe430ddd0817"),
"name" : "Tudof",
"surname" : "Unknown",
"address" : "homeless"
}
The address field is an Array of Objects in the first document, an Object in the second and a String in the third.
My target is to find how many documents of my collection containinig the field address.street. In this case the right count is 1 but with my query I get two:
db.coll.find({"address.street":{"$exists":1}}).count()
I also tried map/reduce. It works but it is slower; so if it is possible, I would avoid it.
The distinction here is that the .count() operation is actually "correct" in returning the "document" count where the field is present. So the general considerations break down to:
If you just want to exlude the documents with the array field
Then the most effective way of excluding those documents where the "street" was a property of the "address" as an "array", then just use the dot-notation property of looking for the 0 index to not exist in the exlcusion:
db.coll.find({
"address.street": { "$exists": true },
"address.0": { "$exists": false }
}).count()
As a natively coded operator test in both cases $exists does the correct job and efficiently.
If you intended to count field occurences
If what you are actually asking is the "field count", where some "documents" contain array entries where that "field" may be present several times.
For that you need the aggregation framework or mapReduce like you mention. MapReduce uses JavaScript based processing and is therefore going to be considerably slower than the .count() operation. The aggregation framework also needs to calculate and "will" be slower than .count(), but not by as much as mapReduce.
In MongoDB 3.2 you get some help here by the expanded ability of $sum to work on an array of values as well as being an grouping accumulator. The other helper here is $isArray which allows a different processing method via $map when the data is in fact "an array":
db.coll.aggregate([
{ "$group": {
"_id": null,
"count": {
"$sum": {
"$sum": {
"$cond": {
"if": { "$isArray": "$address" },
"then": {
"$map": {
"input": "$address",
"as": "el",
"in": {
"$cond": {
"if": { "$ifNull": [ "$$el.street", false ] },
"then": 1,
"else": 0
}
}
}
},
"else": {
"$cond": {
"if": { "$ifNull": [ "$address.street", false ] },
"then": 1,
"else": 0
}
}
}
}
}
}
}}
])
Earlier versions hinge on a bit more conditional processing in order to treat the array and non-array data differently, and generally require $unwind to process array entries.
Either transposing the array via $map with MongoDB 2.6:
db.coll.aggregate([
{ "$project": {
"address": {
"$cond": {
"if": { "$ifNull": [ "$address.0", false ] },
"then": "$address",
"else": {
"$map": {
"input": ["A"],
"as": "el",
"in": "$address"
}
}
}
}
}},
{ "$unwind": "$address" },
{ "$group": {
"_id": null,
"count": {
"$sum": {
"$cond": {
"if": { "$ifNull": [ "$address.street", false ] },
"then": 1,
"else": 0
}
}
}
}}
])
Or providing conditional selection with MongoDB 2.2 or 2.4:
db.coll.aggregate([
{ "$group": {
"_id": "$_id",
"address": {
"$first": {
"$cond": [
{ "$ifNull": [ "$address.0", false ] },
"$address",
{ "$const": [null] }
]
}
},
"other": {
"$push": {
"$cond": [
{ "$ifNull": [ "$address.0", false ] },
null,
"$address"
]
}
},
"has": {
"$first": {
"$cond": [
{ "$ifNull": [ "$address.0", false ] },
1,
0
]
}
}
}},
{ "$unwind": "$address" },
{ "$unwind": "$other" },
{ "$group": {
"_id": null,
"count": {
"$sum": {
"$cond": [
{ "$eq": [ "$has", 1 ] },
{ "$cond": [
{ "$ifNull": [ "$address.street", false ] },
1,
0
]},
{ "$cond": [
{ "$ifNull": [ "$other.street", false ] },
1,
0
]}
]
}
}
}}
])
So the latter form "should" perform a bit better than mapReduce, but probably not by much.
In all cases the logic falls to using $ifNull as the "logical" form of $exists for the aggregation framework. Paired with $cond, a "truthfull" result is obtained when the property actually exsists, and a false value is returned when it is not. This determines whether 1 or 0 is returned respectively to the overall accumulation via $sum.
Ideally you have the modern version that can do this in a single $group pipeline stage, but otherwise you need the longer path.
Can you try this:
db.getCollection('collection_name').find({
"address.street":{"$exists":1},
"$where": "Array.isArray(this.address) == false && typeof this.address === 'object'"
});
In where clause, we are excluding if address is array and
Including address if it's type is object.
I'm fairly new to MongoDB and I'm trying to aggregate some stats on a "Matches" collection that looks like this:
{
team1: {
players: ["player1", "player2"],
score: 10
},
team2: {
players: ["player3", "player4"],
score: 5
}
},
{
team1: {
players: ["player1", "player3"],
score: 15
},
team2: {
players: ["player2", "player4"],
score: 21
}
},
{
team1: {
players: ["player4", "player1"],
score: 21
},
team2: {
players: ["player3", "player2"],
score: 9
}
},
{
team1: {
players: ["player1"],
score: 5
},
team2: {
players: ["player3"],
score: 10
}
}
I'm looking to get games won, loss and win/loss ratio by each player. I'm new to aggregate functions and having trouble getting something going. Could someone point me the right direction?
Dealing with mutiple arrays in a structure is not really a simple task for aggregation, particularly when your results really want to consider the combination of both arrays.
Fortunately there are a few operations and/or techniques that can help here, along with the fact that each game comprises a "set" of unique players per team/match and results.
The most streamlined approach would be using the features of MongoDB 2.6 and upwards to effectively "combine" the arrays into a single array for processing:
db.league.aggregate([
{ "$project": {
"players": {
"$concatArrays": [
{ "$map": {
"input": "$team1.players",
"as": "el",
"in": {
"player": "$$el",
"win": {
"$cond": {
"if": { "$gt": [ "$team1.score", "$team2.score" ] },
"then": 1,
"else": 0
}
},
"loss": {
"$cond": {
"if": { "$lt": [ "$team1.score", "$team2.score" ] },
"then": 1,
"else": 0
}
}
}
}},
{ "$map": {
"input": "$team2.players",
"as": "el",
"in": {
"player": "$$el",
"win": {
"$cond": {
"if": { "$gt": [ "$team2.score", "$team1.score" ] },
"then": 1,
"else": 0
}
},
"loss": {
"$cond": {
"if": { "$lt": [ "$team2.score", "$team1.score" ] },
"then": 1,
"else": 0
}
}
}
}}
]
}
}},
{ "$unwind": "$players" },
{ "$group": {
"_id": "$players.player",
"win": { "$sum": "$players.win" },
"loss": { "$sum": "$players.loss" }
}},
{ "$project": {
"win": 1,
"loss": 1,
"ratio": { "$divide": [ "$win", "$loss" ] }
}},
{ "$sort": { "_id": 1 } }
])
This listing is using $concatArrays from MongoDB 3.2, but that acutal operator can just as easily be replaced by $setUnion considering that the list of players per game is "unique" and therefore a "set". Either operator is basically joining one array with another based on the output of the inner operations.
For those inner operations we are using $map, which processes each array ( "team1/team2" ) in-line and just does a caculation for each player on whether the game result was a "win/loss". This makes things easier for the following stages.
Though the 3.2 and 2.6 releases for MongoDB both introduced operators for making working with arrays easier, the general principle comes back to that if you want to "aggregate" on data within an array, then you process with $unwind first. This exposes each "player" data within each game from the previous mapping.
Now it's just a matter of using $group to bring together the results for each player, with $sum for each total field. In order to get a "ratio" over the summed results, process with a $project to introduce the $divide between the result values, then optionally $sort the resulting key for each player.
Older Solution
Prior to MongoDB 2.6, your only real tool for dealing with arrays was first to $unwind. So the same principles come into play here:
"map" each array with "win/loss".
Combine the content per game into one "distinct list"
Sum content based on common "player" field
The only real difference in approach is that the "distinct list" per game we are going to be here will be "after" pulling apart the mapped arrays, and instead just returning one document per "game/player" combination:
db.league.aggregate([
{ "$unwind": "$team1.players" },
{ "$group": {
"_id": "$_id",
"team1": {
"$push": {
"player": "$team1.players",
"win": {
"$cond": [
{ "$gt": [ "$team1.score", "$team2.score" ] },
1,
0
]
},
"loss": {
"$cond": [
{ "$lt": [ "$team1.score", "$team2.score" ] },
1,
0
]
}
}
},
"team1Score": { "$first": "$team1.score" },
"team2": { "$first": "$team2" }
}},
{ "$unwind": "$team2.players" },
{ "$group": {
"_id": "$_id",
"team1": { "$first": "$team1" },
"team2": {
"$push": {
"player": "$team2.players",
"win": {
"$cond": [
{ "$gt": [ "$team2.score", "$team1Score" ] },
1,
0
]
},
"loss": {
"$cond": [
{ "$lt": [ "$team2.score", "$team1Score" ] },
1,
0
]
}
}
},
"type": { "$first": { "$const": ["A","B" ] } }
}},
{ "$unwind": "$team1" },
{ "$unwind": "$team2" },
{ "$unwind": "$type" },
{ "$group": {
"_id": {
"_id": "$_id",
"player": {
"$cond": [
{ "$eq": [ "$type", "A" ] },
"$team1.player",
"$team2.player"
]
},
"win": {
"$cond": [
{ "$eq": [ "$type", "A" ] },
"$team1.win",
"$team2.win"
]
},
"loss": {
"$cond": [
{ "$eq": [ "$type", "A" ] },
"$team1.loss",
"$team2.loss"
]
}
}
}},
{ "$group": {
"_id": "$_id.player",
"win": { "$sum": "$_id.win" },
"loss": { "$sum": "$_id.loss" }
}},
{ "$project": {
"win": 1,
"loss": 1,
"ratio": { "$divide": [ "$win", "$loss" ] }
}},
{ "$sort": { "_id": 1 } }
])
So this is the interesting part here:
{ "$group": {
"_id": {
"_id": "$_id",
"player": {
"$cond": [
{ "$eq": [ "$type", "A" ] },
"$team1.player",
"$team2.player"
]
},
"win": {
"$cond": [
{ "$eq": [ "$type", "A" ] },
"$team1.win",
"$team2.win"
]
},
"loss": {
"$cond": [
{ "$eq": [ "$type", "A" ] },
"$team1.loss",
"$team2.loss"
]
}
}
}},
That basically gets rid of any duplication per game that would have resulted from each $unwind on different arrays. Being that when you $unwind one array, you get a copy of the whole document for each array member. If you then $unwind another contained array, then the content you just "unwound" is also "copied" again for each of those array members.
Fortunately this is fine since any player is only listed once per game, so every player only has one set of results per game. An alternate way to write that stage, would be to process into another array using $addToSet:
{ "$group": {
"_id": "$_id",
"players": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$type", "A" ] },
"$team1",
"$team2"
]
}
}
}},
{ "$unwind": "$players" }
But since that produces another "array", it's a bit more desirable to just keep the results as separate documents, rather than process with $unwind again.
So again this is really "joining results into a single distinct list", where in this case since we lack the operators to "join" both "team1" and "team2" together, the arrays are pulled apart and then conditionally "combined" depending on the current "A" or "B" value that is being processed.
The end "joining" looks at many "copies" of data, but there is still essentially only "one distinct player record per game" for each player involved, and since we worked out the numbers before the "duplication" occurred, then it's just really a matter of picking one of them from each game first.
Same end results, by them summing up for each player and calculating from totals.
Conclusion
So you might generally conclude here, that in either case most of the work involved is aimed at getting those two arrays of data into a single array, or indeed into singular documents per player per game in order to come to the simple aggregation for totals.
You might well consider then "that" is probably a better structure for the data than the present format, given your need to aggregate totals from those sources.
N.B: The $const operator is undocumented but has been in place since MongoDB 2.2 with the introduction of the aggregation framework. It serves exactly the same function as $literal ( introduced in MongoDB 2.6 ), and in fact is "exactly" the same thing in the codebase, with the newer definition simply pointing to the older one.
It's used in the listing here as the intended MongoDB targets ( pre 2.6 ) would not have $literal, and the other listing is suitable and better for MongoDB 2.6 and upwards. With $setUnion applied of course.
Well, honestly I'd like to not do this kind of manipulation in mongoldb as it's not very efficient. However, for sake of argument you can try:
NOTE: following query targets MongoDB version 3.2
db.matches.aggregate([
{$project:{_id:1, teams:["$team1","$team2"],
tscore:{$max:["$team1.score","$team2.score"]}}},
{$unwind:"$teams"},
{$unwind:"$teams.players"},
{$project:{player:"$teams.players",
won:{$cond:[{$eq:["$teams.score","$tscore"]},1,0]},
lost:{$cond:[{$lt:["$teams.score","$tscore"]},1,0]}}},
{$group:{_id:"$player", won:{$sum:"$won"}, lost:{$sum:"$lost"}}},
{$project:{_id:0, player:"$_id", won:1, lost:1,
ratio:{$cond:[{$eq:[0, "$lost"]},"$won",
{$divide:["$won","$lost"]}]}}}
])
and it will output following from your sample dataset: NOTE: my mathematics could be wrong in calculation of ratio, however, this is not what we are looking here. I'm simply using won/lost
{
"won" : NumberInt(2),
"lost" : NumberInt(1),
"player" : "player4",
"ratio" : 2.0
}
{
"won" : NumberInt(1),
"lost" : NumberInt(3),
"player" : "player3",
"ratio" : 0.3333333333333333
}
{
"won" : NumberInt(2),
"lost" : NumberInt(1),
"player" : "player2",
"ratio" : 2.0
}
{
"won" : NumberInt(2),
"lost" : NumberInt(2),
"player" : "player1",
"ratio" : 1.0
}
I have a document like this:
{fax: '8135551234', cellphone: '8134441234'}
Is there a way to project (without a group stage) this document into this:
{
phones: [{
type: 'fax',
number: '8135551234'
}, {
type: 'cellphone',
number: '8134441234'
}]
}
I could probably use a group stage operator for this, but I'd rather not if there's any other way, because my query also projects several other fields, all of which would require a $first just for the group stage.
Hope that's clear. Thanks in advance!
MongoDB 2.6 Introduces the the $map operator which is an array transformation operator which can be used to do exactly this:
db.phones.aggregate([
{ "$project": {
"phones": { "$map": {
"input": { "$literal": ["fax","cellphone"] },
"as": "el",
"in": {
"type": "$$el",
"number": { "$cond": [
{ "$eq": [ "$$el", "fax" ] },
"$fax",
"$cellphone"
]}
}
}}
}}
])
So your document now looks exactly like you want. The trick of course to to create a new array with members "fax" and "cellphone", then transform that array with the new document fields by matching those values.
Of course you can also do this in earlier versions using $unwind and $group in a similar fashion, but just not as efficiently:
db.phones.aggregate([
{ "$project": {
"type": { "$const": ["fax","cellphone"] },
"fax": 1,
"cellphone": 1
}},
{ "$unwind": "$type" },
{ "$group": {
"_id": "_id",
"phones": { "$push": {
"type": "$type",
"number": { "$cond": [
{ "$eq": [ "$type", "fax" ] },
"$fax",
"$cellphone"
]}
}}
}}
])
Of course it can be argued that unless you are doing some sort of aggregation then you may as well just post process the collection results in code. But this is an alternate way to do that.