Reading data with the Query API
|
This section documents the current MongoDB 8.x server line as published at the MongoDB Server Manual, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Atlas Search, Atlas Vector Search, and parts of encryption and backup) are Atlas-only — they are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official manual before being relied on in production, since MongoDB iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
The Query API reads documents from a collection by matching them against a filter document and shaping the
result with a projection. This page covers find / findOne, the operators that build filters, how
matching works against embedded documents and arrays, and the cursor that carries results back. The canonical
walkthrough is Query Documents in the manual.
find and findOne
db.collection.find(filter, projection) returns a cursor over every matching document;
db.collection.findOne(filter, projection) returns a single document (the first match) or null. An empty or
omitted filter matches every document.
// every document in the collection
db.products.find()
// first match only, as a plain document
db.products.findOne({ sku: "A-1001" })
See db.collection.find() for the
full method signature and options.
Compared with SQL (Queries (SELECT)): the filter document plays the role
of the WHERE clause, the projection plays the role of the SELECT column list, and cursor modifiers
(sort, limit, skip) correspond to ORDER BY, LIMIT, and OFFSET.
The query filter document
A filter is a document. A field paired with a plain value is an implicit equality test; several fields in
the same filter are an implicit $and — every condition must hold.
// status == "active" AND qty == 50
db.products.find({ status: "active", qty: 50 })
// the same, written explicitly
db.products.find({ $and: [ { status: "active" }, { qty: 50 } ] })
An equality test against an array field matches when the array contains that value; an equality test against a document value requires an exact match (see Embedded documents and Arrays).
Projection
The second argument to find selects which fields come back. Values of 1 (or true) include fields;
values of 0 (or false) exclude them. Inclusion and exclusion cannot be mixed in one projection, except
that _id — included by default — may always be switched off.
// include only name and price (plus _id)
db.products.find({}, { name: 1, price: 1 })
// include name and price, drop _id
db.products.find({}, { name: 1, price: 1, _id: 0 })
// exclude internal fields, keep everything else
db.products.find({}, { internalNotes: 0, _vector: 0 })
Projection also accepts array-shaping operators:
// $slice: first 3 elements of the reviews array
db.products.find({}, { reviews: { $slice: 3 } })
// $slice with [skip, limit]: elements 5..14
db.products.find({}, { reviews: { $slice: [5, 10] } })
// $elemMatch: return only the first tags-subdocument matching the condition
db.products.find({}, { variants: { $elemMatch: { color: "red" } } })
// $ positional: return only the first array element matched by the filter
db.products.find({ "grades.score": { $gt: 90 } }, { "grades.$": 1 })
// $meta: expose the text-search relevance score (needs a $text filter)
db.articles.find({ $text: { $search: "mongodb" } }, { score: { $meta: "textScore" } })
Details and restrictions are in Project Fields to Return. Choosing a projection that an index fully satisfies produces a covered query; see Indexes.
Query operators
Operators are documents of the form { field: { $operator: value } }. The full list is the
Query and Projection Operators reference.
Comparison
$eq, $ne, $gt, $gte, $lt, $lte compare a field against a single value; $in and $nin compare
against a list.
db.products.find({
price: { $gte: 10, $lte: 100 }, // 10 <= price <= 100
status: { $ne: "discontinued" },
brand: { $in: ["acme", "globex"] }
})
Logical
$and, $or, $nor take an array of clause documents; $not negates a single operator expression.
db.products.find({
$or: [ { onSale: true }, { price: { $lt: 20 } } ],
qty: { $not: { $lt: 1 } } // qty is not < 1
})
Element
$exists tests for the presence of a field; $type tests its BSON type (by string alias or number).
// discountCode present, and expiresAt stored as a real Date
db.coupons.find({
discountCode: { $exists: true },
expiresAt: { $type: "date" }
})
Evaluation
$regex matches strings against a pattern, $mod matches by remainder, $expr embeds an aggregation
expression (so one field can be compared to another), $text runs a text-index search, and $jsonSchema
validates a document against a schema.
db.products.find({
name: { $regex: /^wid/i }, // starts with "wid", case-insensitive
qty: { $mod: [4, 0] }, // qty divisible by 4
$expr: { $gt: ["$spent", "$budget"] }, // spent > budget (field vs. field)
$jsonSchema: { required: ["sku"], properties: { sku: { bsonType: "string" } } }
})
$expr uses the same expression language as the
aggregation pipeline.
$where and its risk
$where runs a JavaScript predicate against every candidate document. It cannot use indexes, runs the
server-side JavaScript engine per document, and executes arbitrary code, so it is slow and a security
concern. Prefer $expr or a rewritten filter.
// avoid: full collection scan, JS evaluated per document
db.products.find({ $where: "this.price * this.qty > 1000" })
// prefer: $expr, index-friendlier and no JS engine
db.products.find({ $expr: { $gt: [ { $multiply: ["$price", "$qty"] }, 1000 ] } })
See $where for the security notes.
Embedded documents
A filter value that is itself a document requires an exact match: the same fields, the same values, in the same order. To match a single nested field regardless of the rest of the subdocument, use dot notation.
// exact match: address must equal this document exactly
db.people.find({ address: { city: "Oslo", zip: "0150" } })
// dot notation: only address.city matters
db.people.find({ "address.city": "Oslo" })
// dot notation with an operator
db.people.find({ "address.zip": { $in: ["0150", "0151"] } })
Full rules are in Query on Embedded/Nested Documents.
Arrays
Matching rules for array fields are covered in Query an Array.
// contains the element "red"
db.products.find({ tags: "red" })
// at least one element > 90 AND at least one element < 100 (possibly different elements)
db.products.find({ scores: { $gt: 90, $lt: 100 } })
// $elemMatch: ONE element satisfies BOTH conditions
db.products.find({ scores: { $elemMatch: { $gt: 90, $lt: 100 } } })
// $all: array contains every listed value (in any order)
db.products.find({ tags: { $all: ["red", "sale"] } })
// $size: array has exactly 3 elements
db.products.find({ tags: { $size: 3 } })
// positional path: the first element's "grade" field
db.students.find({ "results.0.grade": "A" })
// $elemMatch on an array of subdocuments
db.orders.find({ items: { $elemMatch: { sku: "A-1", qty: { $gte: 2 } } } })
Without $elemMatch, multiple conditions on an array field may be satisfied by different elements; with
$elemMatch, a single element must satisfy all of them.
Cursors
find returns a lazily evaluated cursor. In mongosh the cursor is iterated automatically for display;
in code it is walked with hasNext / next or forEach, fetching documents in batches from the server.
const cursor = db.products.find({ status: "active" });
while (cursor.hasNext()) {
printjson(cursor.next());
}
sort, skip, limit
// newest first, rows 21..40
db.products.find()
.sort({ createdAt: -1 }) // -1 descending, 1 ascending
.skip(20)
.limit(20)
sort corresponds to SQL ORDER BY, limit to LIMIT, and skip to OFFSET
(Queries (SELECT)). See
cursor.sort().
hint
hint forces a specific index instead of letting the query planner choose.
db.products.find({ status: "active", brand: "acme" }).hint({ status: 1, brand: 1 })
Index selection and forcing is covered in Indexes.
countDocuments and distinct
// accurate count of documents matching a filter
db.products.countDocuments({ status: "active" })
// unique values of a field among matching documents
db.products.distinct("brand", { status: "active" })
countDocuments is documented at
countDocuments();
prefer it over the legacy count().
collation
collation controls string comparison and sort order for the query — locale, case sensitivity, and
accent sensitivity.
// case-insensitive match and sort for Norwegian
db.people.find({ name: "aa" })
.collation({ locale: "nb", strength: 1 })
See Collation.
Avoiding large skips
skip(n) still walks and discards the first n documents, so deep pagination gets progressively slower.
Paginate with a range query on an indexed field instead, carrying the last seen key forward.
// page 1
db.products.find().sort({ _id: 1 }).limit(20)
// next page: start after the last _id seen, no skip
db.products.find({ _id: { $gt: lastId } }).sort({ _id: 1 }).limit(20)
See Pagination: Offset vs. Keyset for why offset-based paging degrades
regardless of store, and how this same range-query pattern compares to SQL’s seek method, Solr’s cursorMark,
Elasticsearch’s search_after, and Spring Data’s Window<T> scrolling.
No-timeout cursors
The server closes an idle cursor after about 10 minutes. A long-running scan can request
noCursorTimeout, but it must then be closed explicitly or it leaks server resources.
const cursor = db.events.find().noCursorTimeout();
try {
cursor.forEach(doc => process(doc));
} finally {
cursor.close();
}