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

最后更新:2026-10-09

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

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

相关操作符:$and $gte $lte $nin $nor $not $eq $gt $in $lt $ne $or estimatedDocumentCount() countDocuments toArray()

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

概念说明:find 和 findOne 是 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

概念说明:limit、skip、sort 是查询结果的三大修饰方法,分别控制返回数量、跳过数量和排序规则。三者的执行顺序是 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 统计

概念说明:countDocuments 和 estimatedDocumentCount 是 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 数据

相关操作符:$and $gte $lte $nin $nor $not $eq $gt $in $lt $ne $or estimatedDocumentCount() countDocuments toArray()

❓ 常见问题

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%

🙏 帮我们做得更好

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

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