skip to Main Content

I needed to run a quick and (I thought) trivial query to update some incorrect data. I’m trying to update the very first entry of an array in a document.

Note: My posted queries run against db instead of a collection, and use $documents to generate test data in the pipline so you can evaluate them straight away. No need to create a collection and seed it with test data.

I thought this would be utterly trivial:

db.aggregate([
  { $documents: [{ values: [-354, 52, 234, 1] }] },
  { $set: { 'values.0': 12 } }
]);

However the result of that is bizarre:

{
  values: [
    {
      '0': 12
    },
    {
      '0': 12
    },
    {
      '0': 12
    },
    {
      '0': 12
    }
  ]
}

I finally managed to get it working, but with a convoluted query with some interesting caveats:

db.aggregate([
  { $documents: [{ values: [-354, 52, 234, 1] }] },
  {
    $set: {
      values: {
        $concatArrays: [[12], { $slice: ['$values', 1, 2147483647] }],
      }7
    },
  },
]);

This gives the correct result:

{
  values: [
    12,
    52,
    234,
    1
  ]
}

I must first build a single value array for the new first value. Then concatenate the values in the array, skipping the first. MongoDB doesn’t seem to have a proper operator for that, so as a tenuous stand-in, I’m using $slice with the start parameter set to 1, and the number of entries to 2³¹−1, which is the maximum value for that parameter. It could be that there are more array entries than that. But at that point I’m pretty you’re going to run into bigger problems, since many more operation parameters are 32-bit integers, and all kinds of operations will fail.

Now this seems utterly ridiculous to me, I think I must be doing something simple wrong. There’s no way this janky method is the normal way to to update an array value with a known index in MongoDB.

Since I couldn’t find a method that worked online, I’ll post this here, and hope someone can tell me how it’s really supposed to be done.

2

Answers


  1. The { $set: { 'values.x': ... } } syntax works only in update but not in aggregation pipeline.

    More precisely, you mistake Aggregation Stage $set (aggregation) and Field Update Operator $set

    Array Expression Operators are not as complete as in Javascript Array. You may use $function and implement your requirement in Javascript.

    I would use

    values: {
        $concatArrays: [
          [12], 
          { $slice: ['$values', 1, {$subtract: [{ $size: "$values" }, 1] } ] }
        ],
    }
    

    Operator $reduce would be even worse, see Performance issue for $reduce vs. $map

    With JavaScript it would be

    db.aggregate([
       { $documents: [{ values: [-354, 52, 234, 1] }] },
       {
          $set: {
             values: {
                $function: {
                   body: function (arr, ind, val) {
                      arr[ind] = val;
                      return arr;
                   },
                   args: ["$values", 0, -12],
                   lang: "js"
                }
             }
          }
       }
    ]);
    

    You have test if it performs better than native aggregation pipeline functions.

    Login or Signup to reply.
  2. There are 2 alternatives that I can think of to achieve what you want, though I doubt that they will be simpler than your current approach.

    1. also $concatArrays, but use $lastN to construct "remaining" part of the array(i.e. other than the first entry). The size of "remaining" array is calculated through $subtract 1 from $size of values.
    db.collection.update({},
    [
      {
        "$set": {
          "values": {
            "$concatArrays": [
              [
                12
              ],
              {
                "$lastN": {
                  "n": {
                    "$subtract": [
                      {
                        "$size": "$values"
                      },
                      1
                    ]
                  },
                  "input": "$values"
                }
              }
            ]
          }
        }
      }
    ])
    

    Mongo Playground

    1. $unwind with includeArrayIndex. Perform conditional replacement based on the index value and $group to revert the original structure.
    db.collection.aggregate([
      {
        "$unwind": {
          path: "$values",
          includeArrayIndex: "idx"
        }
      },
      {
        "$set": {
          "values": {
            "$cond": {
              "if": {
                $eq: [
                  "$idx",
                  0
                ]
              },
              "then": 12,
              "else": "$values"
            }
          }
        }
      },
      {
        "$group": {
          "_id": "$_id",
          "someOtherField": {
            $first: "$someOtherField"
          },
          "values": {
            "$push": "$values"
          }
        }
      },
      {
        "$merge": {
          "into": "collection"
        }
      }
    ])
    

    Mongo Playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search