MongoDB Academy · Lección

findOne frente a find: explicación de los cursores

Recuperará documentos mediante findOne y recorrerá un cursor de find, comprendiendo cómo MongoDB transmite grandes conjuntos de resultados.

Lección 2 de 413 pasos

findOne frente a find: explicación de los cursores es una lección gratuita de MongoDB Academy en CoddyKit. Esta es la lección 2 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de MongoDB Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de MongoDB Academy incluye 4 lecciones en total.

Dos formas de leer documentos

MongoDB proporciona dos métodos principales para leer documentos de una colección:

  • findOne(filter, projection) — recupera el primer documento que coincide con el filtro y lo devuelve como un objeto de documento simple (o null si no hay coincidencias)
  • find(filter, projection) — recupera todos los documentos coincidentes y devuelve un cursor, un iterador diferido que transmite los resultados del servidor un lote a la vez

Entender cuándo usar cada método y cómo funcionan los cursores es fundamental para escribir consultas eficientes en MongoDB.

findOne: sencillo y directo

findOne() es la forma más sencilla de recuperar un solo documento. Devuelve el primer documento que coincide con el filtro o null si no hay coincidencias. Si coinciden varios documentos, MongoDB devuelve el primero que encuentra según su orden interno; si necesita uno específico, añada un .sort() antes de realizar esta llamada.

Casos de uso habituales de findOne: buscar un usuario por correo electrónico, recuperar un producto por SKU o comprobar si existe un registro. Como devuelve un objeto simple en lugar de un cursor, puede usar el resultado directamente, sin iteración.

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

¿Qué es un cursor?

Un cursor es un puntero al conjunto de resultados de una consulta. Cuando llama a find(), MongoDB no transfiere inmediatamente al cliente todos los documentos coincidentes. En su lugar, abre un cursor en el servidor y envía los documentos en lotes (101 documentos por lote de forma predeterminada). El cliente obtiene el siguiente lote solo cuando se agota el lote actual.

Este diseño es fundamental para usar la memoria de forma eficiente. Si una consulta coincide con 10 millones de documentos y los carga todos a la vez, el cliente podría bloquearse. Con un cursor, procesa los documentos un lote a la vez y mantiene constante el uso de memoria, independientemente del tamaño del conjunto de resultados.

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

Iteración de cursores en Node.js

Los cursores del controlador de Node.js admiten varios patrones de iteración. El enfoque más moderno es for await...of (iteración asíncrona), que gestiona claramente la contrapresión y el manejo de errores. Otra opción es cursor.toArray(), que carga todos los resultados en memoria; es práctico, pero peligroso para conjuntos de resultados grandes.

Cierre siempre los cursores cuando termine si sale de la iteración antes de tiempo (por ejemplo, después de encontrar lo que necesita). Un cursor abierto mantiene recursos en el servidor de MongoDB. Use cursor.close() explícitamente o confíe en for await...of, que cierra automáticamente el cursor al completar el bucle o si se produce un 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));

Tamaño de lote del cursor y getMore

Internamente, el protocolo de cursores funciona en dos fases:

  1. El comando inicial find devuelve el primer lote (101 documentos o 16 MB de forma predeterminada, lo que ocurra primero)
  2. Cada lote posterior se obtiene mediante un comando getMore que usa el ID del cursor

Puede personalizar el tamaño del lote con cursor.batchSize(n). Un tamaño de lote menor reduce el uso de memoria en ambos extremos, pero requiere más intercambios de red. Un tamaño de lote mayor es más eficiente para exploraciones secuenciales grandes. El valor predeterminado suele ser óptimo; ajústelo solo para cargas de trabajo específicas.

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

Tiempo de espera del cursor y sesiones

De forma predeterminada, los cursores de MongoDB agotan el tiempo de espera tras 10 minutos de inactividad en el servidor. Si el procesamiento de cada lote tarda más que eso, el cursor se cerrará y recibirá un error CursorNotFound al intentar obtener el siguiente lote.

Para operaciones de larga duración, establezca noCursorTimeout: true o use una session para mantener activo el cursor. Sin embargo, tenga en cuenta que noCursorTimeout mantiene abierto indefinidamente un cursor del servidor; cierre siempre estos cursores explícitamente al terminar para evitar fugas de recursos.

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

Encadenamiento de modificadores en find()

El cursor que devuelve find() admite una API fluida: puede encadenar métodos para modificar la consulta antes de iniciar la iteración. El orden importa para la legibilidad, pero no para la ejecución (MongoDB envía todos los modificadores juntos):

  • .sort({ field: 1 }) — dirección de ordenación
  • .limit(n) — número máximo de documentos
  • .skip(n) — omitir los primeros n resultados
  • .projection({ field: 1 }) — seleccionar campos
  • .hint({ index: 1 }) — forzar un índice específico
  • .maxTimeMS(ms) — cancelar si la consulta tarda demasiado
// 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

Cursores persistentes para colecciones limitadas

Un tipo especial de cursor llamado cursor persistente funciona únicamente en colecciones limitadas. A diferencia de los cursores normales, que se cierran cuando se consumen todos los resultados, un cursor persistente se bloquea y espera nuevos documentos, de forma similar al comando tail -f de Unix aplicado a un archivo de registro.

Los cursores persistentes fueron el mecanismo original para transmitir datos en tiempo real en MongoDB, antes de la introducción de Change Streams. Siguen siendo útiles para seguir registros de forma ligera en colecciones limitadas cuando Change Streams sería excesivo.

// 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 frente a find: cómo elegir correctamente

Use esta regla general para elegir entre findOne y find:

  • Use findOne cuando espere exactamente un resultado (búsqueda mediante una clave única), solo necesite comprobar la existencia o quiera el código más sencillo para un endpoint de API de un solo registro
  • Use find cuando la consulta pueda devolver cero, uno o muchos resultados; esté creando un endpoint de listado; necesite controlar el cursor (batchSize, maxTimeMS); o esté procesando resultados sin cargarlos todos en memoria

Evite find({}).toArray() en colecciones grandes, ya que carga todos los resultados en memoria. En su lugar, procéselos con 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!

El método explain() en los cursores

Añadir .explain('executionStats') a un cursor muestra cómo MongoDB ejecuta la consulta en lugar de devolver documentos. El resultado revela:

  • winningPlan.stage: IXSCAN (usa un índice) o COLLSCAN (exploración completa, deficiente en colecciones grandes)
  • nReturned: cuántos documentos se devolvieron
  • totalDocsExamined: cuántos documentos examinó MongoDB para encontrar los resultados (debería ser cercano a nReturned si se usa un índice)
  • executionTimeMillis: tiempo total de ejecución

Ejecutar periódicamente explain() en sus consultas críticas es la base para optimizar el rendimiento de 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)

Conversión de ObjectId en las respuestas de la API

Cuando findOne o find().toArray() devuelven documentos con campos ObjectId, esos ObjectId requieren un tratamiento especial antes de incluirlos en la respuesta JSON de una API. En versiones antiguas del controlador, JSON.stringify serializa un ObjectId como un objeto {} (por lo que se pierde el valor), mientras que en versiones más recientes lo serializa como su representación de cadena.

La opción más segura es llamar explícitamente a .toString() en cualquier campo ObjectId dentro de una función de mapeo antes de enviarlo al cliente. Después, los clientes envían el ID como una cadena y, en el servidor, usted lo convierte con new ObjectId(idString) antes de consultar.

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

Comprobación rápida

Compruebe su comprensión de los conceptos de MongoDB y las bases de datos NoSQL de esta lección.

Repaso de la lección

En esta lección aprendió que findOne devuelve directamente un solo documento, mientras que find devuelve un cursor que transmite los resultados de forma diferida en lotes para evitar problemas de memoria con conjuntos de resultados grandes; los cursores admiten una API fluida de encadenamiento —.sort(), .limit(), .skip(), .maxTimeMS()— que MongoDB envía como una única consulta optimizada; y explain('executionStats') revela si una consulta usa un índice (IXSCAN) o una exploración completa de la colección (COLLSCAN). A continuación, profundizaremos en las consultas de campos anidados y arreglos mediante notación de puntos.

Gratis para empezar

Aprende JavaScript con un tutor de IA — gratis

Escribe y ejecuta código real en tu navegador, obtén ayuda instantánea de un tutor de IA disponible 24/7 y continúa donde lo dejaste en la web o en la aplicación.

Cursos
30
Lecciones
120

Preguntas frecuentes

¿La lección «findOne frente a find: explicación de los cursores» es gratis?

Sí — el texto completo de «findOne frente a find: explicación de los cursores» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de MongoDB Academy, actualiza a CoddyKit PRO. El curso de MongoDB Academy incluye 4 lecciones en total.

¿Qué aprenderé en «findOne frente a find: explicación de los cursores»?

Recuperará documentos mediante findOne y recorrerá un cursor de find, comprendiendo cómo MongoDB transmite grandes conjuntos de resultados. Practicas MongoDB Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar MongoDB Academy?

No se requiere experiencia previa. MongoDB Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 2 de 4.

¿Cuánto tiempo toma la lección «findOne frente a find: explicación de los cursores»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de MongoDB Academy?

Sí. Cada lección de MongoDB Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. insertOne e insertMany
  2. findOne frente a find: explicación de los cursores
  3. Consultar campos anidados y arrays
  4. Leer documentos con el controlador de Node.js
← Volver a MongoDB Academy