Search code examples
mongodbmongodb-query

Understanding $not in combination with $elemMatch in MongoDB


I have this document structure:

{
      "name": "ab",
      "grades": [
        {
          "grade": "A",
          "score": 1
        },
        {
          "grade": "A",
          "score": 12
        },
        {
          "grade": "A",
          "score": 7
        }
      ],
      "borough": "Manhattan2"
    }

Assignment is to write a query to find the restaurants that have all grades with a score greater than 5.

The solution to the problem is following:

db.restaurants.find({
      "grades": {
        "$not": {
          "$elemMatch": {
            "score": {
              "$lte": 5
            }
          }
        }
      }
    })

I have troubles understanding proposed solution.

So far I have only used $elemMatch to match at least one element of array or elements in array's inner objects (grades.score), but how the heck $not is "somehow making" $elemMatch to check for all grades.score in this object?

I do understand general idea, "don't look at score less the equal to 5, and what remains is what we need", but I cannot comprehend what does this code snippet returns:

"$not": {
      "$elemMatch": {
        "score": {
          "$lte": 5
        }
      }
    }

If was asked what does this query do before running & testing it, I would say that it find first score that is greater then 5 and takes that document, but that is wrong, and I cannot figure why. I see that order of fields and keywords plays some role but don't see the connection.


Solution

  • In order to understand $elemMatch and $not, it would help if we start with $all.

    As you we all know, $not is equal to say “No”, the opposite to “Yes”. We shall come to “No” later. let us first start discussing $all, $elemMatch under the context of “Yes”.

    Let us have a sample collection of just one document as below.

    In Mongo shell:

    let t = db.test;
    t.find();
    [ a: [ 1, 2 ] } ]
    

    First of $all :

    Query 1 : found

    t.find({a: { $all:[1] }});
    [ { a: [ 1, 2 ] } ]
    

    Query 2: found

    t.find({a: { $all:[1,2] }});
    [ {  a: [ 1, 2 ] } ]
    

    Query 3: Not found

    t.find({a: { $all:[1,2,3] }});
    <No document>
    

    What does it mean by $all ?

    Th above three queries can be described in one line as “find all documents with the array key “a” has all elements in the given array”.

    • Query 1 : a has all elements in [1] - found
    • Query 2 : a has all elements in [1,2] - found
    • Query 2 : a does not have all elements in [1,2,3] - not found

    Now let us see $elemeMatch

    Query 4 : found

    t.find({a : { $elemMatch: {$eq: 1}}});
    [ { a: [ 1, 2 ] } ]
    

    Query 5 : found

    t.find({a : { $elemMatch: {$eq: 2}}});
    [ { a: [ 1, 2 ] } ]
    

    Query 6 : Not found

    t.find({a : { $elemMatch: {$eq: 3}}});
    <No document>
    

    What does it mean by $elemMatch ?

    The above three queries can be described in one line as “find all documents with the array key “a” has at least one element matching the given condition”.

    • Query 4 : a has one element matching the condition equal to 1 - found
    • Query 5 : a has one element matching the condition equal to 2 - found
    • Query 6 : a has no element matching the condition equal to 3 - not found

    The take-away:

    1. Although the two set of queries with $all and $elemMatch yielded the same results, the interpretation of the queries are different - as it is described above.
    2. Now all these queries are in the context of “Yes”.
    3. If we are able to understanding the basics of these six queries, we can easily guess what will be the outcome of the same 6 queries in “No” context. It would be just the opposite results.

    Please see the test results.

    t.find({a: {$not: { $all:[1] }}}); // no data
    t.find({a: {$not: { $all:[1,2] }}}); // no data
    t.find({a: {$not: { $all:[1,2,3] }}}); // data found
            1. [ { a: [ 1, 2 ] } ]
    t.find({a : {$not : { $elemMatch: {$eq: 1}}}}); // no data
    t.find({a : {$not : { $elemMatch: {$eq: 2}}}}); // no data
    t.find({a : {$not : { $elemMatch: {$eq: 3}}}}); / data found
            1. [ { a: [ 1, 2 ] } ]
    

    Notes:

    $elemMatch can take more than one conditionals, for brevity, the examples in this post include just one condition. When there will be more than one condition, it would be evaluated with AND logical operation which is essentially the same as with just one condition.