What does the $unionWith stage add to a pipeline, and how does it differ from $lookup?
answer
- Think about the direction data is combined
- Does the document count grow or stay the same
- Compare it to concatenation, not to matching
- Ask whether repeats survive
- Order of the combined stream is not promised
basics
~20 s$unionWith appends documents from a second collection into the current pipeline's stream, optionally transformed by a sub-pipeline first. Unlike $lookup, which attaches matches as an array field on existing documents, it concatenates rather than joins, and it does not remove duplicates.
solid answer
~50 s`$lookup` combines *horizontally*: it leaves the document count unchanged and adds a joined array field. `$unionWith` combines *vertically*: it takes documents from another collection and appends them to whatever the pipeline has produced so far, increasing the document count. The full form is `{ $unionWith: { coll: "archived_orders", pipeline: [ ... ] } }`, where the optional sub-pipeline runs against that collection first — usually a `$match` and a `$project` so the incoming documents are filtered and shaped to match. Two properties matter in practice: it does **not** deduplicate, so it behaves like a concatenation rather than a set union, and the order of the combined output is not guaranteed unless you add a `$sort` afterwards. The typical use is querying live and archived data as one stream, or unioning per-tenant or per-period collections before grouping.
code
javascript · 11 linesdb.orders.aggregate([
{ $match: { placedAt: { $gte: ISODate("2024-01-01") } } },
{ $unionWith: {
coll: "archived_orders",
pipeline: [
{ $match: { placedAt: { $gte: ISODate("2024-01-01") } } },
{ $project: { customerId: 1, amount: 1, placedAt: 1 } }
]
} },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } }
])go deeper
Know that $unionWith brings documents from a second collection into the same pipeline, making the stream longer, while $lookup makes each document wider.
Explain the sub-pipeline form, why filtering belongs inside it, and that the stage concatenates without deduplicating or promising any output order.
Show that you handle the real hazards — schema drift between the unioned collections, duplicate records across a hot/cold split, and the cost of unioning a large archive without an indexed filter.
Own the partitioning consequence: if routine queries need to union many collections, question the split that created them rather than treating the union as the fix.
## Vertical, not horizontal The cleanest way to hold the difference is by what happens to the document count. - `$lookup` keeps the count the same and makes each document **wider** — it adds an array field of matched foreign documents. - `$unionWith` keeps documents the same width and makes the stream **longer** — it appends documents from another collection. So they answer different questions. "Which customer placed this order?" is `$lookup`. "Show me last month's orders together with the archived ones from two years ago" is `$unionWith`. ## Syntax ```js db.orders.aggregate([ { $match: { placedAt: { $gte: ISODate("2024-01-01") } } }, { $unionWith: { coll: "archived_orders", pipeline: [ { $match: { placedAt: { $gte: ISODate("2024-01-01") } } } ] } }, { $group: { _id: "$customerId", total: { $sum: "$amount" } } } ]) ``` There is also a shorthand — `{ $unionWith: "archived_orders" }` — which brings in every document from that collection with no filtering. Use it only when you really do want the whole collection, because the shorthand hides the cost. The optional `pipeline` runs against `coll` before its documents join the stream. It is the right place to put the filter and the projection, so that the incoming side is narrowed at the source rather than after being merged. ## It does not deduplicate This is the property most often assumed wrong. `$unionWith` concatenates; it does not compute a set union. If the same logical record exists in both collections, it appears twice in the output. To collapse duplicates you have to do it explicitly, typically with a `$group` on the identifying key after the union: ```js { $group: { _id: "$_id", doc: { $first: "$$ROOT" } } } ``` Similarly, the resulting order is not defined. Documents from the two sources are not interleaved in any promised way, so if order matters, add an explicit `$sort` after the union rather than relying on what you observe. ## Shape mismatch is your problem The two collections are not required to share a schema, and MongoDB will not complain if they do not. A union of collections whose field names diverge produces a heterogeneous stream, and any downstream `$group` or `$project` will quietly produce nulls for the fields one side is missing. When unioning collections that drifted apart — an archive written by an older version of the application, for instance — normalize the shape inside the union's sub-pipeline with `$project`/`$set` so everything reaching the next stage looks the same. ## Restrictions The sub-pipeline is a real aggregation pipeline but cannot contain the output stages `$out` or `$merge` — writing from inside a union has no coherent meaning. The overall aggregation can still end in `$out` or `$merge`, which is a common pattern: union several sources and materialize the combined result into one collection. ## Where it earns its keep - **Hot/cold split.** Recent data in a small working collection, older data in an archive; one query reads both. - **Per-period or per-tenant collections.** Where data was deliberately partitioned into separate collections, `$unionWith` is how a cross-partition report is assembled. - **Adding a synthetic row.** Unioning against a small collection lets you append totals or reference rows to a result set. - **Migration windows.** While records are moving between two collections, a union keeps reads complete without waiting for the move to finish. It is not, however, a substitute for a data model. If every query has to union five collections, the split that produced those collections is fighting the access pattern — that is a modelling signal, not a stage to optimize.
- Two collections both contain a record with the same _id and you $unionWith them. What comes out?Both documents, unchanged. `$unionWith` concatenates rather than computing a set union, so nothing is deduplicated — not even on `_id`, which is unique only within a single collection. If you need one row per key, follow the union with `{ $group: { _id: "$_id", doc: { $first: "$$ROOT" } } }` and then `$replaceRoot`, choosing deliberately which source wins.
- Where should the filter go when you union a large archive collection?Inside the `$unionWith` sub-pipeline, as a `$match` (and ideally a `$project` to trim fields). Filtering there narrows the archive side before its documents enter the stream and can use that collection's own indexes. A `$match` placed after the union has to reject documents that were already read and merged.
- Can the $unionWith sub-pipeline end with $merge to write the combined result?No — `$out` and `$merge` are not permitted inside the `$unionWith` sub-pipeline. You can still finish the *outer* pipeline with `$merge` or `$out`, which is the normal way to materialize a combined result: union the sources, group or shape them, then write once at the end.
saying these in an interview costs you the question
- Says $unionWith removes duplicate documents
- Treats it as an alternative spelling of $lookup
- Relies on the output order of the combined stream
- Assumes the two collections must share a schema
- Puts the archive filter after the union stage