One-to-Many

One-to-Few, One-to-Many, and Unbounded Relationships

Cardinality decides more than taste does, and three buckets cover almost every relationship you will meet. One-to-few — a few children with a known ceiling, such as addresses on a customer: embed them and stop thinking about it. One-to-many — hundreds to a few thousand, still bounded: embed only if you always read them with the parent and never sort them across parents, otherwise reference or keep a subset (Reference Patterns). Unbounded — audit entries, page views, IoT readings: always their own collection, for a reason worth measuring once.

Pushing an unbounded array until the server refuses
db.feeds.drop(); db.feeds.insertOne({ _id: 'u1', events: [] });
const ev = () => ({ at: new Date(), kind: 'view', postId: 1234 });
const size = id => bsonsize(db.feeds.findOne({ _id: id }));
for (let round = 1; round <= 5; round++) {
  try { db.feeds.updateOne({ _id: 'u1' },
    { $push: { events: { $each: Array.from({ length: 100000 }, ev) } } }); }
  catch (e) { print('stuck at ' + size('u1') + ' bytes -- code ' + e.code + ': '
    + e.message.split(' :: caused by :: ')[1]); break; }
  print(round * 100000 + ' events -> ' + (size('u1') / 1048576).toFixed(2) + ' MiB');
}
db.feeds.drop(); db.feeds.insertMany([{ _id: 'short', events: Array.from({length: 10}, ev) },
  { _id: 'long', events: Array.from({ length: 100000 }, ev) }]);
for (const id of ['short', 'long']) { const t = Date.now();
  for (let i = 0; i < 200; i++) db.feeds.updateOne({ _id: id }, { $push: { events: ev() } });
  print(id.padEnd(6) + ' 200 single $push: ' + (Date.now() - t) + ' ms'); }
Output
100000 events -> 4.85 MiB
200000 events -> 9.81 MiB
300000 events -> 14.77 MiB
stuck at 15488920 bytes -- code 10334: Resulting document after update is larger than 16777216
short  200 single $push: 198 ms
long   200 single $push: 10443 ms

A three-field event costs about 51.6 bytes of BSON, so the 16,777,216-byte document limit lands near 324,000 events. Because the result of the update must fit, the document freezes at 300,000 with error code 10334 and 1.2 MB of headroom it can never use — and from there it accepts no writes at all: a dead record you repair by hand.

The last two lines bite long before 16 MB. WiredTiger has no in-place array append: every $push reads the whole document, applies the change and writes it back. One append to a 100,000-element array cost 52 ms against 0.99 ms for the same append to a 10-element array — 53 times slower — and a multikey index over that array makes it worse, one key per element on every push.