MongoDB Mastery
MongoDB Mastery lessons / Module 8: Arrays & Embedded Documents

Updating arrays: $push, $pull, positional operators

Level 2: Intermediate20 min

Add, remove and modify array elements in place, including the positional $, $[] and filtered $[id] operators.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • Use $push with $each, $slice, $sort
  • Use $addToSet, $pull, $pop
  • Update matched elements with $, all elements with $[], selected elements with $[id] and arrayFilters

conceptThe concept

OperatorEffect
$pushappend (with $each, $position, $slice, $sort modifiers)
$addToSetappend only if not already present
$pullremove all elements matching a value or condition
$pullAll / $popremove listed values / first (-1) or last (1) element
items.$.qtythe first element matched by the query filter
items.$[].qtyevery element
items.$[it].qty + arrayFilters: [{ "it.category": "books" }]every element matching the filter

$push with $each + $slice: -N keeps only the last N elements: a capped array, the safe way to store "recent" items.

whyWhy it matters

Changing one element in place is atomic and sends only the change over the wire. The alternative, reading the array, editing it in the application and writing it back, loses concurrent updates.

Use caseReal-world scenario

A "recently viewed products" list on the customer: $push: { recentlyViewed: { $each: [productId], $slice: -10 } }. Marking every book line in an order as shipped: $set: { "items.$[b].shipped": true } with arrayFilters: [{ "b.category": "books" }].

Try itExamples

Run and edit these in the interactive lesson.

$push with $each and $slice, $addToSet
db.customers.updateOne({ _id: 7 }, { $push: { recentlyViewed: { $each: ["P010", "P011", "P012"], $slice: -2 } } })
db.customers.updateOne({ _id: 7 }, { $addToSet: { tags: { $each: ["loyalty", "beta"] } } })
db.customers.findOne({ _id: 7 }, { recentlyViewed: 1, tags: 1 })
$pull with a condition, $pop
db.lab_cart.drop()
db.lab_cart.insertOne({ _id: 1, items: [{ sku: "A", qty: 0 }, { sku: "B", qty: 2 }, { sku: "C", qty: 0 }], history: [1, 2, 3] })
db.lab_cart.updateOne({ _id: 1 }, { $pull: { items: { qty: 0 } }, $pop: { history: -1 } })
db.lab_cart.findOne()
Positional $: the first matched element
db.lab_cart.updateOne({ _id: 1, "items.sku": "B" }, { $inc: { "items.$.qty": 5 } })
db.lab_cart.findOne()
$[] and $[identifier] with arrayFilters
const o = db.orders.findOne({ "items.2": { $exists: true } })
db.orders.updateOne({ _id: o._id }, { $set: { "items.$[].checked": true } })
db.orders.updateOne({ _id: o._id }, { $mul: { "items.$[big].price": 0.9 } }, { arrayFilters: [{ "big.price": { $gte: 100 } }] })
db.orders.findOne({ _id: o._id }, { items: 1 })

internalsUnder the hood

Arrays are BSON documents with numeric keys, so updates rewrite the document in the storage engine (WiredTiger writes a new version; there is no in-place byte patching). Large arrays make every update more expensive and every multikey index bigger. The oplog records array updates as diffs or as the full new array, so a big array updated often also inflates the oplog.

Avoid / preferDo this, not that

Avoid
db.carts.updateOne({ _id: 1 }, { $set: { items: itemsFromClient } })
Prefer
db.carts.updateOne(
  { _id: 1, "items.sku": "A1" },
  { $inc: { "items.$.qty": 1 } })

Rewriting the whole array means the last writer wins and concurrent changes vanish. A positional update changes one element atomically.

Avoid
db.orders.updateMany({}, { $set: { "items.$.taxed": true } })
Prefer
db.orders.updateMany({}, { $set: { "items.$[].taxed": true } })

The positional $ needs the array in the filter and updates only the first match, so this fails. $[] updates every element; $[name] with arrayFilters updates the ones that match a condition.

PitfallsCommon mistakes

  • Using items.$ without a filter condition on items: the server cannot know which element you mean.
  • Expecting $ to update all matching elements; it updates only the first. Use $[id] with arrayFilters.
  • Letting arrays grow forever with $push; add $slice or move the data to its own collection.

LimitsWhen not to use it

Do not use one document's array as a busy queue or a shared counter list that many clients push to at once: all those writes contend for one document and retry on write conflicts. Give each item its own document instead.

SecuritySecurity notes

A client that controls $push input can grow a document toward the 16 MB limit. Cap arrays with $slice and enforce maxItems in a $jsonSchema validator.

ChallengeTry it yourself

In a scratch collection order_lab, insert one order with three lines, then raise qty by 1 on every fashion line only.

Show one solution
One solution
db.order_lab.drop()
db.order_lab.insertOne({ _id: 1, items: [
  { sku: "A", category: "fashion", qty: 1 },
  { sku: "B", category: "books", qty: 2 },
  { sku: "C", category: "fashion", qty: 3 }
] })
db.order_lab.updateOne({ _id: 1 }, { $inc: { "items.$[f].qty": 1 } }, { arrayFilters: [{ "f.category": "fashion" }] })
db.order_lab.findOne()

InterviewQuestions you should be able to answer

How do you update all array elements that match a condition?

Use the filtered positional operator: updateMany(filter, { $set: { "items.$[x].status": "shipped" } }, { arrayFilters: [{ "x.category": "books" }] }). $ only updates the first match and $[] updates every element.

How would you store "last 50 activities" per user?

$push with $each and $slice: -50 keeps a bounded array in the user document. Full history goes to a separate collection (or a time series collection) with a TTL or archival policy.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.