MongoDB: 查询文档基础:find方法与投影
最后更新:2026-08-26
查询是从 MongoDB 提取数据的核心操作——掌握 find 方法是数据库交互的第一步。
本课程深入学习 find/findOne 查询语法、字段投影、分页排序、查询结果的格式化与处理。
1. 你将学到
- find 与 findOne 方法的核心区别
- 查询过滤器的基本语法
- 投影(projection)选择返回字段
- pretty() 格式化输出
- limit / skip / sort 分页与排序
- countDocuments 统计文档数量
- 查询结果在 Node.js / mongoose 中的处理
2. 一个全栈工程师的真实故事
(1) 痛点:查询返回所有字段导致网络开销大
Charlie 是一名电商全栈工程师,正在优化商品列表 API 性能:
"我的商品列表 API 返回 100 个商品,每个商品 5KB,前端只显示标题、价格、图片三个字段。返回了 50KB 不必要的数据,API 响应 800ms,浪费带宽和解析时间。"
原始查询代码:
// ❌ 反例:返回所有字段
app.get('/api/products', async (req, res) => {
const products = await Product.find(); // 返回所有字段
res.json(products);
});
// 每个商品 5KB,100 个 = 500KB
// 网络传输慢 + 前端解析慢
(2) 投影(Projection)的解法
// ✅ 正例:只返回必要字段
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。
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 查询多条文档
// === 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 查详情、按唯一索引查单条、存在性检查(判断某条件是否有文档)。
// === 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 对比
要点解析:
find返回的 Cursor 不会立即加载所有数据,节省内存findOne本质是find().limit(-1),直接返回文档,少一次 Cursor 构造- 在 mongoose 中,
find返回数组Array<T>,findOne返回对象T | null - 判断文档是否存在,
findOne+ 检查 null 比find+ 检查数组长度更高效
| 维度 | find | findOne |
|---|---|---|
| 返回类型 | Cursor(游标) | Document 或 null |
| 匹配数量 | 所有匹配 | 第一个匹配 |
| 内存占用 | 流式(懒加载) | 一次性 |
| 性能 | 快 | 略快(不构造 Cursor) |
| 适用场景 | 列表查询 | 详情查询 |
(4) 查询结果在 mongoose 中
// === 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 完整用法
// === 在 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();
输出:
// mongoose 操作成功执行
// 数据库查询/更新结果
4. 查询过滤器
概念说明:查询过滤器(Query Filter)是 find / findOne 的第一个参数,用于指定匹配条件。过滤器使用 JSON/BSON 语法,支持精确匹配、比较运算、逻辑组合、嵌套查询等多种模式。理解过滤器的语法是 MongoDB 查询的基础。
工作原理:MongoDB 将查询过滤器翻译为查询计划(Query Plan),通过索引或全表扫描匹配文档。过滤器中的每个字段条件可以独立使用索引,多条件组合时 MongoDB 优化器自动选择最优执行路径。
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) 基本过滤
// === 精确匹配 ===
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 (...) |
⚠️ |
// === $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) |
少用 |
// === $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:综合查询示例
// === 场景:查询价格 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();
输出:
// mongoose 操作成功执行
// 数据库查询/更新结果
5. 投影(Projection)
概念说明:投影(Projection)控制查询返回哪些字段,是减少网络传输和前端解析负担的核心优化手段。MongoDB 默认返回文档的所有字段,但在列表页、API 响应等场景中,通常只需要 3-5 个关键字段。合理使用投影可将响应体积减少 90% 以上。
工作原理:投影在服务端执行——MongoDB 读取完整文档后,根据投影规则裁剪字段再返回。这意味着投影不能减少磁盘 I/O(仍需读取完整文档),但可以大幅减少网络传输量和客户端反序列化时间。唯一的例外是 Covered Query(覆盖查询)——当查询和投影的字段全部在索引中时,MongoDB 直接从索引返回数据,无需读取文档。
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) 什么是投影?
投影控制返回哪些字段,减少网络传输和前端解析负担。
graph LR
A[完整文档<br/>20 个字段] --> B{投影}
B -->|字段白名单| C[只返回 3 个字段<br/>~10 KB]
B -->|字段黑名单| D[排除 2 个字段<br/>~18 KB]
style C fill:#d4edda
(2) 投影语法
// === 字段白名单(只返回指定字段)===
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) 投影性能影响
// === 性能测试: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 中投影
// === 方法 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 最佳实践
// === 完整电商列表 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)
}
});
});
输出:
// 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() 格式化输出
// === 默认输出(紧凑)===
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 在脚本中的影响
# pretty 在交互模式有效,在脚本输出中无差异
mongosh "mongodb://localhost:27017" --eval "db.products.find().pretty()"
(3) 自定义格式化
// === 使用 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) 限制返回数量,可提前终止扫描。
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 限制返回数量
// === 返回前 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 跳过文档
// === 跳过前 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 排序
// === 升序(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 组合
// === 完整分页查询 ===
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 流 |
graph TB
A[分页查询] --> B[skip + limit 传统]
A --> C[基于游标的分页<br/>推荐]
B --> B1[skip(10000) 慢<br/>扫描 10000 条]
C --> C1[lastId 查询<br/>直接定位]
style C1 fill:#d4edda
// === 传统分页(深翻页慢)===
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:完整分页 + 排序
// === 综合实战:商品列表分页 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
}
});
});
输出:
// API 端点响应(状态码 200)
// 返回 JSON 数据
8. countDocuments 统计
概念说明:countDocuments 和 estimatedDocumentCount 是 MongoDB 的两种计数方法。前者精确计数但需扫描匹配文档,后者基于集合元数据估算极快但不支持过滤条件。理解两者差异对列表页分页和统计场景至关重要。
工作原理:countDocuments 执行查询计划扫描所有匹配文档计数,性能与匹配数量正相关。estimatedDocumentCount 直接读取集合的元数据(文档数量统计值),不执行任何查询,性能为 O(1)。在大数据集合中,两者性能差异可达 100 倍以上。
| 维度 | countDocuments | estimatedDocumentCount |
|---|---|---|
| 精度 | ✅ 精确 | ⚠️ 估算(误差 < 5%) |
| 性能 | ⚠️ 慢(全表扫描) | ⚡⚡ 极快(O(1)) |
| 过滤条件 | ✅ 支持 | ❌ 不支持 |
| 大集合 | ⚠️ 慢 | ⚡ 快 |
| 实时性 | ✅ 实时 | ⚠️ 准实时 |
(1) countDocuments 精确计数
// === 统计所有文档 ===
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 估算(更快)
// === 估算总数(基于元数据,极快)===
db.products.estimatedDocumentCount();
// 1250(近似值)
// === 适用场景 ===
// - 列表页显示"共 1000 条"(不需要精确)
// - 实时性要求不高的统计
// === 性能对比 ===
// countDocuments({}): ~100ms(全表扫描)
// estimatedDocumentCount(): ~1ms(读元数据)
(3) countDocuments vs estimatedDocumentCount
| 维度 | countDocuments | estimatedDocumentCount |
|---|---|---|
| 精度 | ✅ 精确 | ⚠️ 估算(误差 < 5%) |
| 性能 | ⚠️ 慢(全表扫描) | ⚡⚡ 极快(O(1)) |
| 过滤条件 | ✅ 支持 | ❌ 不支持 |
| 大集合 | ⚠️ 慢 | ⚡ 快 |
| 实时性 | ✅ 实时 | ⚠️ 准实时 |
▶ 示例 5:count 实战
// === 商品分类统计(精确)===
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
});
输出:
// 执行成功
9. 查询结果处理
(1) Cursor 遍历
// === 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 配置
// === 设置批次大小 ===
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 查询链
// === 完整 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:综合查询实战
// === 场景:电商商品搜索 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
});
});
输出:
// API 端点响应(状态码 200)
// 返回 JSON 数据
❓ 常见问题
_id 是默认包含字段,需要显式 _id: 0 排除。否则即使在投影中省略 _id,它也会返回。estimatedDocumentCount()(基于元数据)或在分页 API 中返回估算值。{ _id: { $gt: lastId } })。Sort exceeded memory limit。建索引后排序是 O(log N)。find({ _id: null })。📖 小节
- find 查询多条文档返回 Cursor,findOne 返回单个 Document
- 查询过滤器支持比较、逻辑、元素、数组等 30+ 运算符
- 投影(projection)控制返回字段,可减少 90%+ 响应大小
- limit 限制返回数量,skip 跳过文档,sort 排序结果
- countDocuments 精确计数,estimatedDocumentCount 快速估算
- 深翻页用游标分页(基于 _id),不用 skip
- mongoose 链式查询 + lean() 性能最佳
📝 作业
-
基础题(⭐):在 mongosh 中插入 10 个产品文档,用 find 查询所有 Electronics 分类,用 pretty() 格式化输出。
-
基础题(⭐):用 findOne 查询 sku 为 "PHONE-001" 的商品,并用投影只返回 sku、title、price 三个字段。
-
进阶题(⭐⭐):编写 Node.js API,实现商品列表分页查询(page、limit 参数),使用 projection + lean() 优化性能,返回分页信息(total、pages、hasNext)。
-
进阶题(⭐⭐):查询价格 100-1000、库存 > 0、属于 Electronics 或 Books 分类的商品,按价格升序排序,limit 20。
-
进阶题(⭐⭐):对比 skip(0).limit(20) 和 skip(10000).limit(20) 在 100 万文档集合上的查询耗时,理解深翻页问题。
-
挑战题(⭐⭐⭐):实现基于游标的分页 API(用 lastId 替代 skip),支持任意深度翻页性能不变,包含完整的 API 文档和测试用例。