skip to content

What is the difference between the $push and $addToSet update operators?

level: juniorimportance: should knowfreq 62%

answer

  1. Both append; only one checks first
  2. Think duplicates
  3. One of them is set-like
  4. Watch what happens without $each
  5. Only one of the two takes $slice and $sort

basics

~10 s

$push always appends the value to the array, so duplicates accumulate. $addToSet appends only if an equal value is not already present, giving set semantics. Both create the array when the field is missing.

solid answer

~50 s

Both operators add to an array field. `$push` appends unconditionally — call it twice with the same value and the array holds it twice. `$addToSet` first checks whether an **equal** value already exists and appends only if it does not, so the array behaves like a set; a no-op still matches the document but reports `modifiedCount: 0`. If the field does not exist, either operator creates it as a new array; if the field exists and is not an array, the write fails. To add several values at once you need the `$each` modifier: `{ $push: { tags: { $each: ["a", "b"] } } }`. Without `$each`, passing an array pushes it as a single **nested** array element — a classic bug. `$push` additionally supports `$slice`, `$sort` and `$position` alongside `$each`; `$addToSet` supports only `$each`.

code

javascript · 11 lines
javascript
// appends both values individually
db.posts.updateOne({ _id: 1 }, { $push: { tags: { $each: ["a", "b"] } } })

// adds only values not already present
db.posts.updateOne({ _id: 1 }, { $addToSet: { tags: { $each: ["a", "b"] } } })

// keeps only the ten most recent entries
db.posts.updateOne(
  { _id: 1 },
  { $push: { recent: { $each: [{ ts: new Date() }], $sort: { ts: -1 }, $slice: 10 } } }
)

go deeper

for a junior

Know that $push always appends and $addToSet appends only when the value is absent, and be able to write both calls plus the $each form for adding several values.

for a middle

Explain the $each/$slice/$sort/$position modifiers, what happens when the field is missing or is not an array, and how matchedCount versus modifiedCount reveals a no-op $addToSet.

for a senior

Discuss idempotent appends under client retries, why $addToSet is unreliable for embedded documents, and how unbounded array growth pressures document size and update cost.

for a principal

Own the guidance on when an array belongs in the document at all versus a separate collection, and set the convention for bounding growing arrays before documents approach the 16 MB limit.

## Adding to arrays MongoDB stores arrays as first-class field values, and the update language has operators that mutate them in place on the server, so you never have to read the array, modify it in your process, and write the whole document back. `$push` appends a value to the end of an array. It is unconditional: pushing `"blue"` three times leaves three copies. `$addToSet` appends a value only if the array does not already contain one equal to it. Semantically it turns the field into a set, though the underlying storage is still an ordinary ordered array — MongoDB simply refuses to add a duplicate. ```js db.posts.updateOne({ _id: 1 }, { $push: { tags: "mongo" } }) db.posts.updateOne({ _id: 1 }, { $addToSet: { tags: "mongo" } }) ``` After the first call `tags` contains one `"mongo"`; after the second it still contains one, and the write reports `matchedCount: 1, modifiedCount: 0`. That pair of counters is how you detect "the value was already there". ## Missing fields and wrong types If the target field is absent, both operators create it as a one-element array. If the field exists but holds a non-array value — a string, a number, an embedded document — the write fails with an error saying the field must be an array. This matters when a collection has been written by several code paths over time and some documents got a scalar where later code expects an array. ## The $each modifier The single most common mistake with `$push` is passing an array directly: ```js // tags becomes ["a", ["b", "c"]] -- probably not what you wanted db.posts.updateOne({ _id: 1 }, { $push: { tags: ["b", "c"] } }) // tags becomes ["a", "b", "c"] db.posts.updateOne({ _id: 1 }, { $push: { tags: { $each: ["b", "c"] } } }) ``` Without `$each`, the value is treated as one element, and since that element happens to be an array you get a nested array. The same applies to `$addToSet`: `{ $addToSet: { tags: ["b", "c"] } }` adds the two-element array as a single member, whereas `{ $addToSet: { tags: { $each: ["b", "c"] } } }` considers each value separately. ## Modifiers that only $push supports Combined with `$each`, `$push` accepts three further modifiers: - `$slice` truncates the array after the append. `{ $each: [x], $slice: -10 }` keeps the last ten elements; a positive value keeps the first n, and `0` empties the array. - `$sort` orders the elements after the append — `$sort: { ts: -1 }` for an array of embedded documents, or `$sort: -1` for an array of scalars. It requires `$each`. - `$position` inserts at a given index instead of appending, and also requires `$each`. So `{ $push: { recent: { $each: [event], $sort: { ts: -1 }, $slice: 10 } } }` maintains a most-recent-ten list in a single atomic write. `$addToSet` has none of these; it only takes `$each`. ## How $addToSet decides two values are equal For scalars, equality is the obvious thing, though it is type-aware: the string `"1"` and the number `1` are different values. For embedded documents, equality is exact and **field order matters** — `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` are not the same value, so both can end up in the array. For arrays, equality requires the same elements in the same order. If your "set" holds documents that clients might build with different key order, `$addToSet` will not deduplicate them reliably; storing a scalar identifier instead is the usual fix. ## The removal side The mirror-image operators are worth knowing in the same breath, because interviewers often ask for both directions: - `$pull` removes every element matching a condition: `{ $pull: { tags: "mongo" } }`, or with a predicate `{ $pull: { items: { qty: { $lt: 1 } } } }`. - `$pullAll` removes every element listed, by exact value. - `$pop` removes one element from an end: `-1` for the first, `1` for the last. ## Choosing between them Use `$addToSet` when the array models membership — tags, role names, ids of things that are either in or out — and duplicates would be a bug. Use `$push` when the array models a sequence where order and repetition carry meaning — an event log, a list of line items, a series of readings. When an append is meant to be idempotent under client retries, `$addToSet` gives that for free; making a `$push` idempotent requires either a uniqueness condition in the filter or a deduplicating identifier in the element. One caveat that applies to both: arrays that only ever grow eventually run into the 16 MB BSON document size limit, and large arrays make every update of the document more expensive. `$push` with `$slice` is the built-in way to bound one.

  • What happens if you push an array value without the $each modifier?
    The array you passed becomes a single element, producing a nested array: pushing `["b", "c"]` onto `["a"]` yields `["a", ["b", "c"]]`. `$each` is what tells the server to treat the value as a list of elements to add individually. The same distinction applies to `$addToSet`.
  • How would you keep an array capped at the ten most recent entries in a single write?
    Combine `$push` with `$each`, `$sort` and `$slice`: `{ $push: { recent: { $each: [entry], $sort: { ts: -1 }, $slice: 10 } } }`. The server appends, re-sorts the array by timestamp descending, then truncates to ten — all in one atomic modification of that document, with no read first.
  • Why might $addToSet still create duplicates in an array of embedded documents?
    Because equality for embedded documents is exact, including field order: `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` count as different values, and so do documents that differ in any field or BSON type. If clients serialize keys in different orders, both copies land in the array. Storing a scalar id instead makes deduplication reliable.

saying these in an interview costs you the question

  • Thinks $push deduplicates automatically
  • Passes an array to $push and expects it flattened
  • Believes $addToSet sorts or reorders the array
  • Assumes $addToSet works on non-array fields
  • Claims $addToSet supports $slice and $sort like $push

context