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

Mongodb-Aggregatabfrage oder zu komplex?

Obwohl es in Ihrer Frage klarer hätte sein sollen, deutet Ihr Ausgabebeispiel aus der Quelle darauf hin, dass Sie nach Folgendem suchen:

  • Gesamtzahl der Nachrichten pro "uid"
  • Eindeutige Anzahl von Werten in "bis"
  • Eindeutige Anzahl von Werten in "von"
  • Zusammenfassung der Zählungen pro "Stunde" für jede "uid"

All dies ist in einer einzigen Aggregationsanweisung möglich, und es bedarf lediglich einer sorgfältigen Verwaltung der unterschiedlichen Listen und dann einiger Manipulationen, um die Ergebnisse für jede Stunde in einem Zeitraum von 24 Stunden abzubilden.

Der beste Ansatz hier wird durch Operatoren unterstützt, die in MongoDB 3.2 eingeführt wurden:

db.collection.aggregate([
    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" }
     }},

     // Map out for each hour and count size of distinct lists
     { "$project": {
        "count": "$total",
        "from_count": { "$size": "$from" },
        "to_count": { "$size": "$to" },
        "hours": {
            "$map": {
                "input": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
                 ],
                 "as": "el",
                 "in": {
                      "$ifNull": [
                          { "$arrayElemAt": [
                              { "$map": {
                                  "input": { "$filter": {
                                     "input": "$temp_hours",
                                     "as": "tmp",
                                     "cond": {
                                         "$eq": [ "$$el", "$$tmp.index" ]
                                     }
                                  }},
                                 "as": "out",
                                 "in": "$$out.count"
                              }},
                              0
                          ]},
                          0
                      ]
                 }
            }
        }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
 ])

Vor MongoDB 3.2 müssen Sie sich etwas mehr einbringen, um den Array-Inhalt für alle Stunden des Tages abzubilden:

db.collection.aggregate([

    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct, also adding the indexes array
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" },
        "indexes": { "$first": { "$literal": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
        ] } }
     }},

     // Denormalize both arrays
     { "$unwind": "$temp_hours" },
     { "$unwind": "$indexes" },

     // Marry up the index entries and keep either the value or 0
     // Note you are normalizing the double unwind to distinct index
     { "$group": {
         "_id": {
             "_id": "$_id",
             "index": "$indexes"
         },
         "total": { "$first": "$total" }, 
         "from": { "$first": "$from" },
         "to": { "$first": "$to" },
         "count": {
             "$max": {
                 "$cond": [
                     { "$eq": [ "$indexes", "$temp_hours.index" ] },
                     "$temp_hours.count",
                     0
                 ]
             }
         }
     }},

     // Sort to keep index order - !!Important!!         
     { "$sort": { "_id": 1 } },

     // Put the hours into the array and get sizes for other results
     { "$group": {
         "_id": "$_id._id",
         "count": { "$first": "$total" },
         "from_count": { "$first": { "$size": "$from" } },
         "to_count": { "$first": { "$size": "$to" } },
         "hours": { "$push": "$count" }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
])

Um das aufzuschlüsseln, folgen beide Ansätze hier denselben grundlegenden Schritten, wobei der einzige wirkliche Unterschied in der Zuordnung von "Stunden" für den 24-Stunden-Zeitraum auftritt.

In der ersten Aggregation $group Phase besteht das Ziel darin, Ergebnisse pro Stunde in den Daten und für jeden „uid“-Wert zu erhalten. Der einfache Datenaggregationsoperator von $hour hilft, diesen Wert als Teil des Gruppierungsschlüssels zu erhalten.

Der $addToSet Operationen sind eine Art "Mini-Gruppe" an sich, und dies ermöglicht es, die "unterschiedlichen Sätze" für jeden der "Bis"- und "Von"-Werte beizubehalten, während im Wesentlichen immer noch pro Stunde gruppiert wird.

Die nächste $group ist "organisatorischer", da die aufgezeichneten "Zählungen" für jede Stunde in einem Array gehalten werden, während alle Daten zusammengefasst werden, um nur pro "uid" gruppiert zu werden. Damit erhältst du im Grunde alle "Daten", die du wirklich für das Ergebnis benötigst, aber natürlich das $addToSet Operationen hier fügen nur "Arrays innerhalb von Arrays" der einzelnen Sätze hinzu, die pro Stunde bestimmt werden.

Um diese Werte als wirklich unterschiedliche Listen pro "uid" und nur zu erhalten, ist es notwendig, jedes Array mit $unwind und gruppieren Sie dann schließlich wieder als nur die unterschiedlichen "Sätze". Dasselbe $addToSet komprimiert dies und der $first Operationen nehmen einfach die "ersten" Werte der anderen Felder, die für die Zieldaten "pro UID" bereits alle gleich sind. Wir sind damit zufrieden, also behalte sie einfach so, wie sie sind.

Die letzte(n) Stufe(n) hier sind im Wesentlichen "kosmetischer" Natur und können gleichermaßen in clientseitigem Code erreicht werden. Da nicht für jedes einzelne Stundenintervall Daten vorhanden sind, müssen sie einem Array von Werten zugeordnet werden, die jede Stunde darstellen. Die beiden Ansätze hier variieren je nach den Fähigkeiten der verfügbaren Operatoren zwischen den Versionen.

In der Version MongoDB 3.2 gibt es $filter und $arrayElemAt Operatoren, mit denen Sie effektiv die Logik erstellen können, um eine Eingabequelle aller möglichen Indexpositionen ( 24 Stunden ) in die Werte zu "transponieren", die bereits für die Zählungen dieser Stunden in den verfügbaren Daten bestimmt wurden. Dies ist im Grunde genommen ein "direktes Nachschlagen" von Werten, die bereits für jede verfügbare Stunde aufgezeichnet wurden, um zu sehen, ob sie existiert, wo dies der Fall ist, wird die Zählung in das vollständige Array übertragen. Wo es nicht vorhanden ist, ein Standardwert von 0 wird an Ort und Stelle verwendet.

Ohne diese Operatoren bedeutet dieses "Abgleichen" im Wesentlichen, beide Arrays (die aufgezeichneten Daten und die vollen 24 Positionen) zu denormalisieren, um sie zu vergleichen und zu transponieren. Dies geschieht beim zweiten Ansatz mit einem einfachen Vergleich der "Index"-Werte, um zu sehen, ob es für diese Stunde ein Ergebnis gab. Der $max Der Operator wird hier hauptsächlich wegen der beiden $unwind verwendet Statements, bei denen jeder aufgezeichnete Wert aus den Quelldaten für jede mögliche Indexposition reproduziert wird. Dies "verdichtet" auf genau die Werte, die pro "Indexstunde" benötigt werden.

Bei letzterem Ansatz wird es dann wichtig, $sort auf die Gruppierung _id Wert. Dies liegt daran, dass es die "Index" -Position enthält, die benötigt wird, wenn dieser Inhalt zurück in ein Array verschoben wird, von dem Sie erwarten, dass es geordnet wird. Das ist natürlich die letzte $group Stufe hier, wo die bestellten Positionen mit $push in ein Array gestellt werden .

Zurück zu den "verschiedenen Listen", dem $size Der Operator wird in allen Fällen verwendet, um die "Länge" und damit die "Anzahl" unterschiedlicher Werte in den Listen für "bis" und "von" zu bestimmen. Dies ist zumindest die einzige wirkliche Einschränkung für MongoDB 2.6, kann aber ansonsten einfach dadurch ersetzt werden, dass jedes Array einzeln "abgewickelt" und dann wieder auf der _id gruppiert wird bereits vorhanden, um die Array-Einträge in jedem Satz zu zählen. Es ist ein grundlegender Prozess, aber wie Sie sehen sollten, ist $size Betreiber ist hier die bessere Option für die Gesamtleistung.

Als letzte Anmerkung weichen Ihre Abschlussdaten etwas ab, da möglicherweise der Eintrag mit „ddd“ in „von“ auch in „bis“ gleich sein sollte, aber stattdessen als „bbb“ erfasst wird. Dadurch wird der Distinct Count der dritten „uid“-Gruppierung für „to“ um einen Eintrag nach unten verändert. Aber natürlich sind die logischen Ergebnisse angesichts der Quelldaten solide:

{ "_id" : 1000000, "count" : 3, "from_count" : 2, "to_count" : 2, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 2000000, "count" : 2, "from_count" : 1, "to_count" : 1, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 3000000, "count" : 5, "from_count" : 5, "to_count" : 4, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0 ] }

Hinweis:Die Quelle enthält auch einen Tippfehler, bei dem das Trennzeichen zwischen : eingefügt wurde anstelle eines Kommas direkt nach dem Zeitstempel in allen Zeilen.