Agregación de MongoDB: ¿Cómo obtener el recuento total de registros?


102

He utilizado la agregación para obtener registros de mongodb.

$result = $collection->aggregate(array(
  array('$match' => $document),
  array('$group' => array('_id' => '$book_id', 'date' => array('$max' => '$book_viewed'),  'views' => array('$sum' => 1))),
  array('$sort' => $sort),
  array('$skip' => $skip),
  array('$limit' => $limit),
));

Si ejecuto esta consulta sin límite, se recuperarán 10 registros. Pero quiero mantener el límite en 2. Así que me gustaría obtener el recuento total de registros. ¿Cómo puedo hacer con la agregación? Por favor, avíseme. Gracias


¿Cómo se verían los resultados si solo hubiera 2?
WiredPrairie

Eche un vistazo a $ facet Esto puede ayudar a stackoverflow.com/questions/61812361/…
Soham

Respuestas:


101

Esta es una de las preguntas más frecuentes para obtener el resultado paginado y el número total de resultados simultáneamente en una sola consulta. No puedo explicar cómo me sentí cuando finalmente lo logré LOL.

$result = $collection->aggregate(array(
  array('$match' => $document),
  array('$group' => array('_id' => '$book_id', 'date' => array('$max' => '$book_viewed'),  'views' => array('$sum' => 1))),
  array('$sort' => $sort),

// get total, AND preserve the results
  array('$group' => array('_id' => null, 'total' => array( '$sum' => 1 ), 'results' => array( '$push' => '$$ROOT' ) ),
// apply limit and offset
  array('$project' => array( 'total' => 1, 'results' => array( '$slice' => array( '$results', $skip, $length ) ) ) )
))

El resultado se verá así:

[
  {
    "_id": null,
    "total": ...,
    "results": [
      {...},
      {...},
      {...},
    ]
  }
]

8
Documentación sobre esto: docs.mongodb.com/v3.2/reference/operator/aggregation/group/… ... tenga en cuenta que con este enfoque, todo el conjunto de resultados no paginados debe caber en 16 MB.
btown

7
¡Esto es oro puro! Estaba pasando por un infierno tratando de hacer que esto funcionara.
Henrique Miranda

4
Gracias amigo ! Solo necesito { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}}(insertar después {$group:{}}para el recuento total de la búsqueda.
Liberateur

1
¿Cómo se aplica el límite a los resultados establecidos? Los resultados ahora son una matriz anidada
valen

1
Mi vida está completa ahora, puedo morir feliz
Jack

83

Desde la versión 3.4 (creo), MongoDB tiene ahora un nuevo operador de canalización de agregación llamado ' faceta ' que, en sus propias palabras:

Procesa múltiples canalizaciones de agregación dentro de una sola etapa en el mismo conjunto de documentos de entrada. Cada sub-pipeline tiene su propio campo en el documento de salida donde sus resultados se almacenan como una matriz de documentos.

En este caso particular, esto significa que uno puede hacer algo como esto:

$result = $collection->aggregate([
  { ...execute queries, group, sort... },
  { ...execute queries, group, sort... },
  { ...execute queries, group, sort... },
  $facet: {
    paginatedResults: [{ $skip: skipPage }, { $limit: perPage }],
    totalCount: [
      {
        $count: 'count'
      }
    ]
  }
]);

El resultado será (con por ejemplo 100 resultados totales):

[
  {
    "paginatedResults":[{...},{...},{...}, ...],
    "totalCount":[{"count":100}]
  }
]

13
Esto funciona muy bien, a partir de 3.4 esta debería ser la respuesta aceptada
Adam Reis

Para convertir un resultado tan amplio en un objeto simple de dos campos, ¿necesito otro $project?
SerG

1
esta debe ser ahora la respuesta aceptada. funcionó a las mil maravillas.
Arootin Aghazaryan

9
Esta debería ser la respuesta aceptada hoy. Sin embargo, encontré problemas de rendimiento al usar la paginación con $ facet. La otra respuesta votada también tiene problemas de rendimiento con $ slice. Encontré que era mejor $ skip y $ limit en la tubería y hacer una llamada separada para el recuento. Probé esto contra conjuntos de datos bastante grandes.
Jpepper

59

Use esto para encontrar el recuento total en la colección resultante.

db.collection.aggregate( [
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] );

3
Gracias. Pero, he usado "vistas" en mi codificación para obtener el recuento del recuento del grupo correspondiente (es decir, grupo 1 => 2 registros, grupo 3 => 5 registros, etc.). Quiero obtener el recuento de registros (es decir, total: 120 registros). Espero que lo hayas entendido ...
user2987836

34

Puede usar la función toArray y luego obtener su longitud para el recuento total de registros.

db.CollectionName.aggregate([....]).toArray().length

1
Si bien esto puede no funcionar como una solución "adecuada", me ayudó a depurar algo, funciona, incluso si no es una solución al 100%.
Johann Marx

3
Ésta no es una solución real.
Furkan Başaran

1
TypeError: Parent.aggregate(...).toArray is not a functioneste es el error que di con esta solución.
Mohammad Hossein Shojaeinia

Gracias. Esto es lo que estaba buscando.
skvp

Esto buscará todos los datos agregados y luego devolverá la longitud de esa matriz. no es una buena práctica. en su lugar, puede agregar {$ count: 'count'} en la canalización de agregación
Aslam Shaik

19

Utilice la etapa de canalización de agregación $ count para obtener el recuento total de documentos:

Consulta :

db.collection.aggregate(
  [
    {
      $match: {
        ...
      }
    },
    {
      $group: {
        ...
      }
    },
    {
      $count: "totalCount"
    }
  ]
)

Resultado:

{
   "totalCount" : Number of records (some integer value)
}

Esto funciona como un encanto, pero en cuanto al rendimiento, ¿es bueno?
ana.arede

Solución limpia. Gracias
skvp

13

Lo hice de esta manera:

db.collection.aggregate([
     { $match : { score : { $gt : 70, $lte : 90 } } },
     { $group: { _id: null, count: { $sum: 1 } } }
] ).map(function(record, index){
        print(index);
 });

El agregado devolverá la matriz, así que simplemente repítala y obtén el índice final.

Y otra forma de hacerlo es:

var count = 0 ;
db.collection.aggregate([
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] ).map(function(record, index){
        count++
 }); 
print(count);

fwiw no necesita la vardeclaración ni la mapllamada. Las primeras 3 líneas de su primer ejemplo son suficientes.
Madbreaks

7

La solución proporcionada por @Divergent funciona, pero en mi experiencia es mejor tener 2 consultas:

  1. Primero para filtrar y luego agrupar por ID para obtener la cantidad de elementos filtrados. No filtre aquí, es innecesario.
  2. Segunda consulta que filtra, ordena y pagina.

La solución al presionar $$ ROOT y usar $ slice tiene una limitación de memoria de documentos de 16 MB para colecciones grandes. Además, para colecciones grandes, dos consultas juntas parecen ejecutarse más rápido que la que tiene $$ ROOT presionando. También puede ejecutarlos en paralelo, por lo que está limitado solo por la más lenta de las dos consultas (probablemente la que ordena).

Me he conformado con esta solución usando 2 consultas y un marco de agregación (nota: uso node.js en este ejemplo, pero la idea es la misma):

var aggregation = [
  {
    // If you can match fields at the begining, match as many as early as possible.
    $match: {...}
  },
  {
    // Projection.
    $project: {...}
  },
  {
    // Some things you can match only after projection or grouping, so do it now.
    $match: {...}
  }
];


// Copy filtering elements from the pipeline - this is the same for both counting number of fileter elements and for pagination queries.
var aggregationPaginated = aggregation.slice(0);

// Count filtered elements.
aggregation.push(
  {
    $group: {
      _id: null,
      count: { $sum: 1 }
    }
  }
);

// Sort in pagination query.
aggregationPaginated.push(
  {
    $sort: sorting
  }
);

// Paginate.
aggregationPaginated.push(
  {
    $limit: skip + length
  },
  {
    $skip: skip
  }
);

// I use mongoose.

// Get total count.
model.count(function(errCount, totalCount) {
  // Count filtered.
  model.aggregate(aggregation)
  .allowDiskUse(true)
  .exec(
  function(errFind, documents) {
    if (errFind) {
      // Errors.
      res.status(503);
      return res.json({
        'success': false,
        'response': 'err_counting'
      });
    }
    else {
      // Number of filtered elements.
      var numFiltered = documents[0].count;

      // Filter, sort and pagiante.
      model.request.aggregate(aggregationPaginated)
      .allowDiskUse(true)
      .exec(
        function(errFindP, documentsP) {
          if (errFindP) {
            // Errors.
            res.status(503);
            return res.json({
              'success': false,
              'response': 'err_pagination'
            });
          }
          else {
            return res.json({
              'success': true,
              'recordsTotal': totalCount,
              'recordsFiltered': numFiltered,
              'response': documentsP
            });
          }
      });
    }
  });
});

5
//const total_count = await User.find(query).countDocuments();
//const users = await User.find(query).skip(+offset).limit(+limit).sort({[sort]: order}).select('-password');
const result = await User.aggregate([
  {$match : query},
  {$sort: {[sort]:order}},
  {$project: {password: 0, avatarData: 0, tokens: 0}},
  {$facet:{
      users: [{ $skip: +offset }, { $limit: +limit}],
      totalCount: [
        {
          $count: 'count'
        }
      ]
    }}
  ]);
console.log(JSON.stringify(result));
console.log(result[0]);
return res.status(200).json({users: result[0].users, total_count: result[0].totalCount[0].count});

1
Por lo general, es una buena práctica incluir texto explicativo junto con un código de respuesta.

3

Esto podría funcionar para múltiples condiciones de partido

            const query = [
                {
                    $facet: {
                    cancelled: [
                        { $match: { orderStatus: 'Cancelled' } },
                        { $count: 'cancelled' }
                    ],
                    pending: [
                        { $match: { orderStatus: 'Pending' } },
                        { $count: 'pending' }
                    ],
                    total: [
                        { $match: { isActive: true } },
                        { $count: 'total' }
                    ]
                    }
                },
                {
                    $project: {
                    cancelled: { $arrayElemAt: ['$cancelled.cancelled', 0] },
                    pending: { $arrayElemAt: ['$pending.pending', 0] },
                    total: { $arrayElemAt: ['$total.total', 0] }
                    }
                }
                ]
                Order.aggregate(query, (error, findRes) => {})

2

Necesitaba el recuento total absoluto después de aplicar la agregación. Esto funcionó para mí:

db.mycollection.aggregate([
    {
        $group: { 
            _id: { field1: "$field1", field2: "$field2" },
        }
    },
    { 
        $group: { 
            _id: null, count: { $sum: 1 } 
        } 
    }
])

Resultado:

{
    "_id" : null,
    "count" : 57.0
}

2

Aquí hay algunas formas de obtener el recuento total de registros mientras se realiza la agregación de MongoDB:


  • Usando $count:

    db.collection.aggregate([
       // Other stages here
       { $count: "Total" }
    ])

    Para obtener 1000 registros, esto toma en promedio 2 ms y es la forma más rápida.


  • Usando .toArray():

    db.collection.aggregate([...]).toArray().length

    Para obtener 1000 registros, esto lleva una media de 18 ms.


  • Usando .itcount():

    db.collection.aggregate([...]).itcount()

    Para obtener 1000 registros, esto lleva una media de 14 ms.



0

Si no desea agrupar, utilice el siguiente método:

db.collection.aggregate( [ { $match : { score : { $gt : 70, $lte : 90 } } }, { $count: 'count' } ] );


Creo que la persona que hace la pregunta quiere agruparse, según el tema.
mjaggard
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.