MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

mongoDB-Aggregation:Summe basierend auf Array-Namen

Darin steckt viel, besonders wenn Sie relativ neu in der Verwendung von zusammenfassen , aber es kann getan werden. Ich erkläre die Stufen nach der Auflistung:

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
        ]}}
    }}
])

Zusammenfassung

Dies setzt die Annahme voraus, dass die „Spieler“ in jedem „Gewinn“- und „Verlust“-Array von vornherein einzigartig sind. Das schien vernünftig für das, was hier modelliert zu sein schien:

  1. Wickeln Sie beide Arrays ab. Dadurch werden Duplikate erstellt, die jedoch später entfernt werden.

  2. Beim Projizieren wird teilweise $cond verwendet -Operator (ein Ternär), um einige wörtliche Zeichenfolgenwerte zu erhalten. Und die letzte Verwendung ist etwas Besonderes, weil ein Array hinzugefügt wird. Nach dem Projizieren wird dieses Array also wieder abgewickelt. Mehr Duplikate, aber das ist der Punkt. Ein „Gewinn“, ein „Verlust“-Rekord für jeden.

  3. Mehr Projektion mit $cond -Operator und die Verwendung von $eq Betreiber ebenso. Diesmal verschmelzen wir die beiden Felder zu einem. Wenn also der „Typ“ des Felds mit dem Wert in „Ergebnis“ übereinstimmt, wird dieses „Schlüsselfeld“ für den Feldwert „Ergebnis“ verwendet. Das Ergebnis ist, dass die beiden unterschiedlichen Felder „Gewinn“ und „Verlust“ jetzt den gleichen Namen haben, der durch „Typ“ identifiziert wird.

  4. Beseitigen Sie die Duplikate in jedem Dokument. Einfach nach dem Dokument _id gruppieren und die "Ergebnis"-Felder als Schlüssel. Jetzt sollten die gleichen "Gewinn"- und "Verlust"-Datensätze vorhanden sein wie im Originaldokument, nur in einer anderen Form, da sie aus den Arrays entfernt wurden.

  5. Gruppieren Sie schließlich über alle Dokumente hinweg, um die Summen pro "Spieler" zu erhalten. Mehr Verwendung von $cond und $eq aber dieses Mal, um festzustellen, ob das aktuelle Dokument ein "Gewinn" oder ein "Verlust" ist. Wo dies also übereinstimmt, geben wir 1 zurück und wo falsch, geben wir 0 zurück. Diese Werte werden an $summe um die Gesamtzahl für "Gewinne" und "Verluste" zu erhalten.

Und das erklärt, wie man zum Ergebnis kommt.

Erfahren Sie mehr über die Aggregationsoperatoren aus der Dokumentation. Einige der "lustigen" Verwendungen für $cond in dieser Auflistung sollte durch ein $ ersetzt werden können wörtlich Operator. Aber das wird nicht verfügbar sein, bis Version 2.6 und höher veröffentlicht wird.

„Vereinfachter“ Fall für MongoDB 2.6 und höher

Natürlich gibt es einen neuen Set-Operatoren in der bevorstehenden Version zum Zeitpunkt des Schreibens, die dazu beitragen wird, dies etwas zu vereinfachen:

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
        ]}}
    }}

])

Aber "vereinfacht" ist umstritten. Für mich "fühlt" sich das einfach so an, als würde es "herumtuckern" und mehr Arbeit erledigen. Es ist sicherlich traditioneller, da es sich einfach auf $ stützt setUnion fusionieren die Array-Ergebnisse.

Aber diese "Arbeit" würde zunichte gemacht, wenn Sie Ihr Schema ein wenig ändern, wie hier gezeigt:

{
    "_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
    "win": [
        {
            "player" : "Player2",
            "type" : "win"
        },
        {
            "player" : "Player4",
            "type" : "win"
        }
    ],
    "loss" : [
        {
            "player" : "Player6",
            "type" : "loss"
        },
        {
            "player" : "Player5",
            "type" : "loss"
        },
    ]
}

Und dies beseitigt die Notwendigkeit, den Inhalt des Arrays zu projizieren, indem das Attribut "type" hinzugefügt wird, wie wir es getan haben, und reduziert die Abfrage und die geleistete Arbeit:

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
        ]}}
    }}

])

Und natürlich ändern Sie einfach Ihr Schema wie folgt:

{
    "_id" : ObjectId("531ea2b1fcc997d5cc5cbbc9"),
    "results" : [
        {
            "player" : "Player6",
            "type" : "loss"
        },
        {
            "player" : "Player5",
            "type" : "loss"
        },
        {
            "player" : "Player2",
            "type" : "win"
        },
        {
            "player" : "Player4",
            "type" : "win"
        }
    ]
}

Das macht die Sache sehr einfach. Und dies konnte in Versionen vor 2.6 durchgeführt werden. Sie könnten es also jetzt gleich tun:

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
        ]}}
    }}

])

Wenn es also meine Anwendung wäre, würde ich das Schema in der letzten oben gezeigten Form und nicht das, was Sie haben, wollen. Die gesamte Arbeit der bereitgestellten Aggregationsoperationen (mit Ausnahme der letzten Anweisung) zielt darauf ab, die vorhandene Schemaform zu nehmen und sie in diese zu manipulieren Daher ist es einfach, die einfache Aggregationsanweisung wie oben gezeigt auszuführen.

Da jeder Spieler mit dem Attribut „Sieg/Verlust“ „getaggt“ ist, können Sie ohnehin immer nur diskret auf Ihre „Gewinner/Verlierer“ zugreifen.

Als letzte Sache. Ihr Datum ist eine Zeichenfolge. Das gefällt mir nicht.

Es mag einen Grund dafür gegeben haben, aber ich sehe ihn nicht. Wenn Sie nach Tag gruppieren müssen Dies ist einfach in Aggregation zu tun, indem Sie einfach ein geeignetes BSON-Datum verwenden. Sie können dann auch problemlos mit anderen Zeitintervallen arbeiten.

Wenn Sie also das Datum festgelegt und es zum start_date gemacht haben , und ersetzte "duration" durch end_time , dann kannst du etwas behalten, von dem du die "Dauer" durch einfache Mathematik erhalten kannst + Du bekommst jede Menge extra Vorteile, wenn diese stattdessen als Datumswerte verwendet werden.

Das könnte Ihnen also einige Denkanstöße zu Ihrem Schema geben.

Für diejenigen, die daran interessiert sind, hier ist ein Code, den ich verwendet habe, um einen funktionierenden Datensatz zu generieren:

// 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);
}