Jest w tym wiele, zwłaszcza jeśli jesteś stosunkowo nowy w korzystaniu z agregat , ale może będzie zrobione. Wyjaśnię etapy po wykazie:
db.collection.aggregate([
// 1. Unwind both arrays
{"$unwind": "$win"},
{"$unwind": "$loss"},
// 2. Cast each field with a type and the array on the end
{"$project":{
"win.player": "$win.player",
"win.type": {"$cond":[1,"win",0]},
"loss.player": "$loss.player",
"loss.type": {"$cond": [1,"loss",0]},
"score": {"$cond":[1,["win", "loss"],0]}
}},
// Unwind the "score" array
{"$unwind": "$score"},
// 3. Reshape to "result" based on the value of "score"
{"$project": {
"result.player": {"$cond": [
{"$eq": ["$win.type","$score"]},
"$win.player",
"$loss.player"
] },
"result.type": {"$cond": [
{"$eq":["$win.type", "$score"]},
"$win.type",
"$loss.type"
]}
}},
// 4. Get all unique result within each document
{"$group": { "_id": { "_id":"$_id", "result": "$result" } }},
// 5. Sum wins and losses across documents
{"$group": {
"_id": "$_id.result.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$_id.result.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$_id.result.type","loss"]},1,0
]}}
}}
])
Podsumowanie
Przyjmuje to założenie, że „gracze” w każdej tablicy „wygrana” i „przegrana” są od początku unikatowi. Wydawało się to rozsądne w przypadku tego, co wyglądało na modelowane tutaj:
-
Rozwiń obie tablice. Spowoduje to utworzenie duplikatów, ale zostaną one później usunięte.
-
Podczas projekcji używa się $cond operator (trójargumentowy) w celu uzyskania kilku literalnych wartości łańcuchowych. A ostatnie użycie jest specjalne, ponieważ i tablica jest dodawana. Więc po projekcji ta tablica zostanie ponownie rozwinięta. Więcej duplikatów, ale o to chodzi. Jeden rekord „wygranej”, jeden rekord „przegranej” dla każdego.
-
Więcej projekcji dzięki $cond operator i użycie $eq operatora. Tym razem scalamy dwa pola w jedno. Używając tego, gdy „typ” pola pasuje do wartości w „score”, to „pole klucza” jest używane jako wartość pola „wynik”. Wynik to dwa różne pola „wygrana” i „przegrana” mają teraz tę samą nazwę, identyfikowaną przez „typ”.
-
Pozbycie się duplikatów w każdym dokumencie. Po prostu grupuj według dokumentu
_id
a pola "wynik" jako klucze. Teraz powinny być te same rekordy „wygranej” i „przegranej”, co w oryginalnym dokumencie, tylko w innej formie, ponieważ są one usuwane z tablic. -
Na koniec pogrupuj wszystkie dokumenty, aby uzyskać sumy na „gracza”. Więcej użycia $cond i $eq ale tym razem, aby ustalić, czy aktualny dokument jest „wygraną” czy „przegraną”. Więc tam, gdzie to pasuje, zwracamy 1, a tam, gdzie false zwracamy 0. Te wartości są przekazywane do $ suma aby uzyskać łączną liczbę „wygranych” i „przegranych”.
I to wyjaśnia, jak dojść do wyniku.
Dowiedz się więcej o operatorach agregacji z dokumentacji. Niektóre z „śmiesznych” zastosowań $cond w tym wykazie powinno być możliwe zastąpienie go $ dosłowny operator. Ale to nie będzie dostępne aż do wydania wersji 2.6 i nowszych.
Przypadek „uproszczony” dla MongoDB 2.6 i nowszych
Oczywiście pojawiły się nowe operatory zestawów w jakim jest nadchodzącym wydaniu w momencie pisania, co pomoże to nieco uprościć:
db.collection.aggregate([
{ "$unwind": "$win" },
{ "$project": {
"win.player": "$win.player",
"win.type": { "$literal": "win" },
"loss": 1,
}},
{ "$group": {
"_id" : {
"_id": "$_id",
"loss": "$loss"
},
"win": { "$push": "$win" }
}},
{ "$unwind": "$_id.loss" },
{ "$project": {
"loss.player": "$_id.loss.player",
"loss.type": { "$literal": "loss" },
"win": 1,
}},
{ "$group": {
"_id" : {
"_id": "$_id._id",
"win": "$win"
},
"loss": { "$push": "$loss" }
}},
{ "$project": {
"_id": "$_id._id",
"results": { "$setUnion": [ "$_id.win", "$loss" ] }
}},
{ "$unwind": "$results" },
{ "$group": {
"_id": "$results.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$results.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$results.type","loss"]},1,0
]}}
}}
])
Ale „uproszczony” jest dyskusyjny. Dla mnie to po prostu „wydaje się” jak „ruszenie się” i wykonywanie większej ilości pracy. Z pewnością jest bardziej tradycyjny, ponieważ opiera się po prostu na $ setUnion scalić wyniki tablicy.
Ale ta "praca" zostałaby unieważniona przez niewielką zmianę schematu, jak pokazano tutaj:
{
"_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
"win": [
{
"player" : "Player2",
"type" : "win"
},
{
"player" : "Player4",
"type" : "win"
}
],
"loss" : [
{
"player" : "Player6",
"type" : "loss"
},
{
"player" : "Player5",
"type" : "loss"
},
]
}
Eliminuje to potrzebę projekcji zawartości tablicy przez dodanie atrybutu „type”, tak jak to robiliśmy, oraz zmniejsza zapytanie i wykonaną pracę:
db.collection.aggregate([
{ "$project": {
"results": { "$setUnion": [ "$win", "$loss" ] }
}},
{ "$unwind": "$results" },
{ "$group": {
"_id": "$results.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$results.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$results.type","loss"]},1,0
]}}
}}
])
I oczywiście po prostu zmieniając swój schemat w następujący sposób:
{
"_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
"results" : [
{
"player" : "Player6",
"type" : "loss"
},
{
"player" : "Player5",
"type" : "loss"
},
{
"player" : "Player2",
"type" : "win"
},
{
"player" : "Player4",
"type" : "win"
}
]
}
To sprawia, że rzeczy są bardzo łatwo. I można to zrobić w wersjach wcześniejszych niż 2.6. Więc możesz to zrobić już teraz:
db.collection.aggregate([
{ "$unwind": "$results" },
{ "$group": {
"_id": "$results.player",
"wins": {"$sum": {"$cond": [
{"$eq":["$results.type","win"]},1,0
]}},
"losses": {"$sum":{"$cond": [
{"$eq":["$results.type","loss"]},1,0
]}}
}}
])
Więc dla mnie, gdyby to była moja aplikacja, wolałbym schemat w ostatniej formie pokazanej powyżej, a nie taki, jaki masz. Cała praca wykonana w dostarczonych operacjach agregacji (z wyjątkiem ostatniej instrukcji) ma na celu przejęcie istniejącego formularza schematu i manipulowanie nim w tym formularz, więc łatwo jest uruchomić prostą instrukcję agregacji, jak pokazano powyżej.
Ponieważ każdy gracz jest „oznaczony” atrybutem „wygrana/przegrana”, zawsze możesz dyskretnie uzyskać dostęp do swoich „zwycięzców/przegranych”.
Na koniec. Twoja data jest ciągiem. Nie lubię tego.
Być może był ku temu powód, ale go nie widzę. Jeśli chcesz pogrupować według dnia jest to łatwe do zrobienia w agregacji, używając tylko właściwej daty BSON. Będziesz wtedy mógł również łatwo pracować z innymi przedziałami czasowymi.
Więc jeśli ustaliłeś datę i ustawiłeś ją jako data_początkowa i zamieniono „czas trwania” na end_time , wtedy możesz zachować coś, z czego możesz uzyskać „czas trwania” za pomocą prostej matematyki + Otrzymujesz wiele dodatkowych korzyści, mając je jako wartość daty.
To może dać ci trochę do myślenia na temat twojego schematu.
Dla zainteresowanych, oto kod, którego użyłem do wygenerowania działającego zestawu danych:
// Ye-olde array shuffle
function shuffle(array) {
var m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = array[m];
array[m] = array[i];
array[i] = t;
}
return array;
}
for ( var l=0; l<10000; l++ ) {
var players = ["Player1","Player2","Player3","Player4"];
var playlist = shuffle(players);
for ( var x=0; x<playlist.length; x++ ) {
var obj = {
player: playlist[x],
score: Math.floor(Math.random() * (100000 - 50 + 1)) +50
};
playlist[x] = obj;
}
var rec = {
duration: Math.floor(Math.random() * (50000 - 15000 +1)) +15000,
date: new Date(),
win: playlist.slice(0,2),
loss: playlist.slice(2)
};
db.game.insert(rec);
}