Obtenga un recuento del total de documentos con MongoDB al usar el límite

Estoy interesado en optimizar una solución de "paginación" en la que estoy trabajando con MongoDB. Mi problema es sencillo. Usualmente limito el número de documentos devueltos usando el limit() funcionalidad. Esto me obliga a emitir una consulta redundante sin la limit() para que yo también capture la cantidad total de documentos en la consulta para poder pasar eso al cliente y hacerle saber que tendrá que emitir una solicitud adicional para recuperar el resto de los documentos.

¿Hay alguna manera de condensar esto en 1 consulta? Obtenga el número total de documentos pero al mismo tiempo solo recupere un subconjunto usando limit()? ¿Hay una manera diferente de pensar sobre este problema que yo lo estoy abordando?

preguntado el 15 de febrero de 14 a las 20:02

He tenido este escenario y he escrito el enfoque como un artículo para que otros lo usen aquí. siendonin.medium.com/… -

16 Respuestas

mongodb 3.4 ha introducido $facet agregación

que procesa múltiples canalizaciones de agregación dentro de una sola etapa en el mismo conjunto de documentos de entrada.

Usar $facet y $group puede encontrar documentos con $limit y puede obtener el recuento total.

Puede usar la siguiente agregación en mongodb 3.4

db.collection.aggregate([
  { "$facet": {
    "totalData": [
      { "$match": { }},
      { "$skip": 10 },
      { "$limit": 10 }
    ],
    "totalCount": [
      { "$group": {
        "_id": null,
        "count": { "$sum": 1 }
      }}
    ]
  }}
])

Incluso tu puedes usar $count agregación que se ha introducido en mongodb 3.6.

Puede usar la siguiente agregación en mongodb 3.6

db.collection.aggregate([
  { "$facet": {
    "totalData": [
      { "$match": { }},
      { "$skip": 10 },
      { "$limit": 10 }
    ],
    "totalCount": [
      { "$count": "count" }
    ]
  }}
])

respondido 09 nov., 18:11

mira la implementación siendonin.medium.com/… - siendonin

Si desea obtener un recuento total de los datos después de que haya ocurrido la $coincidencia, coloque la $coincidencia antes de la $faceta para lograrlo. - hedley smith

Esta respuesta explica lo mismo pero mucho más claro. - teuber789

Esta respuesta devuelve el recuento total correctamente, pero no devuelve el totalData - Equipo Rigin

Con respecto al rendimiento: la etapa $facet y sus canalizaciones secundarias no pueden usar índices, incluso si sus canalizaciones secundarias usan $match o si $facet es la primera etapa de la canalización. La etapa $facet siempre realizará un COLLSCAN durante la ejecución. - prisacari dimitrii

No, No hay otra manera. Dos consultas, una para contar, una con límite. O tienes que usar una base de datos diferente. Apache Solr, por ejemplo, funciona como usted quiere. Cada consulta allí es limitada y devuelve totalCount.

Respondido el 08 de diciembre de 15 a las 08:12

No estoy seguro de si "No" es la respuesta correcta ahora que tenemos mongoDb 3.4. Ver stackoverflow.com/a/39784851/3654061 - Felipe

Hay varias formas de hacer esto, ya que yo mismo he estado buscando una solución. Puede crear una operación de agregación para devolver el recuento total y los documentos completos según una condición. También puede hacer un findAll basado en condiciones. Almacene la longitud de esa matriz. Y luego divida los valores de acuerdo con sus valores de límite/compensación. Ambas opciones son solo una llamada a la base de datos. El costo de la agregación depende de cuán compleja sea, al igual que el segmento que ejecuta en la matriz devuelta. ¿Pensamientos sobre esto? - sam gruse

¿Qué tal esta respuesta? stackoverflow.com/a/56693959 para mí parece funcionar. En comparación con la agregación con un límite de 100 documentos, se ejecuta incluso un poco (~ 2-3 ms) más rápido en promedio para mí... - sznrbrt

MongoDB te permite usar cursor.count() incluso cuando pasas limit() or skip().

Digamos que tienes un db.collection con 10 artículos.

Tu puedes hacer:

async function getQuery() {
  let query = await db.collection.find({}).skip(5).limit(5); // returns last 5 items in db
  let countTotal = await query.count() // returns 10-- will not take `skip` or `limit` into consideration
  let countWithConstraints = await query.count(true) // returns 5 -- will take into consideration `skip` and `limit`
  return { query, countTotal } 
}

Respondido el 31 de enero de 18 a las 20:01

¿Qué hay de agregado? - Mahmud Heretani

El mejor para mí, odio las agregaciones ^^. Encuentro esta manera más simple y legible. - TOPKAT

.skip(5).limit(5) NO devuelve los últimos 5 elementos en la base de datos. Devuelve el segundo grupo de 5 elementos. count() siempre devolverá 10 sin importar cuántos artículos haya, siempre que haya al menos 10. - walter tross

¿Por qué countTotal y CountWithConstraints esperan una promesa? - Ricky-U

La versión 4.4 de Mongo y la versión 4 del cliente de nodo mongo no muestran el recuento total de elementos. - Amit Kumar

He aquí cómo hacer esto con MongoDB 3.4+ (con Mangosta) usando $facets. Este ejemplo devuelve un $count basado en los documentos después de han sido emparejados.

const facetedPipeline = [{
    "$match": { "dateCreated": { $gte: new Date('2021-01-01') } },
    "$project": { 'exclude.some.field': 0 },
  },
  {
    "$facet": {
      "data": [
        { "$skip": 10 },
        { "$limit": 10 }
      ],
      "pagination": [
        { "$count": "total" }
      ]
    }
  }
];

const results = await Model.aggregate(facetedPipeline);

Este patrón es útil para obtener información de paginación para devolver desde una API REST.

Referencia: MongoDB $ faceta

respondido 25 mar '21, 09:03

Tenga en cuenta que cuando hace la coincidencia primero en la canalización y la faceta, puede alcanzar los índices. No puede acceder a los índices desde $facet - Willem van der Veen

Los tiempos han cambiado, y creo que puede lograr lo que pide el OP utilizando la agregación con $sort, $group y $project. Para mi sistema, también necesitaba obtener información de usuario de mi users recopilación. Con suerte, esto también puede responder cualquier pregunta al respecto. A continuación se muestra una tubería de agregación. Los últimos tres objetos (ordenar, agrupar y proyectar) son los que manejan obtener el recuento total y luego brindan capacidades de paginación.

db.posts.aggregate([
  { $match: { public: true },
  { $lookup: {
    from: 'users',
    localField: 'userId',
    foreignField: 'userId',
    as: 'userInfo'
  } },
  { $project: {
    postId: 1,
    title: 1,
    description: 1
    updated: 1,
    userInfo: {
      $let: {
        vars: {
          firstUser: {
            $arrayElemAt: ['$userInfo', 0]
          }
        },
        in: {
          username: '$$firstUser.username'
        }
      }
    }
  } },
  { $sort: { updated: -1 } },
  { $group: {
    _id: null,
    postCount: { $sum: 1 },
    posts: {
      $push: '$$ROOT'
    }
  } },
  { $project: {
    _id: 0,
    postCount: 1,
    posts: {
      $slice: [
        '$posts',
        currentPage ? (currentPage - 1) * RESULTS_PER_PAGE : 0,
        RESULTS_PER_PAGE
      ]
    }
  } }
])

Respondido 01 Jul 17, 07:07

Cuál será la respuesta a esta consulta. ¿Devolverá el recuento y el resultado? Kumar

@Kumar sí, el conteo se calcula durante $group usando $sum y el resultado de la matriz proviene de $push. Puede ver en $project que incluyo el recuento de publicaciones (postCount) y luego tomo solo una sección de la matriz de resultados usando $slice. La respuesta final devuelve el número total de publicaciones junto con solo una sección de ellas para la paginación. - prueba bien

hay una manera en Mongodb 3.4: $ faceta

tu puedes hacer

db.collection.aggregate([
  {
    $facet: {
      data: [{ $match: {} }],
      total: { $count: 'total' }
    }
  }
])

entonces podrá ejecutar dos agregados al mismo tiempo

Respondido 15 Oct 17, 18:10

Solo un pequeño total de actualización debe ser una matriz como total: [{ $count: 'total' }] - pachlangia sunil

no funciona con la etapa $sort, obtiene un resultado inesperado. El problema solo existe si se usa $facet. - Alok Deshwal

Por defecto, el método count() ignora los efectos de cursor.skip() y cursor.limit() (Documentos de MongoDB)

Como el método de conteo excluye los efectos de límite y omisión, puede usar cursor.count() para obtener el conteo total

 const cursor = await database.collection(collectionName).find(query).skip(offset).limit(limit)
 return {
    data: await cursor.toArray(),
    count: await cursor.count() // this will give count of all the documents before .skip() and limit()
 };

Respondido el 04 de enero de 20 a las 13:01

Todo depende de la experiencia de paginación que necesite en cuanto a si necesita o no hacer dos consultas.

¿Necesita enumerar cada página o incluso un rango de páginas? ¿Alguien va a la página 1051? Conceptualmente, ¿qué significa eso realmente?

Ha habido un montón de UX en patrones de paginación - Evite los dolores de la paginación cubre varios tipos de paginación y sus escenarios y muchos no necesitan una consulta de conteo para saber si hay una página siguiente. Por ejemplo, si muestra 10 elementos en una página y limita a 13, sabrá si hay otra página.

Respondido 17 Feb 14, 09:02

MongoDB ha introducido un nuevo método para obtener solo el recuento de los documentos que coinciden con una consulta determinada y funciona de la siguiente manera:

const result = await db.collection('foo').count({name: 'bar'});
console.log('result:', result) // prints the matching doc count

Receta para uso en paginación:

const query = {name: 'bar'};
const skip = (pageNo - 1) * pageSize; // assuming pageNo starts from 1
const limit = pageSize;

const [listResult, countResult] = await Promise.all([
  db.collection('foo')
    .find(query)
    .skip(skip)
    .limit(limit),

  db.collection('foo').count(query)
])

return {
  totalCount: countResult,
  list: listResult
}

Para obtener más detalles sobre la visita db.collection.count esta página

Respondido 29 Oct 20, 13:10

It is posible obtener el tamaño total del resultado sin el efecto de limit() usar count() como se responde aquí: ¿Limitando los resultados en MongoDB pero aún obteniendo el conteo completo?

De acuerdo con la documentación, incluso puede controlar si se tiene en cuenta el límite/paginación al llamar count(): https://docs.mongodb.com/manual/reference/method/cursor.count/#cursor.count

Editar: en contraste con lo que está escrito en otros lugares, los documentos establecen claramente que "La operación no realiza la consulta, sino que cuenta los resultados que devolvería la consulta". Lo que, según tengo entendido, significa que solo se ejecuta una consulta.

Ejemplo:

> db.createCollection("test")
{ "ok" : 1 }

> db.test.insert([{name: "first"}, {name: "second"}, {name: "third"}, 
{name: "forth"}, {name: "fifth"}])
BulkWriteResult({
    "writeErrors" : [ ],
    "writeConcernErrors" : [ ],
    "nInserted" : 5,
    "nUpserted" : 0,
    "nMatched" : 0,
    "nModified" : 0,
    "nRemoved" : 0,
    "upserted" : [ ]
})

> db.test.find()
{ "_id" : ObjectId("58ff00918f5e60ff211521c5"), "name" : "first" }
{ "_id" : ObjectId("58ff00918f5e60ff211521c6"), "name" : "second" }
{ "_id" : ObjectId("58ff00918f5e60ff211521c7"), "name" : "third" }
{ "_id" : ObjectId("58ff00918f5e60ff211521c8"), "name" : "forth" }
{ "_id" : ObjectId("58ff00918f5e60ff211521c9"), "name" : "fifth" }

> db.test.count()
5

> var result = db.test.find().limit(3)
> result
{ "_id" : ObjectId("58ff00918f5e60ff211521c5"), "name" : "first" }
{ "_id" : ObjectId("58ff00918f5e60ff211521c6"), "name" : "second" }
{ "_id" : ObjectId("58ff00918f5e60ff211521c7"), "name" : "third" }

> result.count()
5 (total result size of the query without limit)

> result.count(1)
3 (result size with limit(3) taken into account)

contestado el 23 de mayo de 17 a las 13:05

Si vota a la baja, agregue un motivo para que tenga la oportunidad de entender, ¡lo que también podría mejorar las respuestas futuras! - mrechtien

No estoy seguro sobre el voto negativo, pero solo para tu información: count() solo funciona con find() y por lo tanto no es útil con aggregate consultas - Felipe

Prueba como abajo:

cursor.count(falso, función(err, total){ console.log("total", total) })

core.db.users.find(query, {}, {skip:0, limit:1}, function(err, cursor){
    if(err)
        return callback(err);

    cursor.toArray(function(err, items){
        if(err)
            return callback(err);

        cursor.count(false, function(err, total){
            if(err)
                return callback(err);

            console.log("cursor", total)

            callback(null, {items: items, total:total})
        })
    })
 })

Respondido 28 Oct 17, 15:10

Se pensó en proporcionar una precaución al usar el agregado para la paginación. Es mejor usar dos consultas para esto si los usuarios usan la API con frecuencia para obtener datos. Esto es al menos 50 veces más rápido que obtener los datos utilizando agregados en un servidor de producción cuando más usuarios acceden al sistema en línea. El agregado y la faceta $ son más adecuados para Dashboard, informes y trabajos cron que se llaman con menos frecuencia.

Respondido el 28 de junio de 20 a las 18:06

Podemos hacerlo usando 2 consultas.

    const limit = parseInt(req.query.limit || 50, 10);
    let page = parseInt(req.query.page || 0, 10);
    if (page > 0) { page = page - 1}

    let doc = await req.db.collection('bookings').find().sort( { _id: -1 }).skip(page).limit(limit).toArray();
    let count = await req.db.collection('bookings').find().count();
    res.json({data: [...doc], count: count});

respondido 20 nov., 21:15

Tomé el enfoque de dos consultas, y el siguiente código se tomó directamente de un proyecto en el que estoy trabajando, usando MongoDB Atlas y un índice de búsqueda de texto completo:

return new Promise( async (resolve, reject) => {
  try {

    const search = {
      $search: {
        index: 'assets',
        compound: { 
          should: [{
            text: {
              query: args.phraseToSearch,
              path: [
                'title', 'note'
              ]
            }
          }]
        }
      }
    }

    const project = {
      $project: {
        _id: 0,
        id: '$_id',
        userId: 1,
        title: 1,
        note: 1,
        score: {
          $meta: 'searchScore'
        }
      }
    }

    const match = {
      $match: {
        userId: args.userId
      }
    }

    const skip = {
      $skip: args.skip
    }

    const limit = {
      $limit: args.first
    }

    const group = {
      $group: {
        _id: null,
        count: { $sum: 1 }
      }
    }

    const searchAllAssets = await Models.Assets.schema.aggregate([
      search, project, match, skip, limit
    ])

    const [ totalNumberOfAssets ] = await Models.Assets.schema.aggregate([
      search, project, match, group
    ])

    return await resolve({
      searchAllAssets: searchAllAssets,
      totalNumberOfAssets: totalNumberOfAssets.count
    })

  } catch (exception) {
    return reject(new Error(exception))
  }
})

Respondido el 10 de junio de 22 a las 09:06

Tuve el mismo problema y encontré esta pregunta. La solución correcta a este problema está publicada. aquí.

Respondido 21 Oct 22, 09:10

Puede hacer esto en una consulta. Primero ejecuta un conteo y dentro de eso ejecuta la función limit().

En Node.js y Express.js, tendrá que usarlo así para poder usar la función "contar" junto con el "resultado" de toArray.

var curFind = db.collection('tasks').find({query});

Luego puede ejecutar dos funciones después de esto (una anidada en la otra)

curFind.count(function (e, count) {

// Use count here

    curFind.skip(0).limit(10).toArray(function(err, result) {

    // Use result here and count here

    });

});

Respondido el 05 de Septiembre de 16 a las 00:09

Este no es el método correcto. Solo está buscando en todos los documentos en lugar de los primeros 10 documentos en cada solicitud. Para cada solicitud, siempre está buscando en documentos completos. no en los primeros 10. - Udit Kumawat

gracias por el comentario. en ese momento, esta es una solución que se nos ocurrió. puede que no sea perfecto cuando se trata de eficiencia. sugerir una solución para improvisar. - Vibhu Tewary

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.