Найти все дубликаты документов в коллекции MongoDB по ключевому полю - PullRequest
51 голосов
/ 29 февраля 2012

Предположим, у меня есть коллекция с некоторым набором документов. как то так.

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

Я хочу найти все дублированные записи в этой коллекции по полю "имя". Например. «foo» появляется дважды, а «bar» появляется 3 раза.

Ответы [ 5 ]

144 голосов
/ 12 августа 2013

Принятый ответ ужасно медленен для больших коллекций и не возвращает _id s дублирующихся записей.

Агрегирование выполняется намного быстрее и может возвращать _id s:

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

На первом этапе конвейера агрегирования оператор $ group агрегирует документы по полю name и сохраняет в uniqueIds каждое значение _id сгруппированных записей.Оператор $ sum складывает значения переданных ему полей, в данном случае константу 1, подсчитывая таким образом количество сгруппированных записей в поле count.

На втором этапе конвейера мы используем $ match для фильтрации документов с count не менее 2, то есть дубликатов.

Затем сначала сортируем наиболее часто встречающиеся дубликаты,и ограничьте результаты до верхних 10.

Этот запрос выведет до $limit записей с дублирующимися именами вместе с их _id s.Например:

{
  "_id" : {
    "name" : "Toothpick"
},
  "uniqueIds" : [
    "xzuzJd2qatfJCSvkN",
    "9bpewBsKbrGBQexv4",
    "fi3Gscg9M64BQdArv",
  ],
  "count" : 3
},
{
  "_id" : {
    "name" : "Broom"
  },
  "uniqueIds" : [
    "3vwny3YEj2qBsmmhA",
    "gJeWGcuX6Wk69oFYD"
  ],
  "count" : 2
}
17 голосов
/ 29 февраля 2012

Примечание: это решение является самым простым для понимания, но не лучшим.

Вы можете использовать mapReduce, чтобы узнать, сколько раз документ содержит определенное поле:

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});
5 голосов
/ 11 февраля 2013

Типовое решение Mongo можно найти в рецепте поваренной книги MongoDB для поиска дубликатов с использованием group.Обратите внимание, что агрегация происходит быстрее и мощнее, поскольку она может возвращать _id s дублированных записей.

Для принятый ответ (с использованием mapReduce) не так эффективен.Вместо этого мы можем использовать метод 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);

Вывод будет следующим:

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

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

        )

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

Эквивалентный запрос SQL будет: SELECT name, COUNT(name) FROM prb GROUP BY name.Обратите внимание, что нам все еще нужно отфильтровать элементы с количеством 0 из массива.Опять же, обратитесь к рецепту поваренной книги MongoDB для поиска дубликатов, используя group для канонического решения, используя group.

2 голосов
/ 14 октября 2015

Я нашел полезную информацию в официальном блоге лаборатории Монго: http://blog.mongolab.com/2014/03/finding-duplicate-keys-with-the-mongodb-aggregation-framework/

1 голос
/ 23 марта 2019

Наивысший принятый ответ здесь:

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

Это также вернет вам новое поле с именем uniqueIds со списком идентификаторов. Но что, если вы просто хотите поле и его количество? Тогда было бы так:

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

Чтобы объяснить это, если вы пришли из баз данных SQL, таких как MySQL и PostgreSQL, вы привыкли к агрегатным функциям (например, COUNT (), SUM (), MIN (), MAX ()), которые работают с оператором GROUP BY, что позволяет Вы, например, чтобы найти общее количество, что значение столбца появляется в таблице.

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

Как вы можете видеть, наш вывод показывает количество, в котором появляется каждое значение my_type. Чтобы найти дубликаты в MongoDB, мы бы решили проблему аналогичным образом. MongoDB может похвастаться операциями агрегации, которые группируют значения из нескольких документов вместе, и могут выполнять различные операции над сгруппированными данными для возврата одного результата. Это похоже на агрегатные функции в SQL.

Предполагая, что коллекция называется контактами, первоначальная настройка выглядит следующим образом:

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

Эта агрегатная функция принимает массив операторов агрегирования, и в нашем случае нам нужен оператор $ group, поскольку наша цель - сгруппировать данные по количеству полей, то есть по числу вхождений значения поля.

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

В этом подходе есть небольшая особенность. Поле _id необходимо для использования группы по оператору. В этом случае мы группируем поле $ name. Имя ключа в _id может иметь любое имя. Но мы используем имя, поскольку оно интуитивно понятно.

Запустив агрегацию, используя только оператор $ group, мы получим список всех полей имени (независимо от того, появляются ли они в коллекции один или несколько раз):

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

Обратите внимание, как работает агрегация. Он взял документы с полями имени и возвращает новую коллекцию извлеченных полей имени.

Но мы хотим знать, сколько раз значение поля появляется снова. Оператор $ group берет поле count, которое использует оператор $ sum для добавления выражения 1 к итогу для каждого документа в группе. Таким образом, $ group и $ sum вместе возвращают общую сумму всех числовых значений, которые получаются для данного поля (например, name).

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 }

Поскольку целью было устранить дубликаты, требуется один дополнительный шаг. Чтобы получить только те группы, у которых число больше одного, мы можем использовать оператор $ match для фильтрации наших результатов. В операторе $ match мы скажем, чтобы он смотрел на поле count и велел искать счетчики больше единицы, используя оператор $ gt, представляющий «больше чем» и число 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 }

В качестве примечания: если вы используете MongoDB через ORM, например Mongoid для Ruby, вы можете получить эту ошибку:

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

Скорее всего, это означает, что ваш ORM устарел и выполняет операции, которые MongoDB больше не поддерживает. Следовательно, либо обновите свой ORM, либо найдите исправление. Для Mongoid это было исправлением для меня:

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