← Files MongoDB AtlasARCHIVED FILE
references/pattern-outlier.md
5.63 KB · Sep 30, 2026 · 22:47 UTC
---
title: Use Outlier Pattern for Exceptional Documents
impact: MEDIUM
impactDescription: "Isolates unusually large documents so hot-path queries stay optimized for typical cases"
tags: schema, patterns, outlier, arrays, performance, edge-cases
---
## Use Outlier Pattern for Exceptional Documents
**Isolate atypical documents with large arrays to prevent them from degrading performance for typical queries.** When a small subset of documents is much larger than the rest, those outliers can dominate memory, index, and query costs. Split overflow data into a separate collection and flag the document.
**Problem scenario:**
A typical book might have 50 customers in an embedded array, while a bestseller like Harry Potter accumulates 50,000 (~2.5MB). Queries return the full document, so the outlier dominates memory and network cost. A multikey index on that array produces 50,000 entries for a single document.
**Correct (outlier pattern):**
Typical documents keep their full embedded array and set `hasExtras: false`. Outlier documents cap the embedded array at a threshold (e.g. 50), set `hasExtras: true`, store a denormalized `customerCount`, and overflow remaining items into a separate collection in batched documents (e.g. `{ bookId, customers: [...], batch: 1, count: 950 }`). Application code checks the `hasExtras` flag to decide whether to load overflow batches.
**Implementation with threshold (example; tune per workload):**
```javascript
const CUSTOMER_THRESHOLD = 50
async function addCustomer(bookId, customerId) {
// Try the normal case first: atomically add to the embedded array only if
// the current customerCount is below the threshold (treat missing/null as 0).
const result = await db.books.updateOne(
{
_id: bookId,
$or: [
{ customerCount: { $lt: CUSTOMER_THRESHOLD } },
{ customerCount: { $exists: false } },
{ customerCount: null }
]
},
{
$push: { customers: customerId },
$inc: { customerCount: 1 }
}
)
if (result.matchedCount > 0) {
// Normal case succeeded - customer added to embedded array
return
}
// Outlier case - add to overflow collection
const lastBatchDoc = await db.book_customers_extra
.find({ bookId: bookId })
.sort({ batch: -1 })
.limit(1)
.next()
const nextBatch = lastBatchDoc ? lastBatchDoc.batch + 1 : 1
const targetBatch =
lastBatchDoc && lastBatchDoc.count < 1000
? lastBatchDoc.batch
: nextBatch
// First, try to append to the intended batch, enforcing the 1000-item cap under concurrency.
const overflowFilter = { bookId: bookId, batch: targetBatch }
if (targetBatch !== nextBatch) {
// Only enforce the count cap when targeting an existing batch.
overflowFilter.count = { $lt: 1000 }
}
const overflowResult = await db.book_customers_extra.updateOne(
overflowFilter, // Write to the intended batch, respecting the count cap when reusing a batch
{
$push: { customers: customerId },
$inc: { count: 1 },
$setOnInsert: { bookId: bookId, batch: targetBatch }
},
{ upsert: targetBatch === nextBatch }
)
// If we failed to match when trying to reuse the previous batch (it filled concurrently),
// fall back to writing into the next batch.
if (overflowResult.matchedCount === 0 && targetBatch !== nextBatch) {
await db.book_customers_extra.updateOne(
{ bookId: bookId, batch: nextBatch },
{
$push: { customers: customerId },
$inc: { count: 1 },
$setOnInsert: { bookId: bookId, batch: nextBatch }
},
{ upsert: true }
)
}
await db.books.updateOne(
{ _id: bookId },
{
$set: { hasExtras: true },
$inc: { customerCount: 1 }
}
)
}
```
**Index strategy:**
```javascript
// Index on main collection - only 50 entries per outlier doc
db.books.createIndex({ "customers": 1 })
// Index on overflow collection
db.book_customers_extra.createIndex({ bookId: 1 })
db.book_customers_extra.createIndex({ customers: 1 })
```
**When to use outlier pattern:**
| Scenario | What to measure | Example |
|----------|-----------------|---------|
| Book customers | Array-size distribution and long tail | Bestsellers vs. typical books |
| Social followers | Growth rate and read-path impact | Celebrities vs. regular users |
| Product reviews | Index fan-out and read locality | Viral products vs. typical |
| Event attendees | Outlier frequency vs. implementation complexity | Major events vs. small meetups |
**When NOT to use this pattern:**
- **Uniform distribution**: If all documents have similar array sizes, no outliers to isolate.
- **Always need full data**: If you always display all 50,000 customers, pattern doesn't help.
- **Write-heavy outliers**: Complex update logic may not be worth the read optimization.
- **Small outliers**: If outliers are 200 vs typical 50, just use larger threshold.
## Verify with
```javascript
// Find outlier documents
db.books.aggregate([
{ $project: {
title: 1,
customerCount: { $size: { $ifNull: ["$customers", []] } }
}},
{ $sort: { customerCount: -1 } },
{ $limit: 20 }
])
// Calculate distribution
db.books.aggregate([
{ $project: { count: { $size: { $ifNull: ["$customers", []] } } } },
{ $bucket: {
groupBy: "$count",
boundaries: [0, 50, 100, 500, 1000, 10000, 100000],
default: "100000+",
output: { count: { $sum: 1 } }
}}
])
// Look for a long-tail distribution where a small subset is far above median/p95
// Check index sizes
db.books.stats().indexSizes
// Large multikey index suggests outliers are bloating it
```
Reference: [Outlier Pattern](https://mongodb.com/docs/manual/data-modeling/design-patterns/group-data/outlier-pattern/)
SHA-256: d848f5cb700c1fc5f7074290fa3aca5492c1e7ffcb14d0c520dd22697339daae