Section 14: Cheatsheets

🎨 MongoDB Schema Patterns Cheatsheet

Complete guide to schema design patterns, relationships, and best practices with real-world examples

📚

Schema Design Philosophy

MongoDB vs SQL mindset

💡 The Key Difference

SQL: Design schema first, then optimize queries
MongoDB: Design schema around how you'll query the data!

Ask yourself: "How will this data be accessed?" not "How should I normalize this?"

SQL Mindset (Don't do this!)
• Normalize everything
• Multiple tables for relationships
• JOIN operations everywhere
• Schema first, queries later
• One "correct" schema
MongoDB Mindset (Do this!)
• Embed when data accessed together
• Denormalize for read performance
• Minimize lookups/joins
• Query patterns drive schema
• Many valid schemas
Relationship Type Cardinality Best Pattern Example
One-to-Few 1 : 1-10 Embedded Documents User has 3 addresses
One-to-Many 1 : 100s Reference (Child → Parent) Blog has 500 comments
One-to-Squillions 1 : Millions Reference (Parent → Child) Server has millions of log entries
Many-to-Many N : M Two-way References or Embedded IDs Students ↔ Courses
📦

Embedded Document Pattern

One-to-Few relationships

Basic Embedding
One-to-Few
Store related data together in a single document. Best when child data is always accessed with parent.
Example: User with Addresses
> // ✅ Embedded Pattern - All data in one document > db.users.insertOne({ _id: ObjectId("..."), name: "John Doe", email: "john@example.com", addresses: [ { type: "home", street: "123 Main St", city: "New York", zipCode: "10001" }, { type: "work", street: "456 Office Blvd", city: "New York", zipCode: "10002" } ], preferences: { newsletter: true, notifications: { email: true, sms: false } } }) > // Single query gets everything! > db.users.findOne({ email: "john@example.com" })
✅ Use Embedded When:
  • Data is always accessed together
  • Child data has no meaning without parent
  • One-to-Few relationship (< 100 embedded docs)
  • Embedded data doesn't grow unbounded
  • Need atomic updates
⚠️ Avoid Embedding When:
  • Embedded array grows unbounded (could exceed 16MB limit)
  • Child data needs to be accessed independently
  • Many-to-Many relationships
  • Child data updated frequently (write amplification)
🔗

Reference Pattern

One-to-Many relationships

Child References Parent
One-to-Many
Store parent ID in child documents. Best for one-to-many where you query from child side.
Example: Blog Posts and Comments
> // Parent: Blog Post > db.posts.insertOne({ _id: ObjectId("507f1f77bcf86cd799439011"), title: "MongoDB Schema Design", content: "...", author: "John Doe", publishedDate: new Date() }) > // Children: Comments (reference parent) > db.comments.insertMany([ { postId: ObjectId("507f1f77bcf86cd799439011"), // Reference! author: "Alice", text: "Great article!", createdAt: new Date() }, { postId: ObjectId("507f1f77bcf86cd799439011"), author: "Bob", text: "Very helpful, thanks!", createdAt: new Date() } ]) > // Query comments for a post > db.comments.find({ postId: ObjectId("507f1f77bcf86cd799439011") })
Parent References Children (Array of IDs)
One-to-Many
Store array of child IDs in parent. Best when you need to get all children with parent in one query.
Example: Product Categories
> // Parent: Category with product IDs > db.categories.insertOne({ _id: ObjectId("507f1f77bcf86cd799439012"), name: "Electronics", productIds: [ ObjectId("507f191e810c19729de860ea"), ObjectId("507f191e810c19729de860eb"), ObjectId("507f191e810c19729de860ec") ] }) > // Children: Products > db.products.insertOne({ _id: ObjectId("507f191e810c19729de860ea"), name: "Laptop", price: 999 }) > // Get category with all products using $lookup > db.categories.aggregate([ { $match: { name: "Electronics" } }, { $lookup: { from: "products", localField: "productIds", foreignField: "_id", as: "products" } } ])
⚠️ Array Size Limit Don't use this pattern if the array could grow very large (>1000 items). Use child-references-parent instead.
🎭

Polymorphic Pattern

Different document types in same collection

Polymorphic Schema
Flexibility
Store documents with different structures in the same collection using a type field.
Example: E-commerce Products
> // Book product > db.products.insertOne({ _id: ObjectId("..."), type: "book", // Discriminator field name: "MongoDB Handbook", price: 29.99, author: "Jane Smith", // Book-specific isbn: "978-1234567890", // Book-specific pages: 350 }) > // Electronics product > db.products.insertOne({ _id: ObjectId("..."), type: "electronics", name: "Wireless Mouse", price: 24.99, brand: "Logitech", // Electronics-specific warranty: "2 years", // Electronics-specific batteryType: "AA" }) > // Clothing product > db.products.insertOne({ _id: ObjectId("..."), type: "clothing", name: "T-Shirt", price: 19.99, sizes: ["S", "M", "L", "XL"], // Clothing-specific color: "Blue", material: "Cotton" }) > // Query specific product type > db.products.find({ type: "book" }) > // Query all products with price filter > db.products.find({ price: { $lt: 30 } })
✅ Benefits of Polymorphic Pattern:
  • Single collection for related but different entities
  • Easy to query across all types
  • Flexible schema per type
  • Common fields can be indexed
🪣

Bucket Pattern

Time-series and high-volume data

Time-Series Bucketing
Performance
Group time-series data into buckets (hourly, daily) to reduce document count and improve performance.
Example: IoT Sensor Readings
> // ❌ BAD: One document per reading (millions of docs!) > db.sensor_readings.insertOne({ sensorId: "sensor_123", temperature: 22.5, timestamp: ISODate("2024-01-15T10:00:00Z") }) > // ✅ GOOD: Bucket pattern (one doc per hour) > db.sensor_readings.insertOne({ sensorId: "sensor_123", date: ISODate("2024-01-15T10:00:00Z"), // Hour bucket readings: [ { minute: 0, temperature: 22.5 }, { minute: 1, temperature: 22.6 }, { minute: 2, temperature: 22.4 }, // ... up to 60 readings per hour ], count: 60, avgTemp: 22.5, minTemp: 22.1, maxTemp: 22.9 }) > // Query a specific hour's data > db.sensor_readings.findOne({ sensorId: "sensor_123", date: ISODate("2024-01-15T10:00:00Z") })
💡 Bucket Size Guidelines
  • Hourly buckets: High-frequency data (every minute/second)
  • Daily buckets: Medium-frequency data (every hour)
  • Monthly buckets: Low-frequency data (daily)
  • Keep bucket size under 16MB document limit
  • Pre-compute aggregations (avg, min, max) for better performance
📑

Subset Pattern

Partial data for performance

Subset Pattern
Optimization
Store a subset of frequently accessed data in the main document, keep full data separately.
Example: Product Reviews
> // Product with top 10 most helpful reviews > db.products.insertOne({ _id: ObjectId("..."), name: "Wireless Headphones", price: 99.99, avgRating: 4.5, totalReviews: 2547, // Top 10 reviews (subset for display) topReviews: [ { reviewId: ObjectId("..."), author: "Alice", rating: 5, text: "Amazing sound quality!", helpfulVotes: 234 }, // ... 9 more reviews ] }) > // All reviews in separate collection > db.reviews.insertOne({ _id: ObjectId("..."), productId: ObjectId("..."), author: "Alice", rating: 5, text: "Amazing sound quality! The bass is deep...", helpfulVotes: 234, createdAt: new Date() }) > // Fast query: Get product with top reviews (one query!) > db.products.findOne({ _id: ObjectId("...") }) > // Load all reviews only when user clicks "See all" > db.reviews.find({ productId: ObjectId("...") })
✅ When to Use Subset Pattern:
  • Large arrays that would make documents too big
  • Only need recent/top items most of the time
  • Want to avoid loading unnecessary data
  • Examples: Comments, reviews, notifications, messages
🧮

Computed Pattern

Pre-calculated values for performance

Computed Pattern
Performance
Pre-compute and store frequently calculated values instead of computing on each query.
Example: Order Total Calculation
> // ❌ BAD: Calculate total on every query > db.orders.aggregate([ { $unwind: "$items" }, { $group: { _id: "$_id", total: { $sum: { $multiply: ["$items.price", "$items.quantity"] } } } } ]) > // ✅ GOOD: Pre-compute and store > db.orders.insertOne({ _id: ObjectId("..."), customerId: ObjectId("..."), items: [ { product: "Laptop", price: 999, quantity: 1 }, { product: "Mouse", price: 25, quantity: 2 } ], // Pre-computed values subtotal: 1049, // 999 + (25*2) tax: 83.92, // 8% shipping: 10, total: 1142.92, // Computed! itemCount: 3, // Total items createdAt: new Date() }) > // Fast query - no computation needed! > db.orders.find({ total: { $gte: 1000 } }).sort({ total: -1 })
💡 Common Computed Fields
  • Order totals: subtotal, tax, shipping, total
  • Statistics: avgRating, totalReviews, viewCount
  • Counts: followerCount, likeCount, commentCount
  • Derived fields: fullName, ageInYears, daysUntilExpiry
⚠️ Update Strategy When underlying data changes, you must update computed values! Use transactions or change streams for consistency.
🎯

Design Decision Framework

How to choose the right pattern

✅ Schema Design Checklist
  1. Identify access patterns - How will data be queried?
  2. Determine relationship cardinality - One-to-few? One-to-many?
  3. Consider data growth - Will embedded arrays grow unbounded?
  4. Evaluate read vs write ratio - Read-heavy? Optimize for reads
  5. Check document size - Stay well under 16MB limit
  6. Decide atomicity needs - Need atomic updates?
  7. Plan for scaling - Will you need sharding later?
  8. Consider duplication tradeoffs - Storage vs query performance
Question Yes → Use This No → Use This
Data always accessed together? Embedded Documents References
One-to-Few (< 100)? Embedded Array References
Child data needs independent access? References Embedded
Unbounded array growth? References (Child → Parent) Embedded
Read performance critical? Denormalize, Computed Pattern Normalize, References
High-volume time-series? Bucket Pattern Regular Documents
Large arrays, need subset? Subset Pattern Full Embedded
Different entity types? Polymorphic Pattern Separate Collections
💡 Remember: There's No Single "Right" Schema!

Unlike SQL, MongoDB allows you to optimize your schema for YOUR specific use case. The same data can be modeled differently depending on how you'll query it. Start with your access patterns, not with normalization!