Document Query Basics: The `find` Method and Projections
Queries are the core operations for retrieving data from MongoDB—mastering the find method is the first step in interacting with the database.
This course provides an in-depth exploration of the find and findOne query syntax, field projection, pagination and sorting, and the formatting and processing of query results.
1. What You'll Learn
- The Key Difference Between the
findandfindOneMethods - Basic Syntax of Query Filters
- Projection: Select the return field
- pretty() formatted output
- limit / skip / sort: Pagination and Sorting
- countDocuments: Counts the number of documents
- Handling Query Results in Node.js / Mongoose
2. A True Story of a Full-Stack Engineer
(1) Pain Point: Queries that return all fields result in high network overhead
Charlie is a full-stack e-commerce engineer who is optimizing the performance of the product list API:
"My product list API returns 100 products, each 5 KB in size, but the front end only displays three fields: title, price, and image. It returns 50 KB of unnecessary data, and the API response takes 800 ms, wasting bandwidth and parsing time."
Original query code:
// ❌ Counterexample:Return all fields
app.get('/api/products', async (req, res) => {
const products = await Product.find(); // Return all fields
res.json(products);
});
// Each product 5KB,100 items = 500KB
// Slow Internet Connection + Slow front-end parsing
(2) Solution for Projection
// ✅ Correct Example:Return only the necessary fields
app.get('/api/products', async (req, res) => {
const products = await Product.find(
{ isActive: true },
{
projection: {
sku: 1,
title: 1,
price: 1,
thumbnail: 1,
_id: 0 // Exclusion _id
}
}
)
.sort({ createdAt: -1 })
.limit(20)
.lean(); // Skip mongoose hydrate,Performance ↑3-5x
res.json(products);
});
// Each product 200 Byte,20 items = 4KB(Performance ↑100x)
(3) Revenue
| Dimension | Unprojected | Projected |
|---|---|---|
| Response Size | 500 KB | 4 KB |
| API Latency | 800 ms | 50 ms |
| Front-end parsing time | 200 ms | 5 ms |
| Network Bandwidth | High | Low 100x |
3. find and findOne
Concept Explanation: find and findOne are the two main query methods in MongoDB. find returns a cursor of matching documents and is suitable for list queries; findOne returns a single document and is suitable for detail queries. While their syntax is similar, their return types differ; understanding this difference is crucial for correctly handling query results.
How It Works: find does not return all data immediately; instead, it creates a Cursor object. The Cursor uses a lazy-loading strategy—data is fetched from the server in batches (101 records or 1 MB per batch by default) only when you iterate through it or call toArray(). This design ensures that find will not exhaust memory, even with datasets in the millions. findOne is equivalent to find().limit(1), but returns documents directly rather than a Cursor.
sequenceDiagram
participant App as Applications
participant Mongo as MongoDB Server-side
App->>Mongo: find({ category: "Electronics" })
Mongo-->>App: Cursor Object(No data retrieved)
App->>Mongo: cursor.next() / toArray()
Mongo-->>App: The first batch 101 Documents
App->>Mongo: Continue iterating
Mongo-->>App: Subsequent batches(Maximum per batch 16MB)
| Dimension | find | findOne |
|---|---|---|
| Return Type | Cursor | Document or null |
| Number of matches | All matches | First match |
| Memory Usage | Streaming (Lazy Loading) | One-time |
| Performance | Fast | Slightly faster (does not create a cursor) |
| Use Cases | List Query | Detail Query |
(1) Using find to query multiple documents
// === find Basic Syntax ===
db.products.find();
// Back to All Documents(cursor)
// === find Specified Conditions ===
db.products.find({ category: "Electronics" });
// Back to All Electronics
// === find Return an array ===
db.products.find({ category: "Electronics" }).toArray();
// Back Array<Document>
// === find Iterate(Cursor)===
db.products.find({ category: "Electronics" }).forEach(printjson);
(2) findOne: Query a single document
Concept Explanation: findOne is a convenient way to query a single document; internally, it is equivalent to find().limit(1), but it returns a document object directly rather than a Cursor. Returning null indicates that no matching document was found—this is an important distinction from find, as find returns an empty Cursor rather than null.
Use Cases: Query details using _id, retrieve a single document using a unique index, and perform an existence check (to determine whether a document meets a specific condition).
// === findOne Return to a single document ===
db.products.findOne({ sku: "PHONE-001" });
// Return the first matching document(or null)
// === findOne vs find().limit(1) Difference ===
const doc1 = db.products.findOne({ sku: "PHONE-001" });
const doc2 = db.products.find({ sku: "PHONE-001" }).limit(1).next();
// The results are the same,findOne More concise
(3) Comparison of find and findOne
Key Points Analysis:
findThe returned Cursor does not load all data immediately, saving memoryfindOneis essentiallyfind().limit(-1); it returns the document directly, eliminating the need to construct a Cursor.- In Mongoose,
findreturns an arrayArray<T>, andfindOnereturns an objectT | null - To determine whether a document exists,
findOne+ checking for null is more efficient thanfind+ checking the array length.
| Dimension | find | findOne |
|---|---|---|
| Return Type | Cursor | Document or null |
| Number of matches | All matches | First match |
| Memory Usage | Streaming (Lazy Loading) | One-time |
| Performance | Fast | Slightly faster (does not create a cursor) |
| Use Cases | List Query | Detail Query |
(4) Query Results in Mongoose
// === mongoose: find returns an array ===
const products = await Product.find({ category: "Electronics" });
// Array<Product>
// === mongoose: findOne returns an object ===
const product = await Product.findOne({ sku: "PHONE-001" });
// Product | null
// === Handling Cases Where Query Results Are Empty ===
const product = await Product.findOne({ sku: "NOT_EXIST" });
if (!product) {
return res.status(404).json({ error: "Product not found" });
}
▶ Example 1: Complete usage of find
// === Search in mongosh ===
// Search All Documents
db.products.find();
// Query by Specified Criteria
db.products.find({ category: "Electronics" });
// Multi-Condition Query(AND)
db.products.find({
category: "Electronics",
stock: { $gt: 0 } // Inventory greater than 0
});
// Look Up and Format
db.products.find({ category: "Electronics" }).pretty();
// Query and Count
db.products.find({ category: "Electronics" }).count();
// === Search in Node.js ===
const { MongoClient } = require('mongodb');
async function findProducts() {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const collection = client.db('shopdb').collection('products');
// 1. find() Back Cursor
const cursor = collection.find({ category: "Electronics" });
const products = await cursor.toArray();
console.log(`Found ${products.length} products`);
// 2. findOne() Back Document
const product = await collection.findOne({ sku: "PHONE-001" });
console.log(product);
// 3. Iterate Cursor(Flow)
for await (const doc of collection.find({ category: "Electronics" })) {
console.log(doc.title);
}
await client.close();
}
findProducts();
4. Query Filters
Concept Explanation: The query filter is the first parameter of find / findOne and is used to specify matching conditions. Filters use JSON/BSON syntax and support various patterns, including exact matches, comparison operators, logical combinations, and nested queries. Understanding filter syntax is the foundation of MongoDB querying.
How It Works: MongoDB translates query filters into a query plan and matches documents using indexes or full-table scans. Each field condition in a filter can use an index independently; when multiple conditions are combined, the MongoDB optimizer automatically selects the optimal execution path.
graph TB
A[Query Filters] --> B[Exact Match<br/>{ field: value }]
A --> C[Comparison Operators<br/>{ field: { $gt: N } }]
A --> D[Logic Combinations<br/>{ $and / $or / $not }]
A --> E[Nested Queries<br/>{ "path.field": value }]
A --> F[Array Lookup<br/>{ array: value }]
style A fill:#cce5ff
| Filter Type | Syntax | Example |
|---|---|---|
| Exact match | { field: value } |
{ sku: "PHONE-001" } |
| Multi-condition AND | { f1: v1, f2: v2 } |
{ category: "E", stock: 50 } |
| Field does not exist | { field: { $exists: false } } |
{ discount: { $exists: false } } |
| Nested document | { "path.field": value } |
{ "specs.battery": "4500mAh" } |
| Array element | { array: value } |
{ tags: "5g" } |
(1) Basic Filtering
// === Exact Match ===
db.products.find({ sku: "PHONE-001" });
// === Multiple conditions AND ===
db.products.find({
category: "Electronics",
stock: 50,
isActive: true
});
// === Field does not exist ===
db.products.find({ discount: { $exists: false } });
// === Nested Document Query ===
db.products.find({ "specs.battery": "4500mAh" });
// === Array Element Matching ===
db.products.find({ tags: "5g" });
// === Matching Multiple Elements in an Array ===
db.products.find({ tags: { $all: ["5g", "amoled"] } });
(2) Comparison Operators
Concept Overview: Comparison operators are at the core of query filters, supporting operations such as range queries, multi-value matching, and exclusions. MongoDB provides eight comparison operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin. Among these, $eq is the default behavior ({ price: 599 } is equivalent to { price: { $eq: 599 } }), and $in is the most commonly used operator.
| Operator | Meaning | Equivalent SQL | Index-Friendly |
|---|---|---|---|
$eq |
equals | WHERE field = value |
✅ |
$ne |
Not equal to | WHERE field != value |
⚠️ |
$gt/$gte |
Greater than / Greater than or equal to | WHERE field > />= value |
✅ |
$lt/$lte |
Less than / Less than or equal to | WHERE field < /<= value |
✅ |
$in |
Included in | WHERE field IN (...) |
✅ |
$nin |
Not included | WHERE field NOT IN (...) |
⚠️ |
// === $eq(equals,Default)===
db.products.find({ price: { $eq: 599.99 } });
// equivalent to { price: 599.99 }
// === $ne(is not equal to)===
db.products.find({ category: { $ne: "Books" } });
// === $gt / $gte(greater than / Greater than or equal to)===
db.products.find({ price: { $gt: 100 } }); // > 100
db.products.find({ price: { $gte: 100 } }); // >= 100
// === $lt / $lte(Less than / Less than or equal to)===
db.products.find({ price: { $lt: 1000 } });
db.products.find({ price: { $lte: 1000 } });
// === $in / $nin(Includes / Excludes)===
db.products.find({ category: { $in: ["Electronics", "Books"] } });
db.products.find({ category: { $nin: ["Clothing"] } });
// === Range Query ===
db.products.find({
price: { $gte: 100, $lte: 1000 } // 100 <= price <= 1000
});
(3) Logical Operators
Concept Explanation: Logical operators combine multiple query conditions to implement complex filtering logic. MongoDB supports four logical operators: $and (all conditions must be met), $or (any condition must be met), $not (none of the conditions must be met), and $nor (none of the conditions must be met). Among these, the implicit AND (separating multiple fields with commas) is the most commonly used syntax; the explicit $and is required only when "multiple conditions apply to the same field."
| Operator | Meaning | Equivalent SQL | Frequency of Use |
|---|---|---|---|
| Implicit AND | Comma-separated | WHERE a=1 AND b=2 |
⭐⭐⭐ Most commonly used |
$and |
Explicit AND | WHERE (a=1 AND b=2) |
⭐ Multiple conditions for the same field |
$or |
Any that meets | WHERE a=1 OR b=2 |
⭐⭐ |
$not |
Not satisfied | WHERE NOT (condition) |
⭐ |
$nor |
None met | WHERE NOT (a=1 OR b=2) |
Underused |
// === $and(Implicit AND)===
db.products.find({
category: "Electronics",
stock: { $gt: 0 } // Implicit AND
});
// === $and(Explicit AND)===
db.products.find({
$and: [
{ category: "Electronics" },
{ $or: [{ stock: { $gt: 10 } }, { isFeatured: true }] }
]
});
// === $or ===
db.products.find({
$or: [
{ category: "Electronics" },
{ tags: "bestseller" }
]
});
// === $not ===
db.products.find({ price: { $not: { $gt: 1000 } } });
// Price <= 1000
// === $nor(None of them match)===
db.products.find({
$nor: [
{ category: "Electronics" },
{ category: "Books" }
]
});
▶ Example 2: Example of a Composite Query
// === Scene:Check Prices 100-1000、Inventory greater than 0、Electronics or Books Categorized Products ===
db.products.find({
price: { $gte: 100, $lte: 1000 },
stock: { $gt: 0 },
$or: [
{ category: "Electronics" },
{ category: "Books" }
],
isActive: true
}).sort({ price: 1 }).limit(20);
// === mongoose Equivalent Notation ===
const products = await Product.find({
price: { $gte: 100, $lte: 1000 },
stock: { $gt: 0 },
$or: [{ category: "Electronics" }, { category: "Books" }],
isActive: true
})
.sort({ price: 1 })
.limit(20)
.lean();
5. Projection
Concept Explanation: Projection controls which fields are returned by a query and is a key optimization technique for reducing network traffic and the load on front-end parsing. By default, MongoDB returns all fields in a document, but in scenarios such as list pages and API responses, typically only 3–5 key fields are needed. Proper use of projection can reduce response size by more than 90%.
How It Works: Projection is performed on the server side—after MongoDB reads the entire document, it trims the fields according to the projection rules before returning the result. This means that projection does not reduce disk I/O (the entire document must still be read), but it can significantly reduce network traffic and client deserialization time. The only exception is a Covered Query—when all fields in the query and the projection are included in an index, MongoDB returns the data directly from the index without needing to read the document.
graph LR
A[Complete Documentation<br/>20 field ~5KB] --> B{Projection Rules}
B -->|Whitelist Mode<br/>{ sku: 1, title: 1, price: 1 }| C[3 field ~200B]
B -->|Blacklist Mode<br/>{ description: 0, images: 0 }| D[18 field ~4.5KB]
style C fill:#d4edda
| Projection Mode | Syntax | Features | Use Cases |
|---|---|---|---|
| Whitelist | { field: 1 } |
Returns only specified fields | List page (requires few fields) |
| Blacklist | { field: 0 } |
Exclude specified fields | Details page (Exclude sensitive fields) |
| Mixed | ❌ Not allowed | Whitelists and blacklists cannot be used together (except for _id) |
— |
| _id Control | { _id: 0 } |
Returned by default; must be explicitly excluded | Remove _id from API response |
(1) What is a projection?
Projection control returns specific fields, reducing the burden on network transmission and front-end parsing.
graph LR
A[Complete Documentation<br/>20 field] --> B{Projection}
B -->|Field Whitelist| C[Return only 3 field<br/>~10 KB]
B -->|Field Blacklist| D[Exclusion 2 field<br/>~18 KB]
style C fill:#d4edda
(2) Projection Syntax
// === Field Whitelist(Return only the specified fields)===
db.products.find(
{ category: "Electronics" },
{ sku: 1, title: 1, price: 1 }
);
// Back:{ _id, sku, title, price }
// === _id Return by Default,Must be explicitly excluded ===
db.products.find(
{},
{ sku: 1, title: 1, _id: 0 } // _id: 0 Exclusion _id
);
// === Field Blacklist(Exclude Specified Fields)===
db.products.find(
{},
{ internalNotes: 0, debugInfo: 0 } // Exclude Sensitive Fields
);
// === Nested Document Projection ===
db.products.find(
{ sku: "PHONE-001" },
{
sku: 1,
title: 1,
"specs.screen": 1, // Return only specs.screen
"specs.battery": 1 // Return only specs.battery
}
);
// === Array Element Projection($slice)===
db.reviews.find(
{ productId: "PHONE-001" },
{
title: 1,
content: 1,
comments: { $slice: 3 } // Return only the first 3 Comments
}
);
(3) Effects on Projection Performance
// === Performance Testing:100 10,000 Documents,Search 100 items ===
// ❌ No projection:Back 5MB
db.products.find({ category: "Electronics" }).limit(100);
// Time taken 800ms
// ✅ Projection available:Back 200KB
db.products.find(
{ category: "Electronics" },
{ sku: 1, title: 1, price: 1, _id: 0 }
).limit(100);
// Time taken 80ms(Performance ↑10x)
(4) Projection in Mongoose
// === Methods 1:projection option ===
const products = await Product.find({ category: "Electronics" }, "sku title price");
// String Syntax(Space-separated)
// === Methods 2:select() Chain-type ===
const products = await Product.find()
.select("sku title price")
.select("-description -images"); // Exclude certain fields
// === Methods 3:Object Syntax ===
const products = await Product.find(
{ category: "Electronics" },
{ sku: 1, title: 1, price: 1, _id: 0 }
);
// === Methods 4:lean() + select() Optimal Performance ===
const products = await Product.find()
.select("sku title price")
.lean() // Skip mongoose hydrate
.limit(100);
▶ Example 3: Best Practices for E-commerce List APIs
// === Complete List of E-commerce Sites API ===
app.get('/api/products', async (req, res) => {
const {
category,
minPrice,
maxPrice,
search,
sort = 'createdAt',
order = 'desc',
page = 1,
limit = 20
} = req.query;
// 1. Build Query Criteria
const query = { isActive: true };
if (category) query.category = category;
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = NumberDecimal(minPrice);
if (maxPrice) query.price.$lte = NumberDecimal(maxPrice);
}
if (search) query.title = new RegExp(search, 'i');
// 2. Sort
const sortObj = { [sort]: order === 'desc' ? -1 : 1 };
// 3. Pagination
const skip = (page - 1) * limit;
// 4. Search(With Projector + lean)
const products = await Product.find(query)
.select('sku title price thumbnail rating reviewCount') // Return only 6 field
.sort(sortObj)
.skip(skip)
.limit(Number(limit))
.lean(); // Key:Skip mongoose hydrate
// 5. Total Count
const total = await Product.countDocuments(query);
res.json({
products,
pagination: {
page: Number(page),
limit: Number(limit),
total,
pages: Math.ceil(total / limit)
}
});
});
6. pretty() and Result Formatting
Concept Explanation: pretty() is a formatting method in mongosh that converts compact JSON output into a readable, indented format. It does not affect query logic or the data returned; it only changes how the output is displayed in the mongosh terminal. In script execution and Node.js code, pretty() does not work—you must use printjson() or JSON.stringify(obj, null, 2) to achieve a similar effect.
| Formatting Method | Environment | Description |
|---|---|---|
.pretty() |
mongosh interaction mode | Indented layout, best readability |
printjson() |
mongosh script | Output complete JSON structure |
JSON.stringify(obj, null, 2) |
Node.js | Standard JSON Formatting |
console.dir(obj, { depth: null }) |
Node.js | Complete Output of Deeply Nested Structures |
(1) Formatting Output with pretty()
// === Default Output (compact)===
db.products.findOne({ sku: "PHONE-001" });
// { _id: ObjectId('...'), sku: 'PHONE-001', title: 'Phone', ... }
// === pretty() Format ===
db.products.findOne({ sku: "PHONE-001" }).pretty();
// {
// _id: ObjectId('507f1f77bcf86cd799439011'),
// sku: 'PHONE-001',
// title: 'Smartphone X',
// price: NumberDecimal('599.99'),
// ...
// }
// === find Also supported pretty ===
db.products.find({ category: "Electronics" }).pretty();
(2) The Impact of "pretty" in Scripts
# pretty Active in interactive mode,No differences in the script output
mongosh "mongodb://localhost:27017" --eval "db.products.find().pretty()"
(3) Custom Formatting
// === Usage printjson() ===
db.products.find().forEach(printjson);
// Output the complete JSON Structure
// === Usage tojson() ===
const doc = db.products.findOne();
print(tojson(doc));
// === Format the output(pretty 2)===
printjson(doc, null, 2);
7. limit / skip / sort
Concept Explanation: limit, skip, and sort are the three main ways to modify query results; they control the number of results returned, the number of results to skip, and the sorting rule, respectively. The order in which these are applied is sort → skip → limit, regardless of the order in which they are written in the code—MongoDB always sorts first, skips results next, and finally limits the number of results.
How It Works: sort Requires MongoDB to sort matching documents before returning results. If the sort field is indexed, it uses the index order (highly efficient); otherwise, it sorts in memory (an error will occur if the size exceeds 32 MB). skip(N) Requires scanning the first N documents and discarding them; the larger N is, the worse the performance—this is the root cause of the deep pagination problem. limit(N) Limits the number of documents returned, allowing the scan to terminate early.
graph TB
A[Query Results Set<br/>1000 Matches found] --> B[sort Sort<br/>By specified field]
B --> C[skip Skip<br/>First N items]
C --> D[limit Excerpt<br/>Return M items]
B --> B1{The sort field is indexed?}
B1 -->|Have| B2[Index Scan<br/>O(log N)]
B1 -->|No| B3[Memory Sorting<br/>O(N log N)<br/>More than 32MB throws error]
style B2 fill:#d4edda
style B3 fill:#f8d7da
| Method | Function | Impact on Performance | Precautions |
|---|---|---|---|
sort({ field: 1/-1 }) |
Sorting | In-memory sorting without an index | 1: ascending, -1: descending |
skip(N) |
Skip the first N entries | The larger N is, the slower it gets | Avoid using deep pagination |
limit(N) |
Limit the number of results | Improve efficiency | Recommended: ≤ 100 |
(1) limit: Limit the number of results returned
// === Back 10 items ===
db.products.find().limit(10);
// === Conditions for Cooperation ===
db.products.find({ category: "Electronics" }).limit(5);
// === limit(0) equivalent to limit(1) ===
db.products.find().limit(0); // Back 1 items
// === limit(-1) Return All (Special)===
db.products.find().limit(-1); // Back to All(Used as a reverse sort)
(2) skip Skip the document
// === Skip to the beginning 10 items,Back to Page 11-20 items ===
db.products.find().skip(10).limit(10);
// === Page Numbering Formula ===
// Page N(per page 20 items):skip = (N - 1) * 20
db.products.find().skip((page - 1) * 20).limit(20);
// === skip + sort Consistency ===
db.products.find().sort({ _id: 1 }).skip(10).limit(10);
(3) sort
// === Ascending (1)===
db.products.find().sort({ price: 1 }); // Price (ascending)
// === Descending (-1)===
db.products.find().sort({ createdAt: -1 }); // Latest First
// === Sorting by Multiple Fields ===
db.products.find().sort({ category: 1, price: -1 });
// Press first category ascending,Press again price descending
// === Sorting Nested Fields ===
db.products.find().sort({ "specs.rating": -1 });
// === Sorting Array Fields ===
db.products.find().sort({ "tags.0": 1 }); // by tags Sorting the First Element
(4) limit / skip / sort Combination
// === Full Paginated Query ===
db.products
.find({ category: "Electronics", isActive: true })
.sort({ price: 1, createdAt: -1 }) // Price (ascending),Reverse Chronological Order
.skip(20) // Skip 20 items
.limit(10); // Back 10 items
// === mongoose Equivalent Notation ===
const products = await Product
.find({ category: "Electronics", isActive: true })
.sort({ price: 1, createdAt: -1 })
.skip(20)
.limit(10)
.lean();
(5) Pagination Performance Optimization
Concept Explanation: Traditional skip + limit pagination suffers a drastic drop in performance when navigating deep into the pagination—skip(10000) requires scanning 10,000 documents before discarding them. Cursor-based pagination uses _id or a sort key to locate the starting position and jumps directly to the target document, so its performance is not affected by the depth of the pagination.
Comparative Analysis:
| Dimension | skip + limit | Cursor pagination |
|---|---|---|
| Deep Page-Turn Performance | ❌ O(N) Linear Decreasing | ✅ O(log N) Stable |
| Page Jumping Support | ✅ Any page number | ❌ Only forward and backward navigation |
| Total Count | Requires countDocuments | Not required |
| Use Cases | Backend Management (Page Navigation) | Infinite Scrolling, Feed |
graph TB
A[Pagination] --> B[skip + limit Traditional]
A --> C[Cursor-Based Pagination<br/>Recommendations]
B --> B1[skip(10000) Slow<br/>Scan 10000 items]
C --> C1[lastId Search<br/>Direct Targeting]
style C1 fill:#d4edda
// === Traditional Pagination(Slow page turns)===
const page1 = await Product.find().skip(0).limit(20);
const page1000 = await Product.find().skip(20000).limit(20); // Slow!
// === Cursor-Based Pagination(Recommendations)===
const lastId = null; // The First Time
const products1 = await Product.find({ _id: { $gt: lastId } }).limit(20);
const nextLastId = products1[products1.length - 1]._id;
const products2 = await Product.find({ _id: { $gt: nextLastId } }).limit(20);
// Stable performance,Not affected by page depth
▶ Example 4: Full Pagination + Sorting
// === Comprehensive Practical Training:Pagination of Product List API ===
app.get('/api/products', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const sortBy = req.query.sort || 'createdAt';
const order = req.query.order === 'asc' ? 1 : -1;
const products = await Product.find({ isActive: true })
.select('sku title price thumbnail rating')
.sort({ [sortBy]: order })
.skip((page - 1) * limit)
.limit(limit)
.lean();
const total = await Product.countDocuments({ isActive: true });
res.json({
data: products,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
});
});
8. countDocuments Count
Concept Explanation: countDocuments and estimatedDocumentCount are two counting methods in MongoDB. The former provides an exact count but requires scanning matching documents, while the latter estimates the count based on collection metadata—making it extremely fast—but does not support filter conditions. Understanding the difference between the two is crucial for pagination on list pages and statistical scenarios.
How It Works: countDocuments executes a query plan to count all matching documents; its performance is directly proportional to the number of matches. estimatedDocumentCount directly reads the collection’s metadata (document count) without executing any queries; its performance is O(1). In large data collections, the performance difference between the two can be more than 100-fold.
| Dimension | countDocuments | estimatedDocumentCount |
|---|---|---|
| Accuracy | ✅ Exact | ⚠️ Estimated (error < 5%) |
| Performance | ⚠️ Slow (full table scan) | ⚡⚡ Extremely fast (O(1)) |
| Filter Criteria | ✅ Supported | ❌ Not supported |
| Big Roundup | ⚠️ Slow | ⚡ Fast |
| Real-time | ✅ Real-time | ⚠️ Near real-time |
(1) countDocuments: Exact Count
// === Count all documents ===
db.products.countDocuments();
// 1250
// === Statistics Meet the Criteria ===
db.products.countDocuments({ category: "Electronics" });
// 250
// === With options ===
db.products.countDocuments(
{ category: "Electronics" },
{ limit: 1000 } // Most Scans 1000 items
);
// === mongoose Equivalent ===
const count = await Product.countDocuments({ category: "Electronics" });
// 250
(2) estimatedDocumentCount Estimate (Faster)
// === Estimated Total(Metadata-Based,Extremely fast)===
db.products.estimatedDocumentCount();
// 1250(Approximate value)
// === Applicable Scenarios ===
// - List Page Display"Total 1000 items"(No need for precision)
// - Statistics that do not require real-time processing
// === Performance Comparison ===
// countDocuments({}): ~100ms(Full Table Scan)
// estimatedDocumentCount(): ~1ms(Read Metadata)
(3) countDocuments vs estimatedDocumentCount
| Dimension | countDocuments | estimatedDocumentCount |
|---|---|---|
| Accuracy | ✅ Exact | ⚠️ Estimated (error < 5%) |
| Performance | ⚠️ Slow (full table scan) | ⚡⚡ Extremely fast (O(1)) |
| Filter Criteria | ✅ Supported | ❌ Not supported |
| Big Roundup | ⚠️ Slow | ⚡ Fast |
| Real-time | ✅ Real-time | ⚠️ Near real-time |
▶ Example 5: Practical Use of count
// === Product Category Statistics(Accurate)===
const stats = await Product.aggregate([
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
// [
// { _id: 'Electronics', count: 250 },
// { _id: 'Books', count: 200 },
// { _id: 'Clothing', count: 180 }
// ]
// === Total Number of List Pages(Estimate)===
const totalProducts = await Product.estimatedDocumentCount();
const electronicsCount = await Product.countDocuments({ category: "Electronics" });
res.json({
total: totalProducts, // Estimate 1250
electronics: electronicsCount // Accurate 250
});
9. Processing Query Results
(1) Cursor Iteration
// === mongosh Iterate through the middle ===
db.products.find({ category: "Electronics" }).forEach(doc => {
print(`SKU: ${doc.sku}, Title: ${doc.title}`);
});
// === Node.js Iterate through the middle ===
const cursor = collection.find({ category: "Electronics" });
// Methods 1:toArray()
const products = await cursor.toArray();
// Methods 2:for await...of
for await (const doc of collection.find({ category: "Electronics" })) {
console.log(doc.title);
}
// Methods 3:Manual next()
const cursor2 = collection.find({ category: "Electronics" });
while (await cursor2.hasNext()) {
const doc = await cursor2.next();
console.log(doc);
}
(2) Cursor Configuration
// === Set the batch size ===
const cursor = collection.find({ category: "Electronics" })
.batchSize(100); // Per batch 100 items
// === Limit the Maximum Return ===
const cursor = collection.find({ category: "Electronics" })
.limit(1000);
// === Limit Cursor Timeout ===
const cursor = collection.find({ category: "Electronics" })
.maxTimeMS(5000); // 5 Timeout in seconds
(3) Mongoose Query Chains
// === Complete mongoose Query Chain ===
const products = await Product.find({ category: "Electronics" })
.where('price').gt(100).lt(1000) // Price 100-1000
.where('stock').gt(0) // In stock
.select('sku title price') // Projection
.sort({ price: 1 }) // Sort
.skip(20) // Pagination
.limit(10) // Restrictions
.populate('categoryId', 'name slug') // Joined Queries
.lean(); // Performance Optimization
// === Equivalent, concise notation ===
const products2 = await Product.find({
category: "Electronics",
price: { $gt: 100, $lt: 1000 },
stock: { $gt: 0 }
})
.select('sku title price')
.sort({ price: 1 })
.skip(20)
.limit(10)
.lean();
▶ Example 6: Practical Guide to Composite Queries
// === Scene:E-commerce Product Search API ===
app.get('/api/products/search', async (req, res) => {
const { q, category, minPrice, maxPrice, sortBy = 'relevance' } = req.query;
// 1. Build a Query
const query = { isActive: true };
if (q) query.$text = { $search: q };
if (category) query.category = category;
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = NumberDecimal(minPrice);
if (maxPrice) query.price.$lte = NumberDecimal(maxPrice);
}
// 2. Sort
const sortObj = sortBy === 'price_asc' ? { price: 1 } :
sortBy === 'price_desc' ? { price: -1 } :
sortBy === 'newest' ? { createdAt: -1 } :
{ score: { $meta: 'textScore' } }; // Full-Text Search with Relevance Ranking
// 3. Search
const products = await Product.find(query, sortObj.score ? { score: { $meta: 'textScore' } } : {})
.sort(sortObj)
.limit(40)
.lean();
// 4. Statistics
const total = await Product.countDocuments(query);
res.json({
query: { q, category, minPrice, maxPrice },
total,
products
});
});
❓ FAQ
find and findOne?find creates a Cursor (lazy loading), while findOne returns a Document directly. The performance difference is minimal (< 5%), but findOne is simpler. Use find for lists and findOne for details._id is a field included by default and must be explicitly _id: 0 excluded. Otherwise, even if _id is omitted from the projection, it will still be returned.countDocuments slow?estimatedDocumentCount() (based on metadata) or returning an estimated value in the pagination API.skip get slower the deeper you go?skip(N) needs to scan the first N documents before returning results, so the larger N is, the slower it gets. For deep pagination, we recommend using cursor pagination ({ _id: { $gt: lastId } }).Sort exceeded memory limit. With an index, sorting is O(log N).find().limit(0) mean?find({ _id: null }).lean() in Mongoose?save() and populate()). It is suitable for pure query scenarios.📖 Summary
findreturns a Cursor when querying multiple documents;findOnereturns a single Document- Query filters support over 30 operators, including comparison, logical, element, and array operators
- The "projection" control returns a field that can reduce the response size by 90% or more
- limit: Limit the number of results returned; skip: Skip documents; sort: Sort the results
- countDocuments: Exact count; estimatedDocumentCount: Quick estimate
- Use cursor pagination (based on _id) for deep pagination; do not use
skip - Mongoose chained queries +
lean()offer the best performance
📝 Exercises
-
Basic Question (⭐): Insert 10 product documents into Mongosh, use
findto query all documents in the "Electronics" category, and format the output usingpretty(). -
Basic Question (⭐): Use
findOneto query the product with SKU "PHONE-001," and use projection to return only the three fields: SKU, title, and price. -
Advanced Exercise (⭐⭐): Write a Node.js API that implements paginated queries for a product list (using the
pageandlimitparameters), optimize performance usingprojectionandlean(), and return pagination information (total,pages, andhasNext). -
Advanced Question (⭐⭐): Query products with prices between 100 and 1000, inventory > 0, and belonging to the Electronics or Books categories; sort them in ascending order by price, and limit the results to 20.
-
Advanced Problem (⭐⭐): Compare the query execution times for
skip(0).limit(20)andskip(10000).limit(20)on a collection of 1 million documents to understand the issue of deep pagination. -
Challenge (⭐⭐⭐): Implement a cursor-based pagination API (using
lastIdinstead ofskip) that supports pagination of any depth without compromising performance, and includes complete API documentation and test cases.



