Search code examples
mongodbmongoose

Update many documents in MongoDB with new values


I have mongodb version 3.6 on server. And my server sources are in nodeJS

I have some documents like this:

{"title": "test1", sort: 1, parent: 1},
{"title": "test2", sort: 7, parent: 1},
{"title": "test3", sort: 5, parent: 1},

{"title": "test4", sort: 1, parent: 2},
{"title": "test5", sort: 7, parent: 2},
{"title": "test6", sort: 5, parent: 2}

Now I want to update one group of documents (For example documents with parent=1) and set sort field values of this group with new values by the pattern like previous value plus 10 starting the group with number 1.

So I want the result below:

{"title": "test1", sort: 1, parent: 1},
{"title": "test2", sort: 21, parent: 1},
{"title": "test3", sort: 11, parent: 1},

{"title": "test4", sort: 1, parent: 2},
{"title": "test5", sort: 7, parent: 2},
{"title": "test6", sort: 5, parent: 2}

What is the best way and with the best performance to perform this?


Solution

  • Using only operations available in Mongo 3.6:

    Since you want to update only one parent at a time, the last stage of this is a $merge. And the docs only have the fields for the original _id and the new sort.

    db.collection.aggregate([
      {
        // match on the parent you want
        $match: { "parent": 1 }
      },
      {
        // sort the docs under that parent
        $sort: { "sort": 1 }
      },
      {
        // create a group for that one parent which is
        // currently all docs due to first $match stage
        $group: {
          _id: null,
          docs: {
            // docs will be already sorted in this array
            $push: "$$ROOT"
          }
        }
      },
      {
        // set each doc `sort` field to the value it should be
        // based on its index in docs
        $set: {
          docs: {
            $map: {
              input: "$docs",
              in: {
                "_id": "$$this._id",
                "sort": {
                  $add: [
                    1,
                    {
                      $multiply: [
                        { $indexOfArray: ["$docs", "$$this"] },
                        10
                      ]
                    }
                  ]
                }
              }
            }
          }
        }
      },
      { $unwind: "$docs" },
      { $replaceRoot: { newRoot: "$docs" } },
      { $merge: "collection" }
    ])
    

    Mongo Playground

    If you want to see what the full resulting docs would be, see this similar pipeline. The main difference is the $map part of the $set stage which includes all the doc fields. You can also add a final { $merge: "collection" } stage to use this pipeline as-is. But since you asked about performance, then the merge should only have the updated fields and not the full doc.

    Btw, for performance, it's better to apply the update to ALL docs without setting the parent each time. ie Update the sort value for all docs per parent group.

    For bulk updating all docs in the collection, using the sort per parent, replace the first three stages with these two instead:

    db.collection.aggregate([
      {
        // sort the docs under that parent
        $sort: {
           "parent": 1,
           "sort": 1
        }
      },
      {
        // create a group for that one parent which is
        // currently all docs due to first $match stage
        $group: {
          _id: "$parent",
          docs: {
            // docs will be already sorted in this array
            $push: "$$ROOT"
          }
        }
      },
      // everything else remains the same, paste that here
    ])
    

    Mongo Playground to update all docs in the collection


    If you were on Mongo 5.0+ and $setWindowFields was available, then this applies:

    The change needed in this answer, when compared to your related previous question is quite minor, wrt my answer there.

    1. Use the $rank function of $setWindowFields to provide ranks 1, 2, 3, etc. which will be based on the existing sort value.

      • add a partitionBy for the parent so that each partition (group) is separated by parent.
    2. The final new-sort will be (rank - 1) x 10 + 1

      • which gets calculated for each document per group
    3. Use the $out operation to write all the updated docs back to the same collection.

    The only change is the addition of partitionBy: "$parent" in $setWindowFields:

    db.collection.aggregate([
      {
        $setWindowFields: {
          partitionBy: "$parent",
          sortBy: { sort: 1 },
          output: {
            sort: { $rank: {} }
          }
        }
      },
      {
        $set: {
          sort: {
            $add: [
              1,
              {
                $multiply: [
                  { $subtract: ["$sort", 1] },
                  10
                ]
              }
            ]
          }
        }
      },
      { $out: "collection" }
    ])
    

    Mongo Playground