MongoDB で重複データを抽出、削除する方法

今回は「MongoDB で重複データを抽出、削除する方法」について見ていく。

createIndex() で一意制約を付けようとしたとき、重複データがあると設定できない。今回は、そのような場合に必要な「重複データを抽出して削除する具体的な方法」について説明する。

事前データ登録

以下のデータを mongo コマンドで一括登録した状態で、重複したデータ({ item: "pen" })の削除について見ていく。

> db.orders.insertMany([
  { datetime: ISODate("2021-12-15T12:00:00+09:00"), item: "pen", amount: 70 },
  { datetime: ISODate("2021-12-15T12:00:00+09:00"), item: "note", amount: 80 },
  { datetime: ISODate("2021-12-15T12:00:00+09:00"), item: "eraser", amount: 100 },
  { datetime: ISODate("2021-11-13T12:00:00+09:00"), item: "pen", amount: 20 },
  { datetime: ISODate("2021-11-02T12:00:00+09:00"), item: "pen", amount: 20 },
  { datetime: ISODate("2021-10-23T12:00:00+09:00"), item: "pen", amount: 30 },
  { datetime: ISODate("2021-10-18T12:00:00+09:00"), item: "pen", amount: 10 }
]);

重複データ抽出

重複データだけを抽出するには、aggregate()$group を使用する。抽出時に $$ROOT を使用すると、元データをそのまま items 配列へ push できる。

また、aggregate() の制約としてメモリ操作は 100MB までとなっているため、それを超えるデータを一度に操作する場合は、オプションで allowDiskUse を指定する。

コード (index.js)

var MongoClient = require("mongodb").MongoClient;
var URL = "mongodb://localhost:27017";
 
MongoClient.connect(URL, (err, client) => {
  if (err) {
    console.log(err);
    return;
  };
 
  var db = client.db("sample");
 
  db.collection("orders").aggregate([
    // 重複データを抽出する。
    {
      $group: {
        _id: "$item",
        items: { $push: "$$ROOT" },
        count: { $sum: 1 }
      }
    },
    {
      $match: { count: { $gt: 1 } }
    }
  ]).toArray().then((docs) => {
    console.log(docs);
  }).catch((err) => {
    console.log(err);
  }).then(() => {
    client.close();
  });
});

実行と結果

> node .\index.js
[ { _id: 'pen',
    items: [ [Object], [Object], [Object], [Object], [Object] ],
    count: 5 } ]

実行結果では [Object] と表示されているが、デバッグで停止して確認するときに JSON.stringify() を使用すると内容を確認できる。

重複データ削除

重複データの削除とはいっても、1 件のデータは残す必要がある。ここでは、どのデータを残し、それ以外を削除するかを決める 2 つの方法を紹介する。

  • 先頭データを決めて残す
  • ObjectId 指定で残す

先頭データを決めて残す

まず、ソートして「ある値が最大、または最小のデータを残す」方法である。

サンプルコードでは L.14 でソートし、L.30 で最初の 1 件を削除対象から外してから削除している。

コード (index.js)

var MongoClient = require("mongodb").MongoClient;
var URL = "mongodb://localhost:27017";
 
MongoClient.connect(URL, (err, client) => {
  if (err) {
    console.log(err);
    return;
  };
 
  var db = client.db("sample");
 
  db.collection("orders").aggregate([
    {
      $sort: { amount: -1 }
    },
    {
      $group: {
        _id: "$item",
        targets: { $push: "$_id" },
        count: { $sum: 1 }
      }
    },
    {
      $match: { count: { $gt: 1 } }
    },
  ]).toArray().then((docs) => {
    console.log(JSON.stringify(docs));
    var procs = [];
    for (var doc of docs) {
      doc.targets.shift();
      procs[procs.length] = db.collection("orders").deleteMany({
        _id: { $in: doc.targets }
      });
    }
    return Promise.all(procs);
  }).then((results) => {
    console.log("Remove dupulicate data.");
  }).catch((err) => {
    console.log(err);
  }).then(() => {
    client.close();
  });
});

実行と結果

> node .\index.js
Remove dupulicate data.

ObjectId 指定で残す

どちらかといえば、データを確認した後で残したいデータを指定し、それ以外を削除する作業が一般的だと思われる。この方法は、残しておきたいデータの ObjectId を指定し、それ以外を削除する方法である。除外データの指定は、以下のサンプルコード L.32 にある配列へカンマ区切りで追加することで指定できる。

コード (index.js)

var MongoClient = require("mongodb").MongoClient;
var ObjectId = require("mongodb").ObjectId;
var URL = "mongodb://localhost:27017";
 
MongoClient.connect(URL, (err, client) => {
  if (err) {
    console.log(err);
    return;
  };
 
  var db = client.db("sample");
 
  db.collection("orders").aggregate([
    // 重複データを抽出する。
    {
      $group: {
        _id: "$item",
        items: { $push: "$$ROOT" },
        count: { $sum: 1 }
      }
    },
    {
      $match: { count: { $gt: 1 } }
    },
    // 重複データだけを取得する。
    {
      $unwind: "$items"
    },
    // 残すデータを削除対象から除外する。
    {
      $match: {
        "items._id": { $nin: [new ObjectId("5a5091c4ff3735d50c439d41")] }
      }
    },
    // 削除対象の ObjectId を集める。
    {
      $group: {
        _id: null,
        targets: { $push: "$items._id" }
      }
    }
  ]).toArray().then((docs) => {
    console.log(JSON.stringify(docs));
    var procs = [];
    for (var doc of docs) {
      procs[procs.length] = db.collection("orders").deleteMany({
        _id: { $in: doc.targets }
      });
    }
    return Promise.all(procs);
  }).then((results) => {
    console.log("Remove dupulicate data.");
  }).catch((err) => {
    console.log(err);
  }).then(() => {
    client.close();
  });
});

実行と結果

> node .\index.js
Remove dupulicate data.

まとめ

「MongoDB で重複データを抽出、削除する方法」について見てきた。ポイントは次のとおりである。

  • データ抽出は aggregate()$group
  • データ削除は aggregate() を行ってから deleteMany()