Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to solve empty array with $unwind?

I have a table and save like following:

{ "_id" : ObjectId("5716617f4af77ca97a9614bd"), "count" : 1, "author" : "Tony", "music" : [ { "_id" : ObjectId("571661cd4af77ca97a9614c1"), "count" : 2, "author" : "Tony" } ] }
{ "_id" : ObjectId("5716617f4af77ca97a9614be"), "count" : 2, "author" : "Joe", "music" : [ { "_id" : ObjectId("571661cd4af77ca97a9614c0"), "count" : 1, "author" : "Joe" } ] }
{ "_id" : ObjectId("5716617f4af77ca97a9614bf"), "count" : 3, "author" : "Mary", "music" : [ ] }

I hope to find the number of record that "$count" > "$music.count". But when I do {$unwind:"$music"}, I get following:

{ "_id" : ObjectId("5716617f4af77ca97a9614bd"), "count" : 1, "author" : "Tony", "music" : { "_id" : ObjectId("571661cd4af77ca97a9614c1"), "count" : 2, "author" : "Tony" } }
{ "_id" : ObjectId("5716617f4af77ca97a9614be"), "count" : 2, "author" : "Joe", "music" : { "_id" : ObjectId("571661cd4af77ca97a9614c0"), "count" : 1, "author" : "Joe" } }

The third record disappear. How can I get the result like:

{ "_id" : ObjectId("5716617f4af77ca97a9614bd"), "count" : 1, "author" : "Tony", "music" : { "_id" : ObjectId("571661cd4af77ca97a9614c1"), "count" : 2, "author" : "Tony" } }
{ "_id" : ObjectId("5716617f4af77ca97a9614be"), "count" : 2, "author" : "Joe", "music" : { "_id" : ObjectId("571661cd4af77ca97a9614c0"), "count" : 1, "author" : "Joe" } }
{ "_id" : ObjectId("5716617f4af77ca97a9614bf"), "count" : 3, "author" : "Mary", "music" : {"count": 0} }

The initial records are got by $loopup, The total code is like following:

db.bookAuthors.aggregate([{
$lookup:{from:"musicAuthors", localField:"author", foreignField:"author",as:"music"}},
{$unwind:"$music"},
{$project:{_id:"$author",count:1,music:1}},
{$match:{$gt:["$count","$music.count"]}},
{$group:{_id:null,count:{$sum:1}}}
])

How can I do to find the number of record that "$count" > "$music.count"? In this example, the result should be 2. But now due to the unwind problem, I get 1. Thanks.

like image 261
testcode Avatar asked Apr 19 '16 17:04

testcode


People also ask

How do I unwind an array in MongoDB?

MongoDB provides a variety of state operators. The $unwind operator is one of those operators. The $unwind operator is used to deconstructing an array field in a document and create separate output documents for each item in the array.

How do you use unwind?

Unwind sentence example. He'd messed around with Jenn only a few hours earlier and already felt the need to unwind again. So this is where you come to unwind. There was nothing unusual about Alex riding Ed to unwind after a trip, but this was the first time he had done so without changing his clothes.

How do I unwind an object in MongoDB?

The MongoDB $unwind stages operator is used to deconstructing an array field from the input documents to output a document for each element. Every output document is the input document with the value of the array field replaced by the element. Points to remember: If the value of a field is not an array, db.

How does unwind work MongoDB?

MongoDB $unwind transforms complex documents into simpler documents, which increase readability and understanding. This also allows us to perform additional operations, like grouping and sorting on the resulting output.


Video Answer


1 Answers

In MongoDB 3.2 ( which you are using if you have $lookup ) the $unwind operator has the preserveNullAndEmptyArrays option. This changes the behaviour to "not" remove the document from results where the array is in fact "empty":

db.bookAuthors.aggregate([
  { "$lookup":{
    "from": "musicAuthors", 
    "localField": "author", 
    "foreignField": "author",
    "as": "music"
  }},
  { "$unwind": { "path": "$music", "preserveNullAndEmptyArrays": true },
  { "$project": {
      "count": 1,
      "author": 1,
      "music": {
        "$ifNull": [ "$music", { "$literal": { "count": 0 } }] },
      }
  }}
])

And the $ifNull replaces the missing value in this case.

But actually since your association here is 1:1 then you could just forego the $unwind altogether, and simply replace the empty array:

db.bookAuthors.aggregate([
  { "$lookup":{
    "from": "musicAuthors", 
    "localField": "author", 
    "foreignField": "author",
    "as": "music"
  }},
  { "$project": {
    "count": 1,
    "author": 1,
    "music": {
      "$ifNull": [
        { "$arrayElemAt": [ "$music", 0 ] },
        { "$literal": { "count": 0 } }
      ]
    }
  }}
])

And there if $arrayElemAt found nothing at the 0 index ( therefore "empty" ) then the $ifNull would return the alternate value just as before. Of course where it did find something, then that value is returned instead.

But again, your specific problem still has a better solution, which again does not need $unwind. Since you can just calculate the "count" condition "in-line" with the array:

db.bookAuthors.aggregate([
  { "$lookup":{
    "from": "musicAuthors", 
    "localField": "author", 
    "foreignField": "author",
    "as": "music"
  }},
  { "$group": {
    "_id": null,
    "count": {
      "$sum": {
        "$cond": {
          "if": {
            "$gt": [
              "$count",
              { "$sum": {
                "$map": {
                  "input": "$music",
                  "as": "el",
                  "in": "$$el.count"
                }
              }}
            ]
          },
          "then": 1,
          "else": 0
        }
      }
    }
  }}
])

Here the $sum operator is used in both of it's use cases, as it's traditional "accumulator" and new role in "summing" values in an array. The $map operator looks at each array element and returns the value to $sum to produce a total. An "empty" array would return as 0.

Then there is the $cond comparison to determine if the returned total from the array was less than the "count" property on the document. Where true a 1 is returned for the accumulator, and where false it gets 0.

The end result is of course 2, since both the "first" and "third" documents actually match the condition inside the accumulator. So that really is the most efficient way to do this, even if it looks a bit "long winded" in the process.

like image 113
Neil Lunn Avatar answered Oct 02 '22 18:10

Neil Lunn