Working with Arrays

Store and change lists inside documents: add items with $push and $addToSet, remove them with $pull and $pop, keep arrays sorted and capped with $each, $sort and $slice, and update matching elements with the positional $ operator and arrayFilters.

Lesson 9 of 43Intermediate19 min read

Arrays are one of MongoDB's biggest strengths. A product can hold its list of tags, a blog post its comments, an order its line items, and a user their saved addresses, all inside one document. Reading the whole thing takes one query, with no joins.

Arrays also need their own update tools. You don't want to read an order, add an item in your code and write the whole array back: two requests doing that at once would lose an item. MongoDB provides array update operators that change arrays atomically on the server. $push adds items, $addToSet adds them only if they're not already present, $pull removes items matching a condition, and $pop removes the first or last element.

For arrays of documents, such as order lines, you often need to change one element: increase the quantity of the mug in an order, or mark one notification as read. The positional operators $, $[] and $[name] with arrayFilters do exactly that. In this lesson you'll use them all, and learn how to keep arrays from growing without limit.

Adding to arrays

MongoDB Shell
db.posts.updateOne({ _id: 1 }, { $push: { tags: 'mongodb' } })
db.posts.updateOne({ _id: 1 }, { $push: { tags: { $each: ['nosql', 'tips'] } } })
db.posts.updateOne({ _id: 1 }, { $addToSet: { tags: 'mongodb' } })   // no duplicate
  • $push appends a value, creating the array if the field doesn't exist.
  • $each pushes several values at once.
  • $addToSet adds a value only if it isn't already in the array; it also accepts $each.
  • With $each, $sort keeps the array ordered and $slice keeps only part of it: { $push: { recent: { $each: [item], $sort: { at: -1 }, $slice: 10 } } } stores the ten newest items.

Removing from arrays

  • $pull removes every element matching a value or condition: { $pull: { tags: 'draft' } }, or { $pull: { scores: { $lt: 50 } } }, or for documents { $pull: { items: { sku: 'PEN' } } }.
  • $pullAll removes every listed value: { $pullAll: { tags: ['a', 'b'] } }.
  • $pop removes the last element with 1 or the first with -1.

Updating elements in place

OperatorUpdatesExample
$The first element matched by the queryupdateOne({ _id: 1, 'items.sku': 'MUG' }, { $inc: { 'items.$.qty': 1 } })
$[]Every element{ $set: { 'items.$[].packed': false } }
$[name]Elements matching an arrayFilters condition{ $set: { 'items.$[i].onSale': true } }, { arrayFilters: [{ 'i.price': { $lt: 10 } }] }

The $ operator needs the array field in the query, so MongoDB knows which element matched. arrayFilters can match several elements and use any query operators.

How $addToSet and $pull decide what matches

$addToSet only skips a value if an identical value is already in the array. For strings and numbers that's simple. For embedded documents, identical means the same fields with the same values in the same order:

MongoDB Shell
// items is [{ sku: 'MUG', qty: 1 }]
db.carts.updateOne({ _id: 1 }, { $addToSet: { items: { sku: 'MUG', qty: 1 } } })   // no change
db.carts.updateOne({ _id: 1 }, { $addToSet: { items: { qty: 1, sku: 'MUG' } } })   // added: field order differs

So $addToSet is a poor way to keep "one line per SKU" in a cart. Instead, guard the push with a filter that checks the SKU isn't there yet: updateOne({ _id: 1, 'items.sku': { $ne: 'MUG' } }, { $push: { items: { sku: 'MUG', qty: 1 } } }). If the SKU is already present, the filter matches nothing and modifiedCount is 0, which tells your code to increment the quantity instead.

$pull works the other way round. A document condition in $pull is treated as a query, so { $pull: { items: { sku: 'MUG' } } } removes every element whose sku is 'MUG', whatever other fields it has, including both of the elements above.

Two more surprises with $addToSet:

  • Adding an array without $each, as in { $addToSet: { tags: ['b', 'c'] } }, adds the whole array as one nested element.
  • $addToSet doesn't remove duplicates that are already in the array; it only refuses to add new ones.

MongoDB Data Modelling Syntax

Array update operators

{ $push: { tags: 'new' } }
{ $push: { log: { $each: [a, b], $sort: { at: -1 }, $slice: 50 } } }
{ $addToSet: { roles: 'editor' } }
{ $pull: { items: { qty: 0 } } }
{ $pop: { queue: -1 } }
db.c.updateOne({ _id: 1, 'items.sku': 'X' }, { $set: { 'items.$.qty': 3 } })
db.c.updateOne({ _id: 1 }, { $inc: { 'items.$[].qty': 1 } })
db.c.updateOne({ _id: 1 }, { $set: { 'items.$[i].flag': true } }, { arrayFilters: [{ 'i.qty': { $gt: 5 } }] })

MongoDB Data Modelling Summary

  • $push adds to an array; $each adds several; $addToSet avoids duplicates.
  • $push with $each, $sort and $slice keeps arrays ordered and capped.
  • $pull removes matching elements; $pullAll removes listed values; $pop removes the first or last.
  • $ updates the first matched element; $[] updates all; $[name] with arrayFilters updates chosen ones.
  • Keep arrays bounded; unbounded lists belong in their own collection.

MongoDB Data Modelling Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Tags with $push, $addToSet, $pull and $pop

MongoDB Shell
db.posts.insertOne({ _id: 1, title: 'Intro to MongoDB', tags: ['database'] })
db.posts.updateOne({ _id: 1 }, { $push: { tags: { $each: ['nosql', 'beginner'] } } })
db.posts.updateOne({ _id: 1 }, { $addToSet: { tags: 'nosql' } }).modifiedCount
db.posts.updateOne({ _id: 1 }, { $addToSet: { tags: { $each: ['tutorial', 'database'] } } }).modifiedCount
db.posts.findOne({ _id: 1 }).tags
db.posts.updateOne({ _id: 1 }, { $pull: { tags: 'beginner' } }).modifiedCount
db.posts.updateOne({ _id: 1 }, { $pop: { tags: -1 } }).modifiedCount
db.posts.findOne({ _id: 1 }).tags
Output
{ acknowledged: true, insertedId: 1 }
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
0
1
[ 'database', 'nosql', 'beginner', 'tutorial' ]
1
1
[ 'nosql', 'tutorial' ]
Line-by-line explanation
  1. Line 2$each pushes two tags in one update.
  2. Line 3nosql is already there, so $addToSet changes nothing.
  3. Line 4Only tutorial is new; database already exists, so it isn't added again.
  4. Line 6$pull removes every element equal to beginner.
  5. Line 7$pop with -1 removes the first element, database.
Example 2

Updating order lines in place

MongoDB Shell
db.orders.insertOne({
  _id: 7,
  items: [
    { sku: 'MUG', qty: 1, price: 8.5 },
    { sku: 'LAMP', qty: 1, price: 24 },
    { sku: 'PEN', qty: 3, price: 1.2 }
  ]
})
db.orders.updateOne({ _id: 7, 'items.sku': 'MUG' }, { $inc: { 'items.$.qty': 2 } })
db.orders.updateOne({ _id: 7 }, { $set: { 'items.$[].packed': false } })
db.orders.updateOne({ _id: 7 }, { $set: { 'items.$[cheap].discount': 0.1 } }, { arrayFilters: [{ 'cheap.price': { $lt: 10 } }] })
db.orders.updateOne({ _id: 7 }, { $pull: { items: { sku: 'LAMP' } } })
db.orders.findOne({ _id: 7 }).items
Output
{ acknowledged: true, insertedId: 7 }
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
[
  { sku: 'MUG', qty: 3, price: 8.5, packed: false, discount: 0.1 },
  { sku: 'PEN', qty: 3, price: 1.2, packed: false, discount: 0.1 }
]
Line-by-line explanation
  1. Line 9The query matches the mug line, and $ refers to that element.
  2. Line 10$[] updates every element in the array.
  3. Line 11$[cheap] updates only elements that pass the arrayFilters condition, prices under 10.
  4. Line 12$pull removes array elements that match a condition, here the lamp line.
Example 3

A capped list of recent activity

MongoDB Shell
db.users.insertOne({ _id: 'ada', recent: [] })
for (let i = 1; i <= 7; i++) {
  db.users.updateOne({ _id: 'ada' }, {
    $push: { recent: { $each: [{ page: `/p/${i}`, n: i }], $sort: { n: -1 }, $slice: 3 } }
  })
}
db.users.findOne({ _id: 'ada' }).recent
Output
{ acknowledged: true, insertedId: 'ada' }
[
  { page: '/p/7', n: 7 },
  { page: '/p/6', n: 6 },
  { page: '/p/5', n: 5 }
]
Line-by-line explanation
  1. Line 4Each visit is pushed with $each, which $sort and $slice require.
  2. Line 4$sort keeps the newest first, and $slice: 3 keeps only three entries.
  3. Line 7However many pages are visited, the array never grows beyond three items.

Common MongoDB Data Modelling Mistakes

Reading, changing and writing back an array

Don't
const o = db.orders.findOne({ _id: 7 })
o.items.push(newItem)
db.orders.updateOne({ _id: 7 }, { $set: { items: o.items } })   // overwrites changes made by others meanwhile
Do
db.orders.updateOne({ _id: 7 }, { $push: { items: newItem } })

Using $ without the array in the query

Don't
db.orders.updateOne({ _id: 7 }, { $inc: { 'items.$.qty': 1 } })   // error: the positional operator did not find the match needed
Do
db.orders.updateOne({ _id: 7, 'items.sku': 'PEN' }, { $inc: { 'items.$.qty': 1 } })

An array that grows forever

Don't
db.products.updateOne({ _id: 1 }, { $push: { views: { at: new Date() } } })   // millions of entries over time
Do
db.productViews.insertOne({ productId: 1, at: new Date() })
db.products.updateOne({ _id: 1 }, { $inc: { viewCount: 1 } })

MongoDB Data Modelling in Real Projects

Arrays in real documents

E-commerce orders store their line items in an array and update quantities with the positional operator. Social apps keep a capped list of recent notifications with $push, $sort and $slice. Permission systems store roles with $addToSet so they're never duplicated, and wish lists use $pull to remove items. Comments, which can grow without limit, usually live in their own collection with a reference to the post.

MongoDB Data Modelling FAQ

What is the difference between $push and $addToSet?

$push always appends the value, even if it's already in the array, so duplicates are possible. $addToSet appends only when no identical value exists, which suits tags, roles and other sets. Both accept $each to add several values, but only $push supports $sort, $slice and $position.

How do I remove a specific element from an array in MongoDB?

Use $pull with the value or a condition: { $pull: { tags: 'draft' } } removes every 'draft', and { $pull: { items: { sku: 'PEN' } } } removes order lines with that SKU. To remove by position, there's no direct operator; $pop only removes the first or last element. For a middle position, set it to null with $unset on 'items.2' and then $pull: { items: null }, or rewrite the array with an update pipeline.

How do I add an element to the beginning of an array?

Use $push with $each and $position: 0: { $push: { recent: { $each: ['new'], $position: 0 } } }. $position only works together with $each, even when you're adding a single value. Without it, $push always appends to the end.

How do I find documents where an array contains a specific value?

Query the array field with the value directly: db.posts.find({ tags: 'mongodb' }) matches documents whose tags array contains 'mongodb'. Use $all for "contains all of these" and $in for "contains any of these". For arrays of documents with several conditions on one element, use $elemMatch.

How do I update every element of an array in MongoDB?

Use the all-positional operator $[]: db.orders.updateMany({}, { $set: { 'items.$[].packed': false } }). To update only some elements, use a named identifier like $[item] with an arrayFilters option describing which elements qualify. The plain $ operator only updates the first element that matched the query.

MongoDB Data Modelling Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the update so 'admin' is added only if it isn't already present.

db.users.updateOne({ _id: 1 }, { : { roles: 'admin' } })
Multiple choiceeasy

Which operator removes elements that match a condition?

Predict the outputmedium

What is tags after this update if it was ['a', 'b', 'c']?

db.c.updateOne({ _id: 1 }, { $pop: { tags: 1 } })
Multiple choicemedium

What does 'items.$.qty' refer to?

Multiple choicehard

How do you keep only the 10 newest entries when pushing to an array?

MongoDB Data Modelling Quiz

5 questions to check what you learned.

  1. What does $push do if the array field doesn't exist?

    • Throws an error
    • Does nothing
    • Creates the array with the new value
    • Creates a string
  2. Which operator updates every element of an array?

    • $
    • $[]
    • $all
    • $each
  3. What is $each used for?

    • Adding several values with $push or $addToSet
    • Looping in mongosh
    • Matching array elements in queries
    • Deleting elements
  4. What does arrayFilters provide?

    • A filter for the whole collection
    • Sorting for arrays
    • A projection
    • Conditions that choose which array elements a $[name] update changes
  5. Why shouldn't comments on a popular post be stored as an ever-growing array inside the post?

    • Arrays can't store documents
    • $push doesn't work on large arrays
    • Comments need ObjectIds
    • Documents have a 16 MB limit and huge arrays make reads and updates slow