¿Hay alguna manera de realizar eficientemente el equivalente de DENSE_RANK en MongoDB?

8

SQL Server y Oracle tienen funciones DENSE_RANK. ¿Hay alguna manera de hacer algo similar en MongoDB sin tener que recurrir a MapReduce? En otras palabras, suponga que tiene una cláusula de selección T-SQL como esta:

SELECT DENSE_RANK() OVER(ORDER BY SomeField DESC) SomeRank

¿Cuál es la mejor manera de hacer lo mismo en MongoDB?

(Nota: Esta es una nueva publicación de la pregunta de MongoDB aquí . Espero recibir más comentarios de los DBA ...)

kgriffs
fuente
Pregunta audaz, de hecho. Si encuentra las respuestas a sus preguntas de MongoDB satisfactorias aquí en el DBA.SE, por favor, avíseles a los demás para que también traigan sus preguntas y respuestas aquí. +1 !!!
RolandoMySQLDBA

Respuestas:

5

MongoDB no tiene ningún concepto de clasificación. Lo más cercano que pude encontrar proviene de aquí :

Aquí hay algunos datos de muestra:

 > db.scoreboard.find()`
 { "_id" : ObjectId("4d99f71450f0ae2165669ea9"), "user" : "dave", "score" : 4 }
 { "_id" : ObjectId("4d99f71b50f0ae2165669eaa"), "user" : "steve", "score" : 5 }`
 { "_id" : ObjectId("4d99f72350f0ae2165669eab"), "user" : "tom", "score" : 3 }

Primero, encuentre el puntaje del usuario "dave":

 db.scoreboard.find({ user : "dave" }, { score : 1 }) { "_id" : ObjectId("4d99f71450f0ae2165669ea9"), "score" : 4 }

Luego, cuente cuántos usuarios tienen una puntuación más alta:

 db.scoreboard.find({ score : { $gt : 4 }}).count() 
 1

Como hay 1 puntaje más alto, el rango de dave es 2 (solo agregue 1 al recuento de puntajes más altos para obtener el rango).

Obviamente, esto está lejos de ser ideal. Sin embargo, MongoDB simplemente no tiene ningún tipo de funcionalidad para esto, ya que simplemente no está diseñado para este tipo de consultas.

Ricardo
fuente
2
En realidad, tiene la funcionalidad a través de MapReduce, es lento.
kgriffs
@ Kurt ¡Oh, deberías publicar eso como respuesta! Los internet realmente lo apreciarían, estoy seguro. ;)
Richard
5

Después de experimentar un poco, descubrí que es posible crear una función de clasificación basada en MapReduce, suponiendo que el conjunto de resultados puede caber en el tamaño máximo del documento.

Por ejemplo, supongamos que tengo una colección como esta:

{ player: "joe", points: 1000, foo: 10, bar: 20, bang: "some text" }
{ player: "susan", points: 2000, foo: 10, bar: 20, bang: "some text" }
{ player: "joe", points: 1500, foo: 10, bar: 20, bang: "some text" }
{ player: "ben", points: 500, foo: 10, bar: 20, bang: "some text" }
...

Puedo realizar el equivalente aproximado de un DENSE_RANK así:

var m = function() { 
  ++g_counter; 

  if ((this.player == "joe") && (g_scores.length != g_fake_limit)) { 
    g_scores.push({
      player: this.player, 
      points: this.points, 
      foo: this.foo,
      bar: this.bar,
      bang: this.bang,
      rank: g_counter
    });   
  }

  if (g_counter == g_final)
  {
    emit(this._id, g_counter);
  }
}}


var r = function (k, v) { }
var f = function(k, v) { return g_scores; }

var test_mapreduce = function (limit) {
  var total_scores = db.scores.count();

  return db.scores.mapReduce(m, r, {
    out: { inline: 1 }, 
    sort: { points: -1 }, 
    finalize: f, 
    limit: total_scores, 
    verbose: true,
    scope: {
      g_counter: 0, 
      g_final: total_scores, 
      g_fake_limit: limit, 
      g_scores:[]
    }
  }).results[0].value;
}

A modo de comparación, aquí está el enfoque "ingenuo" mencionado en otra parte:

var test_naive = function(limit) {
  var cursor = db.scores.find({player: "joe"}).limit(limit).sort({points: -1});
  var scores = [];

  cursor.forEach(function(score) {
    score.rank = db.scores.count({points: {"$gt": score.points}}) + 1;
    scores.push(score);
  });

  return scores;
}

Comparé ambos enfoques en una sola instancia de MongoDB 1.8.2 usando el siguiente código:

var rand = function(max) {
  return Math.floor(Math.random() * max);
}

var create_score = function() {
  var names = ["joe", "ben", "susan", "kevin", "lucy"]
  return { player: names[rand(names.length)], points: rand(1000000), foo: 10, bar: 20, bang: "some kind of example text"};
}

var init_collection = function(total_records) {
  db.scores.drop();

  for (var i = 0; i != total_records; ++i) {
    db.scores.insert(create_score());
  }

  db.scores.createIndex({points: -1})
}


var benchmark = function(test, count, limit) {
  init_collection(count);

  var durations = [];
  for (var i = 0; i != 5; ++i) {
    var start = new Date;
    result = test(limit)
    var stop = new Date;

    durations.push(stop - start);
  }

  db.scores.drop();

  return durations;
}

Si bien MapReduce fue más rápido de lo que esperaba, el enfoque ingenuo lo dejó fuera del agua para tamaños de colección más grandes, especialmente una vez que el caché se calentó:

> benchmark(test_naive, 1000, 50);
[ 22, 16, 17, 16, 17 ]
> benchmark(test_mapreduce, 1000, 50);
[ 16, 15, 14, 11, 14 ]
> 
> benchmark(test_naive, 10000, 50);
[ 56, 16, 17, 16, 17 ]
> benchmark(test_mapreduce, 10000, 50);
[ 154, 109, 116, 109, 109 ]
> 
> benchmark(test_naive, 100000, 50);
[ 492, 15, 18, 17, 16 ]
> benchmark(test_mapreduce, 100000, 50);
[ 1595, 1071, 1099, 1108, 1070 ]
> 
> benchmark(test_naive, 1000000, 50);
[ 6600, 16, 15, 16, 24 ]
> benchmark(test_mapreduce, 1000000, 50);
[ 17405, 10725, 10768, 10779, 11113 ]

Entonces, por ahora, parece que el enfoque ingenuo es el camino a seguir, aunque me interesará ver si la historia cambia más adelante este año a medida que el equipo de MongoDB continúa mejorando el rendimiento de MapReduce.

kgriffs
fuente