Existe alguma maneira de executar com eficiência o equivalente a DENSE_RANK no MongoDB?

8

O SQL Server e o Oracle têm funções DENSE_RANK. Existe uma maneira de fazer algo semelhante no MongoDB sem precisar recorrer ao MapReduce? Em outras palavras, suponha que você tenha uma cláusula de seleção T-SQL como esta:

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

Qual é a melhor maneira de fazer a mesma coisa no MongoDB?

(Nota: esta é uma repostagem da pergunta do MongoDB por aqui . Espero obter mais feedback dos DBAs ...)

kgriffs
fonte
Pergunta ousada, de fato. Se você achar as respostas para suas perguntas do MongoDB satisfatórias aqui no DBA.SE, informe os outros para trazer suas perguntas e respostas também aqui. +1 !!!
RolandoMySQLDBA

Respostas:

5

MongoDB não tem nenhum conceito de classificação. O mais próximo que pude encontrar vem daqui :

Aqui estão alguns dados de exemplo:

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

Primeiro, encontre a pontuação do usuário "dave":

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

Em seguida, conte quantos usuários têm uma pontuação mais alta:

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

Como há 1 pontuação mais alta, a classificação de dave é 2 (basta adicionar 1 à contagem de pontuações mais altas para obter a classificação).

Obviamente, isso está longe de ser o ideal. No entanto, o MongoDB simplesmente não possui nenhum tipo de funcionalidade para isso, pois simplesmente não foi projetado para esse tipo de consulta.

Richard
fonte
2
Na verdade, ele tem a funcionalidade via MapReduce, é apenas lento.
precisa saber é o seguinte
@ Kurt Oh, você deve postar isso como resposta! Os internets realmente apreciariam, tenho certeza. ;)
Richard
5

Após algumas experiências, descobri que é possível criar uma função de classificação com base no MapReduce, assumindo que o conjunto de resultados possa caber no tamanho máximo do documento.

Por exemplo, suponha que eu tenha uma coleção 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" }
...

Eu posso executar o equivalente aproximado de um DENSE_RANK da seguinte forma:

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

Para comparação, aqui está a abordagem "ingênua" mencionada em outro lugar:

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

Comparei as duas abordagens em uma única instância do MongoDB 1.8.2 usando o seguinte 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;
}

Embora o MapReduce tenha sido mais rápido do que eu esperava, a abordagem ingênua expulsou-a da água para tamanhos de coleta maiores, especialmente depois que o cache foi aquecido:

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

Portanto, por enquanto, parece que a abordagem ingênua é o caminho a seguir, embora eu esteja interessado em ver se a história muda ainda este ano, pois a equipe do MongoDB continua melhorando o desempenho do MapReduce.

kgriffs
fonte