sql >> Databasteknik >  >> NoSQL >> MongoDB

Mongodb aggregerad fråga, eller för komplex?

Även om det borde ha gjorts tydligare i din fråga, tyder ditt utdataexempel från källan på att du letar efter:

  • Totalt antal meddelanden per "uid"
  • Distinkt antal värden i "till"
  • Särskilt antal värden i "från"
  • Sammanfattning av antalet per "timme" för varje "uid"

Allt detta är möjligt i en enda sammanställningssats, och det krävs bara lite noggrann hantering av de distinkta listorna och sedan lite manipulation för att kartlägga resultaten för varje timme under en 24-timmarsperiod.

Det bästa tillvägagångssättet här får hjälp av operatörer som introducerats i MongoDB 3.2:

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

Före MongoDB 3.2 måste du engagera dig lite mer för att kartlägga arrayinnehållet under alla timmar på dygnet:

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

För att bryta ner det följer båda tillvägagångssätten här samma grundläggande steg, med den enda verkliga skillnaden som uppstår på kartläggningen av "timmar" för 24-timmarsperioden.

I den första sammanställningen $group steget är målet att få resultat per timme som finns i data och för varje "uid"-värde. Den enkla datumaggregationsoperatorn för $hour hjälper till att få detta värde som en del av grupperingsnyckeln.

$addToSet operationer är en sorts "minigrupp" i sig, och detta gör det möjligt att behålla de "distinkta uppsättningarna" för var och en av "till" och "från" värden samtidigt som de i princip fortfarande grupperas per timme.

Nästa $group är mer "organisatoriskt", eftersom de registrerade "antalerna" för varje timme hålls i en array samtidigt som all data rullas upp för att bara grupperas per "uid". Detta ger dig i princip all "data" du verkligen behöver för resultatet, men naturligtvis $addToSet operationer här är bara att lägga till "arrayer inom arrays" av de distinkta uppsättningarna som bestäms per timme.

För att få dessa värden som verkligt distinkta listor per varje "uid" och endast, är det nödvändigt att dekonstruera varje array med $unwind och sedan slutligen gruppera tillbaka som bara de distinkta "uppsättningarna". Samma $addToSet komprimerar detta och $first operationer tar bara de "första" värdena för de andra fälten, som redan är desamma för måldata för "per uid". Vi är nöjda med dem, så behåll dem som de är.

De sista stegen här är i huvudsak "kosmetiska" till sin natur och kan likaså uppnås i kod på klientsidan. Eftersom det inte finns data för varje timmes intervall måste de mappas till en matris med värden som representerar varje timme. De två tillvägagångssätten här varierar beroende på kapaciteten hos de tillgängliga operatörerna mellan versionerna.

I MongoDB 3.2-versionen finns $filter och $arrayElemAt operatorer som effektivt låter dig skapa logiken för att "transponera" en ingångskälla för alla möjliga indexpositioner ( 24 timmar ) till de värden som redan är fastställda för räkningarna från dessa timmar i tillgänglig data. Detta är i grund och botten en "direkt uppslagning" av värden som redan har registrerats för varje tillgänglig timme för att se om det finns, där det gör omvandlas räkningen till hela arrayen. Om det inte finns, ett standardvärde på 0 används på plats.

Utan dessa operatörer innebär detta "matcha upp" i huvudsak att denormalisera båda arrayerna (de inspelade data och hela 24 positioner) för att jämföra och transponera. Detta är vad som händer i den andra metoden med en enkel jämförelse av "index"-värdena för att se om det fanns ett resultat för den timmen. $max operatorn här används huvudsakligen på grund av de två $unwind uttalanden, där varje registrerat värde från källdata kommer att reproduceras för varje möjlig indexposition. Detta "komprimerar" ner till bara de värden som önskas per "indextimme".

I det senare tillvägagångssättet blir det då viktigt att $sort kod> på grupperingen _id värde. Detta beror på att det innehåller "index"-positionen, och det kommer att behövas när du flyttar detta innehåll tillbaka till en array som du förväntar dig att beställas. Vilket naturligtvis är den sista $gruppen steg här där de ordnade positionerna placeras i en array med $push .

Tillbaka till de "distinkta listorna", $size operatorn används i alla fall för att bestämma "längden" och därför "antalet" av distinkta värden i listorna för "till" och "från". Detta är åtminstone den enda verkliga begränsningen på MongoDB 2.6, men kan annars ersättas med att helt enkelt "avveckla" varje array individuellt och sedan gruppera tillbaka på _id redan närvarande för att räkna arrayposterna i varje uppsättning. Det är en grundläggande process, men som du borde se $size operatör är det bättre alternativet här för övergripande prestanda.

Som en sista notering är din slutsatsdata lite avvikande, eftersom posten med "ddd" i "från" var tänkt att också vara densamma i "till", men är istället registrerad som "bbb". Detta ändrar det distinkta antalet för den tredje "uid"-grupperingen för "till" ned med en post. Men de logiska resultaten givet källdata är naturligtvis sunda:

{ "_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 ] }

OBS Källan har också ett stavfel där avgränsaren är insatt med : istället för ett kommatecken direkt efter tidsstämpeln på alla rader.




  1. Varför använder mongoose schema när fördelen med mongodb är att den är schemalös?

  2. Hur får man värde från kapslade objekt i mongoose?

  3. fel vid anslutning till värd:kunde inte ansluta till server:servervalsfel:serverval timeout aktuell topologi:Typ:Singelservrar

  4. Mongoid 3 - få tillgång till map_reduce resultat