DocsHub
Mongoose

Queries

Learn the Mongoose query API — chaining methods like where, select, sort, limit, and skip, and how queries execute.

Queries

Mongoose queries are chainable — you can build up a query step by step, adding filters, projections, sorting, and limits, before finally executing it. This file covers the query API in depth.


Queries Are Thenable, Not Promises

When you call Student.find({ grade: "10th" }), Mongoose does not immediately run the query. It returns a Query object — a builder that only executes when you await it, call .then(), or call .exec().

// This does NOT run the query yet — it builds a query object
const query = Student.find({ grade: "10th" });

// This runs it
const results = await query;

// Or chain more methods before running it
const results = await Student.find({ grade: "10th" }).sort({ name: 1 }).limit(5);

This is why you can chain methods — each one returns the same query object with more configuration added, and the query only runs at the very end.


where()

where() lets you build filters in a more readable, chainable style:

// These are equivalent
const students = await Student.find({ grade: "10th", enrolled: true });

const students = await Student
  .find()
  .where("grade").equals("10th")
  .where("enrolled").equals(true);

Chaining comparison operators with where()

// Students aged between 15 and 17
const students = await Student
  .find()
  .where("age").gte(15).lte(17);

// Students older than 16
const students = await Student
  .find()
  .where("age").gt(16);

// Students in a list of grades
const students = await Student
  .find()
  .where("grade").in(["9th", "10th"]);
MethodEquivalent operator
.equals(value)$eq
.gt(value)$gt
.gte(value)$gte
.lt(value)$lt
.lte(value)$lte
.ne(value)$ne
.in(array)$in
.nin(array)$nin

where() is mostly a stylistic choice — it does not do anything you cannot do with a plain filter object. Many developers prefer the plain object syntax { grade: "10th", enrolled: true } because it is shorter and matches MongoDB's native query language. Use whichever is more readable for your team.


select()

select() controls which fields are returned — Mongoose's version of projection.

Include specific fields

// Only return name and grade (plus _id by default)
const students = await Student.find({ grade: "10th" }).select("name grade");

Exclude specific fields

Prefix a field with - to exclude it:

// Return everything except address and guardian
const students = await Student.find().select("-address -guardian");

Exclude _id

const students = await Student.find().select("name grade -_id");

Object syntax

You can also use the object syntax — same as find() projection in the raw driver:

const students = await Student.find(
  { grade: "10th" },
  { name: 1, grade: 1, _id: 0 }
);

You cannot mix include and exclude in select() — except for _id. select("name -age") throws an error. Either include fields or exclude fields, not both.


sort()

Sorts results — 1 for ascending, -1 for descending. Same as the raw driver.

// Sort by age ascending
const students = await Student.find().sort({ age: 1 });

// Sort by age descending
const students = await Student.find().sort({ age: -1 });

// Sort by multiple fields
const students = await Student.find().sort({ grade: 1, name: 1 });

String shorthand

Mongoose also accepts a string — prefix with - for descending:

// Sort by age descending
const students = await Student.find().sort("-age");

// Sort by grade ascending, then name ascending
const students = await Student.find().sort("grade name");

// Sort by grade ascending, name descending
const students = await Student.find().sort("grade -name");

limit() and skip()

Same as the raw driver — control how many documents are returned and how many to skip:

// First 10 students
const students = await Student.find().limit(10);

// Skip first 10, return next 10 — page 2
const students = await Student.find().skip(10).limit(10);

Pagination example

async function getStudentsPage(page, pageSize = 10) {
  const skip = (page - 1) * pageSize;

  const students = await Student
    .find({ enrolled: true })
    .sort({ name: 1 })
    .skip(skip)
    .limit(pageSize);

  return students;
}

const page1 = await getStudentsPage(1); // students 1-10
const page2 = await getStudentsPage(2); // students 11-20

Chaining Everything Together

// 10th grade enrolled students
// aged 15-17
// sorted by name
// page 2 (10 per page)
// only name, age, and grade fields
const students = await Student
  .find({ grade: "10th", enrolled: true })
  .where("age").gte(15).lte(17)
  .select("name age grade")
  .sort("name")
  .skip(10)
  .limit(10);

The order you chain methods does not matter — Mongoose builds up the full query object and executes it only when awaited. The actual execution order on the database side is always: filter → sort → skip → limit → project.


exec() vs await

You can run a query in two ways:

// Using await directly
const students = await Student.find({ grade: "10th" });

// Using exec() — returns a real Promise
const students = await Student.find({ grade: "10th" }).exec();

Both work the same way. exec() converts the query object into a true Promise — useful in some edge cases like passing the query to Promise.all():

// Run multiple queries in parallel
const [students, teachers, courses] = await Promise.all([
  Student.find({ enrolled: true }).exec(),
  Teacher.find({ active: true }).exec(),
  Course.find({ grade: "10th" }).exec()
]);

In modern code with async/await, you rarely need .exec() explicitly — await works directly on query objects. Some developers add .exec() out of habit from older codebases, or to get better stack traces in error messages. Either style is fine.


findOne() vs find().limit(1)

Both return one document, but they behave differently:

// Returns a single document or null
const student = await Student.findOne({ grade: "10th" });

// Returns an array with one document, or an empty array
const students = await Student.find({ grade: "10th" }).limit(1);
const student = students[0]; // need to access the first element

Always use findOne() when you expect a single result — it is cleaner and slightly more efficient.


Counting with Queries

// Count all matching documents
const count = await Student.find({ grade: "10th" }).countDocuments();

// Or use the model method directly — same result, cleaner
const count = await Student.countDocuments({ grade: "10th" });

distinct()

Returns the unique values for a field across all matching documents:

// All distinct grades that exist
const grades = await Student.distinct("grade");
console.log(grades); // ["9th", "10th", "11th", "12th"]

// Distinct cities for enrolled students
const cities = await Student.distinct("address.city", { enrolled: true });
console.log(cities); // ["Lahore", "Karachi", "Islamabad"]

Querying Nested and Array Fields

Same dot notation as the raw driver:

// Query nested field
const lahoreStudents = await Student.find({ "address.city": "Lahore" });

// Query array contains value
const mathStudents = await Student.find({ subjects: "Math" });

// Query array of objects with elemMatch
const highScorers = await Student.find({
  grades: {
    $elemMatch: { subject: "Math", score: { $gt: 90 } }
  }
});

All the query operators you learned in the Querying section — $gt, $in, $elemMatch, $regex, and so on — work exactly the same way with Mongoose.


School System Query Examples

// All enrolled students, sorted by name, name and grade only
const students = await Student
  .find({ enrolled: true })
  .select("name grade -_id")
  .sort("name");

// 10th grade students aged 15-17, paginated
const students = await Student
  .find({ grade: "10th" })
  .where("age").gte(15).lte(17)
  .sort("name")
  .skip(0)
  .limit(10);

// Find a single student by email
const student = await Student.findOne({ email: "ali@school.com" });

// All distinct grades in the system
const grades = await Student.distinct("grade");

// Students who take both Math and Physics
const students = await Student.find({
  subjects: { $all: ["Math", "Physics"] }
});

// Top 5 students by exam score
const topStudents = await Student
  .find({ enrolled: true })
  .sort("-examScore")
  .limit(5)
  .select("name grade examScore");

// Count enrolled students per check
const enrolledCount = await Student.countDocuments({ enrolled: true });

// Run multiple independent queries in parallel
const [students, teachers, courses] = await Promise.all([
  Student.countDocuments({ enrolled: true }),
  Teacher.countDocuments({ active: true }),
  Course.countDocuments()
]);

Quick Reference

MethodWhat it doesExample
.find(filter)Find matching documentsStudent.find({ grade: "10th" })
.findOne(filter)Find first matching documentStudent.findOne({ name: "Ali" })
.where(field)Build a filter chain.where("age").gte(15)
.select(fields)Choose returned fields.select("name grade -_id")
.sort(fields)Sort results.sort("-age") or .sort({ age: -1 })
.limit(n)Limit results.limit(10)
.skip(n)Skip results.skip(10)
.countDocuments(filter)Count matchesStudent.countDocuments({ enrolled: true })
.distinct(field)Unique valuesStudent.distinct("grade")
.exec()Convert to real Promise.find().exec()

Mongoose queries are lazy — nothing happens until you await them. This means you can build a query conditionally, adding filters and sorts step by step based on user input, and only execute it once at the end. This pattern is extremely useful for building search and filter features.

On this page