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


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:

JAVASCRIPT
// ❌ 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

JAVASCRIPT
// ✅ 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.

100%
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

JAVASCRIPT
// === 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).

JAVASCRIPT
// === 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:

  1. find The returned Cursor does not load all data immediately, saving memory
  2. findOne is essentially find().limit(-1); it returns the document directly, eliminating the need to construct a Cursor.
  3. In Mongoose, find returns an array Array<T>, and findOne returns an object T | null
  4. To determine whether a document exists, findOne + checking for null is more efficient than find + 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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.

100%
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

JAVASCRIPT
// === 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 (...) ⚠️
JAVASCRIPT
// === $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
JAVASCRIPT
// === $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

JAVASCRIPT
// === 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.

100%
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.

100%
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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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()

JAVASCRIPT
// === 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

BASH
# pretty Active in interactive mode,No differences in the script output
mongosh "mongodb://localhost:27017" --eval "db.products.find().pretty()"

(3) Custom Formatting

JAVASCRIPT
// === 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.

100%
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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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
100%
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
JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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)

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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

Q How much of a performance difference is there between find and findOne?
A 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.
Q Is the _id field returned by default?
A Yes. _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.
Q Can projections reduce query time?
A Yes, but only to a limited extent. MongoDB needs to read the entire document to apply a projection (unless a covered query is used). The main optimizations are in network transfer and deserialization time; the impact on query execution time is minimal.
Q Why is countDocuments slow?
A An exact count requires scanning all matching documents. For large collections, we recommend using estimatedDocumentCount() (based on metadata) or returning an estimated value in the pagination API.
Q Does skip get slower the deeper you go?
A Yes! 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 } }).
Q Do I need to create an index on the sort field?
A Highly recommended. Otherwise, MongoDB will have to sort in memory, and if the size exceeds 32 MB, an error will occur Sort exceeded memory limit. With an index, sorting is O(log N).
Q What does find().limit(0) mean?
A It returns 1 document (a special behavior in MongoDB). To return 0 documents, use find({ _id: null }).
Q What is the purpose of lean() in Mongoose?
A It skips Mongoose’s document hydration process and returns a plain JavaScript object directly. This provides a 3–5x performance boost, but you lose access to Mongoose’s document methods (such as save() and populate()). It is suitable for pure query scenarios.

📖 Summary


📝 Exercises

  1. Basic Question (⭐): Insert 10 product documents into Mongosh, use find to query all documents in the "Electronics" category, and format the output using pretty().

  2. Basic Question (⭐): Use findOne to query the product with SKU "PHONE-001," and use projection to return only the three fields: SKU, title, and price.

  3. Advanced Exercise (⭐⭐): Write a Node.js API that implements paginated queries for a product list (using the page and limit parameters), optimize performance using projection and lean(), and return pagination information (total, pages, and hasNext).

  4. 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.

  5. Advanced Problem (⭐⭐): Compare the query execution times for skip(0).limit(20) and skip(10000).limit(20) on a collection of 1 million documents to understand the issue of deep pagination.

  6. Challenge (⭐⭐⭐): Implement a cursor-based pagination API (using lastId instead of skip) that supports pagination of any depth without compromising performance, and includes complete API documentation and test cases.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏