Thomas Ferro - Web development engineer at Sfeir DEV Community LinkedIn Twitter

MongoDB query operators and arrays manipulation (M001 - Part 3)

When exact matching is not enough.

We already covered the basic CRUD operations in the previous article. Before wrapping this section up, I would like to introduce more advanced concepts that are often used.

Comparing data

Until now, we almost exclusively search for documents by fields that should be strictly matching the provided query. This is far from the only type of comparison we use day to day.

Query operators: Tools to locale the requested data in a database.

All of the operators we will use begin with the $ sign.

For instance, here is how to use a query operator: { <field>: { <operator>: <value> } }.

Here are some common operators to use:

For instance, searching for a city with more than 10.000 citizens can look like this:

db.cities.find({ "citizens": { "$gt": 10000 } })

Looking for cities outside the USA can be done using this type of query:

db.cities.find({ "country": { "$ne": "USA" } })

Wait, what if I need to find the cities with more than 10.000 citizens, but outside of the USA at the same time? We will need to chain our statements to do so.

Chaining statements

Please do not stop reading, I promise that I am not going to hit you hard with the theory of logic gates.

You may find yourself in a situation where you want documents matching several statements. Sometimes you may need to match all of the statements, sometime at least one of them, or even none.

This is a common need in software development and it is also common when querying a database.

For instance, we can represent the rules behind a gas station serving a customer with the following statement:

const stationCanServeGas = customerPaymentAuthorized() && thereIsGasLeft()

MongoDB provides the tools to achieve the same goal using a different syntax and logic operators.

Logic operators: Tools to chain statement with specific behaviours

The first three operators $and, $or and $nor have a similar syntax with an array of statements:

{ <operator>: [ { statement1 }, { statement2 }, ... ] }

The $not operator is completely different, please read through the documentation for more information about its behavior and syntax.

Going back to the first operators, we can for instance find every valid users by making a query with every needed matching:

db.users.find({
    "$and": [{
        "$or": [{
            "registrationCompleted": true
        }, {
            "ongoingRegistration": true
        }]
    }, {
        "ongoingDeletion": false
    }]
})

The $and operator is implicit by default, meaning that you can ignore it in order to simplify your requests. Here is the previous request using the implicit $and:

db.users.find({
    "$or": [{
        "registrationCompleted": true
    }, {
        "ongoingRegistration": true
    }],
    "$not": { "ongoingDeletion": true }
})

We can go even further with the implicit $and when doing multiple statements on the same field. Here are two ways of returning courses that take between 5 and 10 hours to complete:

db.courses.find({
    "$and": [{
        "estimateTimeToComplete": {
            "$gt": 5
        }
    }, {
        "estimateTimeToComplete": {
            "$lt": 10
        }
    }]
})

db.courses.find({
    "estimateTimeToComplete": {
        "$gt": 5,
        "$lt": 10
    }
})

As you can see, there are several ways to achieve the same goal. Some are more verbose or readable than others. It is up to you to make them maintainable and easy to understand!

Comparing fields within a document with the expressive query operator

Expressive query operator: Adds expressiveness to the query language by allowing us to use variables and conditions statements. It also allows for the use of aggregation expressions, discussed in a next article.

Here is the basic syntax of this type of operator:

{ "$expr": { <statement> } }

We can use the $ symbol inside of an expressive query operator to reference a document’s field value.

Let us make this concept more concrete by seeing it in action. Here is how we can try to find cities that currently welcome more tourists than citizens:

db.cities.find({
    "$expr": {
        "$gt": ["$population", "$tourists"]
    }
})

Notice the change in the syntax, we will cover it in-depth when talking about the aggregation pipeline.

Querying and manipulating array fields

The MQL syntax offers many operators to query and manipulate arrays. We already used the $push operator in the previous article, here is a distilled explanation of it:

$push: Adds one or many elements to an array or turn the field into an array if it was of a different type.

Here is how we can add a student in a course:

db.courses.updateMany({
    "subject": "Mongodb"
}, {
    "$push": {
        "students": "Eliot Horowitz"
    }
})

To search for a document based on elements in an array, we can use several approaches:


One of the advanced tools for array querying is $elemMatch.

$elemMatch: Returns every document with the array field containing at least one element matching the query.

Let us say that our students in the courses collection are now complex objects with their name, age and other useful information in this context. For legal purpose, we want to find courses with at least one student under 18. We can achieve that with the following query:

db.courses.find({
    "students": {
        "$elemMatch": {
            "age": {
                "$lte": 18
            }
        }
    }
})

Another useful tool provided by the MQL allows us to search for a specific element in an array based on his index.

Say for instance that we want to get every courses where the first student is named “Merriman”. We could achieve this goal using the following syntax:

db.courses.find({
    "students.0.name": "Merriman"
})

Leaner find results with projections

We can decide which fields to return in a query by specifying a projection.

db.<collection>.find({ <query> }, { <projection> })

With the projection being a set of key-value pairs with the value being 1 if we want the field to be included or 0 if we want it excluded. Note that you cannot mix zeros and ones in a single projection, except for the “_id” field that can be excluded in a projection with included fields.

For instance, if we only want the students and the subject of a course, we can define the following projection:

db.courses.find({}, {
    "subject": 1,
    "students": 1
})

Combining projections and query is also possible. Here is how we can use our previous query, but only return the subject of the matching courses:

db.courses.find({
    "students": {
        "$size": 10,
        "$all": [
            "Eliot Horowitz",
            "Kevin Ryan",
            "Dwight Merriman",
        ]
    }
}, {
    "subject": 1,
})

This article wraps the first part of the series regarding the MongoDB certification. It has been a long yet interesting journey! I hope that you are as eager as I am to cover basic cluster administration, the aggregation framework and the last subjects of the course 😁