findOne 与 find:理解游标
您将使用 findOne 获取文档并遍历 find 游标,了解 MongoDB 如何流式传输大型结果集。
findOne 与 find:理解游标 是 CoddyKit 上的免费 MongoDB Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 MongoDB Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 MongoDB Academy 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Two Ways to Read Documents
MongoDB provides two primary methods for reading documents from a collection:
- findOne(filter, projection) — retrieves the first document matching the filter and returns it as a plain document object (or
nullif nothing matches) - find(filter, projection) — retrieves all matching documents and returns a cursor, a lazy iterator that streams results from the server one batch at a time
Understanding when to use each method—and how cursors work—is fundamental to writing efficient MongoDB queries.
findOne: Simple and Direct
findOne() is the simplest way to retrieve a single document. It returns the first document that matches the filter, or null if no document matches. If multiple documents match, MongoDB returns whichever it encounters first in its internal order—add a .sort() before calling this if you need a specific one.
Common use cases for findOne: looking up a user by email, fetching a product by SKU, or checking if a record exists. Because it returns a plain object rather than a cursor, you use the result directly without iteration.
// findOne by _id (most common lookup)
const user = await db.collection('users').findOne(
{ _id: ObjectId('64a2f3b1...') }
);
if (!user) {
throw new Error('User not found');
}
console.log(user.name); // 'Alice'
// findOne with a filter
const admin = await db.collection('users').findOne({ role: 'admin' });
// Returns ONE admin doc (undefined order), or nullWhat Is a Cursor?
A cursor is a pointer to the result set of a query. When you call find(), MongoDB does not immediately transfer all matching documents to the client. Instead it opens a server-side cursor and sends documents in batches (default 101 documents per batch). The client fetches the next batch only when the current batch is exhausted.
This design is crucial for memory efficiency. If a query matches 10 million documents and you loaded them all at once, you would crash the client. With a cursor, you process documents one batch at a time, keeping memory usage constant regardless of result set size.
// find() returns a cursor, not documents
const cursor = db.collection('orders').find({ status: 'pending' });
// No data fetched yet!
// Data flows as you iterate:
for await (const order of cursor) {
// Each iteration fetches from server in batches
console.log(order._id);
}
// Cursor is exhausted — server releases itIterating Cursors in Node.js
Node.js driver cursors support multiple iteration patterns. The most modern approach is for await...of (async iteration), which cleanly handles backpressure and error handling. Alternatives include cursor.toArray() which loads all results into memory—convenient but dangerous for large result sets.
Always close cursors when done if you break out of iteration early (e.g., after finding what you need). An open cursor holds resources on the MongoDB server. Use cursor.close() explicitly, or rely on for await...of which auto-closes the cursor on loop completion or error.
// Pattern 1: async for...of (recommended)
const cursor = db.collection('products').find({ inStock: true });
for await (const product of cursor) {
await processProduct(product);
}
// Pattern 2: toArray() - loads all into memory
const products = await db.collection('products')
.find({ inStock: true }).toArray();
// Pattern 3: forEach
await cursor.forEach(product => console.log(product.name));Cursor Batch Size and getMore
Internally, the cursor protocol works in two phases:
- The initial
findcommand returns the first batch (default 101 documents or 16 MB, whichever comes first) - Each subsequent batch is fetched via a
getMorecommand using the cursor ID
You can customize the batch size with cursor.batchSize(n). A smaller batch size reduces memory usage on both sides but requires more network round-trips. A larger batch size is more efficient for large sequential scans. The default is usually optimal—only tune it for specific workloads.
// Set a custom batch size (rarely needed)
const cursor = db.collection('logs')
.find({})
.batchSize(500);
// Count documents in a cursor without loading them
// (MongoDB 4.4+ supports .count() on cursor for backwards compat)
// Prefer countDocuments() for accurate counts:
const count = await db.collection('logs').countDocuments({});
console.log('Total logs:', count);Cursor Timeout and Sessions
By default, MongoDB cursors time out after 10 minutes of inactivity on the server side. If your processing of each batch takes longer than that, the cursor will be killed and you will get a CursorNotFound error when you try to fetch the next batch.
For long-running operations, set noCursorTimeout: true or use a session to keep the cursor alive. However, be aware that noCursorTimeout holds a server cursor open indefinitely—always explicitly close these cursors when done to avoid resource leaks.
// Long-running cursor that won't time out
const cursor = db.collection('bigCollection').find(
{},
{ noCursorTimeout: true }
);
try {
for await (const doc of cursor) {
await slowProcessing(doc); // Takes > 10 minutes total
}
} finally {
// Always close explicitly when using noCursorTimeout
await cursor.close();
}Chaining Modifiers on find()
The cursor returned by find() supports a fluent API—you chain methods to modify the query before iteration begins. Order matters for readability but not execution (MongoDB sends all modifiers together):
.sort({ field: 1 })— sort direction.limit(n)— max documents.skip(n)— skip first n results.projection({ field: 1 })— select fields.hint({ index: 1 })— force a specific index.maxTimeMS(ms)— abort if query takes too long
// Full chained query: filter → sort → skip → limit → projection
const page2Products = db.collection('products').find(
{ category: 'Electronics', inStock: true },
{ name: 1, price: 1, _id: 0 } // projection as 2nd arg
)
.sort({ price: -1 }) // Descending price
.skip(20) // Skip page 1 (20 items)
.limit(20) // Page size 20
.maxTimeMS(5000); // Abort if > 5sTailable Cursors for Capped Collections
A special cursor type called a tailable cursor works only on capped collections. Unlike normal cursors that close when all results are consumed, a tailable cursor blocks and waits for new documents, similar to Unix's tail -f command on a log file.
Tailable cursors were the original mechanism for real-time data streaming in MongoDB before Change Streams were introduced. They are still useful for lightweight log tailing on capped collections where Change Streams are overkill.
// Tailable cursor on a capped collection
const tailCursor = db.collection('appLogs').find(
{},
{ tailable: true, awaitData: true }
);
// Blocks and awaits new log entries indefinitely
for await (const log of tailCursor) {
console.log('[' + log.level + '] ' + log.message);
// Prints each new log as it is inserted
}findOne vs find: Choosing Correctly
Use this rule of thumb for choosing between findOne and find:
- Use findOne when: you expect exactly one result (lookup by unique key), you only need to check existence, or you want the simplest code for a single-record API endpoint
- Use find when: the query may return zero, one, or many results; you are building a list endpoint; you need cursor control (batchSize, maxTimeMS); or you are processing results without loading all into memory
Avoid find({}).toArray() on large collections—it loads all results into memory. Process with for await...of instead.
// GOOD: findOne for unique key lookup
const user = await db.collection('users').findOne({ email: 'alice@test.com' });
// GOOD: find with streaming for large sets
for await (const doc of db.collection('users').find({ active: true })) {
await sendNewsletter(doc);
}
// BAD: loading millions of docs into memory
const allUsers = await db.collection('users').find({}).toArray();
// Could OOM crash your server!The explain() Method on Cursors
Appending .explain('executionStats') to a cursor shows how MongoDB executes the query instead of returning documents. The output reveals:
winningPlan.stage:IXSCAN(uses an index) orCOLLSCAN(full scan—bad for large collections)nReturned: how many documents were returnedtotalDocsExamined: how many documents MongoDB looked at to find results (should be close to nReturned if an index is used)executionTimeMillis: total execution time
Regularly running explain() on your critical queries is the foundation of MongoDB performance tuning.
// Check query execution plan
const plan = await db.collection('users')
.find({ email: 'alice@test.com' })
.explain('executionStats');
console.log(plan.queryPlanner.winningPlan.stage);
// 'IXSCAN' if email is indexed, 'COLLSCAN' if not
console.log(plan.executionStats.nReturned); // 1
console.log(plan.executionStats.totalDocsExamined); // 1 (indexed) or 50000 (COLLSCAN)Converting ObjectId in API Responses
When findOne or find().toArray() return documents with ObjectId fields, those ObjectIds need special handling before returning them in a JSON API response. JSON.stringify serializes an ObjectId as an object {} (losing the value) in older driver versions, or as its string representation in newer ones.
The safest pattern is to explicitly call .toString() on any ObjectId fields in a mapping function before sending to the client. Clients then send the ID back as a string, and you convert it with new ObjectId(idString) in the server before querying.
function toPublicDoc(doc) {
if (!doc) return null;
return {
...doc,
_id: doc._id.toString(), // ObjectId -> string for JSON
authorId: doc.authorId ? doc.authorId.toString() : null
};
}
// Usage:
const post = await db.collection('posts').findOne({ slug: 'intro' });
res.json(toPublicDoc(post));
// Client receives: { _id: '64a2f3b1c9e7...', title: '...' }Quick Check
Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.
Lesson Recap
In this lesson you learned: findOne returns a single document directly while find returns a cursor that lazily streams results in batches to avoid memory issues with large result sets, cursors support a fluent chain API—.sort(), .limit(), .skip(), .maxTimeMS()—which MongoDB sends as a single optimized query, and explain('executionStats') reveals whether a query uses an index (IXSCAN) or a full collection scan (COLLSCAN). Next up we dive into querying nested fields and arrays with dot notation.
常见问题解答
「findOne 与 find:理解游标」课时是免费的吗?
是的 — 「findOne 与 find:理解游标」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 MongoDB Academy 课程的其余内容,请升级到 CoddyKit PRO。 MongoDB Academy 课程共包含 4 节课。
「findOne 与 find:理解游标」这节课中我会学到什么?
您将使用 findOne 获取文档并遍历 find 游标,了解 MongoDB 如何流式传输大型结果集。 你通过在浏览器中直接运行的动手代码来练习 MongoDB Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 MongoDB Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 MongoDB Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「findOne 与 find:理解游标」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 MongoDB Academy 课中编写并运行代码吗?
能。每节 MongoDB Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- insertOne 与 insertMany
- findOne 与 find:理解游标
- 查询嵌套字段与数组
- 使用 Node.js 驱动读取文档