MongoDB: marco de agregación: obtener el último documento por ID de grupo

Quiero obtener el último documento para cada estación con todos los demás campos:

{ "_id" : ObjectId("535f5d074f075c37fff4cc74"), "station" : "OR", "t" : 86, "dt" : ISODate("2014-04-29T08:02:57.165Z") } { "_id" : ObjectId("535f5d114f075c37fff4cc75"), "station" : "OR", "t" : 82, "dt" : ISODate("2014-04-29T08:02:57.165Z") } { "_id" : ObjectId("535f5d364f075c37fff4cc76"), "station" : "WA", "t" : 79, "dt" : ISODate("2014-04-29T08:02:57.165Z") } 

Necesito tener t y la estación para el último dt por estación. Con el marco de agregación:

 db.temperature.aggregate([{$sort:{"dt":1}},{$group:{"_id":"$station", result:{$last:"$dt"}, t:{$last:"$t"}}}]) 

devoluciones

 { "result" : [ { "_id" : "WA", "result" : ISODate("2014-04-29T08:02:57.165Z"), "t" : 79 }, { "_id" : "OR", "result" : ISODate("2014-04-29T08:02:57.165Z"), "t" : 82 } ], "ok" : 1 } 

¿Es esta la manera más eficiente de hacer eso?

Gracias

Para responder directamente a su pregunta, sí, es la forma más eficiente. Pero creo que debemos aclarar por qué esto es así.

Como se sugirió en las alternativas, lo único que la gente está mirando es “clasificar” los resultados antes de pasar a la etapa $group y lo que están viendo es el valor “timestamp”, por lo que querrá asegurarse de que todo esté en orden orden de “marca de tiempo”, por lo tanto, la forma:

 db.temperature.aggregate([ { "$sort": { "station": 1, "dt": -1 } }, { "$group": { "_id": "$station", "result": { "$first":"$dt"}, "t": {"$first":"$t"} }} ]) 

Y como se dijo, por supuesto querrá un índice para reflejar eso con el fin de hacer que el tipo sea eficiente:

Sin embargo, y este es el verdadero punto. Lo que parece haber sido pasado por alto por otros (si no es así para usted) es que todos estos datos probablemente se inserten ya en orden cronológico, en el sentido de que cada lectura se registra como añadida.

Entonces, la belleza de esto es que el campo _id (con un ObjectId predeterminado) ya está en orden de “indicación de fecha y hora”, ya que realmente contiene un valor de tiempo y esto hace que la statement sea posible:

 db.temperature.aggregate([ { "$group": { "_id": "$station", "result": { "$last":"$dt"}, "t": {"$last":"$t"} }} ]) 

Y es más rápido. ¿Por qué? Bueno, no es necesario seleccionar un índice (código adicional para invocar) tampoco es necesario “cargar” el índice además del documento.

Ya sabemos que los documentos están en orden (por _id ) por lo que los $last límites son perfectamente válidos. Está escaneando todo de todos modos, y también podría “rango” consulta en los valores _id como igualmente válido para entre dos fechas.

Lo único que se puede decir aquí es que, en el uso del “mundo real”, podría ser más práctico para usted $match rangos de fechas al hacer este tipo de acumulación en lugar de obtener el “primero” y el “último” _id valores para definir un “rango” o algo similar en su uso real.

Entonces, ¿dónde está la prueba de esto? Bueno, es bastante fácil de reproducir, así que lo hice generando algunos datos de muestra:

 var stations = [ "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY" ]; for ( i=0; i<200000; i++ ) { var station = stations[Math.floor(Math.random()*stations.length)]; var t = Math.floor(Math.random() * ( 96 - 50 + 1 )) +50; dt = new Date(); db.temperatures.insert({ station: station, t: t, dt: dt }); } 

En mi hardware (computadora portátil de 8GB con disco espía, que no es estelar, pero ciertamente adecuado) ejecutar cada forma de la statement muestra claramente una pausa notable con la versión usando un índice y una ordenación (las mismas claves en el índice que la statement de clasificación). Es solo una pausa menor, pero la diferencia es lo suficientemente significativa como para notarlo.

Incluso al mirar el resultado de la explicación (versión 2.6 y superior, o en realidad está en 2.4.9 aunque no está documentado) puede ver la diferencia en eso, aunque el $sort está optimizado debido a la presencia de un índice, el tiempo empleado parece estar con la selección del índice y luego cargando las entradas indexadas. Incluir todos los campos para una consulta de índice "cubierto" no hace diferencia.

También para el registro, la indexación pura de la fecha y la única clasificación de los valores de fecha da el mismo resultado. Posiblemente ligeramente más rápido, pero aún más lento que el índice natural sin el género.

Por lo tanto, siempre que pueda "rango" feliz en los primeros y últimos valores _id , entonces es cierto que usar el índice natural en el orden de inserción es realmente la forma más eficiente de hacerlo. Su millaje en el mundo real puede variar dependiendo de si esto es práctico para usted o no, y podría terminar siendo más conveniente para implementar el índice y ordenar la fecha.

Pero si estuvo contento con el uso de rangos de _id o más que el "último" _id en su consulta, entonces tal vez un ajuste para obtener los valores junto con sus resultados para que de hecho pueda almacenar y usar esa información en consultas sucesivas:

 db.temperature.aggregate([ // Get documents "greater than" the "highest" _id value found last time { "$match": { "_id": { "$gt": ObjectId("536076603e70a99790b7845d") } }}, // Do the grouping with addition of the returned field { "$group": { "_id": "$station", "result": { "$last":"$dt"}, "t": {"$last":"$t"}, "lastDoc": { "$last": "$_id" } }} ]) 

Y si realmente estaba "siguiendo" los resultados de esa manera, entonces puede determinar el valor máximo de ObjectId partir de sus resultados y usarlo en la próxima consulta.

De todos modos, diviértete jugando con eso, pero de nuevo Sí, en este caso esa consulta es la manera más rápida.

Un índice es todo lo que realmente necesitas:

 db.temperature.ensureIndex({ 'station': 1, 'dt': 1 }) for s in db.temperature.distinct('station'): db.temperature.find({ station: s }).sort({ dt : -1 }).limit(1) 

por supuesto, usar la syntax es realmente válida para tu idioma.

Editar: Tiene razón en que un ciclo como este incurre en un viaje de ida y vuelta por estación, y es genial para algunas estaciones, y no tan bueno para 1000. Sin embargo, todavía quiere el índice compuesto en la estación + dt, y para tomar ventaja de un tipo descendente:

 db.temperature.aggregate([ { $sort: { station: 1, dt: -1 } }, { $group: { _id: "$station", result: {$first:"$dt"}, t: {$first:"$t"} } } ]) 

En cuanto a la consulta de agregación que ha publicado, me aseguraré de que tenga un índice en dt:

 db.temperature.ensureIndex({'dt': 1 }) 

Esto asegurará que $ sort al comienzo de la canalización de agregación sea lo más eficiente posible.

En cuanto a si esta es o no la forma más eficiente de obtener estos datos, frente a una consulta en un bucle, es probable que sea una función de cuántos puntos de datos tiene. Al principio, con “miles de estaciones” y tal vez cientos de miles de puntos de datos, creo que el enfoque de agregación será más rápido.

Sin embargo, a medida que agrega más y más datos, un problema es que la consulta de agregación continuará tocando todos los documentos. Esto se volverá cada vez más costoso a medida que aumente la escala de millones o más de documentos. Un enfoque para ese caso sería agregar un $ limit justo después de $ sort para limitar el número total de documentos que se están considerando. Eso es un poco raro e inexacto, pero ayudaría a limitar la cantidad total de documentos a los que se debe acceder.