正在加载,请稍候…

MongoDB Schema Design Patterns: Embedding, Bucketing, Time-Series, and Validation

Master MongoDB schema design: embedding vs referencing trade-offs, bucket pattern for time-series, outlier pattern, native time-series collections, and schema validation.

MongoDB Schema Design Patterns for Production

MongoDB's flexible document model is a double-edged sword. Done right, exceptional performance. Done wrong, slow queries and bloated documents.

Design Around Access Patterns

Unlike SQL where you normalize data, MongoDB schema design is driven by access patterns:

  1. What queries run most frequently?
  2. What is the read/write ratio?
  3. How does data change over time?

Embedding vs. Referencing

// Embedding: All data in one document
// Good for: Always accessed together, bounded size, one-to-few
{
  _id: ObjectId("..."),
  name: "John Doe",
  addresses: [
    { type: "home", street: "123 Main St", city: "Austin" },
    { type: "work", street: "456 Corp Ave", city: "Austin" }
  ],
  preferences: { notifications: true, theme: "dark" }
}

// Referencing: Foreign key style
// Good for: Large/unbounded data, accessed independently, many-to-many
{
  _id: ObjectId("..."),
  name: "John Doe",
  order_ids: [ObjectId("..."), ObjectId("...")]
}

Bucket Pattern for Time-Series

Group measurements into time buckets to dramatically reduce document count:

// One document per measurement = 1M docs/sensor/year

// Bucket pattern: One doc per HOUR = 8,760 docs/sensor/year (114x fewer!)
{
  sensor_id: "s1",
  date: ISODate("2026-01-01T00:00:00"),
  nMeasurements: 60,
  measurements: [23.4, 23.5, 23.6],
  summary: { min: 23.1, max: 23.8, avg: 23.5 }
}

// Efficient update: add to existing bucket
db.sensor_data.updateOne(
  {
    sensor_id: "s1",
    date: ISODate("2026-01-01T00:00:00"),
    nMeasurements: { $lt: 60 }
  },
  {
    $push: { measurements: 23.7 },
    $inc: { nMeasurements: 1 },
    $min: { "summary.min": 23.7 },
    $max: { "summary.max": 23.7 }
  },
  { upsert: true }
)

Native Time-Series Collections (MongoDB 5.0+)

db.createCollection("sensor_readings", {
  timeseries: {
    timeField: "timestamp",
    metaField: "sensor_id",
    granularity: "minutes"
  },
  expireAfterSeconds: 7776000  // 90 days TTL
})

// Efficient hourly aggregation
db.sensor_readings.aggregate([
  { $match: { sensor_id: "temp_001", timestamp: { $gte: new Date("2026-01-01") } } },
  {
    $group: {
      _id: { $dateTrunc: { date: "$timestamp", unit: "hour" } },
      avgTemp: { $avg: "$temperature" },
      count: { $sum: 1 }
    }
  },
  { $sort: { _id: 1 } }
])

The Outlier Pattern

Handle occasional documents that would violate your normal schema:

// Normal post: embedded comments
{ _id: ObjectId("post_1"), comments: [...up to 50], has_extra: false }

// Viral post: overflow to separate collection
{ _id: ObjectId("post_2"), comments: [...first 50], has_extra: true }

// Overflow collection for additional comments
{ post_id: ObjectId("post_2"), comments: [...next batch] }

async function getComments(postId) {
  const post = await db.posts.findOne({ _id: postId });
  if (!post.has_extra_comments) return post.comments;
  const overflow = await db.overflow.find({ post_id: postId }).toArray();
  return [...post.comments, ...overflow.flatMap(o => o.comments)];
}

Schema Validation

db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["email", "name", "created_at"],
      properties: {
        email: {
          bsonType: "string",
          pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
quot; }, role: { enum: ["user", "admin", "moderator"] }, age: { bsonType: "int", minimum: 0, maximum: 150 } } } }, validationAction: "error" })

Indexing Best Practices

// Compound index for common queries
db.orders.createIndex({ user_id: 1, status: 1, created_at: -1 })

// Partial index: only active records
db.users.createIndex(
  { email: 1 },
  {
    partialFilterExpression: { deleted_at: { $exists: false } },
    unique: true
  }
)

// Text search with field weighting
db.articles.createIndex(
  { title: "text", content: "text" },
  { weights: { title: 10, content: 1 } }
)

// Profile query performance
db.orders.find({ user_id: "123" }).explain("executionStats")
// Look for: IXSCAN over COLLSCAN
// totalDocsExamined should be close to nReturned

Anti-Patterns to Avoid

// AVOID: Unbounded arrays (16MB document limit)
{ liked_posts: [/* millions of IDs */] }
// Fix: Separate collection for likes

// AVOID: Monotonically increasing shard key (creates write hotspot)
// Fix: Hash-based sharding
db.adminCommand({
  shardCollection: "mydb.events",
  key: { user_id: "hashed" }
})

Good MongoDB schema design means thinking about access patterns first, then choosing the right embedding/referencing strategy and indexes.