← Home Lecture 12 Β· MongoDB 101: The Document Model
1 / 17 πŸ“„ Notes
Module 1 Β· Lecture 12

MongoDB 101: The Document Model

Documents, BSON, _id, flexible schema β€” and the one modelling decision that determines whether your database is fast or miserable.

⏱ 90 minutes 🎯 CLO1 πŸ“¦ Lab: 15,000 rides
Where We Are

From "why" to "how"

Lecture 11 argued that document databases exist because applications think in nested objects and tables do not. Today we stop arguing and start typing.

You already have

A free Atlas M0 cluster, a bdma_student user, and a connection string you saved somewhere safe.

You leave with

15,000 Delhi NCR ride bookings in bdma.rides β€” the dataset every remaining lecture uses.

Today's real question: when related data appears, do you put it inside the document or beside it?
The Unit of Storage

This is a document

{
  _id: ObjectId("65f1a2c4e8b9d4a1c2f30142"),
  booking_id: "CNR3000142",
  booked_at: ISODate("2024-03-14T18:22:07Z"),
  status: "Completed",
  customer_id: "CID482910",
  vehicle_type: "Go Sedan",
  route:   { pickup: "Connaught Place", drop: "Aerocity" },
  booking_value: 412.50,
  ride_distance: 18.4,
  ratings: { driver: 4.6, customer: 4.8 },
  payment_method: "UPI"
}
Relationally this is three tables and two joins. Here it is one object, one read, in the exact shape the app displays.
Mental Model

The hierarchy, mapped to what you know

MongoDBRelationalIn This Course
DeploymentServer / instanceYour Atlas M0 cluster
DatabaseDatabasebdma
CollectionTablerides
DocumentRowCNR3000142
FieldColumnbooking_value, route.pickup
Three places the analogy breaks: no fixed columns Β· a value can be a whole structure Β· collections are created lazily, with no CREATE TABLE.
Flexible Schema Β· Demonstrated

Same collection, different shapes

Completed β€” 9,343

{ status: "Completed",
  avg_vtat: 7.4,
  avg_ctat: 31.2,
  booking_value: 412.50,
  ride_distance: 18.4,
  ratings: {driver:4.6,
            customer:4.8},
  payment_method: "UPI" }

Cancelled by Driver β€” 2,698

{ status:
    "Cancelled by Driver",
  avg_vtat: 2.84,
  cancellation: {
    by: "driver",
    reason: "Personal & Car
             related issues" } }
No fare, no distance, no rating β€” because the ride never happened. Absence is the data. 4,756 of 15,000 documents have no booking_value.
Storage Format

You write JSON. Mongo stores BSON.

JSONBSON
FormTextBinary
Types6~20 β€” ObjectId, Date, Decimal128, Int32/64, Binary
Field orderUndefinedPreserved
Reading one fieldParse the whole stringLength-prefixed β€” skip straight to it
Why it costs money: a Double cannot hold β‚Ή0.10 exactly. Sum a million fares and the ledger drifts. Money gets Decimal128; distances and ratings are fine as doubles.
Primary Key

Every document has an _id

ObjectId("65f1a2c4  e8b9d4a1c2  f30142")
          --------  ----------  ------
          4 bytes   5 bytes     3 bytes
          timestamp random      counter
          (seconds) per process per process
ComponentConsequence
TimestampSorting by _id β‰ˆ sorting by creation time, free, no extra index
RandomTwenty app servers can generate IDs at once without colliding
CounterOne process can generate thousands per second, still unique
The throughput point: AUTO_INCREMENT needs a round trip to a single shared counter. An ObjectId needs no coordination at all.
Discipline

"Schemaless" is the most damaging word in NoSQL

The schema does not disappear. It moves from the database into your application code, your analysts' queries, and your dashboards.

What flexibility buys

Additive change with no downtime Β· heterogeneity that matches reality Β· fast iteration while the record shape is still unknown.

What it costs

payment_method vs paymentMethod β€” both writes succeed, reports quietly undercount. 412.5 vs "412.5" β€” range queries silently skip the strings.

A team that never writes a validator has not chosen flexibility. It has chosen neglect.
Re-Tightening

$jsonSchema β€” strictness you opt into

db.createCollection("rides", {
  validator: { $jsonSchema: {
    bsonType: "object",
    required: ["booking_id", "booked_at", "status", "route"],
    properties: {
      booking_id: { bsonType: "string", pattern: "^CNR[0-9]{7}$" },
      status: { enum: ["Completed", "Cancelled by Driver",
                       "Cancelled by Customer", "No Driver Found",
                       "Incomplete"] },
      booking_value: { bsonType: ["double","int"], minimum: 0 }
    } } },
  validationLevel: "moderate",
  validationAction: "error"
})
Note what is not required: booking_value. For 4,756 bookings its absence is correct. validationAction: "warn" first, "error" once the backlog is clean.
The Core Decision

Embed or reference?

Embed

{ booking_id: "CNR3000142",
  route: { pickup: "CP",
           drop: "Aerocity" },
  ratings: { driver: 4.6,
             customer: 4.8 } }

One query. No join. Atomic single-document write.

Reference

// rides
{ booking_id: "CNR3000142",
  customer_id: "CID482910" }

// customers
{ _id: "CID482910",
  name: "Priya Sharma" }

Two queries, or one $lookup in Lecture 14.

In SQL, normalisation decides this for you. In MongoDB you decide, every time β€” and this is where document models succeed or fail.
The Rule of Thumb

Five tests, one decision

TestEmbed when…Reference when…
AccessRead together with the parentRead on its own
SizeSmall and boundedLarge or unbounded
ChangeChanges with the parentChanges independently
SharingBelongs to one parentShared by many parents
CardinalityOne-to-one, one-to-fewOne-to-many, many-to-many
Compressed: embed what is read together, bounded, and changes together. When the tests disagree, access pattern wins β€” it is what users feel.
Applied

Our dataset, decided

RelationshipCallWhy
Booking β†’ routeEmbedTwo fields, fixed at booking time, never read alone
Booking β†’ ratingsEmbedTwo scalars, written once at trip end
Booking β†’ cancellationEmbedSame business event, present only when relevant
Booking β†’ customerReferenceOne customer, hundreds of rides; a phone change must not rewrite history
Customer β†’ bookingsReferenceUnbounded β€” a commuter adds 500 a year, forever
Hybrid: embed a snapshot customer: { id, name } to skip the join. The name on an old receipt staying old is arguably correct, not a bug.
The Hard Limit

16 MB per document β€” and the trap it catches

// ANTI-PATTERN
{ _id: "CID482910",
  name: "Priya Sharma",
  bookings: [
    { booking_id: "CNR3000142", value: 412.50 },
    { booking_id: "CNR3000288", value: 180.00 },
    // ...one more every day, forever
  ] }

Four failures

Grows without bound Β· reading the name drags the whole history Β· every write relocates the document Β· a multikey index gets 5,000 entries per doc.

The fix

rides as its own collection with customer_id on each booking β€” exactly how our dataset is shaped.

Arrays are fine when bounded: tags, saved_addresses, fare_breakdown. If you cannot state the maximum with a straight face, reference.
Getting In

Three doors to the same cluster

⌨️ mongosh

JavaScript REPL with the database API attached. Everything in Lectures 13–16 is written for it.

πŸ–₯️ Compass

GUI: browse, build queries visually, read the inferred schema, chart explain plans.

πŸ”Œ Drivers

Node, Python, Java, C#, Go. Production code β€” and the Lecture 20 dashboard.

mongosh "mongodb+srv://bdma_student:<password>@cluster0.xxxxx.mongodb.net/"

# safer β€” prompt for the password instead of typing it
mongosh "mongodb+srv://cluster0.xxxxx.mongodb.net/" --username bdma_student
⚠️ That string is a live credential. Local .env, listed in .gitignore. Never a commit, a screenshot, or a Slack message.
Lab Β· Part 1

First document, by hand

use bdma
db                                // -> bdma
show collections                  // -> (empty)

db.rides.insertOne({
  booking_id: "CNR9999001",
  booked_at: new Date("2024-03-14T18:22:07Z"),
  status: "Completed",
  customer_id: "CID482910",
  vehicle_type: "Go Sedan",
  route: { pickup: "Connaught Place", drop: "Aerocity" },
  booking_value: 412.50,
  ride_distance: 18.4,
  ratings: { driver: 4.6, customer: 4.8 },
  payment_method: "UPI"
})

db.rides.find()
show collections                  // -> rides
use creates nothing. The database and collection spring into existence on the first write.
Lab Β· Part 2

Import 15,000 real bookings

# in mongosh β€” clear your test docs first
db.rides.deleteMany({})
# in your terminal, from the repo root
mongoimport --uri "mongodb+srv://bdma_student:<password>@cluster0.xxxxx.mongodb.net/bdma" \
  --collection rides --file data/uber_bookings.json --jsonArray
db.rides.countDocuments()                          // 15000
db.rides.countDocuments({ status: "Completed" })   // 9343

// turn booked_at from string into a real BSON Date
db.rides.updateMany(
  { booked_at: { $type: "string" } },
  [ { $set: { booked_at: { $toDate: "$booked_at" } } } ]
)
Then open Compass β†’ Schema tab: booking_value appears in only ~68% of documents. Not an error β€” the hook for Lecture 13.
Wrap-Up

Six things to carry forward

  1. A document is the unit of storage, and it nests. Collections have no fixed columns; fields can hold whole structures.
  2. You write JSON, Mongo stores BSON. Real types, preserved order, fast traversal. Money gets Decimal128.
  3. _id is mandatory; ObjectId is 12 bytes β€” timestamp + random + counter, so it is time-sortable and needs no coordination.
  4. Flexible schema defers the strictness decision β€” it does not delete it. $jsonSchema is how you take it back.
  5. Embed what is read together, bounded, and changes together. Reference what is large, unbounded, shared, or independent.
  6. 16 MB is a smoke alarm. The unbounded array is the fire it is listening for.
Next β€” Lecture 13: CRUD, the operator families, dot notation, projection and paging. Arrive with 15,000 documents loaded and verified.
← β†’ or Space to navigate Β· F for fullscreen