MongoDB: 查询文档基础:find方法与投影

最后更新:2026-08-26

查询是从 MongoDB 提取数据的核心操作——掌握 find 方法是数据库交互的第一步。

本课程深入学习 find/findOne 查询语法、字段投影、分页排序、查询结果的格式化与处理。

1. 你将学到


2. 一个全栈工程师的真实故事

(1) 痛点:查询返回所有字段导致网络开销大

Charlie 是一名电商全栈工程师,正在优化商品列表 API 性能:

"我的商品列表 API 返回 100 个商品,每个商品 5KB,前端只显示标题、价格、图片三个字段。返回了 50KB 不必要的数据,API 响应 800ms,浪费带宽和解析时间。"

原始查询代码:

JAVASCRIPT
// ❌ 反例:返回所有字段
app.get('/api/products', async (req, res) => {
  const products = await Product.find();  // 返回所有字段
  res.json(products);
});
// 每个商品 5KB,100 个 = 500KB
// 网络传输慢 + 前端解析慢

(2) 投影(Projection)的解法

JAVASCRIPT
// ✅ 正例:只返回必要字段
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   // 排除 _id
      }
    }
  )
    .sort({ createdAt: -1 })
    .limit(20)
    .lean();   // 跳过 mongoose hydrate,性能 ↑3-5x

  res.json(products);
});
// 每个商品 200 字节,20 个 = 4KB(性能 ↑100x)

(3) 收益

维度 无投影 有投影
响应大小 500 KB 4 KB
API 延迟 800 ms 50 ms
前端解析时间 200 ms 5 ms
网络带宽 低 100x

3. find 与 findOne

概念说明findfindOne 是 MongoDB 的两大查询方法。find 返回匹配文档的游标(Cursor),适用于列表查询;findOne 返回单个文档,适用于详情查询。两者语法相似但返回类型不同,理解差异对正确处理查询结果至关重要。

工作原理find 不立即返回全部数据,而是创建一个 Cursor 对象。Cursor 采用懒加载策略——只有当你遍历或调用 toArray() 时,才从服务端分批获取数据(默认每批 101 条或 1MB)。这种设计使 find 即使在百万级数据集上也不会撑爆内存。findOne 等价于 find().limit(1),但直接返回文档而非 Cursor。

100%
sequenceDiagram
    participant App as 应用程序
    participant Mongo as MongoDB 服务端

    App->>Mongo: find({ category: "Electronics" })
    Mongo-->>App: Cursor 对象(未获取数据)
    App->>Mongo: cursor.next() / toArray()
    Mongo-->>App: 第一批 101 条文档
    App->>Mongo: 继续遍历
    Mongo-->>App: 后续批次(每批最多 16MB)
维度 find findOne
返回类型 Cursor(游标) Document 或 null
匹配数量 所有匹配 第一个匹配
内存占用 流式(懒加载) 一次性
性能 略快(不构造 Cursor)
适用场景 列表查询 详情查询

(1) find 查询多条文档

JAVASCRIPT
// === find 基本语法 ===
db.products.find();
// 返回所有文档(cursor)

// === find 指定条件 ===
db.products.find({ category: "Electronics" });
// 返回所有电子产品

// === find 返回数组 ===
db.products.find({ category: "Electronics" }).toArray();
// 返回 Array<Document>

// === find 遍历(Cursor)===
db.products.find({ category: "Electronics" }).forEach(printjson);

(2) findOne 查询单个文档

概念说明findOne 是查询单条文档的便捷方法,内部等价于 find().limit(1),但直接返回文档对象而非 Cursor。返回 null 表示未找到匹配文档——这是与 find 的重要区别,find 返回空 Cursor 而非 null。

使用场景:按 _id 查详情、按唯一索引查单条、存在性检查(判断某条件是否有文档)。

JAVASCRIPT
// === findOne 返回单个文档 ===
db.products.findOne({ sku: "PHONE-001" });
// 返回第一个匹配的文档(或 null)

// === findOne 与 find().limit(1) 的区别 ===
const doc1 = db.products.findOne({ sku: "PHONE-001" });
const doc2 = db.products.find({ sku: "PHONE-001" }).limit(1).next();
// 结果相同,findOne 更简洁

(3) find vs findOne 对比

要点解析

  1. find 返回的 Cursor 不会立即加载所有数据,节省内存
  2. findOne 本质是 find().limit(-1),直接返回文档,少一次 Cursor 构造
  3. 在 mongoose 中,find 返回数组 Array<T>findOne 返回对象 T | null
  4. 判断文档是否存在,findOne + 检查 null 比 find + 检查数组长度更高效
维度 find findOne
返回类型 Cursor(游标) Document 或 null
匹配数量 所有匹配 第一个匹配
内存占用 流式(懒加载) 一次性
性能 略快(不构造 Cursor)
适用场景 列表查询 详情查询

(4) 查询结果在 mongoose 中

JAVASCRIPT
// === mongoose 中 find 返回数组 ===
const products = await Product.find({ category: "Electronics" });
// Array<Product>

// === mongoose 中 findOne 返回对象 ===
const product = await Product.findOne({ sku: "PHONE-001" });
// Product | null

// === 处理查询结果为空的情况 ===
const product = await Product.findOne({ sku: "NOT_EXIST" });
if (!product) {
  return res.status(404).json({ error: "Product not found" });
}

▶ 示例 1:find 完整用法

JAVASCRIPT
// === 在 mongosh 中查询 ===
// 查询所有文档
db.products.find();

// 查询指定条件
db.products.find({ category: "Electronics" });

// 多条件查询(AND)
db.products.find({
  category: "Electronics",
  stock: { $gt: 0 }     // 库存大于 0
});

// 查询并格式化
db.products.find({ category: "Electronics" }).pretty();

// 查询并计数
db.products.find({ category: "Electronics" }).count();

// === 在 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() 返回 Cursor
  const cursor = collection.find({ category: "Electronics" });
  const products = await cursor.toArray();
  console.log(`Found ${products.length} products`);

  // 2. findOne() 返回 Document
  const product = await collection.findOne({ sku: "PHONE-001" });
  console.log(product);

  // 3. 遍历 Cursor(流式)
  for await (const doc of collection.find({ category: "Electronics" })) {
    console.log(doc.title);
  }

  await client.close();
}

findProducts();

输出:

TEXT 📖 仅展示
// mongoose 操作成功执行
// 数据库查询/更新结果

4. 查询过滤器

概念说明:查询过滤器(Query Filter)是 find / findOne 的第一个参数,用于指定匹配条件。过滤器使用 JSON/BSON 语法,支持精确匹配、比较运算、逻辑组合、嵌套查询等多种模式。理解过滤器的语法是 MongoDB 查询的基础。

工作原理:MongoDB 将查询过滤器翻译为查询计划(Query Plan),通过索引或全表扫描匹配文档。过滤器中的每个字段条件可以独立使用索引,多条件组合时 MongoDB 优化器自动选择最优执行路径。

100%
graph TB
    A[查询过滤器] --> B[精确匹配<br/>{ field: value }]
    A --> C[比较运算符<br/>{ field: { $gt: N } }]
    A --> D[逻辑组合<br/>{ $and / $or / $not }]
    A --> E[嵌套查询<br/>{ "path.field": value }]
    A --> F[数组查询<br/>{ array: value }]

    style A fill:#cce5ff
过滤器类型 语法 示例
精确匹配 { field: value } { sku: "PHONE-001" }
多条件 AND { f1: v1, f2: v2 } { category: "E", stock: 50 }
字段不存在 { field: { $exists: false } } { discount: { $exists: false } }
嵌套文档 { "path.field": value } { "specs.battery": "4500mAh" }
数组元素 { array: value } { tags: "5g" }

(1) 基本过滤

JAVASCRIPT
// === 精确匹配 ===
db.products.find({ sku: "PHONE-001" });

// === 多条件 AND ===
db.products.find({
  category: "Electronics",
  stock: 50,
  isActive: true
});

// === 字段不存在 ===
db.products.find({ discount: { $exists: false } });

// === 嵌套文档查询 ===
db.products.find({ "specs.battery": "4500mAh" });

// === 数组元素匹配 ===
db.products.find({ tags: "5g" });

// === 数组多元素匹配 ===
db.products.find({ tags: { $all: ["5g", "amoled"] } });

(2) 比较运算符

概念说明:比较运算符是查询过滤器的核心,支持范围查询、多值匹配、排除等操作。MongoDB 提供 8 个比较运算符:$eq$ne$gt$gte$lt$lte$in$nin。其中 $eq 是默认行为({ price: 599 } 等价于 { price: { $eq: 599 } }),$in 是最常用的高频运算符。

运算符 含义 等价 SQL 索引友好
$eq 等于 WHERE field = value
$ne 不等于 WHERE field != value ⚠️
$gt/$gte 大于/大于等于 WHERE field > />= value
$lt/$lte 小于/小于等于 WHERE field < /<= value
$in 包含于 WHERE field IN (...)
$nin 不包含 WHERE field NOT IN (...) ⚠️
JAVASCRIPT
// === $eq(等于,默认)===
db.products.find({ price: { $eq: 599.99 } });
// 等同于 { price: 599.99 }

// === $ne(不等于)===
db.products.find({ category: { $ne: "Books" } });

// === $gt / $gte(大于 / 大于等于)===
db.products.find({ price: { $gt: 100 } });        // > 100
db.products.find({ price: { $gte: 100 } });       // >= 100

// === $lt / $lte(小于 / 小于等于)===
db.products.find({ price: { $lt: 1000 } });
db.products.find({ price: { $lte: 1000 } });

// === $in / $nin(包含 / 不包含)===
db.products.find({ category: { $in: ["Electronics", "Books"] } });
db.products.find({ category: { $nin: ["Clothing"] } });

// === 范围查询 ===
db.products.find({
  price: { $gte: 100, $lte: 1000 }  // 100 <= price <= 1000
});

(3) 逻辑运算符

概念说明:逻辑运算符组合多个查询条件,实现复杂的筛选逻辑。MongoDB 支持 4 个逻辑运算符:$and(全部满足)、$or(任一满足)、$not(不满足)、$nor(全部不满足)。其中隐式 AND(逗号分隔多个字段)是最常用的写法,显式 $and 仅在"同一字段多个条件"时必须使用。

运算符 含义 等价 SQL 常用程度
隐式 AND 逗号分隔 WHERE a=1 AND b=2 ⭐⭐⭐ 最常用
$and 显式 AND WHERE (a=1 AND b=2) ⭐ 同字段多条件
$or 任一满足 WHERE a=1 OR b=2 ⭐⭐
$not 不满足 WHERE NOT (condition)
$nor 全部不满足 WHERE NOT (a=1 OR b=2) 少用
JAVASCRIPT
// === $and(隐式 AND)===
db.products.find({
  category: "Electronics",
  stock: { $gt: 0 }    // 隐式 AND
});

// === $and(显式 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 } } });
// 价格 <= 1000

// === $nor(都不匹配)===
db.products.find({
  $nor: [
    { category: "Electronics" },
    { category: "Books" }
  ]
});

▶ 示例 2:综合查询示例

JAVASCRIPT
// === 场景:查询价格 100-1000、库存大于 0、Electronics 或 Books 分类的商品 ===
db.products.find({
  price: { $gte: 100, $lte: 1000 },
  stock: { $gt: 0 },
  $or: [
    { category: "Electronics" },
    { category: "Books" }
  ],
  isActive: true
}).sort({ price: 1 }).limit(20);

// === mongoose 等价写法 ===
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();

输出:

TEXT 📖 仅展示
// mongoose 操作成功执行
// 数据库查询/更新结果

5. 投影(Projection)

概念说明:投影(Projection)控制查询返回哪些字段,是减少网络传输和前端解析负担的核心优化手段。MongoDB 默认返回文档的所有字段,但在列表页、API 响应等场景中,通常只需要 3-5 个关键字段。合理使用投影可将响应体积减少 90% 以上。

工作原理:投影在服务端执行——MongoDB 读取完整文档后,根据投影规则裁剪字段再返回。这意味着投影不能减少磁盘 I/O(仍需读取完整文档),但可以大幅减少网络传输量和客户端反序列化时间。唯一的例外是 Covered Query(覆盖查询)——当查询和投影的字段全部在索引中时,MongoDB 直接从索引返回数据,无需读取文档。

100%
graph LR
    A[完整文档<br/>20 个字段 ~5KB] --> B{投影规则}
    B -->|白名单模式<br/>{ sku: 1, title: 1, price: 1 }| C[3 个字段 ~200B]
    B -->|黑名单模式<br/>{ description: 0, images: 0 }| D[18 个字段 ~4.5KB]
    
    style C fill:#d4edda
投影模式 语法 特点 适用场景
白名单 { field: 1 } 只返回指定字段 列表页(需少量字段)
黑名单 { field: 0 } 排除指定字段 详情页(排除敏感字段)
混合 ❌ 不允许 白名单和黑名单不能混用(_id 除外)
_id 控制 { _id: 0 } 默认返回,需显式排除 API 返回时去掉 _id

(1) 什么是投影?

投影控制返回哪些字段,减少网络传输和前端解析负担。

100%
graph LR
    A[完整文档<br/>20 个字段] --> B{投影}
    B -->|字段白名单| C[只返回 3 个字段<br/>~10 KB]
    B -->|字段黑名单| D[排除 2 个字段<br/>~18 KB]

    style C fill:#d4edda

(2) 投影语法

JAVASCRIPT
// === 字段白名单(只返回指定字段)===
db.products.find(
  { category: "Electronics" },
  { sku: 1, title: 1, price: 1 }
);
// 返回:{ _id, sku, title, price }

// === _id 默认返回,需要显式排除 ===
db.products.find(
  {},
  { sku: 1, title: 1, _id: 0 }  // _id: 0 排除 _id
);

// === 字段黑名单(排除指定字段)===
db.products.find(
  {},
  { internalNotes: 0, debugInfo: 0 }  // 排除敏感字段
);

// === 嵌套文档投影 ===
db.products.find(
  { sku: "PHONE-001" },
  {
    sku: 1,
    title: 1,
    "specs.screen": 1,    // 只返回 specs.screen
    "specs.battery": 1   // 只返回 specs.battery
  }
);

// === 数组元素投影($slice)===
db.reviews.find(
  { productId: "PHONE-001" },
  {
    title: 1,
    content: 1,
    comments: { $slice: 3 }  // 只返回前 3 条评论
  }
);

(3) 投影性能影响

JAVASCRIPT
// === 性能测试:100 万文档,查询 100 条 ===

// ❌ 无投影:返回 5MB
db.products.find({ category: "Electronics" }).limit(100);
// 耗时 800ms

// ✅ 有投影:返回 200KB
db.products.find(
  { category: "Electronics" },
  { sku: 1, title: 1, price: 1, _id: 0 }
).limit(100);
// 耗时 80ms(性能 ↑10x)

(4) mongoose 中投影

JAVASCRIPT
// === 方法 1:projection 选项 ===
const products = await Product.find({ category: "Electronics" }, "sku title price");
// 字符串语法(空格分隔)

// === 方法 2:select() 链式 ===
const products = await Product.find()
  .select("sku title price")
  .select("-description -images");  // 排除某些字段

// === 方法 3:对象语法 ===
const products = await Product.find(
  { category: "Electronics" },
  { sku: 1, title: 1, price: 1, _id: 0 }
);

// === 方法 4:lean() + select() 最佳性能 ===
const products = await Product.find()
  .select("sku title price")
  .lean()  // 跳过 mongoose hydrate
  .limit(100);

▶ 示例 3:电商列表 API 最佳实践

JAVASCRIPT
// === 完整电商列表 API ===
app.get('/api/products', async (req, res) => {
  const {
    category,
    minPrice,
    maxPrice,
    search,
    sort = 'createdAt',
    order = 'desc',
    page = 1,
    limit = 20
  } = req.query;

  // 1. 构建查询条件
  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. 排序
  const sortObj = { [sort]: order === 'desc' ? -1 : 1 };

  // 3. 分页
  const skip = (page - 1) * limit;

  // 4. 查询(带投影 + lean)
  const products = await Product.find(query)
    .select('sku title price thumbnail rating reviewCount')  // 只返回 6 个字段
    .sort(sortObj)
    .skip(skip)
    .limit(Number(limit))
    .lean();   // 关键:跳过 mongoose hydrate

  // 5. 统计总数
  const total = await Product.countDocuments(query);

  res.json({
    products,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total,
      pages: Math.ceil(total / limit)
    }
  });
});

输出:

TEXT 📖 仅展示
// API 端点响应(状态码 200)
// 返回 JSON 数据

6. pretty() 与结果格式化

概念说明pretty() 是 mongosh 的格式化输出方法,将紧凑的 JSON 输出转为缩进排列的可读格式。它不影响查询逻辑和数据返回,仅改变 mongosh 终端的显示方式。在脚本执行和 Node.js 代码中,pretty() 无效——需要使用 printjson()JSON.stringify(obj, null, 2) 实现类似效果。

格式化方式 环境 说明
.pretty() mongosh 交互模式 缩进排列,可读性最佳
printjson() mongosh 脚本 完整 JSON 结构输出
JSON.stringify(obj, null, 2) Node.js 标准 JSON 格式化
console.dir(obj, { depth: null }) Node.js 深层嵌套完整输出

(1) pretty() 格式化输出

JAVASCRIPT
// === 默认输出(紧凑)===
db.products.findOne({ sku: "PHONE-001" });
// { _id: ObjectId('...'), sku: 'PHONE-001', title: 'Phone', ... }

// === pretty() 格式化 ===
db.products.findOne({ sku: "PHONE-001" }).pretty();
// {
//   _id: ObjectId('507f1f77bcf86cd799439011'),
//   sku: 'PHONE-001',
//   title: 'Smartphone X',
//   price: NumberDecimal('599.99'),
//   ...
// }

// === find 也支持 pretty ===
db.products.find({ category: "Electronics" }).pretty();

(2) pretty 在脚本中的影响

BASH
# pretty 在交互模式有效,在脚本输出中无差异
mongosh "mongodb://localhost:27017" --eval "db.products.find().pretty()"

(3) 自定义格式化

JAVASCRIPT
// === 使用 printjson() ===
db.products.find().forEach(printjson);
// 输出完整的 JSON 结构

// === 使用 tojson() ===
const doc = db.products.findOne();
print(tojson(doc));

// === 美化输出(pretty 2)===
printjson(doc, null, 2);

7. limit / skip / sort

概念说明limitskipsort 是查询结果的三大修饰方法,分别控制返回数量、跳过数量和排序规则。三者的执行顺序是 sort → skip → limit,与代码书写顺序无关——MongoDB 总是先排序、再跳过、最后限制数量。

工作原理sort 要求 MongoDB 在返回结果前对匹配文档排序,如果排序字段有索引则使用索引顺序(高效),否则在内存中排序(超过 32MB 会报错)。skip(N) 要求扫描前 N 条文档并丢弃,N 越大性能越差——这是深翻页问题的根源。limit(N) 限制返回数量,可提前终止扫描。

100%
graph TB
    A[查询结果集<br/>1000 条匹配] --> B[sort 排序<br/>按指定字段]
    B --> C[skip 跳过<br/>前 N 条]
    C --> D[limit 截取<br/>返回 M 条]
    
    B --> B1{排序字段有索引?}
    B1 -->|有| B2[索引扫描<br/>O(log N)]
    B1 -->|无| B3[内存排序<br/>O(N log N)<br/>超过32MB报错]
    
    style B2 fill:#d4edda
    style B3 fill:#f8d7da
方法 作用 性能影响 注意事项
sort({ field: 1/-1 }) 排序 无索引时内存排序 1 升序,-1 降序
skip(N) 跳过前 N 条 N 越大越慢 深翻页避免使用
limit(N) 限制返回数量 提高效率 推荐 ≤ 100

(1) limit 限制返回数量

JAVASCRIPT
// === 返回前 10 条 ===
db.products.find().limit(10);

// === 配合条件 ===
db.products.find({ category: "Electronics" }).limit(5);

// === limit(0) 等同于 limit(1) ===
db.products.find().limit(0);  // 返回 1 条

// === limit(-1) 返回所有(特殊)===
db.products.find().limit(-1);  // 返回所有(用作反向 sort)

(2) skip 跳过文档

JAVASCRIPT
// === 跳过前 10 条,返回第 11-20 条 ===
db.products.find().skip(10).limit(10);

// === 分页公式 ===
// 第 N 页(每页 20 条):skip = (N - 1) * 20
db.products.find().skip((page - 1) * 20).limit(20);

// === skip + sort 一致性 ===
db.products.find().sort({ _id: 1 }).skip(10).limit(10);

(3) sort 排序

JAVASCRIPT
// === 升序(1)===
db.products.find().sort({ price: 1 });     // 价格升序

// === 降序(-1)===
db.products.find().sort({ createdAt: -1 }); // 最新优先

// === 多字段排序 ===
db.products.find().sort({ category: 1, price: -1 });
// 先按 category 升序,再按 price 降序

// === 嵌套字段排序 ===
db.products.find().sort({ "specs.rating": -1 });

// === 数组字段排序 ===
db.products.find().sort({ "tags.0": 1 });  // 按 tags 第一个元素排序

(4) limit / skip / sort 组合

JAVASCRIPT
// === 完整分页查询 ===
db.products
  .find({ category: "Electronics", isActive: true })
  .sort({ price: 1, createdAt: -1 })  // 价格升序,时间倒序
  .skip(20)                            // 跳过 20 条
  .limit(10);                          // 返回 10 条

// === mongoose 等价写法 ===
const products = await Product
  .find({ category: "Electronics", isActive: true })
  .sort({ price: 1, createdAt: -1 })
  .skip(20)
  .limit(10)
  .lean();

(5) 分页性能优化

概念说明:传统 skip + limit 分页在深翻页时性能急剧下降——skip(10000) 需要先扫描 10000 条文档再丢弃。基于游标的分页(Cursor-based Pagination)通过 _id 或排序键定位起始位置,直接跳到目标文档,性能不受翻页深度影响。

对比分析

维度 skip + limit 游标分页
深翻页性能 ❌ O(N) 线性下降 ✅ O(log N) 稳定
跳页支持 ✅ 任意页码 ❌ 只能前后翻
总数统计 需要 countDocuments 不需要
适用场景 后台管理(页码跳转) 无限滚动、Feed 流
100%
graph TB
    A[分页查询] --> B[skip + limit 传统]
    A --> C[基于游标的分页<br/>推荐]

    B --> B1[skip(10000) 慢<br/>扫描 10000 条]
    C --> C1[lastId 查询<br/>直接定位]

    style C1 fill:#d4edda
JAVASCRIPT
// === 传统分页(深翻页慢)===
const page1 = await Product.find().skip(0).limit(20);
const page1000 = await Product.find().skip(20000).limit(20);  // 慢!

// === 基于游标的分页(推荐)===
const lastId = null;  // 第一次
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);
// 性能稳定,不受翻页深度影响

▶ 示例 4:完整分页 + 排序

JAVASCRIPT
// === 综合实战:商品列表分页 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
    }
  });
});

输出:

TEXT 📖 仅展示
// API 端点响应(状态码 200)
// 返回 JSON 数据

8. countDocuments 统计

概念说明countDocumentsestimatedDocumentCount 是 MongoDB 的两种计数方法。前者精确计数但需扫描匹配文档,后者基于集合元数据估算极快但不支持过滤条件。理解两者差异对列表页分页和统计场景至关重要。

工作原理countDocuments 执行查询计划扫描所有匹配文档计数,性能与匹配数量正相关。estimatedDocumentCount 直接读取集合的元数据(文档数量统计值),不执行任何查询,性能为 O(1)。在大数据集合中,两者性能差异可达 100 倍以上。

维度 countDocuments estimatedDocumentCount
精度 ✅ 精确 ⚠️ 估算(误差 < 5%)
性能 ⚠️ 慢(全表扫描) ⚡⚡ 极快(O(1))
过滤条件 ✅ 支持 ❌ 不支持
大集合 ⚠️ 慢 ⚡ 快
实时性 ✅ 实时 ⚠️ 准实时

(1) countDocuments 精确计数

JAVASCRIPT
// === 统计所有文档 ===
db.products.countDocuments();
// 1250

// === 统计符合条件 ===
db.products.countDocuments({ category: "Electronics" });
// 250

// === 带选项 ===
db.products.countDocuments(
  { category: "Electronics" },
  { limit: 1000 }   // 最多扫描 1000 条
);

// === mongoose 等价 ===
const count = await Product.countDocuments({ category: "Electronics" });
// 250

(2) estimatedDocumentCount 估算(更快)

JAVASCRIPT
// === 估算总数(基于元数据,极快)===
db.products.estimatedDocumentCount();
// 1250(近似值)

// === 适用场景 ===
// - 列表页显示"共 1000 条"(不需要精确)
// - 实时性要求不高的统计

// === 性能对比 ===
// countDocuments({}): ~100ms(全表扫描)
// estimatedDocumentCount(): ~1ms(读元数据)

(3) countDocuments vs estimatedDocumentCount

维度 countDocuments estimatedDocumentCount
精度 ✅ 精确 ⚠️ 估算(误差 < 5%)
性能 ⚠️ 慢(全表扫描) ⚡⚡ 极快(O(1))
过滤条件 ✅ 支持 ❌ 不支持
大集合 ⚠️ 慢 ⚡ 快
实时性 ✅ 实时 ⚠️ 准实时

▶ 示例 5:count 实战

JAVASCRIPT
// === 商品分类统计(精确)===
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 }
// ]

// === 列表页总数(估算)===
const totalProducts = await Product.estimatedDocumentCount();
const electronicsCount = await Product.countDocuments({ category: "Electronics" });

res.json({
  total: totalProducts,        // 估算 1250
  electronics: electronicsCount // 精确 250
});

输出:

TEXT 📖 仅展示
// 执行成功

9. 查询结果处理

(1) Cursor 遍历

JAVASCRIPT
// === mongosh 中遍历 ===
db.products.find({ category: "Electronics" }).forEach(doc => {
  print(`SKU: ${doc.sku}, Title: ${doc.title}`);
});

// === Node.js 中遍历 ===
const cursor = collection.find({ category: "Electronics" });

// 方法 1:toArray()
const products = await cursor.toArray();

// 方法 2:for await...of
for await (const doc of collection.find({ category: "Electronics" })) {
  console.log(doc.title);
}

// 方法 3:手动 next()
const cursor2 = collection.find({ category: "Electronics" });
while (await cursor2.hasNext()) {
  const doc = await cursor2.next();
  console.log(doc);
}

(2) Cursor 配置

JAVASCRIPT
// === 设置批次大小 ===
const cursor = collection.find({ category: "Electronics" })
  .batchSize(100);  // 每批 100 条

// === 限制最大返回 ===
const cursor = collection.find({ category: "Electronics" })
  .limit(1000);

// === 限制游标超时 ===
const cursor = collection.find({ category: "Electronics" })
  .maxTimeMS(5000);  // 5 秒超时

(3) mongoose 查询链

JAVASCRIPT
// === 完整 mongoose 查询链 ===
const products = await Product.find({ category: "Electronics" })
  .where('price').gt(100).lt(1000)        // 价格 100-1000
  .where('stock').gt(0)                   // 有库存
  .select('sku title price')              // 投影
  .sort({ price: 1 })                     // 排序
  .skip(20)                               // 分页
  .limit(10)                              // 限制
  .populate('categoryId', 'name slug')    // 关联查询
  .lean();                                // 性能优化

// === 等价简洁写法 ===
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();

▶ 示例 6:综合查询实战

JAVASCRIPT
// === 场景:电商商品搜索 API ===
app.get('/api/products/search', async (req, res) => {
  const { q, category, minPrice, maxPrice, sortBy = 'relevance' } = req.query;

  // 1. 构建查询
  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. 排序
  const sortObj = sortBy === 'price_asc' ? { price: 1 } :
                  sortBy === 'price_desc' ? { price: -1 } :
                  sortBy === 'newest' ? { createdAt: -1 } :
                  { score: { $meta: 'textScore' } };  // 全文搜索相关性排序

  // 3. 查询
  const products = await Product.find(query, sortObj.score ? { score: { $meta: 'textScore' } } : {})
    .sort(sortObj)
    .limit(40)
    .lean();

  // 4. 统计
  const total = await Product.countDocuments(query);

  res.json({
    query: { q, category, minPrice, maxPrice },
    total,
    products
  });
});

输出:

TEXT 📖 仅展示
// API 端点响应(状态码 200)
// 返回 JSON 数据

❓ 常见问题

Q find 和 findOne 性能差多少?
A find 构造 Cursor(懒加载),findOne 直接返回 Document。性能差异很小(< 5%),但 findOne 更简单。列表用 find,详情用 findOne。
Q _id 字段默认会返回吗?
A 是的。_id 是默认包含字段,需要显式 _id: 0 排除。否则即使在投影中省略 _id,它也会返回。
Q 投影能减少查询时间吗?
A 能,但有限。MongoDB 需要读取完整文档才能应用投影(除非使用 covered query)。主要优化是网络传输和反序列化时间,对查询执行时间影响小。
Q countDocuments 为什么慢?
A 精确计数需扫描所有匹配的文档。对于大集合,推荐使用 estimatedDocumentCount()(基于元数据)或在分页 API 中返回估算值。
Q skip 翻页越深越慢吗?
A 是的!skip(N) 需要扫描前 N 条文档才返回结果,N 越大越慢。深翻页推荐使用游标分页({ _id: { $gt: lastId } })。
Q sort 的字段需要建索引吗?
A 强烈推荐。否则 MongoDB 需要在内存中排序,超过 32MB 会报错 Sort exceeded memory limit。建索引后排序是 O(log N)。
Q find().limit(0) 是什么意思?
A 返回 1 条文档(MongoDB 特殊行为)。如需返回 0 条,应使用 find({ _id: null })
Q lean() 在 mongoose 中有什么用?
A 跳过 mongoose 的文档 hydrate 过程,直接返回纯 JavaScript 对象。性能提升 3-5x,但失去 mongoose 文档方法(save()、populate() 等)。适用于纯查询场景。

📖 小节


📝 作业

  1. 基础题(⭐):在 mongosh 中插入 10 个产品文档,用 find 查询所有 Electronics 分类,用 pretty() 格式化输出。

  2. 基础题(⭐):用 findOne 查询 sku 为 "PHONE-001" 的商品,并用投影只返回 sku、title、price 三个字段。

  3. 进阶题(⭐⭐):编写 Node.js API,实现商品列表分页查询(page、limit 参数),使用 projection + lean() 优化性能,返回分页信息(total、pages、hasNext)。

  4. 进阶题(⭐⭐):查询价格 100-1000、库存 > 0、属于 Electronics 或 Books 分类的商品,按价格升序排序,limit 20。

  5. 进阶题(⭐⭐):对比 skip(0).limit(20) 和 skip(10000).limit(20) 在 100 万文档集合上的查询耗时,理解深翻页问题。

  6. 挑战题(⭐⭐⭐):实现基于游标的分页 API(用 lastId 替代 skip),支持任意深度翻页性能不变,包含完整的 API 文档和测试用例。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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