534

In MongoDB, is it possible to update the value of a field using the value from another field? The equivalent SQL would be something like:

UPDATE Person SET Name = FirstName + ' ' + LastName

And the MongoDB pseudo-code would be:

db.person.update( {}, { $set : { name : firstName + ' ' + lastName } );
styvane
  • 59,869
  • 19
  • 150
  • 156
Chris Fulstow
  • 41,170
  • 10
  • 86
  • 110

12 Answers12

540

The best way to do this is in version 4.2+ which allows using the aggregation pipeline in the update document and the updateOne, updateMany, or update(deprecated in most if not all languages drivers) collection methods.

MongoDB 4.2+

Version 4.2 also introduced the $set pipeline stage operator, which is an alias for $addFields. I will use $set here as it maps with what we are trying to achieve.

db.collection.<update method>(
    {},
    [
        {"$set": {"name": { "$concat": ["$firstName", " ", "$lastName"]}}}
    ]
)

Note that square brackets in the second argument to the method specify an aggregation pipeline instead of a plain update document because using a simple document will not work correctly.

MongoDB 3.4+

In 3.4+, you can use $addFields and the $out aggregation pipeline operators.

db.collection.aggregate(
    [
        { "$addFields": { 
            "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
        }},
        { "$out": <output collection name> }
    ]
)

Note that this does not update your collection but instead replaces the existing collection or creates a new one. Also, for update operations that require "typecasting", you will need client-side processing, and depending on the operation, you may need to use the find() method instead of the .aggreate() method.

MongoDB 3.2 and 3.0

The way we do this is by $projecting our documents and using the $concat string aggregation operator to return the concatenated string. You then iterate the cursor and use the $set update operator to add the new field to your documents using bulk operations for maximum efficiency.

Aggregation query:

var cursor = db.collection.aggregate([ 
    { "$project":  { 
        "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
    }}
])

MongoDB 3.2 or newer

You need to use the bulkWrite method.

var requests = [];
cursor.forEach(document => { 
    requests.push( { 
        'updateOne': {
            'filter': { '_id': document._id },
            'update': { '$set': { 'name': document.name } }
        }
    });
    if (requests.length === 500) {
        //Execute per 500 operations and re-init
        db.collection.bulkWrite(requests);
        requests = [];
    }
});

if(requests.length > 0) {
     db.collection.bulkWrite(requests);
}

MongoDB 2.6 and 3.0

From this version, you need to use the now deprecated Bulk API and its associated methods.

var bulk = db.collection.initializeUnorderedBulkOp();
var count = 0;

cursor.snapshot().forEach(function(document) { 
    bulk.find({ '_id': document._id }).updateOne( {
        '$set': { 'name': document.name }
    });
    count++;
    if(count%500 === 0) {
        // Excecute per 500 operations and re-init
        bulk.execute();
        bulk = db.collection.initializeUnorderedBulkOp();
    }
})

// clean up queues
if(count > 0) {
    bulk.execute();
}

MongoDB 2.4

cursor["result"].forEach(function(document) {
    db.collection.update(
        { "_id": document._id }, 
        { "$set": { "name": document.name } }
    );
})
styvane
  • 59,869
  • 19
  • 150
  • 156
  • I think there is an issue with the code for "MongoDB 3.2 or newer". Since forEach is async nothing will typically get written in the last bulkWrite. – Viktor Hedefalk Oct 04 '19 at 08:35
  • 17
    4.2+ Doesn't work. MongoError: The dollar ($) prefixed field '$concat' in 'name.$concat' is not valid for storage. – Josh Woodcock Oct 16 '19 at 04:25
  • 2
    @JoshWoodcock, I think you had a typo in the query you are running. I suggest you double check. – styvane Oct 17 '19 at 21:33
  • @JoshWoodcock It works beautifully. Please test this using the [MongoDB Web Shell](https://docs.mongodb.com/manual/tutorial/getting-started/) – styvane Oct 19 '19 at 08:39
  • 25
    For those running into the same problem @JoshWoodcock described: pay attention that the answer for 4.2+ describes an *aggregation pipeline*, so don't miss the *square brackets* in the second parameter! – philsch Jan 07 '20 at 20:05
  • 2
    Is it possible to do the same thing as in this solution but instead of concatenating two strings together, add two numbers together? – Isaac Vidrine Jan 30 '20 at 14:03
  • 1
    @IsaacVidrine Yes, use `$add` instead of `$concat`. https://docs.mongodb.com/master/reference/operator/aggregation/add/#exp._S_add – Paul Oct 08 '20 at 11:58
  • @styvane Is it possible to do this using Mongoose? – Azamat Abdullaev Apr 02 '21 at 05:10
  • [Yes](https://mongoosejs.com/docs/api.html#model_Model.aggregate) @AzamatAbdullaev – styvane Apr 02 '21 at 12:17
  • @styvane It only reads data, not for updates. – Azamat Abdullaev Apr 02 '21 at 16:48
  • 5
    How many times are they going to change this before it becomes a joke? – ajsp Apr 03 '21 at 15:32
  • Until there is no room for improvement I guess:) @ajsp – styvane Apr 03 '21 at 16:34
  • Does this work the same way with nodejs driver? – bertonc96 Apr 28 '21 at 13:24
  • this is not a valid answer, addfields or set does not add to the collection persistently. – codemonkey Nov 14 '21 at 15:16
  • 1
    @codemonkey, you should read the full answer. I failed to see where I mentioned that `$set` and `addFields` are update operators. – styvane Nov 15 '21 at 10:51
  • @styvane ok i see it now. just the structure of the answer is a bit misleading for me. it would be more clear if the header "MongoDB 4.2+" was on the top of the answer. – codemonkey Nov 15 '21 at 16:24
  • Is there a way to do this for bulk ie all existing documents. I have a case where need to disp a field on UI but old records dont have that field – pr0mpT_07 Nov 29 '21 at 16:11
  • About the solution before 4.2. What if we want to do this using a filter? For example, copy the field if not empty. If I remember correctly, the $out operation works in a way that would cause all filtered-out items to be deleted. Unfortunatelly use use Scalingo which does not support mongo 4.2 due to some licence issue. :( JS is also out of the question because the queries will run in liquibase - which does not support JS. – Alkis Mavridis Jan 14 '22 at 16:10
  • can someone solve this using db.runCommand, dollar symbol to refer existing field doesn't work with it. – kumar Anny Jun 10 '22 at 10:32
262

You should iterate through. For your specific case:

db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
evandrix
  • 6,041
  • 4
  • 27
  • 38
Carlos Morales
  • 5,676
  • 4
  • 34
  • 42
  • 6
    What happens if another user has changed the document between your find() and your save()? – UpTheCreek Feb 15 '13 at 11:33
  • As far as I know, MongoDB is not transaction safe – Carlos Morales Feb 19 '13 at 08:10
  • 3
    True, but copying between fields should not require transactions to be atomic. – UpTheCreek Feb 19 '13 at 09:25
  • 3
    It's important to notice that `save()` fully replaces the document. Should use `update()` instead. – Carlos Melo Mar 22 '13 at 21:44
  • @CarlosBarcelona I believe the change you made isn't the correct syntax for the `update` method. Maybe you should use `save` after all. – sergiopereira Jul 16 '13 at 21:15
  • 12
    How about `db.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );` – Philipp Jardas Aug 19 '13 at 13:34
  • Works for me when you don't have access to `db.eval()` due to user access rights – Jonno_FTW Mar 20 '15 at 04:53
  • 1
    I created a function called `create_guid` that only produced a unique guid per document when iterating with `forEach` in this way (i.e. simply using `create_guid` in an `update` statement with `mutli=true` caused the same guid to be generated for all documents). This answer worked perfectly for me. +1 – rmirabelle Apr 09 '15 at 15:02
  • 1
    This could be made more "atomic" by having a query filter such as `{ _id: elem._id, firstname: elem.firstname, lastname: elem.lastname}`, instead of just the `_id` filter. Dunno about performance, partial failures, etc, cause I'm a MongoDB noob, but I think it would have stronger consistency guarantees. Not sure if this would lock the collection or each document as you loop through, but might be worth checking out. – Merlyn Morgan-Graham Apr 20 '16 at 01:42
  • Can this be done if I simply want to update a field on a document with a certain `id` using another field this document contains. It is this latter field that I am having trouble extracting. – MadPhysicist Aug 23 '16 at 20:22
  • This is works, thanks. But isn't there a faster / more efficient way of doing it? – Tal Tikotzki Feb 09 '17 at 18:58
  • @EdMelo: is there anything wrong with using `save()` if no other updates occur to the database while the `forEach` runs? Also, [`snapshot` is deprecated since v3.6](https://jira.mongodb.org/browse/JAVA-2809). – Dan Dascalescu Mar 31 '19 at 07:01
108

Apparently there is a way to do this efficiently since MongoDB 3.4, see styvane's answer.


Obsolete answer below

You cannot refer to the document itself in an update (yet). You'll need to iterate through the documents and update each document using a function. See this answer for an example, or this one for server-side eval().

Niels van der Rest
  • 31,664
  • 16
  • 80
  • 86
  • 31
    Is this still valid today? – Christian Engel Jan 12 '13 at 22:08
  • 3
    @ChristianEngel: It appears so. I wasn't able to find anything in the MongoDB docs that mentions a reference to the current document in an `update` operation. [This related feature request](https://jira.mongodb.org/browse/SERVER-458) is still unresolved as well. – Niels van der Rest Jan 14 '13 at 12:28
  • 4
    Is it still valid in April 2017? Or there are already new features which can do this? – Kim Apr 26 '17 at 12:01
  • 1
    @Kim It looks like it is still valid. Also, the [feature request](https://jira.mongodb.org/browse/SERVER-458) that @niels-van-der-rest pointed out back in 2013 is still in `OPEN`. – Danziger May 03 '17 at 22:30
  • 8
    this is not a valid answer anymore, have a look at @styvane answer – aitchkhan Mar 11 '18 at 18:28
48

For a database with high activity, you may run into issues where your updates affect actively changing records and for this reason I recommend using snapshot()

db.person.find().snapshot().forEach( function (hombre) {
    hombre.name = hombre.firstName + ' ' + hombre.lastName; 
    db.person.save(hombre); 
});

http://docs.mongodb.org/manual/reference/method/cursor.snapshot/

Eric Kigathi
  • 1,815
  • 21
  • 23
  • 2
    What happens if another user edited the person between the find() and save()? I have a case where multiple calls can be done to the same object changing them based on their current values. The 2nd user should have to wait with reading until the 1st is done with saving. Does this accomplish that? – Marco Oct 11 '17 at 12:48
  • 8
    About the `snapshot()`: `Deprecated in the mongo Shell since v3.2. Starting in v3.2, the $snapshot operator is deprecated in the mongo shell. In the mongo shell, use cursor.snapshot() instead.` [link](https://docs.mongodb.com/v3.4/reference/method/cursor.snapshot/#cursor.snapshot) – ppython Dec 20 '17 at 14:52
36

Starting Mongo 4.2, db.collection.update() can accept an aggregation pipeline, finally allowing the update/creation of a field based on another field:

// { firstName: "Hello", lastName: "World" }
db.collection.updateMany(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }]
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • The first part {} is the match query, filtering which documents to update (in our case all documents).

  • The second part [{ $set: { name: { ... } }] is the update aggregation pipeline (note the squared brackets signifying the use of an aggregation pipeline). $set is a new aggregation operator and an alias of $addFields.

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
  • 2
    Worked for me. Was assigning one field to another without concatenation and it worked. Thank you! – Mosheer Oct 22 '21 at 16:54
  • what is the difference between your first point and your third? {} mean all doc then why { multi: true } – Coder17 Nov 29 '21 at 12:48
  • @Coder17 the first part `{}` is a filtering part: for instance you might want to update `doc1` and `doc2` but not `doc3`. Without the third part, by default, the update will be applied only on 1 document for instance `doc1`, and `doc2` wouldn't be updated. Note that you can also use `db.collection.updateMany` to get rid of the third parameter. – Xavier Guihot Nov 29 '21 at 14:10
  • at this point we'd use updateMany – David Sep 15 '22 at 12:36
17

Regarding this answer, the snapshot function is deprecated in version 3.6, according to this update. So, on version 3.6 and above, it is possible to perform the operation this way:

db.person.find().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
Aldo
  • 1,199
  • 12
  • 19
9

update() method takes aggregation pipeline as parameter like

db.collection_name.update(
  {
    // Query
  },
  [
    // Aggregation pipeline
    { "$set": { "id": "$_id" } }
  ],
  {
    // Options
    "multi": true // false when a single doc has to be updated
  }
)

The field can be set or unset with existing values using the aggregation pipeline.

Note: use $ with field name to specify the field which has to be read.

8

I tried the above solution but I found it unsuitable for large amounts of data. I then discovered the stream feature:

MongoClient.connect("...", function(err, db){
    var c = db.collection('yourCollection');
    var s = c.find({/* your query */}).stream();
    s.on('data', function(doc){
        c.update({_id: doc._id}, {$set: {name : doc.firstName + ' ' + doc.lastName}}, function(err, result) { /* result == true? */} }
    });
    s.on('end', function(){
        // stream can end before all your updates do if you have a lot
    })
})
Chris Gibb
  • 845
  • 8
  • 11
  • 1
    How is this different? Will the steam be throttled by the update activity? Do you have any reference to it? The Mongo docs are quite poor. – Nico Nov 21 '16 at 14:58
2

Here's what we came up with for copying one field to another for ~150_000 records. It took about 6 minutes, but is still significantly less resource intensive than it would have been to instantiate and iterate over the same number of ruby objects.

js_query = %({
  $or : [
    {
      'settings.mobile_notifications' : { $exists : false },
      'settings.mobile_admin_notifications' : { $exists : false }
    }
  ]
})

js_for_each = %(function(user) {
  if (!user.settings.hasOwnProperty('mobile_notifications')) {
    user.settings.mobile_notifications = user.settings.email_notifications;
  }
  if (!user.settings.hasOwnProperty('mobile_admin_notifications')) {
    user.settings.mobile_admin_notifications = user.settings.email_admin_notifications;
  }
  db.users.save(user);
})

js = "db.users.find(#{js_query}).forEach(#{js_for_each});"
Mongoid::Sessions.default.command('$eval' => js)
Chris Bloom
  • 3,526
  • 1
  • 33
  • 47
0

With MongoDB version 4.2+, updates are more flexible as it allows the use of aggregation pipeline in its update, updateOne and updateMany. You can now transform your documents using the aggregation operators then update without the need to explicity state the $set command (instead we use $replaceRoot: {newRoot: "$$ROOT"})

Here we use the aggregate query to extract the timestamp from MongoDB's ObjectID "_id" field and update the documents (I am not an expert in SQL but I think SQL does not provide any auto generated ObjectID that has timestamp to it, you would have to automatically create that date)

var collection = "person"

agg_query = [
    {
        "$addFields" : {
            "_last_updated" : {
                "$toDate" : "$_id"
            }
        }
    },
    {
        $replaceRoot: {
            newRoot: "$$ROOT"
        } 
    }
]

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})
Yi Xiang Chong
  • 744
  • 11
  • 9
  • 1
    You don't need `{ $replaceRoot: { newRoot: "$$ROOT" } }`; it means replacing the document by itself, which is pointless. If you replace `$addFields` by its alias `$set` and `updateMany` which is one of the aliases for `update`, then you get to the exact same answer as [this one](https://stackoverflow.com/a/56551339/9297144) above. – Xavier Guihot Apr 15 '20 at 13:19
  • Thanks @Xavier Guihot for the comment. The reason why ```$replaceRoot``` is used because we needed to use the ```$toDate``` query, while ```$set``` does not provide us a way to use ```$toDate``` – Yi Xiang Chong Jul 09 '20 at 09:45
0

(I would have posted this as a comment, but couldn't)

For anyone who lands here trying to update one field using another in the document with the c# driver... I could not figure out how to use any of the UpdateXXX methods and their associated overloads since they take an UpdateDefinition as an argument.

// we want to set Prop1 to Prop2
class Foo { public string Prop1 { get; set; } public string Prop2 { get; set;} } 

void Test()
{ 
     var update = new UpdateDefinitionBuilder<Foo>();
     update.Set(x => x.Prop1, <new value; no way to get a hold of the object that I can find>)
}

As a workaround, I found that you can use the RunCommand method on an IMongoDatabase (https://docs.mongodb.com/manual/reference/command/update/#dbcmd.update).

var command = new BsonDocument
        {
            { "update", "CollectionToUpdate" },
            { "updates", new BsonArray 
                 { 
                       new BsonDocument
                       {
                            // Any filter; here the check is if Prop1 does not exist
                            { "q", new BsonDocument{ ["Prop1"] = new BsonDocument("$exists", false) }}, 
                            // set it to the value of Prop2
                            { "u", new BsonArray { new BsonDocument { ["$set"] = new BsonDocument("Prop1", "$Prop2") }}},
                            { "multi", true }
                       }
                 }
            }
        };

 database.RunCommand<BsonDocument>(command);
user1239961
  • 36
  • 1
  • 3
0

MongoDB 4.2+ Golang

result, err := collection.UpdateMany(ctx, bson.M{},
    mongo.Pipeline{
        bson.D{{"$set",
          bson.M{"name": bson.M{"$concat": []string{"$lastName", " ", "$firstName"}}}
    }},
)
        
igorushi
  • 1,855
  • 21
  • 19