112

In the following example, assume the document is in the db.people collection.

How to remove the 3rd element of the interests array by it's index?

{
  "_id" : ObjectId("4d1cb5de451600000000497a"),           
  "name" : "dannie",  
  "interests" : [  
    "guitar",  
    "programming",           
    "gadgets",  
    "reading"  
  ]   
}

This is my current solution:

var interests = db.people.findOne({"name":"dannie"}).interests;  
interests.splice(2,1)  
db.people.update({"name":"dannie"}, {"$set" : {"interests" : interests}});

Is there a more direct way?

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
dannie.f
  • 2,395
  • 2
  • 22
  • 20

9 Answers9

155

There is no straight way of pulling/removing by array index. In fact, this is an open issue http://jira.mongodb.org/browse/SERVER-1014 , you may vote for it.

The workaround is using $unset and then $pull:

db.lists.update({}, {$unset : {"interests.3" : 1 }}) 
db.lists.update({}, {$pull : {"interests" : null}})

Update: as mentioned in some of the comments this approach is not atomic and can cause some race conditions if other clients read and/or write between the two operations. If we need the operation to be atomic, we could:

  • Read the document from the database
  • Update the document and remove the item in the array
  • Replace the document in the database. To ensure the document has not changed since we read it, we can use the update if current pattern described in the mongo docs
Javier Ferrero
  • 8,741
  • 8
  • 45
  • 50
  • 38
    It's been a year and a half? Is this still the state of things with mongodb? – Abe Jul 07 '12 at 03:26
  • The next answer is more direct and worked for me. Although it is not removing by index, but rather removing by value. – vish Jun 03 '13 at 03:38
  • 2
    @Javier - wouldn't this solution leave you open to issues if the db changed between the time that you counted the position in the index, and the time that you did the unset? – UpTheCreek Jun 25 '13 at 14:01
  • 2
    This isn't atomic, so it's likely to cause obscure race conditions if you have any code that isn't prepared to cope with a null entry in the array. – Glenn Maynard Jul 15 '13 at 21:32
  • @UpTheCreek you are right. If you need to make sure the document has not changed you may use the update if current pattern described here http://docs.mongodb.org/manual/tutorial/isolate-sequence-of-operations/ – Javier Ferrero Jul 24 '13 at 14:34
  • 4
    8 years and [this ticket](https://jira.mongodb.org/browse/SERVER-1014) still hasn't been implemented... – Olly John May 10 '18 at 15:11
19

You can use $pull modifier of update operation for removing a particular element in an array. In case you provided a query will look like this:

db.people.update({"name":"dannie"}, {'$pull': {"interests": "guitar"}})

Also, you may consider using $pullAll for removing all occurrences. More about this on the official documentation page - http://www.mongodb.org/display/DOCS/Updating#Updating-%24pull

This doesn't use index as a criteria for removing an element, but still might help in cases similar to yours. IMO, using indexes for addressing elements inside an array is not very reliable since mongodb isn't consistent on an elements order as fas as I know.

Sunseeker
  • 1,503
  • 9
  • 21
  • 6
    His element is an array in the question. Mongo is consistent on order in this case. In fact, all of the documents are actually ordered collections but many drivers don't handle them in this manner. Further complicating the matter, if a document has to be moved after an update operation (due to growth beyond the padding factor) the order of keys suddenly becomes alphabetical (under the covers, I think they are sorted by their binary value, this suspicion is based on my knowledge that arrays are pretty much standard documents with keys that are consecutive integers instead of strings). – marr75 Nov 30 '12 at 05:00
  • This avoids the problems of unset+pull, but requires that your dictionary be strictly ordered. The dictionaries in most languages are unordered, so it can be a pain to make this happen. – Glenn Maynard Jul 15 '13 at 21:34
  • @marr75: Do you have a reference? Mongo documents are always supposed to retain order, and if it ever reordered subdocuments a lot of things would break. – Glenn Maynard Jul 15 '13 at 21:39
  • I don't have a reference. I know this from personal experience with 2.2, having fixed bugs that were introduced by updated and moved documents. I haven't done much mongo work in the past few months so I can't speak to more current versions. – marr75 Jul 26 '13 at 16:20
  • As far as element consistency, my desired use case is. A client requests json from mongo and then if the client chooses to say, 'delete' a item from a json array, it will pass the array index to the server to be deleted. if the array items position moved, that would be weird, but would explain why they cant implement the feature – Northstrider Jul 12 '14 at 07:48
  • Arrays always will maintain their "order" because they are basically hashtables where the keys ARE the given indices. That's how JSON works because that's (pretty much) how JS works. `["zero", "one", "two"]` is similar to `{"0":"zero", "1":"one", "2":"two"}` and those keys translate into the Array order. – Jimbo Jonny Nov 15 '16 at 18:34
11

in Mongodb 4.2 you can do this:

db.example.update({}, [
     {$set: {field: {
           $concatArrays: [ 
                  {$slice: ["$field", P]}, 
                  {$slice: ["$field", {$add: [1, P]}, {$size: "$field"}]}
           ]
     }}}
]);

P is the index of element you want to remove from array.

If you want to remove from P till end:

db.example.update({}, [
  { $set: { field: { $slice: ["$field", 1] } } },
]);
Siyavash Hamdi
  • 2,764
  • 2
  • 21
  • 32
Ali
  • 21,572
  • 15
  • 83
  • 95
  • To note, this is the solution recommended by a MongoDB dev when they decided to not implement it directly ([see](https://jira.mongodb.org/browse/SERVER-1014?focusedCommentId=2305681&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-2305681)). – tanius Jan 09 '23 at 01:33
8

Starting in Mongo 4.4, the $function aggregation operator allows applying a custom javascript function to implement behaviour not supported by the MongoDB Query Language.

For instance, in order to update an array by removing an element at a given index:

// { "name": "dannie", "interests": ["guitar", "programming", "gadgets", "reading"] }
db.collection.update(
  { "name": "dannie" },
  [{ $set:
    { "interests":
      { $function: {
          body: function(interests) { interests.splice(2, 1); return interests; },
          args: ["$interests"],
          lang: "js"
      }}
    }
  }]
)
// { "name": "dannie", "interests": ["guitar", "programming", "reading"] }

$function takes 3 parameters:

  • body, which is the function to apply, whose parameter is the array to modify. The function here simply consists in using splice to remove 1 element at index 2.
  • args, which contains the fields from the record that the body function takes as parameter. In our case "$interests".
  • lang, which is the language in which the body function is written. Only js is currently available.
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
4

Rather than using the unset (as in the accepted answer), I solve this by setting the field to a unique value (i.e. not NULL) and then immediately pulling that value. A little safer from an asynch perspective. Here is the code:

    var update = {};
    var key = "ToBePulled_"+ new Date().toString();
    update['feedback.'+index] = key;
    Venues.update(venueId, {$set: update});
    return Venues.update(venueId, {$pull: {feedback: key}});

Hopefully mongo will address this, perhaps by extending the $position modifier to support $pull as well as $push.

Stephen Orr
  • 291
  • 3
  • 7
2

I would recommend using a GUID (I tend to use ObjectID) field, or an auto-incrementing field for each sub-document in the array.

With this GUID it is easy to issue a $pull and be sure that the correct one will be pulled. Same goes for other array operations.

Climax
  • 663
  • 6
  • 17
1

For people who are searching an answer using mongoose with nodejs. This is how I do it.

exports.deletePregunta = function (req, res) {
let codTest = req.params.tCodigo;
let indexPregunta = req.body.pregunta; // the index that come from frontend
let inPregunta = `tPreguntas.0.pregunta.${indexPregunta}`; // my field in my db
let inOpciones = `tPreguntas.0.opciones.${indexPregunta}`; // my other field in my db
let inTipo = `tPreguntas.0.tipo.${indexPregunta}`; // my  other field in my db

Test.findOneAndUpdate({ tCodigo: codTest },
    {
        '$unset': {
            [inPregunta]: 1, // put the field with [] 
            [inOpciones]: 1,
            [inTipo]: 1
        }
    }).then(()=>{ 
    Test.findOneAndUpdate({ tCodigo: codTest }, {
        '$pull': {
            'tPreguntas.0.pregunta': null,
            'tPreguntas.0.opciones': null,
            'tPreguntas.0.tipo': null
        }
    }).then(testModificado => {
        if (!testModificado) {
            res.status(404).send({ accion: 'deletePregunta', message: 'No se ha podido borrar esa pregunta ' });
        } else {
            res.status(200).send({ accion: 'deletePregunta', message: 'Pregunta borrada correctamente' });
        }
    })}).catch(err => { res.status(500).send({ accion: 'deletePregunta', message: 'error en la base de datos ' + err }); });
 }

I can rewrite this answer if it dont understand very well, but I think is okay.

Hope this help you, I lost a lot of time facing this issue.

Schwarz54
  • 964
  • 1
  • 9
  • 18
-1

It is little bit late but some may find it useful who are using robo3t-

db.getCollection('people').update( 
  {"name":"dannie"}, 
  { $pull: 
   {
       interests: "guitar" // you can change value to 
   } 
  }, 
  { multi: true }
);

If you have values something like -

property: [ 
    {
        "key" : "key1",
        "value" : "value 1"
    }, 
    {
        "key" : "key2",
        "value" : "value 2"
    }, 
    {
        "key" : "key3",
        "value" : "value 3"
    }
]

and you want to delete a record where the key is key3 then you can use something -

 db.getCollection('people').update( 
  {"name":"dannie"}, 
  { $pull: 
   {
       property: { key: "key3"} // you can change value to 
   } 
  }, 
  { multi: true }
);

The same goes for the nested property.

Vikas Chauhan
  • 1,276
  • 15
  • 23
-1

this can be done using $pop operator,
db.getCollection('collection_name').updateOne( {}, {$pop: {"path_to_array_object":1}})

src3369
  • 1,839
  • 2
  • 17
  • 18