web-dev-qa-db-fra.com

Rechercher tous les documents en double dans une collection MongoDB par un champ clé

Supposons que j'ai une collection avec un ensemble de documents. quelque chose comme ça.

{ "_id" : ObjectId("4f127fa55e7242718200002d"), "id":1, "name" : "foo"}
{ "_id" : ObjectId("4f127fa55e7242718200002d"), "id":2, "name" : "bar"}
{ "_id" : ObjectId("4f127fa55e7242718200002d"), "id":3, "name" : "baz"}
{ "_id" : ObjectId("4f127fa55e7242718200002d"), "id":4, "name" : "foo"}
{ "_id" : ObjectId("4f127fa55e7242718200002d"), "id":5, "name" : "bar"}
{ "_id" : ObjectId("4f127fa55e7242718200002d"), "id":6, "name" : "bar"}

Je veux trouver toutes les entrées dupliquées dans cette collection par le champ "nom". Par exemple. "foo" apparaît deux fois et "bar" apparaît 3 fois.

51
Fraz

Remarque: cette solution est la plus simple à comprendre, mais pas la meilleure.

Vous pouvez utiliser mapReduce pour savoir combien de fois un document contient un certain champ:

var map = function(){
   if(this.name) {
        emit(this.name, 1);
   }
}

var reduce = function(key, values){
    return Array.sum(values);
}

var res = db.collection.mapReduce(map, reduce, {out:{ inline : 1}});
db[res.result].find({value: {$gt: 1}}).sort({value: -1});
17
ggreiner

La réponse acceptée est terriblement lente sur les grandes collections et ne renvoie pas le _ids des enregistrements en double.

L'agrégation est beaucoup plus rapide et peut renvoyer le _ids:

db.collection.aggregate([
  { $group: {
    _id: { name: "$name" },   // replace `name` here twice
    uniqueIds: { $addToSet: "$_id" },
    count: { $sum: 1 } 
  } }, 
  { $match: { 
    count: { $gte: 2 } 
  } },
  { $sort : { count : -1} },
  { $limit : 10 }
]);

Dans la première étape du pipeline d'agrégation, l'opérateur $ group agrège les documents par le champ name et stocke dans uniqueIds chaque _id valeur des enregistrements groupés. L'opérateur $ sum additionne les valeurs des champs qui lui sont passés, dans ce cas la constante 1 - comptant ainsi le nombre d'enregistrements groupés dans le champ count.

Dans la deuxième étape du pipeline, nous utilisons $ match pour filtrer les documents avec un count d'au moins 2, c'est-à-dire des doublons.

Ensuite, nous trions d'abord les doublons les plus fréquents et limitons les résultats aux 10 premiers.

Cette requête générera jusqu'à $limit enregistrements avec des noms en double, avec leur _ids. Par exemple:

{
  "_id" : {
    "name" : "Toothpick"
},
  "uniqueIds" : [
    "xzuzJd2qatfJCSvkN",
    "9bpewBsKbrGBQexv4",
    "fi3Gscg9M64BQdArv",
  ],
  "count" : 3
},
{
  "_id" : {
    "name" : "Broom"
  },
  "uniqueIds" : [
    "3vwny3YEj2qBsmmhA",
    "gJeWGcuX6Wk69oFYD"
  ],
  "count" : 2
}
146
expert

Pour une solution générique Mongo, consultez la recette de livre de recettes MongoDB pour trouver des doublons à l'aide de group . Notez que l'agrégation est plus rapide et plus puissante dans la mesure où elle peut renvoyer les _id S des enregistrements en double.

Pour pymongo , la réponse acceptée (en utilisant mapReduce) n'est pas aussi efficace. Au lieu de cela, nous pouvons utiliser la méthode group :

$connection = 'mongodb://localhost:27017';
$con        = new Mongo($connection); // mongo db connection

$db         = $con->test; // database 
$collection = $db->prb; // table

$keys       = array("name" => 1); Select name field, group by it

// set intial values
$initial    = array("count" => 0);

// JavaScript function to perform
$reduce     = "function (obj, prev) { prev.count++; }";

$g          = $collection->group($keys, $initial, $reduce);

echo "<pre>";
print_r($g);

La sortie sera la suivante:

Array
(
    [retval] => Array
        (
            [0] => Array
                (
                    [name] => 
                    [count] => 1
                )

            [1] => Array
                (
                    [name] => MongoDB
                    [count] => 2
                )

        )

    [count] => 3
    [keys] => 2
    [ok] => 1
)

La requête SQL équivalente serait: SELECT name, COUNT(name) FROM prb GROUP BY name. Notez que nous devons toujours filtrer les éléments avec un nombre de 0 dans le tableau. Encore une fois, reportez-vous à la recette de livre de recettes MongoDB pour trouver des doublons en utilisant group pour la solution canonique en utilisant group.

5
Prasanth Bendra

J'ai trouvé des informations utiles sur le blog officiel du laboratoire mongo: http://blog.mongolab.com/2014/03/finding-duplicate-keys-with-the-mongodb-aggregation-framework/

2
Krunal Shah

La réponse la plus élevée acceptée ici a ceci:

uniqueIds: { $addToSet: "$_id" },

Cela vous renverrait également un nouveau champ appelé uniqueIds avec une liste d'id. Mais que faire si vous voulez juste le champ et son nombre? Alors ce serait ceci:

db.collection.aggregate([ 
  {$group: { _id: {name: "$name"}, 
             count: {$sum: 1} } }, 
  {$match: { count: {"$gt": 1} } } 
]);

Pour expliquer cela, si vous venez de bases de données SQL comme MySQL et PostgreSQL, vous avez l'habitude d'agréger des fonctions (par exemple COUNT (), SUM (), MIN (), MAX ()) qui fonctionnent avec l'instruction GROUP BY vous permettant, pour par exemple, pour trouver le nombre total qu'une valeur de colonne apparaît dans un tableau.

SELECT COUNT(*), my_type FROM table GROUP BY my_type;
+----------+-----------------+
| COUNT(*) | my_type         |
+----------+-----------------+
|        3 | Contact         |
|        1 | Practice        |
|        1 | Prospect        |
|        1 | Task            |
+----------+-----------------+

Comme vous pouvez le voir, notre sortie indique le nombre d'apparitions de chaque valeur my_type. Pour trouver des doublons dans MongoDB, nous aborderions le problème de la même manière. MongoDB propose des opérations d'agrégation, qui regroupent les valeurs de plusieurs documents et peuvent effectuer diverses opérations sur les données groupées pour retourner un seul résultat. C'est un concept similaire pour agréger des fonctions en SQL.

En supposant une collection appelée contacts, la configuration initiale se présente comme suit:

db.contacts.aggregate([ ... ]);

Cette fonction d'agrégation prend un tableau d'opérateurs d'agrégation, et dans notre cas, nous souhaitons l'opérateur $ group, car notre objectif est de regrouper les données par le nombre de champs, c'est-à-dire le nombre d'occurrences de la valeur de champ.

db.contacts.aggregate([  
    {$group: { 
        _id: {name: "$name"} 
        } 
    }
]);

Il y a un peu de particularité dans cette approche. Le champ _id est requis pour utiliser le groupe par opérateur. Dans ce cas, nous regroupons le champ $ name. Le nom de clé dans _id peut avoir n'importe quel nom. Mais nous utilisons le nom car il est intuitif ici.

En exécutant l'agrégation en utilisant uniquement l'opérateur $ group, nous obtiendrons une liste de tous les champs de nom (indépendamment du fait qu'ils apparaissent une ou plusieurs fois dans la collection):

db.contacts.aggregate([  
  {$group: { 
    _id: {name: "$name"} 
    } 
  }
]);

{ "_id" : { "name" : "John" } }
{ "_id" : { "name" : "Joan" } }
{ "_id" : { "name" : "Stephen" } }
{ "_id" : { "name" : "Rod" } }
{ "_id" : { "name" : "Albert" } }
{ "_id" : { "name" : "Amanda" } }

Remarquez ci-dessus comment fonctionne l'agrégation. Il a pris des documents avec des champs de nom et retourne une nouvelle collection des champs de nom extraits.

Mais ce que nous voulons savoir, c'est combien de fois la valeur du champ réapparaît. L'opérateur $ group prend un champ count qui utilise l'opérateur $ sum pour ajouter l'expression 1 au total de chaque document du groupe. Ainsi, $ group et $ sum ensemble renvoient la somme collective de toutes les valeurs numériques qui résultent pour un champ donné (par exemple nom).

db.contacts.aggregate([  
  {$group: { 
    _id: {name: "$name"},
    count: {$sum: 1}
    } 
  }
]);

{ "_id" : { "name" : "John" },  "count" : 1  }
{ "_id" : { "name" : "Joan" },  "count" : 3  }
{ "_id" : { "name" : "Stephen" },  "count" : 2 }
{ "_id" : { "name" : "Rod" },  "count" : 3 }
{ "_id" : { "name" : "Albert" },  "count" : 2 }
{ "_id" : { "name" : "Amanda" },  "count" : 1 }

Étant donné que l'objectif était d'éliminer les doublons, cela nécessite une étape supplémentaire. Pour obtenir uniquement les groupes dont le nombre est supérieur à un, nous pouvons utiliser l'opérateur $ match pour filtrer nos résultats. Dans l'opérateur $ match, nous lui dirons de regarder le champ count et lui dirons de rechercher des nombres supérieurs à un en utilisant l'opérateur $ gt représentant "supérieur à" et le nombre 1.

db.contacts.aggregate([ 
  {$group: { _id: {name: "$name"}, 
             count: {$sum: 1} } }, 
  {$match: { count: {"$gt": 1} } } 
]);

{ "_id" : { "name" : "Joan" },  "count" : 3  }
{ "_id" : { "name" : "Stephen" },  "count" : 2 }
{ "_id" : { "name" : "Rod" },  "count" : 3 }
{ "_id" : { "name" : "Albert" },  "count" : 2 }

En remarque, si vous utilisez MongoDB via un ORM comme Mongoid pour Ruby, vous pourriez obtenir cette erreur:

The 'cursor' option is required, except for aggregate with the explain argument 

Cela signifie très probablement que votre ORM est obsolète et effectue des opérations que MongoDB ne prend plus en charge. Par conséquent, mettez à jour votre ORM ou trouvez un correctif. Pour Mongoid, c'était la solution pour moi:

module Moped
  class Collection
    # Mongo 3.6 requires a `cursor` option be passed as part of aggregate queries.  This overrides
    # `Moped::Collection#aggregate` to include a cursor, which is not provided by Moped otherwise.
    #
    # Per the [MongoDB documentation](https://docs.mongodb.com/manual/reference/command/aggregate/):
    #
    #   Changed in version 3.6: MongoDB 3.6 removes the use of `aggregate` command *without* the `cursor` option unless
    #   the command includes the `explain` option. Unless you include the `explain` option, you must specify the
    #   `cursor` option.
    #
    #   To indicate a cursor with the default batch size, specify `cursor: {}`.
    #
    #   To indicate a cursor with a non-default batch size, use `cursor: { batchSize: <num> }`.
    #
    def aggregate(*pipeline)
      # Ordering of keys apparently matters to Mongo -- `aggregate` has to come before `cursor` here.
      extract_result(session.command(aggregate: name, pipeline: pipeline.flatten, cursor: {}))
    end

    private

    def extract_result(response)
      response.key?("cursor") ? response["cursor"]["firstBatch"] : response["result"]
    end
  end
end
1
Donato