Existe-t-il un moyen d'effectuer efficacement l'équivalent de DENSE_RANK dans MongoDB?

8

SQL Server et Oracle ont tous deux des fonctions DENSE_RANK. Existe-t-il un moyen de faire quelque chose de similaire dans MongoDB sans avoir à recourir à MapReduce? En d'autres termes, supposons que vous ayez une clause de sélection T-SQL comme celle-ci:

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

Quelle est la meilleure façon de faire la même chose dans MongoDB?

(Remarque: il s'agit d'une rediffusion de la question MongoDB ici . J'espère obtenir plus de commentaires des administrateurs de base de données ...)

kgriffs
la source
Question audacieuse, en effet. Si vous trouvez les réponses à vos questions MongoDB satisfaisantes ici dans le DBA.SE, veuillez en informer les autres pour apporter leurs questions et réponses ici également. +1 !!!
RolandoMySQLDBA

Réponses:

5

MongoDB n'a aucun concept de classement. Le plus proche que j'ai pu trouver vient d' ici :

Voici quelques exemples de données:

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

Tout d'abord, trouvez le score de l'utilisateur "dave":

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

Ensuite, comptez combien d'utilisateurs ont un score plus élevé:

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

Puisqu'il y a 1 score plus élevé, le rang de dave est 2 (il suffit d'ajouter 1 au nombre de scores plus élevés pour obtenir le rang).

Évidemment, c'est loin d'être idéal. Cependant, MongoDB n'a tout simplement aucun type de fonctionnalité pour cela car il n'est tout simplement pas conçu pour ce type d'interrogation.

Richard
la source
2
En fait, il a la fonctionnalité via MapReduce, c'est juste lent.
kgriffs
@Kurt Oh, tu devrais poster ça comme réponse! Les internets l'apprécieraient vraiment, j'en suis sûr. ;)
Richard
5

Après quelques expérimentations, j'ai trouvé qu'il était possible de construire une fonction de classement basée sur MapReduce, en supposant que l'ensemble de résultats peut tenir dans la taille maximale du document.

Par exemple, supposons que j'ai une collection comme celle-ci:

{ 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" }
...

Je peux effectuer l'équivalent approximatif d'un DENSE_RANK comme ceci:

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 titre de comparaison, voici l'approche "naïve" mentionnée ailleurs:

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;
}

J'ai comparé les deux approches sur une seule instance de MongoDB 1.8.2 en utilisant le code suivant:

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;
}

Alors que MapReduce était plus rapide que ce à quoi je m'attendais, l'approche naïve l'a fait sortir de l'eau pour des tailles de collection plus importantes, en particulier une fois le cache réchauffé:

> 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 ]

Donc pour l'instant, il semble que l'approche naïve soit la voie à suivre, bien que je serai intéressé de voir si l'histoire change plus tard cette année alors que l'équipe MongoDB continue d'améliorer les performances de MapReduce.

kgriffs
la source