In any business, data is your biggest asset. By analyzing data, you can make decisions on customer trends and behavior prediction. This boosts business profitability and effective decision-making.

Without database software, a simple task like finding the average of all the values in a system full of records would be tedious. Luckily, databases have made analyzing data easier and faster with functions and operators.

This article will shed some light on the operators used in the MongoDB database software.

What Are MongoDB Operators?

MongoDB is a NoSQL database software that manages document-oriented information.

One of MongoDB’s key features is its speed. To return queries faster, MongoDB may use operators to perform specific functions.

Operators are special symbols that help compilers perform mathematical or logical tasks. MongoDB offers several types of operators to interact with the database.

MongoDB Operator Types

There are nine types of operators, each named for its function. For instance, logical operators use logic operations. To execute them, you need to use a specific keyword and follow the syntax. However, they’re fairly easy to follow!

By the end of the article, you’ll be able to learn the basics of each operator and its functions.

Logical Operators

Logical operators are often used to filter data based on the given conditions. They also allow for the evaluation of many conditions, which we’ll discuss in more detail.

Below are a few logical operators that you can use:

$and

An “and” condition performs a logical “and” operation on an array of two or more expressions. It selects the documents where all the conditions of the expressions are satisfied.

This is the standard syntax for the $and expression:

{ $and: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } For example, if we want to select documents where the price is $10 and the quantity is less than 15, we can input the following query:
db.inventory.find( { $and: [ { quantity: { $lt: 15 } }, { price: 10 } ] } )

$or

An “or” condition performs a logical “or” operation on an array of two or more expressions. It selects the documents where at least one of the expressions is true.

This is the standard syntax for the $or expression:

{ $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] }.

For example, if we want to select documents where the price is $10 or the quantity is less than 15, we can input the following query:

db.inventory.find( { $or: [ { quantity: { $lt: 15 } }, { price: 10 } ] } )

We don’t have to limit the expression to two criteria — we can add more. For instance, the below query selects those documents where the price equals $10, the quantity is under 15, or the tag is stationary:

db.inventory.find( { $or: [ { quantity: { $lt: 15 } }, { price: 10 }, { tag: "stationary" }] } )

When running these clauses, MongoDB either performs a collection scan or an index scan. If all indexes support the clauses, then MongoDB uses indexes to check an $or expression. Otherwise, it uses a collection scan instead.

But if you want to test the criteria in the same field, you may want to use the $in operator rather than the $or operator. For example, if you want a collection of documents where the quantity is either 10 or 20, you may have to run the below $in query instead:

db.inventory.find ( { quantity: { $in: [20, 50] } } )

We’ll cover more on the $in operator later on.

$nor

This operator performs a logical “nor” operation on an array using one or more expressions. Next, it selects the documents that fail the query expressions. In simpler terms, it does the opposite of the $or condition.

This is the general syntax:

{ $nor: [ { <expression1> }, { <expression2> }, ...  { <expressionN> } ] }

Let’s consider the following query:

db.inventory.find( { $nor: [ { price: 3.99 }, { sale: true } ]  } )

This query selects the documents that contain:

  • a price field value not equal to $3.99, and a sale value not equal to true; or
  • a price field value not equal to $3.99, and an empty or absent sale field; or
  • no price field, and a sale field not equal to true; or
  • neither price field nor sale field populated or present.

$not

This operator performs a logical “not” operation on an array for the specified expression. It then selects the documents that don’t match the query expressions. This includes the documents that don’t contain the field.

This is the general syntax:

{ field: { $not: { <operator-expression> } } }

For instance, take the following query:

db.inventory.find( { price: { $not: { $lt: 3.99 } } } )

This query would select those documents that contain:

  • a price field whose value is greater than or equal to $3.99; and
  • a price field is unpopulated or does not exist.

Comparison Operators

Comparison operators can be used to compare values in one or more documents.

Below is a sample code of a simple inventory collection for a supermarket store:

{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] },
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] },
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] },
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] },
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] },
{ _id: 6, item: { name: "strawberry", code: "123" }, tags: [ "B" ] }

We’ll use this example while detailing each comparison operator next.

Equal to ($eq)

This operator matches values that are equal to the given value:

{ <field>: { $eq: <value> } }

For example, if we want to retrieve a specific document from the inventory collection having the exact quantity value “20”, we’d input the following command:

db.inventory.find( { qty: { $eq: 20 } } )

The query would return the following:

{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] }, 
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }

Greater than ($gt)

This operator matches if the values are greater than the given value:

{ field: { $gt: value } }

In this example, we retrieve the documents where the quantity is greater than 15:

db.inventory.find({"qty": { $gt: 15}})

The query would return the following:

{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }

Less than ($lt)

This operator matches if values are lesser than the provided value:

{ field: { $lt: value } }

Let’s find the documents with a quantity of less than 25:

db.inventory.find({"qty": { $lt: 25}})

The query would return the following:

{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }

Greater or equal to ($gte)

This operator matches when the values are greater than or equal to the given value:

{ field: { $gte: value } }

In this example, we retrieve the documents where the quantity is greater than or equal to 25:

db.inventory.find({"qty": { $gte: 25}})

This query would return the following:

{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }

Less or equal to ($lte)

This operator matches only if the values are less or equal to the given value:

{ field: { $lte: value } }

Let’s find the documents with a quantity under than or equal to 25.

db.inventory.find({"qty": { $lte: 25}})

We can expect this query to return the following:

{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }

In ($in)

This operator returns the documents that match the specified values:

{ field: { $in: [<value1>, <value2>, ... <valueN> ] } }

The value of a field equals any value in the specified array. To retrieve the documents with values “30” and “15” in the inventory collection, for instance, you’d do this:

db.inventory.find({ "qty": { $in: [30, 15]}})

The output would be:

{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }

Not in ($nin)

This operator returns the documents that don’t match the given values. Here’s the basic syntax of the $nin operator:

{ field: { $nin: [ <value1>, <value2> ... <valueN> ]

$nin selects the documents where:

  • the field value is not in the specified array; or
  • the field does not exist.

If the field holds arrays, it will pick arrays where no element specified in the value section is present. For example, we want to select those documents where the quantity does not equal either 20 or 15.

Additionally, it also matches documents that do not have a quantity field:

db.inventory.find({ "qty": { $nin: [ 20, 15 ]}})

The output would be:

{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 6, item: { name: "strawberry", code: "123" }, tags: [ "B" ] }

Not Equal ($ne)

The $ne operator returns the documents where the specified value isn’t equal:

{ $ne: value } }

For instance, say we want to select all documents where the quantity isn’t equal to 20:

db.inventory.find( { qty: { $ne: 20 } } )

The output would be:

{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 6, item: { name: "strawberry", code: "123" }, tags: [ "B" ] }

From the above output, we can see that the query will select documents that don’t have a quantity field.

Element Operators

The element query operators can identify documents using the fields of the document. Element operators consist of $exist and $type.

$exists

This operator matches documents that have a specified field. This operator has a boolean value that can be either true or false.

If specified to be true, it matches the documents that contain that field, including documents where the field value is null. If <boolean> is false, then the query returns only the documents that do not contain the field.

Here’s the standard syntax:

{ field: { $exists: <boolean> } } )

Let’s take an example where we’ve a collection data for an array named “bagofmarbles”, where each bag contains marbles of different colors:

{ red: 5, green: 5, blue: null }
{ red: 3, green: null, blue: 8 }
{ red: null, green: 3, blue: 9 }
{ red: 1, green: 2, blue: 3 }
{ red: 2, blue: 5 }
{ red: 3, green: 2 }
{ red: 4 }
{ green: 2, blue: 4 }
{ green: 2 }
{ blue: 6 }

Let’s say we want a query that would return only those bags where the red marbles exist. This means we’d have to input the boolean value as true. Let’s take a look:

db.bagofmarbles.find( { red: { $exists: true } } )

The results would consist of those documents that contain the field “red”, even if the value was null. However, it wouldn’t consist of the documents where the field “red” didn’t even exist:

{ red: 5, green: 5, blue: null }
{ red: 3, green: null, blue: 8 }
{ red: null, green: 3, blue: 9 }
{ red: 1, green: 2, blue: 3 }
{ red: 2, blue: 5 }
{ red: 3, green: 2 }
{ red: 4 }

If we only wanted those bags where red marbles don’t even exist as a field, we can input the below query:

db.bagofmarbles.find( { red: { $exists: false} )

The results would consist of those documents that do not contain the field “red”:

{ green: 2, blue: 4 }
{ green: 2 }
{ blue: 6 }

$type

This operator matches documents according to the specified field type. This is useful when you’ve got highly unstructured data, or when the data types aren’t predictable. These field types are specified BSON types and can be defined either by type number or alias.

This is the general syntax for $type:

{ field: { $type: <BSON type> } }

Let’s say we have an address book containing the documents below:

db={
  addressBook: [
    {
      "_id": 1,
      address: "2100 Jupiter Spot",
      zipCode: "9036325"
    },
    {
      "_id": 2,
      address: "25 Moon Place",
      zipCode: 26237
    },
    {
      "_id": 3,
      address: "2324 Neptune Ring",
      zipCode: NumberLong(77622222)
    },
    {
      "_id": 4,
      address: "33 Saturns Moon",
      zipCode: NumberInt(117)
    },
    {
      "_id": 5,
      address: "1044 Venus Lane",
      zipCode: [
        "99883637232",
        "73488976234"
      ]
    }
  ]
}

On observing the above documents, the zip code has different data types. This includes long, double, integer, and string values.

If we want only those documents that have a specified datatype as the zipcode — let’s take string for this instance — we’d have to input the following query into the compiler:

db.addressBook.find({
  "zipCode": {
    $type: "string"
  }
})

This would return the following documents:

[
  {
    "_id": 1,
    "address": "2100 Jupiter Spot",
    "zipCode": "9036325"
  },
  {
    "_id": 5,
    "address": "1044 Venus Lane",
    "zipCode": [
      "99883637232",
      "73488976234"
    ]
  }
]

Additionally, there is a “number” type, which includes all the long, integer, or double values as an array containing an element of the specified types:

db.addressBook.find( { "zipCode" : { $type : "number" } } )

Output:

[
{
      "_id": 2,
      address: "25 Moon Place",
      zipCode: 26237
    },
    {
      "_id": 3,
      address: "2324 Neptune Ring",
      zipCode: NumberLong(77622222)
    },
    {
      "_id": 4,
      address: "33 Saturns Moon",
      zipCode: NumberInt(117)
    }
]

If the documents have an array field type, the $type operator returns the documents in which at least one array element matches the type passed to the operator.

Array Operators

MongoDB also consists of array operators, to query documents containing arrays.

There are three primary operators: $all, $elemMatch and $size. We’ll discuss each one in detail below.

$all

The $all operator chooses the documents in which a field’s value is an array containing the specified elements:

{ : { $all: [ <value1> , <value2> ... ] } }

For example, let’s say we’ve got a collection of documents for a clothing store, with the following under inventory.

{
   _id: ObjectId("5234cc89687ea597eabee675"),
   code: "shirt",
   tags: [ "sale", "shirt", "button", "y2k", "casual" ],
   qty: [
          { size: "S", num: 10, color: "blue" },
          { size: "M", num: 45, color: "blue" },
          { size: "L", num: 100, color: "green" }
        ]
},

{
   _id: ObjectId("5234cc8a687ea597eabee676"),
   code: "pant",
   tags: [ "y2k", "trendy", "shine" ],
   qty: [
          { size: "6", num: 100, color: "green" },
          { size: "6", num: 50, color: "blue" },
          { size: "8", num: 100, color: "brown" }
        ]
},

{
   _id: ObjectId("5234ccb7687ea597eabee677"),
   code: "pant2",
   tags: [ "trendy", "shine" ],
   qty: [
          { size: "S", num: 10, color: "blue" },
          { size: "M", num: 100, color: "blue" },
          { size: "L", num: 100, color: "green" }
        ]
},

{
   _id: ObjectId("52350353b2eff1353b349de9"),
   code: "shirt2",
   tags: [ "y2k", "trendy" ],
   qty: [
          { size: "M", num: 100, color: "green" }
        ]
}

We’d want to retrieve any documents (in this case, the clothes) from the inventory that are linked with the tags “trendy” and “y2k”. The below query uses the $all operator where the value of the tags field is an array whose elements include “y2k” and “trendy”:

db.inventory.find( { tags: { $all: [ "y2k", "trendy" ] } } )

The above query returns the following:

{
   _id: ObjectId("5234cc8a687ea597eabee676"),
   code: "pant",
   tags: [ "y2k", "trendy", "shine" ],
   qty: [
          { size: "6", num: 100, color: "green" },
          { size: "6", num: 50, color: "blue" },
          { size: "8", num: 100, color: "brown" }
        ]
}

{
   _id: ObjectId("52350353b2eff1353b349de9"),
   code: "shirt2",
   tags: [ "y2k", "trendy" ],
   qty: [
          { size: "M", num: 100, color: "green" }
        ]
}

From the example above, we also find that the $all operator simply performs the same function as the $and operation.

Alternatively, we could use the below query which would give a similar output to the above:

db.inventory.find({
  $and: [
    {
      tags: "y2k"
    },
    {
      tags: "trendy"
    }
  ]
})

$elemMatch

The $elemMatch operator matches documents that contain an array field with at least one element that matches all the specified query criteria:

{ : { $elemMatch: { <query1>, <query2>, ... } } }

While we may use comparison operators like $lte and $gte, if we specify only a single query condition inside $elemMatch, and are not using the $not or the $ne operators, utilizing $elemMatch can be omitted as it would essentially be performing the same function.

There are a few more things to keep in mind while using this operator, mainly:

  • You cannot specify a $where expression in an $elemMatch operation.
  • You cannot specify a $text query expression in an $elemMatch operation.

For example, we’ve got the following documents in the student results collection:

{ _id: 1, results: [ 92, 89, 98 ] }
{ _id: 2, results: [ 85, 99, 99 ] }

The following query matches only those documents where the results array contains at least one element that is both greater than or equal to 90 and is less than 95:

db.studentresults.find(  { results: { $elemMatch: { $gte: 90, $lt: 95 } } })

Our query returns the following document, since the element 92 is both greater than or equal to 90 and is less than 95:

{ "_id" : 1, "results" :[ 92, 89, 98 ] }

$size

The $size operator returns those documents where the size of the array matches the number of elements specified in the argument:

{ field: { $size: value } }

Here’s an example:

db.collection.find( { field: { $size: 2 } });

This would return all the documents in the specified collection where the field is an array with 2 elements: { field: [ orange, apple] } and { field: [ blue, red] }, but not { field: blue} or { field: [ raspberry, lemon, grapefruit ] }.

However, while we can input the specific value as the size, we cannot specify ranges of values as the size.

Geospatial Operators

MongoDB allows you to store geospatial data in the form of GeoJSON types. GeoJSON is an open-standard format based on the JavaScript object notation that can represent geographical features and support non-spatial attributes. There are two types of geospatial operators that we’ll talk about in this article: geometry specifiers and query selectors.

$geometry

This operator mentions GeoJSON geometry for use with the following geospatial query operators: $geoIntersects, $geoWithin,$nearSphere, and $near. $geometry leverages EPSG:4326 as the default coordinate reference system (CRS).

To mention GeoJSON objects with the default CRS, you can leverage the following snippet for $geometry:

$geometry: {
   type: "<GeoJSON object type>",
   coordinates: [ <coordinates> ]
}

To mention a single-ringed GeoJSON polygon with a tailored MongoDB CRS, you can use the following snippet (you can only use this for $geoWithin and $geoIntersects):

$geometry: {
   type: "Polygon",
   coordinates: [ <coordinates> ],
   crs: {
      type: "name",
      properties: { name: "urn:x-mongodb:crs:strictwinding:EPSG:4326" }
   }
}

$polygon

The $polygon operator can be used to specify a polygon for a geospatial $geoWithin query on legacy coordinate pairs. This query will then return pairs that fall within the confines of the polygon. However, $polygon will not query for any GeoJSON objects. To define a polygon, you need to specify an array of coordinate points as follows:

{
   : {
      $geoWithin: {
         $polygon: [ [ <x1> , <y1> ], [ <x2> , <y2> ], [ <x3> , <y3> ], ... ]
      }
   }
}

Here, the last point is implicitly connected to the first. You can mention as many points or sides as you like.

For example, the following query will return all the documents that have coordinates that exist within the polygon defined by [0,0], [1,5], and [3,3]:

db.places.find(
  {
     loc: {
       $geoWithin: { $polygon: [ [ 0 , 0 ], [ 1 , 5 ], [ 3 , 3 ] ] }
     }
  }
)

$geoWithin

This operator can be used to choose documents with geospatial data that are completely contained in a specific shape. The specified shape can either be a GeoJSON multipolygon, a GeoJSON polygon (either multi-ringed or single-ringed), or a shape that can be defined by legacy coordinate pairs.

The $geoWithin operator will leverage the $geometry operator to mention the GeoJSON object.

To mention the GeoJSON multipolygons or polygons via the default Coordinate Reference System (CRS), you can utilize the syntax mentioned below:

{
   : {
      $geoWithin: {
         $geometry: {
            type: <"Polygon" or "MultiPolygon"> ,
            coordinates: [ <coordinates> ]
         }
      }
   }
}

For $geoWithin queries that mention the GeoJSON geometries with areas larger than a single hemisphere, the use of the default CRS would lead to queries for the complementary geometries.

To mention a single-ringed GeoJSON polygon with a custom MongoDB CRS, you can leverage the prototype mentioned below in the $geometry expression:

{
   : {
      $geoWithin: {
         $geometry: {
           type: "Polygon" ,
           coordinates: [ <coordinates> ],
           crs: {
              type: "name",
              properties: { name: "urn:x-mongodb:crs:strictwinding:EPSG:4326" }
           }
         }
      }
   }
}

The following example picks all the loc data that exists completely within a GeoJSON polygon, the area of the polygon being less than the area of a single hemisphere:

db.places.find(
   {
     loc: {
       $geoWithin: {
          $geometry: {
             type : "Polygon" ,
             coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]
          }
       }
     }
   }
)

$box

You can use $box to specify a rectangle for a geospatial $geoWithin query to provide documents that are within the confines of the rectangle, according to their point-based location data. When you use $geoWithin with the $box, you’ll get documents based on query coordinates. In this scenario, $geoWithin won’t query for any GeoJSON shapes.

To leverage the $box operator, you need to mention the top right and bottom left corners of the rectangle in an array object:

{ <location field> : { $geoWithin: { $box: [ [ <bottom left coordinates> ],
 [ <upper right coordinates> ] ] } } }

The aforementioned query will calculate the distance by utilizing planar (flat) geometry. The following query will return all the documents that are within the box having points at: [0,0], [0,30], [30,0], [30,30]:

db.places.find ( { 
 loc: { $geoWithin: { $box: [ [ 0,0 ], [ 30,30 ] ] } }
} )

$nearSphere

You can use $nearSphere to mention a point for which a geospatial query returns the documents from nearest to farthest.

MongoDB uses spherical geometry to calculate the distances for $nearSphere. It will need a geospatial index as follows:

  1. 2d index for location data described as legacy coordinate pairs. To leverage a 2d index on GeoJSON points, you need to generate the index on the coordinates field of the GeoJSON object.
  2. 2dsphere index for location data described as GeoJSON points.

To mention a GeoJSON point, you can leverage the following syntax:

{
  $nearSphere: {
     $geometry: {
        type : "Point",
        coordinates : [ <longitude>, <latitude> ]
     },
     $minDistance: <distance in meters>,
     $maxDistance: <distance in meters> 
  }
}

Here, $minDistance and $maxDistance are optional. $minDistance can limit the results to those documents that are at least the specified distance from the center. You can use $maxDistance for either index.

Now, consider a collection of “places” that consists of documents with a location field that has a 2dsphere index. The following example would return the points whose location is at least 2,000 meters and at most 6,000 meters from the point you choose, ordered from nearest to farthest:

db.places.find(
   {
     location: {
        $nearSphere: {
           $geometry: {
              type : "Point",
              coordinates : [ -43.9532, 50.32 ]
           },
           $minDistance: 2000,
           $maxDistance: 6000
        }
     }
   }
)

$geoIntersects

The $geoIntersects operator allows you to select documents whose geospatial data intersects with a particular GeoJSON object (i.e. where the convergence of the specified object and the data is non-empty). It leverages the $geometry operator to specify the GeoJSON object.

To mention GeoJSON multipolygons or polygons through the default coordinate reference system (CRS), you can use the following syntax:

{ <location field>: {
     $geoIntersects: {
        $geometry: {
           type: "<GeoJSON object type>" ,
           coordinates: [ <coordinates> ]
        }
     }
  }
}

The following instance will use $geoIntersects to pick all the loc data that intersects with the polygon described by the coordinates array:

db.places.find(
   {
     loc: {
       $geoIntersects: {
          $geometry: {
             type: "Polygon" ,
             coordinates: [
               [ [ 0, 0 ], [ 2, 6 ], [ 4, 1 ], [ 0, 0 ] ]
             ]
          }
       }
     }
   }
)

$center

The $center operator mentions a circle for a $geoWithin query that returns legacy coordinate pairs that are within the confines of the circle.

$center doesn’t return GeoJSON objects. To leverage the $center operator, you need to specify an array that contains:

  1. The circle’s radius, as measured in the units used by the coordinate system.
  2. The grid coordinates of the circle’s center point.
{
  <location field> : {
      $geoWithin: { $center: [ [ <x> , <y> ] , <radius> ] }
   }
}

The example mentioned below will return all the documents that have coordinates that can be found within the circle centered on [2,3] and with a radius of 40:

db.places.find(
   { loc: { $geoWithin: { $center: [ [2, 3], 40 ] } } }
)

Projection Operators

You can use projection operators to mention the fields returned by an operation. MongoDB projection operators allows the find() function to be used with data filtering arguments. This helps users extract only the required data fields from a document. So, it allows you to project transparent and concise data without affecting the overall database performance.

$elemMatch (projection)

The $elemMatch operator is responsible for limiting the content of an field from the query results to only contain the first element matching the $elemMatch condition.

Here are a few things you need to keep in mind before using $elemMatch:

  • From MongoDB 4.4, irrespective of the ordering of the fields in the document, the $elemMatch projection of an existing field returns the field following the inclusion of other existing fields.
  • Both the $elemMatch and $ operators depict the first matching element from an array based on a specified condition. The $ operator would project the first matching array element from every document in a collection based on some condition from the query statement, whereas the $elemMatch projection operator takes an explicit condition argument. This lets you project based on a condition not present in the query, or if you need to project based on various fields in the array’s embedded documents.

You should also be aware of the following restrictions before using the $elemMatch operator on your data:

  • You cannot mention a $text query expression within an $elemMatch operator.
  • db.collection.find() operations on views don’t support the $elemMatch projection operator.

The following example on the $elemMatch projection operator assumes a collection schools with the following documents:

{
 _id: 1,
 zipcode: "63108",
 students: [
              { name: "mark", school: 102, age: 9 },
              { name: "geoff", school: 101, age: 13 },
              { name: "frank", school: 104, age: 12 }
           ]
}
{
 _id: 2,
 zipcode: "63110",
 students: [
              { name: "harry", school: 103, age: 14 },
              { name: "george", school: 103, age: 7 },
           ]
}
{
 _id: 3,
 zipcode: "63108",
 students: [
              { name: "harry", school: 103, age: 14 },
              { name: "george", school: 103, age: 7 },
           ]
}
{
 _id: 4,
 zipcode: "63110",
 students: [
              { name: "jim", school: 103, age: 9 },
              { name: "michael", school: 103, age: 12 },
           ]
}

In this instance, the find() operation queries for all documents where the value of the zipcode field is 63110. The $elemMatch projection would return only the first matching element of the students array where the school field has a value of 103:

db.schools.find( { zipcode: "63110" },
                 { students: { $elemMatch: { school: 103 } } } )

This is what the result would look like:

{ "_id" : 2, "students" : [ { "name" : "harry", "school" : 103, "age" : 14 } ] }
{ "_id" : 4, "students" : [ { "name" : "jim", "school" : 103, "age" : 9 } ] }

$slice (projection)

The $slice projection operator can be used to specify the number of elements in an array to return in the query result:

db.collection.find(
   <query> ,
   { <arrayField> : { $slice: <number> } }
);

It can also be expressed this way:

db.collection.find(
  <query> ,
   { <arrayField> : { $slice: [ <number> , <number> ] } }
);

To demonstrate the same, you can create an example collection of tweets with the following documents:

db.posts.insertMany([
   {
     _id: 1,
     title: "Nuts are not blueberries.",
     comments: [ { comment: "0. true" }, { comment: "1. blueberries aren't nuts."} ]
   },
   {
     _id: 2,
     title: "Coffee please.",
     comments: [ { comment: "0. Indubitably" }, { comment: "1. Cuppa tea please" }, { comment: "2. frappucino" }, { comment: "3. Mocha latte" }, { comment: "4. whatever" } ]
   }
])

The following operation would use the $slice projection operator on the tweets array to return the array with its first two elements. If an array contains less than two elements, all elements in the array are returned:

db.posts.find( {}, { comments: { $slice: 2 } } )

That operation would return the following documents:

{
   "_id" : 1,
   "title" : "Nuts are not blueberries.",
   "comments" : [ { "comment" : "0. true" }, { "comment" : "1. blueberries aren't nuts." } ]
}
{
   "_id" : 2,
   "title" : "Coffee please.",
   "comments" : [ { "comment" : "0. Indubitably" }, { "comment" : "1. Cuppa tea please" } ]
}

$ (projection)

The positional $ operator limits the contents of an array to return the first element that matches the query condition of that array. You can use $ in the projection document of the find() method or the findOne() method when you only require one particular array element in chosen documents.

This is what the syntax for the $ operator looks like:

db.collection.find( { <array>: <condition> ... },
                    { "<array>.$": 1 } )
db.collection.find( { <array.field>: <condition> ...},
                    { "<array>.$": 1 } )

In this example, the students collection consists of the following documents:

{ "_id" : 1, "semester" : 2, "grades" : [ 75, 67, 93 ] }
{ "_id" : 2, "semester" : 2, "grades" : [ 60, 68, 72 ] }
{ "_id" : 3, "semester" : 2, "grades" : [ 95, 82, 67 ] }
{ "_id" : 4, "semester" : 3, "grades" : [ 89, 95, 70 ] }
{ "_id" : 5, "semester" : 3, "grades" : [ 68, 98, 82 ] }
{ "_id" : 6, "semester" : 3, "grades" : [ 65, 70, 76 ] }

In the following query, the projection { "grades.$": 1 } returns only the first element greater than or equal to 89 for the grades field:

db.students.find( { semester: 2, grades: { $gte: 89 } },
                  { "grades.$": 1 } )

This operation returns the following documents:

{"_id": 1, "grades": [93] }

Evaluation Operators

You can leverage MongoDB evaluation operators to gauge the overall data structure or individual field inside a document.

Let’s look at some common MongoDB evaluation operators.

$mod

You can use this operator to match documents where a specified field’s value is equal to the remainder after being divided by a specified value:

{ field: { $mod: [ divisor, remainder ] } }

Let’s say you have a table of cars belonging to different brands that you own in your showroom. The following query would give you all the car brands whose stock numbers are in multiples of 250.

db.cars.find ( { qty: { $mod: [ 250,0 ] } } )

$jsonSchema

The $jsonSchema lets you match the documents that match the specified JSON schema. MongoDB’s implementation of the JSON schema includes the addition of the bsonType keyword, which lets you use all BSON types within the $jsonSchema operator.

bsonType can accept the same string aliases you’d use for the type operator. This is what $jsonSchema‘s syntax would look like:

{ $jsonSchema: <JSON Schema object> }

Here, the JSON schema object is formatted based on the JSON schema standard’s draft 4:

{ <keyword1>: <value1>, ... }

Here’s an example to demonstrate how $jsonSchema works:

{ $jsonSchema: {
     required: [ "name", "major", "gpa", "address" ],
     properties: {
        name: {
           bsonType: "string",
           description: "must be a string and is required"
        },
        address: {
           bsonType: "object",
           required: [ "zipcode" ],
           properties: {
               "street": { bsonType: "string" },
               "zipcode": { bsonType: "string" }
           }
        }
     }
  }
}

You can also use $jsonSchema in a document validator to enforce the specified schema on update and insert operations:

db.createCollection(<collection> , { validator: { $jsonSchema: <schema> } } )
db.runCommand( { collMod: <collection>, validator:{ $jsonSchema: <schema> } } )

Bear in mind that there are several things not supported by the $jsonSchema operator:

  1. The integer type. You need to leverage the BSON type long or int with the bsonType keyword.
  2. Unknown keywords.
  3. Linking properties and the hypermedia of JSON schema, along with the use of JSON references and JSON pointers.

$text

The $text operator would look for a text within the content of the specified field, indexed with a text index:

{  
  $text:  
    {  
      $search: <string>,  
      $language: <string>,  
      $caseSensitive: <boolean>,  
      $diacriticSensitive: <boolean>   
    }  
}

In this instance, the following code snippet will sift through the table to filter out any cars that have the text “Porsche” in them:

db.cars.find( { $text: { $search: "Porsche" } } )

$regex

The $regex operator offers regular expression abilities to pattern match strings in queries. MongoDB leverages regular expressions that are compatible with Perl:

{<field> : /pattern/ <options>}

The following example would help filter out all cars that have the string “$78900” present in them:

db.cars.find( { price: { $regex: /$78900/ } } )

$expr

The $expr operator allows you to leverage aggregation expressions within the query language:

{ $expr: { <expression> } }

You can also use $expr to build query expressions that compare fields from the same document in a $match stage. If the $match stage happens to be the part of a $lookup stage, $expr can compare fields with the help of let variables.

$where

You can leverage the $where operator to either pass a string containing a full JavaScript function or a JavaScript expression to the query system. The $where operator provides greater flexibility but needs the database to process the JavaScript function or expression for every document in the collection. You can reference this document in the JavaScript function or expression by using either obj or this.

Here’s an example of the syntax:

{ $where: <string|JavaScript Code> }

There are a few key considerations to keep in mind before we dive into an example while using the $where operator:

  • You should only use the $where query operator to top-level documents. The $where query operator won’t function in a nested document, like in a $elemMatch query.
  • Generally, you should use $where only when you cannot express your query via another operator. If you have to use $where, make sure you include at least one other standard query operator to filter the result set. Using $where independently requires a collection scan for proper execution.

Here’s an example to illustrate this:

db.cars.find( { $where: function() {  
   return (hex_md5(this.name)== "9a43e617b50cd379dca1bc6e2a8")  
} } );

Bitwise Operators

Bitwise operators return data based on bit position conditions. Simply put, they are used to match numeric or binary values in which any bit from a set of bit positions has a value of 1 or 0.

$bitsAllSet

This operator will match all the documents where all of the bit positions provided by the query are set (i.e. 1) in the field:

{ <field> : { $bitsAllSet: <numeric bitmask> } }
{ <field> : { $bitsAllSet: < BinData bitmask> } }
{ <field> : { $bitsAllSet: [ <position1> , <position2> , ... ] } }

The field value should either be a BinData instance or numeric for $bitsAllSet to match the current document.

In the following instance, we’re leveraging a collection with the following documents:

db.collection.save({ _id: 1, a: 54, binaryValueofA: "00110110" })
db.collection.save({ _id: 2, a: 20, binaryValueofA: "00010100" })
db.collection.save({ _id: 3, a: 20.0, binaryValueofA: "00010100" })
db.collection.save({ _id: 4, a: BinData(0, "Zg=="), binaryValueofA: "01100110" })

The query mentioned below will use the $bitsAllSet operator to test whether field a has bits set at position 1 and position 5, where the least significant bit would be at position 0:

db.collection.find( { a: { $bitsAllSet: [ 1, 5 ] } })

This query would match the following documents:

{ "_id" : 1, "a" : 54, "binaryValueofA" : "00110110" }
{ "_id" : 4, "a" : BinData(0,"Zg=="), "binaryValueofA" : "01100110" }

$bitsAllClear

The $bitsAllClear operator will match documents in which all of the bit positions provided by the query are clear or 0:

{ <field> : { $bitsAllClear: <numeric bitmask> } }
{ <field> : { $bitsAllClear: < BinData bitmask> } }
{ <field> : { $bitsAllClear: [ <position1> , <position2> , ... ] } }

We’ll use the example used for $bitsAllSet here to demonstrate the usage of $bitsAllClear. The following query would use this operator to check whether field a has the bits clear at positions 1 and 5:

db.collection.find( { a: { $bitsAllClear: [ 1, 5 ] } } )

This query would match the following documents:

{ "_id" : 2, "a" : 20, "binaryValueofA" : "00010100" }
{ "_id" : 3, "a" : 20, "binaryValueofA" : "00010100" }

Meta Operators

There are various query modifiers that let you modify the behavior or output of a query in MongoDB. The driver interfaces might provide cursor methods that wrap them for your use.

$hint

MongoDB deprecated $hint since v3.2. But, this operator might still be available for MongoDB drivers like Go, Java, Scala, Ruby, Swift, etc. It can force the query optimizer to leverage a specific index to fulfill the query, which can then be mentioned either by document or by index name.

You can also use the $hint operator to test indexing strategies and query performance. For instance, take the following operation:

db.users.find().hint( { age: 1 } )

This operation would return all the documents within the collection called users by leveraging the index on the age field.

You can also mention a hint by using either of the following forms:

db.users.find()._addSpecial( "$hint", { age : 1 } )
db.users.find( { $query: {}, $hint: { age : 1 } } )

If an index filter exists for the query shape, MongoDB would simply ignore the $hint.

$comment

The $comment operator allows you to attach a comment to a query in any context that $query may appear. Since comments propagate to the profile log, adding a comment can make it easier to interpret and trace your profile.

You can leverage $comment in one of three ways:

db.collection.find( { <query> } )._addSpecial( "$comment", <comment> )
db.collection.find( { <query> } ).comment( <comment> )
db.collection.find( { $query: { <query> }, $comment: <comment> } )

If you want to attach comments to query expressions in other contexts, such as with db.collection.update(), leverage the $comment query operator instead of the meta-operator.

$max

You can mention a $max value to specify the exclusive upper bound for a particular index to constrain the results of find(). This operator will specify the upper bound for all keys of a specific order in the index.

Mongosh gives you the following max() wrapper method:

db.collection.find( { <query> } ).max( { field1: <max value> , ... fieldN: <max valueN> } )

You can also mention $max with the following two forms:

db.collection.find( { <query> } )._addSpecial( "$max", { field1: <max value1> ,
 ... fieldN: <max valueN> } )
db.collection.find( { $query: { <query> }, $max: { field1: <max value1> ,
 ... fieldN: <max valueN> } } )

For instance, if you want to specify the exclusive upper bound, keep in mind the following operations on a collection named collection that contains an index { age: 1 }:

db.collection.find( { <query> } ).max( { age: 100 } ).hint( { age: 1 } )

This operation will limit the query to those documents where the field age is less than 100 and forces a query plan that will scan the { age: 1 } index from minKey to 100.

$explain

This operator will give you information about the query plan. It returns a document that describes the indexes and processes used to return the query. This can be useful when trying to optimize a query.

You can mention $explain operator in either of the following forms:

db.collection.find()._addSpecial( "$explain", 1 )
db.collection.find( { $query: {}, $explain: 1 } )

Best Practices for MongoDB Operators

In this section, we’ll take a look at some of the best practices while using these MongoDB operators.

Embedding and Referencing

Embedding is a natural extension of data modeling. It allows you to avoid application joins, which can reduce updates and queries.

You can embed data with a 1:1 relationship within a single document. That said, data with a many:1 relationship in which “many” objects appear with their parent documents can also be good candidates.

Storing these types of data in the same document sounds like a prudent choice. However, embedding provides better performance for read operations with this kind of data locality.

Embedded data models can also help developers update associated data in a single write operation. This works because single document writes are transactional.

You should consider using referencing for the following scenarios:

  • When you update a document segment and it keeps getting longer, while the rest of the document is static.
  • When a document is accessed but contains data that is rarely used. Embedding would only increase in-memory requirements, so referencing makes more sense.
  • When the document size goes over MongoDB’s 16MB document limit. This can happen when modeling many:1 relationships (for instance, employees:department).

Examine Profiling and Query Patterns

For most developers, the first step in optimizing performance is to understand the actual and expected query patterns. Once you know your application’s query patterns well enough, you can make your data model and choose appropriate indices.

MongoDB developers have access to various powerful tools that let them improve performance. But that doesn’t mean query profiles and patterns can be ignored.

For instance, one easy way to boost performance is by analyzing your query patterns and understanding where you can embed data. Other ways to bolster MongoDB performance after identifying your major query patterns include:

  • Making sure that you have indices on any fields you query against.
  • Storing the results of frequent sub-queries on documents to reduce the read load.
  • Taking a look at your logs to look at slow queries, then checking your indexes.

Review Data Indexing and Modeling

While making your data model, you’ll be deciding how to model relationships between data. Choosing when to embed a document versus creating a reference across separate documents in different collections instead, for instance, is an example of application-specific consideration.

A major advantage of JSON documents is that they let developers model data based on the requirements of the application. Nesting subdocuments and arrays help you model complex relationships between data by leveraging simple text documents.

You can also use MongoDB to model the following:

  • Geospatial data
  • Tabular, flat, and columnar structures
  • Simple key-value pairs
  • Time-series data
  • Edges and nodes of connected graph data structures and the like

Monitor Sharding and Replication

Replication can be pivotal to improving performance since it increases data availability through horizontal scaling. Replication can lead to better performance and more security through redundancy.

Performance monitoring can be a hassle needing additional resources and time to ensure smooth functioning. You can leverage performance monitoring tools available in the market that cater to your specific needs.

For instance, Kinsta APM can grab timestamped information about your WordPress site’s MySQL database queries, PHP processes, external HTTP calls, and much more. You can also use this free tool to debug:

  • Long API calls
  • Long external URL requests
  • Slow database queries to name a few.

In MongoDB, replication can be achieved through replica sets that let developers copy data from a primary node or server across multiple secondaries. This lets your replication run some queries on secondaries as opposed to the primary, avoiding contention and leading to better load balancing.

Sharded clusters in MongoDB are another way to potentially improve performance. Similar to replication, sharding can be used to distribute large data sets across multiple servers.

By leveraging a shard key, developers can copy shards or pieces of data across multiple servers. These servers can work together to use all of the data.

Sharding has its fair share of advantages, including horizontal scaling for writes/reads, higher availability, and increased storage capacity.

Determine Memory Use

MongoDB performs best when an application’s working set (i.e. frequently accessed data and indices) fits in memory without issue. While other factors are pivotal for performance, RAM size is the most important for instance sizing.

When an application’s working set fits in RAM, read activity from the disk needs to be low. But if your working set exceeds the RAM of the instance server or size, read activity will start to shoot up.

If you see this happening, you might be able to solve the problem by moving over to a larger instance that has more memory.

Place Multi-Value Fields at the End

If you are indexing a couple of fields, and one of the fields you want to query uses one of those “multi-value” operators, then you should put them at the end of the index. You need to order the index so that the queried fields for exact values come first and the “multi-value” operators show up last in the index.

An exception to this would be sorting against the fields. Place these between the “multi-value” and exact fields to cut the amount of in-memory sorting needed.

Summary

For MongoDB, speed is the name of the game. To return queries quickly, MongoDB leverages operators to execute mathematical or logical tasks. Simply put, understanding MongoDB operators is the key to mastering MongoDB.

This article highlighted some of the key MongoDB operators you can use on your data such as comparison operators, logical operators, meta operators, and projection operators, to name a few. It also helps you understand how you can use MongoDB operators and the best practices that’ll let you get the best out of them.

Among all the operators, which one(s) do you use most often, and why? Share in the comments below — we’d love to hear your thoughts!

Salman Ravoof

Salman Ravoof is a self-taught web developer, writer, creator, and a huge admirer of Free and Open Source Software (FOSS). Besides tech, he's excited by science, philosophy, photography, arts, cats, and food. Learn more about him on his website, and connect with Salman on Twitter.