Learning MongoDB - Advanced Query Filters & Operators
Episode 5 of 21

Learning MongoDB - Advanced Query Filters & Operators

Deepening query capabilities with comparison, logical, element, and array operators; mastering sorting, limit, and skip; and dissecting offset-based pagination versus cursor-based pagination for large-scale applications.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episodes 3 and 4 you already wrote simple queries: filters based on equality ({ name: "Sneaker" }) and updates with operators. But real-world applications are rarely that simple. Questions like "show products between 100 thousand and 500 thousand", "find users who are active OR have the mentor role", or "find orders where all items are paid" need far richer operators.

Episode 5 is your query arsenal. The roadmap: first comparison operators, second logical operators, third element operators, fourth array operators, and fifth sorting and pagination. By the end of the episode, you'll be able to answer almost all of an application's filtering needs with one expressive query language.

Comparison Operators

Comparison operators are the most basic and most frequently used. Notice the shape: the field on the left, the operator inside the object on the right:

OperatorMeaningExample
$eqEqual to{ price: { $eq: 150000 } }
$neNot equal to{ role: { $ne: "admin" } }
$gtGreater than{ price: { $gt: 150000 } }
$gteGreater than or equal{ stock: { $gte: 5 } }
$ltLess than{ age: { $lt: 30 } }
$lteLess than or equal{ age: { $lte: 30 } }
$inAny of the list{ category: { $in: ["elektronik", "fashion"] } }
$ninNone of the list{ category: { $nin: ["makanan"] } }
Combining comparison operators
db.products.find({
  category: { $in: ["elektronik", "fashion"] },
  price: { $gte: 100000, $lte: 1000000 }
})

Notice the second example: the price field accepts several operators at once in a single object — meaning all conditions must be satisfied. This is far more concise than writing two separate filters.

Logical Operators

Logical operators govern how conditions are combined:

OperatorMeaning
$andAll conditions must be true (implicit default when writing several fields)
$orAt least one condition must be true
$notInverts a condition's result
$norAll conditions must be false

The most useful example is $or — finding documents that match any one of the conditions:

Combining conditions with $or and $and
db.users.find({
  $or: [
    { role: "mentor" },
    { isActive: true }
  ],
  $and: [
    { age: { $gte: 18 } },
    { age: { $lte: 40 } }
  ]
})

Info

It's important to know: when you write several fields without an explicit logical operator, MongoDB combines them with $and implicitly. So { price: { $gt: 100 }, category: "elektronik" } means the same as writing { $and: [{ price: { $gt: 100 } }, { category: "elektronik" }] }. Use the explicit form when you need clarity or when there are conditions using the same field.

Element Operators

Because MongoDB's schema is flexible, not all documents in one collection have the same fields. Two element operators help handle this situation:

  • $exists — checks whether the field exists in the document (regardless of its value).
  • $type — checks the BSON type of a field.
Checking field existence and type
db.products.find({ specs: { $exists: true } })
 
db.products.find({ price: { $type: "decimal" } })

The first query finds all products that have a specs field. The second query finds products whose price is stored as the decimal type — useful for finding wrongly-typed data that crept in in the past.

Array Query Operators

Array operations are the area where MongoDB most distinguishes itself from RDBMS. When querying against an array field, MongoDB has a default behavior and three special operators:

  • Matching against an array: find({ tags: "mongodb" }) matches if the array contains that element.
  • $all — the array must contain all the elements mentioned.
  • $elemMatch — at least one array element satisfies several conditions at once.
  • $size — the array length equals the given value.
Array query operators examples
db.products.find({ tags: { $all: ["best-seller", "baru"] } })
 
db.orders.find({
  items: {
    $elemMatch: { product: "laptop", qty: { $gte: 2 } }
  }
})
 
db.orders.find({ items: { $size: 3 } })

The $elemMatch example is the most important: it looks for orders that have a single item named laptop and a qty of at least 2 in the same element of the array. Without $elemMatch, the condition { "items.product": "laptop", "items.qty": { $gte: 2 } } could match two different elements — behavior that is often confusing.

Sorting, Limiting, and Skipping

Sorting

Order the results with .sort(). A value of 1 means ascending (small to large), -1 means descending (large to small):

Ascending and descending sorting
db.products.find({}).sort({ price: 1 })
 
db.products.find({}).sort({ price: -1, createdAt: -1 })

Limiting and Skipping

.limit(n) limits the number of results; .skip(n) skips the first n documents:

Combining sort, limit, and skip
db.products.find({})
  .sort({ price: -1 })
  .limit(10)
  .skip(20)

The query above retrieves the third page (assuming 10 items per page) of products from most expensive to cheapest. This is the basis of offset-based pagination.

Pagination: Offset vs Cursor

Offset-Based Pagination

The pattern above (skip + limit) is offset-based pagination — the simplest, suitable for small pages. But it has a fundamental weakness: .skip() still scans the documents it skips. As the dataset grows (e.g. skipping 100,000), the query slows down dramatically. Not to mention that if new data arrives mid-navigation, results can "jump" and pages duplicate.

Cursor-Based Pagination

For large or constantly changing datasets, use cursor-based pagination: save the last unique value from the previous page, then filter based on that value. Because it uses an index, performance stays fast no matter how deep the page:

Cursor-based pagination with the last value
db.products.find({
  $or: [
    { price: { $lt: 150000 } },
    { price: 150000, _id: { $gt: ObjectId("66xxxxxxxxxxxxxxxxxxxxxxxx") } }
  ]
}).sort({ price: -1, _id: -1 }).limit(10)

The $or pattern above retrieves 10 products priced below 150 thousand, or exactly 150 thousand with an _id greater than the last product seen — the cleanly ordered (price, _id) composition guarantees no data is missed or duplicated.

Warning

Deep pagination with skip on a collection of millions of documents is one of the most common causes of slow queries in production. If you see a large skip (thousands and up) being called often, consider switching to cursor-based pagination — or combine it with a temporal filter (e.g. "get this month's orders") to narrow the scope.

Conclusion

In episode 5 you significantly expanded your querying capabilities: comparison operators $gt, $gte, $lt, $lte, $in, $nin, and $ne; logical operators $and, $or, $not, and $nor; element operators $exists and $type; array operators $all, $elemMatch, and $size; as well as sorting, limit, skip, and the two pagination approaches — offset-based and cursor-based.

Key takeaways:

  • Multiple-field filters are combined with implicit $and; use $or for alternatives.
  • $elemMatch matches conditions on the same array element — avoid the ordinary array query trap.
  • $exists and $type are very useful in flexible schemas.
  • .sort().limit().skip() is the basis of simple pagination.
  • For large datasets, cursor-based pagination is far faster and more consistent.

In the next episode, episode 6, we level up from "being able to query" to "designing data properly": Schema Design Patterns & Best Practices. You'll understand that a flexible schema doesn't mean no design, learn the principle of data locality, and recognize the anti-patterns to avoid such as massive arrays and excessive nesting. See you in episode 6!

Learning MongoDB - Advanced Query Filters & Operators | Learning MongoDB