MongoDB select count (distinto x) en una columna indexada: cuente resultados únicos para grandes conjuntos de datos

82

He revisado varios artículos y ejemplos, y todavía tengo que encontrar una forma eficiente de hacer esta consulta SQL en MongoDB (donde hay millones de filas documentos)

Primer intento

(por ejemplo, de esta pregunta casi duplicada: ¿ equivalente de Mongo de SELECT DISTINCT de SQL? )

db.myCollection.distinct("myIndexedNonUniqueField").length

Obviamente recibí este error porque mi conjunto de datos es enorme

Thu Aug 02 12:55:24 uncaught exception: distinct failed: {
        "errmsg" : "exception: distinct too big, 16mb cap",
        "code" : 10044,
        "ok" : 0
}

Segundo intento

Decidí intentar hacer un grupo

db.myCollection.group({key: {myIndexedNonUniqueField: 1},
                initial: {count: 0}, 
                 reduce: function (obj, prev) { prev.count++;} } );

Pero recibí este mensaje de error en su lugar:

exception: group() can't handle more than 20000 unique keys

Tercer intento

No lo he probado todavía, pero hay varias sugerencias que implican mapReduce

p.ej

también

Parece que hay una solicitud de extracción en GitHub que corrige el .distinctmétodo para mencionar que solo debería devolver un recuento, pero aún está abierto: https://github.com/mongodb/mongo/pull/34

Pero en este punto pensé que vale la pena preguntar aquí, ¿qué es lo último sobre el tema? ¿Debo pasar a SQL u otra base de datos NoSQL para distintos recuentos? o hay una forma eficiente?

Actualizar:

Este comentario sobre los documentos oficiales de MongoDB no es alentador, ¿es cierto?

http://www.mongodb.org/display/DOCS/Aggregation#comment-430445808

Actualización2:

Parece que el nuevo Aggregation Framework responde al comentario anterior ... (MongoDB 2.1 / 2.2 y superior, vista previa de desarrollo disponible, no para producción)

http://docs.mongodb.org/manual/applications/aggregation/

Eran Medan
fuente
Supongo que debe hacer esto con frecuencia o el rendimiento no importaría tanto. En ese caso, almacenaría los valores distintos en una colección separada que se actualiza cuando inserta un nuevo documento en lugar de intentar hacer una diferencia en una colección tan grande. O eso o volvería a evaluar mi uso de MongoDb y posiblemente pasaría a otra cosa. Como descubrió, MongoDb actualmente no es bueno en lo que está tratando de hacer.
Tim Gautier
@TimGautier gracias, tenía miedo, me tomó horas insertar todos esos valores, y debería haberlo pensado antes :) Creo que dedicaré el tiempo ahora a insertarlo en MySQL para esas estadísticas ...
Eran Medan
También puede hacer un MR incremental básicamente emulando la indexación delta de datos agregados. Quiero decir, depende de cuándo necesita los resultados en cuanto a lo que usa. Puedo imaginar que MySQL probablemente obtendría una gran cantidad de IO y qué no al hacer esto (puedo matar un servidor pequeño distinguiendo solo 100k documentos en línea en un índice) pero supongo que es más flexible en la consulta de este tipo de cosas todavía .
Sammaye
No estoy de acuerdo con que Mongo no sea bueno en este tipo de cosas. Este tipo de cosas es en lo que sobresale Mongo.
superluminario
1
Desafortunadamente, el moderador eliminó mi respuesta que también publiqué en la pregunta duplicada. No puedo eliminarlo allí y volver a publicarlo aquí, por lo tanto, enlace: stackoverflow.com/a/33418582/226895
experto

Respuestas:

75

1) La forma más sencilla de hacerlo es a través del marco de agregación. Esto requiere dos comandos "$ group": el primero agrupa por valores distintos, el segundo cuenta todos los valores distintos

pipeline = [ 
    { $group: { _id: "$myIndexedNonUniqueField"}  },
    { $group: { _id: 1, count: { $sum: 1 } } }
];

//
// Run the aggregation command
//
R = db.runCommand( 
    {
    "aggregate": "myCollection" , 
    "pipeline": pipeline
    }
);
printjson(R);

2) Si desea hacer esto con Map / Reduce, puede hacerlo. Este también es un proceso de dos fases: en la primera fase creamos una nueva colección con una lista de cada valor distinto para la clave. En el segundo hacemos un recuento () de la nueva colección.

var SOURCE = db.myCollection;
var DEST = db.distinct
DEST.drop();


map = function() {
  emit( this.myIndexedNonUniqueField , {count: 1});
}

reduce = function(key, values) {
  var count = 0;

  values.forEach(function(v) {
    count += v['count'];        // count each distinct value for lagniappe
  });

  return {count: count};
};

//
// run map/reduce
//
res = SOURCE.mapReduce( map, reduce, 
    { out: 'distinct', 
     verbose: true
    }
    );

print( "distinct count= " + res.counts.output );
print( "distinct count=", DEST.count() );

Tenga en cuenta que no puede devolver el resultado del mapa / reducir en línea, porque eso potencialmente sobrepasará el límite de tamaño del documento de 16 MB. Usted puede ahorrar el cálculo de una colección y luego contar () el tamaño de la colección, o puede obtener el número de resultados a partir del valor de retorno de MapReduce ().

William Z
fuente
5
Descargué Mongo 2.2 RC0 y utilicé su primera sugerencia, ¡y funciona! ¡y rápido! gracias (bien hecho 10gen ...) Creé una esencia aquí (usé el comando agregado de acceso directo y lo puso en una línea) gist.github.com/3241616
Eran Medan
@EranMedan Debo advertirle, sin embargo, no sugerí el marco de agregación porque 2.2 rc0 todavía no está realmente listo para la implementación completa, solo algo a tener en cuenta, esperaría hasta la versión completa de 2.2 antes de recomendar la implementación de la agregación marco de referencia.
Sammaye
@Sammaye sí, gracias, soy consciente de ello, no entrará en producción todavía, lo necesitaba para estadísticas internas y quería evitar mover datos a SQL si es posible (y saciar mi curiosidad)
Eran Medan
¿Por qué Mongo no acepta: this.plugins.X-Powered-By.string? ¿Cómo escaparé de esto?
EarlyPoster
Me pregunto si esta respuesta es confiable para un entorno fragmentado. Según tengo entendido, los fragmentos harán su propia agregación y luego devolverán el resultado donde se agregarán los resultados. Entonces, en este escenario, ¿no tendríamos la oportunidad de que existan duplicados ya que los valores distintos se han perdido en la segunda $groupdeclaración antes de ser devueltos a mongos?
Verran
37
db.myCollection.aggregate( 
   {$group : {_id : "$myIndexedNonUniqueField"} }, 
   {$group: {_id:1, count: {$sum : 1 }}});

directo al resultado:

db.myCollection.aggregate( 
   {$group : {_id : "$myIndexedNonUniqueField"} }, 
   {$group: {_id:1, count: {$sum : 1 }}})
   .result[0].count;
Stackee007
fuente
1
Bien, eso es mejor. ¿Pero no es esa la misma respuesta que William ya dio?
JohnnyHK
2
Similar, pero me gusta el hecho de que está en una sola línea. Sin embargo, recibí un error: "No se puede leer la propiedad '0' de indefinido" Elimine la última línea y funciona a la perfección.
Nico
y si hablamos de una base de datos realmente enorme, no olvide {allowDiskUse: true} entonces, db.myCollection.aggregate ([{$ group ..}, {$ group:}], {allowDiskUse: true}). resultado [ 0] .count;
hi_artem
3

La siguiente solución funcionó para mí

db.test.distinct ('usuario'); ["alex", "Inglaterra", "Francia", "Australia"]

db.countries.distinct ('país'). longitud 4

Munib mir
fuente