MongoDB Academy · Урок

findOne и find: объяснение курсоров

Вы получите документы с помощью findOne и переберёте курсор find, поняв, как MongoDB передаёт большие наборы результатов потоком.

Урок 2 из 413 шагов

«findOne и find: объяснение курсоров» — бесплатный урок MongoDB Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения MongoDB Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс MongoDB Academy содержит 4 уроков всего.

Два способа чтения документов

MongoDB предоставляет два основных метода чтения документов из коллекции:

  • findOne(filter, projection) — извлекает первый документ, соответствующий фильтру, и возвращает его как обычный объект документа (или null, если совпадений нет)
  • find(filter, projection) — извлекает все соответствующие документы и возвращает cursor — ленивый итератор, который передаёт результаты с сервера по одной порции за раз

Понимание того, когда использовать каждый метод и как работают курсоры, необходимо для написания эффективных запросов MongoDB.

findOne: просто и напрямую

findOne() — самый простой способ получить один документ. Он возвращает первый документ, соответствующий фильтру, или null, если ни один документ не подходит. Если совпадает несколько документов, MongoDB возвращает тот, который первым встретится во внутреннем порядке. Если нужен определённый документ, перед вызовом добавьте .sort().

Распространённые случаи использования findOne: поиск пользователя по адресу электронной почты, получение товара по SKU или проверка существования записи. Поскольку метод возвращает обычный объект, а не курсор, результат можно использовать напрямую, без перебора.

// 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 null

Что такое курсор

cursor — это указатель на набор результатов запроса. Когда Вы вызываете find(), MongoDB не передаёт клиенту сразу все подходящие документы. Вместо этого она открывает курсор на сервере и отправляет документы порциями (по умолчанию по 101 документу в порции). Клиент запрашивает следующую порцию только после того, как текущая исчерпана.

Такая конструкция очень важна для эффективного использования памяти. Если запрос соответствует 10 миллионам документов и Вы загрузите их все сразу, клиент завершится с ошибкой. С курсором документы обрабатываются по одной порции, поэтому потребление памяти остаётся постоянным независимо от размера набора результатов.

// 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 it

Перебор курсоров в Node.js

Курсоры драйвера Node.js поддерживают несколько способов перебора. Самый современный подход — for await...of (асинхронный перебор), который корректно обрабатывает обратное давление и ошибки. В качестве альтернативы можно использовать cursor.toArray(), загружающий все результаты в память. Это удобно, но опасно для больших наборов результатов.

Всегда закрывайте курсоры после завершения работы, если Вы досрочно прерываете перебор (например, найдя нужный результат). Открытый курсор удерживает ресурсы на сервере MongoDB. Явно вызывайте cursor.close() или полагайтесь на for await...of: он автоматически закрывает курсор после завершения цикла или при возникновении ошибки.

// 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));

Размер порции курсора и getMore

Внутренне протокол курсора работает в два этапа:

  1. Исходная команда find возвращает первую порцию (по умолчанию 101 документ или 16 MB — в зависимости от того, что наступит раньше)
  2. Каждая последующая порция извлекается с помощью команды getMore с использованием ID курсора

Размер порции можно настроить с помощью cursor.batchSize(n). Меньший размер порции снижает потребление памяти с обеих сторон, но требует большего числа сетевых обменов. Больший размер порции эффективнее при больших последовательных сканированиях. Значение по умолчанию обычно оптимально — изменяйте его только для конкретных рабочих нагрузок.

// 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);

Тайм-аут курсора и сессии

По умолчанию курсоры MongoDB прекращают работу после 10 минут бездействия на стороне сервера. Если обработка каждой порции занимает больше времени, курсор будет уничтожен, и при попытке получить следующую порцию Вы получите ошибку CursorNotFound.

Для длительных операций установите noCursorTimeout: true или используйте сессию, чтобы курсор оставался активным. Однако помните, что noCursorTimeout оставляет курсор на сервере открытым на неопределённый срок. Всегда явно закрывайте такие курсоры после завершения работы, чтобы избежать утечек ресурсов.

// 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();
}

Цепочка модификаторов в find()

Курсор, возвращаемый find(), поддерживает цепочку вызовов: перед началом перебора Вы можете последовательно вызывать методы, изменяющие запрос. Порядок важен для читаемости, но не для выполнения (MongoDB отправляет все модификаторы вместе):

  • .sort({ field: 1 }) — направление сортировки
  • .limit(n) — максимальное число документов
  • .skip(n) — пропустить первые n результатов
  • .projection({ field: 1 }) — выбрать поля
  • .hint({ index: 1 }) — принудительно использовать определённый индекс
  • .maxTimeMS(ms) — прервать выполнение, если запрос занимает слишком много времени
// 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 > 5s

Хвостовые курсоры для коллекций с ограниченным размером

Специальный тип курсора, называемый хвостовым курсором, работает только с коллекциями с ограниченным размером. В отличие от обычных курсоров, которые закрываются после получения всех результатов, хвостовой курсор блокируется и ждёт новые документы, подобно команде Unix tail -f для файла журнала.

Хвостовые курсоры были исходным механизмом потоковой передачи данных в реальном времени в MongoDB, до появления потоков изменений. Они по-прежнему полезны для лёгкого отслеживания журналов в коллекциях с ограниченным размером, когда потоки изменений были бы избыточны.

// 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 и find: правильный выбор

При выборе между findOne и find руководствуйтесь следующим правилом:

  • Используйте findOne, когда: Вы ожидаете ровно один результат (поиск по уникальному ключу), Вам нужно только проверить существование записи или нужен максимально простой код для конечной точки API, возвращающей одну запись
  • Используйте find, когда: запрос может вернуть ноль, один или несколько результатов; Вы создаёте конечную точку для списка; Вам нужен контроль над курсором (batchSize, maxTimeMS); или Вы обрабатываете результаты, не загружая их все в память

Избегайте find({}).toArray() для больших коллекций: этот вызов загружает все результаты в память. Вместо этого обрабатывайте их с помощью for await...of.

// 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!

Метод explain() для курсоров

Добавление .explain('executionStats') к курсору показывает, как MongoDB выполняет запрос, вместо возврата документов. Результат содержит следующие сведения:

  • winningPlan.stage: IXSCAN (используется индекс) или COLLSCAN (полное сканирование — плохо для больших коллекций)
  • nReturned: сколько документов было возвращено
  • totalDocsExamined: сколько документов MongoDB просмотрела в поисках результатов (при использовании индекса это число должно быть близко к nReturned)
  • executionTimeMillis: общее время выполнения

Регулярный запуск explain() для критически важных запросов — основа оптимизации производительности MongoDB.

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

Преобразование ObjectId в ответах API

Когда findOne или find().toArray() возвращают документы с полями ObjectId, эти ObjectId необходимо специально обработать перед отправкой в ответе JSON API. В старых версиях драйвера JSON.stringify сериализует ObjectId как объект {} (значение теряется), а в новых версиях — как его строковое представление.

Самый надёжный подход — явно вызвать .toString() для всех полей ObjectId в функции отображения перед отправкой данных клиенту. Затем клиенты отправляют ID обратно в виде строки, а на сервере перед выполнением запроса Вы преобразуете его с помощью new ObjectId(idString).

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: '...' }

Быстрая проверка

Проверьте своё понимание концепций MongoDB и баз данных NoSQL из этого урока.

Итоги урока

В этом уроке Вы узнали, что findOne напрямую возвращает один документ, а find возвращает курсор, который лениво передаёт результаты порциями, предотвращая проблемы с памятью при больших наборах результатов; куроры поддерживают цепочку вызовов — .sort(), .limit(), .skip(), .maxTimeMS() — и MongoDB отправляет их как один оптимизированный запрос; а explain('executionStats') показывает, использует ли запрос индекс (IXSCAN) или выполняет полное сканирование коллекции (COLLSCAN). Далее мы рассмотрим запросы к вложенным полям и массивам с помощью точечной нотации.

Можно начать бесплатно

Изучай JavaScript с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
30
Уроки
120

Часто задаваемые вопросы

Урок «findOne и find: объяснение курсоров» бесплатный?

Да — полный текст урока «findOne и find: объяснение курсоров» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс MongoDB Academy, подпишись на CoddyKit PRO. Курс MongoDB Academy содержит 4 уроков всего.

Чему я научусь в уроке «findOne и find: объяснение курсоров»?

Вы получите документы с помощью findOne и переберёте курсор find, поняв, как MongoDB передаёт большие наборы результатов потоком. Ты практикуешь MongoDB Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать MongoDB Academy?

Предыдущий опыт не требуется. MongoDB Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «findOne и find: объяснение курсоров»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке MongoDB Academy?

Да. Каждый урок MongoDB Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. insertOne и insertMany
  2. findOne и find: объяснение курсоров
  3. Запросы к вложенным полям и массивам
  4. Чтение документов с драйвером Node.js
← Назад к MongoDB Academy