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 📍 Prerequisites: Lecture 11 — a running Atlas M0 cluster and your connection string 🔧 Lab: Connect with Compass + mongosh, import the rides dataset

Contents

  1. Learning Objectives
  2. Session Timeline
  3. From Rows to Documents
  4. BSON — Why Not Just JSON
  5. The _id Field and the ObjectId
  6. Flexible Schema and the Discipline It Demands
  7. Embedding vs Referencing — The Central Decision
  8. Connecting: mongosh, Compass, Drivers
  9. Hands-On — Load the Course Dataset
  10. Key Takeaways

Learning Objectives

Session Timeline

TimeSegmentActivity
0–6 minRecapLecture 11 in one slide; confirm everyone's Atlas cluster is live
6–18 minThe HierarchyDocument, collection, database — mapped to table/row/column and where the analogy breaks
18–30 minBSONWhy binary, which types it adds, and the business cost of untyped storage
30–40 min_id & ObjectIdThe 12-byte anatomy; client-side generation; time-sortability
40–52 minFlexible SchemaWhat it buys, what it costs, and $jsonSchema validation as the counterweight
52–72 minEmbed vs ReferenceThe core decision, the 16 MB limit, the unbounded-array trap, worked examples
72–86 minLabCompass + mongosh, insertOne, find, then mongoimport of 15,000 rides
86–90 minWrap-UpVerify counts, assignment reminder, Lecture 13 preview

Section 1: From Rows to Documents

In Lecture 11 we argued that document databases exist because applications think in nested objects and tables do not. This lecture makes that concrete. The unit of storage in MongoDB is the document: an ordered set of field-and-value pairs, where a value may itself be a document or an array of documents.

Here is one real record from the dataset you will load today — a completed booking from the Delhi NCR ride-hailing data.

{
  _id: ObjectId("65f1a2c4e8b9d4a1c2f30142"),
  booking_id: "CNR3000142",
  booked_at: ISODate("2024-03-14T18:22:07Z"),
  date: "2024-03-14",
  status: "Completed",
  customer_id: "CID482910",
  vehicle_type: "Go Sedan",
  route: { pickup: "Connaught Place", drop: "Aerocity" },
  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"
}

Note what has happened. In a relational design this booking would be spread across a bookings table, a routes table, and a ratings table, joined on every read. Here it is one object, retrieved in one operation, in exactly the shape the application wants to display.

The Four-Level Hierarchy

LevelMongoDBRelational EquivalentIn Our Course
1Deployment (cluster)Database server / instanceYour Atlas M0 cluster
2DatabaseDatabase / schemabdma
3CollectionTablerides
4DocumentRowOne booking, e.g. CNR3000142
5FieldColumnbooking_value, route.pickup

Where the analogy breaks — and it matters:

1. A collection has no fixed columns. A table declares its columns once, for every row. A collection declares nothing. Two documents in rides may carry entirely different field sets — and in our dataset they genuinely do.

2. A field value can be a whole structure. route is not a string; it is a sub-document with its own fields. ratings is another. A relational column holds one scalar, full stop.

3. Collections are created lazily. Writing to db.rides creates the collection if it does not exist. There is no CREATE TABLE step and no migration ceremony.

Different Shapes in One Collection

Our dataset is deliberately realistic: a booking that never completed does not have a fare, a distance, or ratings, because those facts never came into existence. Compare the completed document above with a cancelled one:

{
  booking_id: "CNR3004193",
  booked_at: ISODate("2024-01-01T13:00:56Z"),
  date: "2024-01-01",
  status: "Cancelled by Driver",
  customer_id: "CID695170",
  vehicle_type: "Premier Sedan",
  route: { pickup: "New Delhi Railway Station", drop: "Anand Vihar ISBT" },
  avg_vtat: 2.84,
  cancellation: { by: "driver", reason: "Personal & Car related issues" }
}

No booking_value. No ride_distance. No ratings. No payment_method. Instead there is a cancellation sub-document that the completed booking does not have.

StatusCountShareFields Present Beyond the Common Core
Completed9,34362.29%avg_vtat, avg_ctat, booking_value, ride_distance, ratings, payment_method
Cancelled by Driver2,69817.99%avg_vtat, cancellation
No Driver Found1,0366.91%none — not even avg_vtat
Cancelled by Customer1,0226.81%avg_vtat, cancellation
Incomplete9016.01%avg_vtat, avg_ctat, booking_value, ride_distance, payment_method, incomplete

Business framing: in a relational table these 15,000 rows would need roughly six nullable columns and two nullable sub-tables, and every analyst query would have to remember which NULL means "did not happen" and which means "we failed to record it". In the document model the absence of booking_value is itself the signal: no fare exists because no ride happened. Lecture 13 turns that into a query with $exists.

Section 2: BSON — Why Not Just JSON

You write JSON. MongoDB stores BSON — Binary JSON. The difference is invisible most of the time and decisive the rest of the time.

JSON is a text format designed for humans and for interchange. It has exactly six types: string, number, boolean, null, array, object. That is fine for sending data over a wire. It is inadequate for a database, for three reasons.

Reason 1 — JSON Has No Real Types

In JSON, 412.50 is just "a number". Is it a 32-bit integer? A 64-bit integer? A float? An exact decimal suitable for money? JSON does not say, so every reader has to guess — and different languages guess differently. BSON records the type explicitly on every value.

BSON TypeWhat It SolvesWhere It Appears in rides
ObjectIdA compact, globally unique, roughly time-ordered 12-byte key_id on every document
DateA real timestamp (ms since epoch), sortable and range-queryablebooked_at, once converted from its ISO string
Int32 / Int64Exact integers that never drift into floating pointCounts and derived hour-of-day values
DoubleStandard 64-bit floatride_distance, avg_vtat, ratings.driver
Decimal128Exact decimal arithmetic — no 0.1 + 0.2 surprisesWhat booking_value should be in a real billing system
BinaryRaw bytes: images, hashes, encrypted blobsNot used here; common for documents and KYC images
Null vs missingTwo distinct states: "known to be nothing" vs "not recorded"Our dataset uses missing, deliberately

Why an MBA should care about Decimal128: a Double cannot represent ₹0.10 exactly. Sum 9,343 fares stored as doubles and the total drifts by fractions of a paisa. On a 15,000-row teaching dataset that is harmless. On a payments ledger processing millions of transactions a day, it is an audit finding. Money gets Decimal128; distances and ratings are fine as doubles.

Reason 2 — BSON Is Traversable

Every BSON element is prefixed with its length. That means the database can skip over a field it does not need instead of parsing the whole document character by character. When you ask for route.pickup from a document with fourteen fields, the storage engine jumps straight to it. On 15,000 documents the saving is small; on 150 million it is the difference between a two-second query and a two-minute one.

Reason 3 — BSON Preserves Field Order

JSON object key order is formally undefined. BSON guarantees it, which is what makes compound index key ordering and certain comparison semantics deterministic.

JSONBSON
FormTextBinary
Readable by humansYesNo — tools render it as JSON for you
Type system6 types~20 types including dates, decimals, binary, ObjectId
Field orderUndefinedPreserved
SpaceCompact for small textSlightly larger (length prefixes) but far faster to traverse
Where you meet itWhat you type, what APIs returnWhat is on disk and on the wire

The practical rule: you think and write in JSON; the driver converts to BSON on the way in and back on the way out. The only time the distinction surfaces is when you need a type JSON cannot express — a date, a decimal, or an ObjectId — and then you must say so explicitly. We do exactly that in the lab when converting booked_at from a string into a real Date.

Section 3: The _id Field and the ObjectId

Every document in every MongoDB collection has an _id field. It is the primary key, it is unique within the collection, and it is indexed automatically — you cannot drop that index. If you insert a document without an _id, the driver generates one for you, and by default it is an ObjectId.

Anatomy of an ObjectId — 12 Bytes

ObjectId("65f1a2c4  e8b9d4a1c2  f30142")
          --------  ----------  ------
          |         |           |
          |         |           +-- 3 bytes: counter, starts at a random
          |         |                value, increments per process
          |         +-- 5 bytes: random value, unique per machine+process
          +-- 4 bytes: seconds since the Unix epoch (creation time)
BytesContentConsequence
0–3Unix timestamp in secondsSorting by _id is approximately sorting by creation time — for free, with no extra index
4–8Random per-process valueTwo application servers generating IDs at the same instant will not collide
9–11Incrementing counterThe same process generating thousands of IDs in one second stays unique

Why This Design Matters Commercially

A relational AUTO_INCREMENT key requires a round trip to the database: the application must insert, wait, and read back the assigned ID. That works on one server. On twenty application servers writing to a sharded cluster it becomes a global coordination bottleneck — a single counter everyone must queue behind.

An ObjectId is generated client-side, with no coordination at all, and is still unique. The application can construct a whole object graph, assign IDs, and write it in one batch. That is a throughput property, not a cosmetic one.

// The timestamp is extractable — no separate created_at needed
const id = ObjectId("65f1a2c4e8b9d4a1c2f30142")
id.getTimestamp()        // ISODate("2024-03-13T10:34:12Z")

// Which means you can range-query on _id alone
db.rides.find({ _id: { $gt: ObjectId("65f1a2c400000000 00000000") } })

You May Supply Your Own _id

_id does not have to be an ObjectId. It can be any unique value: a string, an integer, even a sub-document. In our dataset every booking already carries a natural key — booking_id — so a defensible design would be:

db.rides.insertOne({
  _id: "CNR3000142",          // natural key as the primary key
  status: "Completed",
  vehicle_type: "Go Sedan",
  route: { pickup: "Connaught Place", drop: "Aerocity" }
})

Trade-off: using the natural key saves 12 bytes and a redundant index, and makes _id meaningful to a human reading the data. It costs you the free time-ordering an ObjectId provides, and it breaks badly if the business ever reissues a booking reference. We keep the generated ObjectId and leave booking_id as an ordinary indexed field — the conventional choice.

Section 4: Flexible Schema and the Discipline It Demands

"Schemaless" is the single most damaging word in the NoSQL vocabulary. MongoDB collections are not schemaless. They are schema-flexible: the schema is not declared to the database up front, but it absolutely still exists — in your application code, in your analysts' queries, and in your dashboards.

What Flexibility Genuinely Buys You

What It Costs You

RiskHow It Shows UpMitigation
Silent typosOne service writes payment_method, another writes paymentMethod. Both succeed. Reports quietly undercount.Schema validation + code review
Type driftbooking_value arrives as 412.5 from one path and "412.5" from another. Range queries silently skip the strings.bsonType in the validator
ArchaeologyNobody knows what fields exist. Every new analyst starts by sampling documents.Documented schema; Compass's Schema tab
Query complexityEvery aggregation must defend against missing fields.Deliberate, documented absence rules

Re-Tightening: $jsonSchema Validation

MongoDB lets you attach a validator to a collection. It is opt-in, it can be as strict or as loose as you like, and it can be tightened progressively as a product matures. This is the professional answer to "flexible schema is dangerous": you choose, per collection, where on the spectrum to sit.

db.createCollection("rides", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["booking_id", "booked_at", "status", "customer_id",
                 "vehicle_type", "route"],
      properties: {
        booking_id: {
          bsonType: "string",
          pattern: "^CNR[0-9]{7}$",
          description: "must match CNR followed by 7 digits"
        },
        booked_at: { bsonType: "date" },
        status: {
          enum: ["Completed", "Cancelled by Driver", "Cancelled by Customer",
                 "No Driver Found", "Incomplete"],
          description: "must be one of the five known booking outcomes"
        },
        vehicle_type: {
          enum: ["Go Mini", "Go Sedan", "Auto", "eBike", "Bike",
                 "Premier Sedan", "Uber XL"]
        },
        route: {
          bsonType: "object",
          required: ["pickup", "drop"],
          properties: {
            pickup: { bsonType: "string" },
            drop:   { bsonType: "string" }
          }
        },
        booking_value: { bsonType: ["double", "int"], minimum: 0 },
        ride_distance: { bsonType: ["double", "int"], minimum: 0 },
        ratings: {
          bsonType: "object",
          properties: {
            driver:   { bsonType: "double", minimum: 1, maximum: 5 },
            customer: { bsonType: "double", minimum: 1, maximum: 5 }
          }
        }
      }
    }
  },
  validationLevel: "moderate",
  validationAction: "error"
})

Read that validator as a business contract. It says: every booking must have an ID in the house format, a real timestamp, one of five known outcomes, one of seven vehicle types, and a route with both ends. It says nothing at all about booking_value being required — because for 5,657 of our 15,000 bookings it legitimately does not exist.

SettingOptionsWhen to Use Which
validationLeveloff · moderate · strictmoderate checks inserts and updates to already-valid documents only — the right setting when retrofitting rules onto legacy data
validationActionerror · warnStart with warn to measure how much existing data would fail, then switch to error once the backlog is clean

The managerial line to remember: relational databases force the strictness decision on day one, before you know the answer. MongoDB lets you defer it and then apply exactly as much as the business actually needs — but somebody has to make that decision deliberately. A team that never writes a validator has not chosen flexibility; it has chosen neglect.

Section 5: Embedding vs Referencing — The Central Decision

Everything else in this lecture is vocabulary. This section is the actual skill. In a relational database the model is largely dictated by normalisation rules. In MongoDB you decide, for every relationship, whether the related data lives inside the parent document or in a separate collection pointed at by an identifier. Get it right and reads are single-operation and fast. Get it wrong and you have rebuilt a slow relational database with none of the safety.

Embedding

The related data is stored inside the parent document. Our route and ratings fields are embedded sub-documents.

{
  booking_id: "CNR3000142",
  status: "Completed",
  route:   { pickup: "Connaught Place", drop: "Aerocity" },
  ratings: { driver: 4.6, customer: 4.8 }
}
// One query returns everything. No join. Atomic single-document write.

Referencing

The related data lives elsewhere and the parent stores a pointer. Our customer_id is a reference to a separate customers collection.

// bdma.rides
{ booking_id: "CNR3000142", customer_id: "CID482910", status: "Completed" }

// bdma.customers
{ _id: "CID482910", name: "Priya Sharma", phone: "+91-98xxxxxx21",
  home_city: "Delhi", joined: ISODate("2022-06-11T00:00:00Z") }
// Two queries, or one $lookup in an aggregation (Lecture 14).

The Rule of Thumb

TestEmbed when…Reference when…
Access patternThe data is almost always read together with the parentThe data is usually needed on its own, or only occasionally with the parent
SizeThe embedded part is small and bounded — you can state a maximumIt is large, or grows without a natural limit
Change frequencyIt changes with the parent, in the same business eventIt changes independently, on its own lifecycle
SharingIt belongs to exactly one parentMany parents refer to the same entity
CardinalityOne-to-one, or one-to-few (tens)One-to-many (thousands) or many-to-many

The compressed version: embed what is read together, bounded, and changes together. Reference what is large, unbounded, shared, or changes independently. If a relationship satisfies some tests and not others, let access pattern win — it is the one your users feel.

Applying It to Our Dataset

RelationshipDecisionReasoning
Booking → route (pickup, drop)EmbedExactly two fields, never read without the booking, fixed at booking time and never updated
Booking → ratingsEmbedTwo scalars, bounded, written once at trip end, meaningless outside the booking
Booking → cancellation reasonEmbedOne sub-document, present only when relevant, part of the same event
Booking → customerReferenceOne customer has hundreds of bookings; a phone-number change must not require rewriting every historical ride
Booking → driverReferenceSame reason, plus driver records carry licences and documents that no booking needs
Customer → all their bookingsReference (from the booking side)Unbounded — a daily commuter generates 500 rides a year, forever

The hybrid pattern (extended reference): you may embed a snapshot of a few referenced fields to avoid the join, while keeping the reference for the full record. Storing customer: { id: "CID482910", name: "Priya S." } on each booking lets a trip-history screen render with no second query. The deliberate trade-off is that if the customer changes their name, historical bookings keep the old one — which for an invoice or a receipt is arguably correct, not a bug.

The Hard Limit: 16 MB per Document

A single BSON document may not exceed 16 megabytes. This is not a tuning parameter; it cannot be raised. It exists to stop any one document from monopolising memory and network bandwidth.

16 MB is enormous for a booking — our documents are roughly 300 bytes each, so the entire 15,000-record dataset is about 4.5 MB. You will never hit the limit with well-modelled data. You will hit it instantly with badly modelled data, and that is the point of the limit: it is a design smoke alarm.

The Unbounded Array Anti-Pattern

This is the single most common modelling mistake in document databases, so it is worth seeing explicitly.

// ANTI-PATTERN — do not do this
{
  _id: "CID482910",
  name: "Priya Sharma",
  bookings: [
    { booking_id: "CNR3000142", date: "2024-03-14", value: 412.50 },
    { booking_id: "CNR3000288", date: "2024-03-15", value: 180.00 },
    // ... and one more every single day, forever
  ]
}

Four distinct failures, in the order you meet them:

1. The document grows without bound. A daily commuter adds ~500 entries a year. The array has no natural ceiling, so the only question is when the document breaks, not whether.

2. Every read pays for every booking. Displaying the customer's name pulls their entire ride history off disk and across the network.

3. Every write rewrites the document. As the document outgrows its allocated space the storage engine must relocate it — turning a cheap append into an expensive move.

4. Indexing degrades. A multikey index on a 5,000-element array creates 5,000 index entries per document.

The fix: keep rides as its own collection with customer_id on each booking — which is exactly how our dataset is shaped. Embed the one-to-few; reference the one-to-many.

When an Array Is the Right Answer

Arrays are not the problem; unbounded arrays are. These are fine because each has a natural ceiling:

{ tags: ["airport", "night", "surge"] }                   // a handful
{ saved_addresses: [ {label:"Home",...}, {label:"Work",...} ] }  // ~5 max
{ fare_breakdown: [ {component:"base", amount:60},
                    {component:"distance", amount:294},
                    {component:"surge", amount:58.50} ] }  // fixed set

If you cannot state the maximum number of elements with a straight face, reference instead.

Section 6: Connecting — mongosh, Compass, Drivers

There are three ways into a MongoDB cluster and you will use all three this term.

⌨️ mongosh

The official shell. A full JavaScript REPL with the database API attached. Everything in Lectures 13–16 is written for it.

Best for: precise work, scripting, anything you need to reproduce or hand in.

🖥️ Compass

The official GUI. Browse collections, build queries visually, read the automatically inferred schema, and run explain plans with charts.

Best for: exploring unfamiliar data and showing results to people who do not write queries.

🔌 Drivers

Official libraries for Node.js, Python, Java, C#, Go and more. The same operations, expressed in your application's language.

Best for: production code — and for the Node dashboard you build in Lecture 20.

The Connection String

All three take the same SRV connection string you saved in Lecture 11:

mongodb+srv://bdma_student:<password>@cluster0.xxxxx.mongodb.net/
PartMeaning
mongodb+srv://Use DNS SRV records to discover every node in the replica set — one hostname instead of three
bdma_student:<password>The database user you created. URL-encode special characters: @ becomes %40
cluster0.xxxxx.mongodb.netYour cluster's hostname — unique to your Atlas project
Trailing /bdmaOptional default database. Supply it for mongoimport; omit it and use use bdma in the shell
# Connect the shell to your cluster
mongosh "mongodb+srv://bdma_student:<password>@cluster0.xxxxx.mongodb.net/"

# Safer: let the shell prompt for the password instead of typing it
mongosh "mongodb+srv://cluster0.xxxxx.mongodb.net/" \
  --username bdma_student

Reminder from Lecture 11: that string contains a live credential. It belongs in a local .env file that is listed in .gitignore — never in a commit, a slide, a screenshot, or a Slack message. Automated scanners find exposed Atlas credentials within minutes of a public push.

Section 7: Hands-On — Load the Course Dataset

From here to the end of the course, every query you write runs against the same data: 15,000 Uber-style ride bookings from Delhi NCR covering calendar year 2024, in database bdma, collection rides. Load it now.

1

Install the tools. Download MongoDB Compass and the MongoDB Database Tools (which contain mongoimport) from mongodb.com/try/download. mongosh ships with Compass, or installs separately.

2

Open Compass, paste your connection string, and connect. You should see the built-in admin and local databases — proof the cluster is reachable and your user works.

3

Now connect from the shell and select a database. use does not create anything yet; the database springs into existence on the first write.

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

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

Insert your first document by hand. Watch the response: MongoDB returns the _id it generated.

db.rides.insertOne({
  booking_id: "CNR9999001",
  booked_at: new Date("2024-03-14T18:22:07Z"),
  date: "2024-03-14",
  status: "Completed",
  customer_id: "CID482910",
  vehicle_type: "Go Sedan",
  route: { pickup: "Connaught Place", drop: "Aerocity" },
  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"
})

// { acknowledged: true,
//   insertedId: ObjectId("65f1a2c4e8b9d4a1c2f30142") }
5

Read it back, then confirm the collection now exists.

db.rides.find()
db.rides.find().pretty()           // formatted output
show collections                   // -> rides
db.rides.countDocuments()          // -> 1
6

Insert a second document with a different shape — a cancellation. It succeeds without complaint. That is flexible schema, demonstrated rather than described.

db.rides.insertOne({
  booking_id: "CNR9999002",
  booked_at: new Date("2024-03-14T19:05:44Z"),
  date: "2024-03-14",
  status: "Cancelled by Driver",
  customer_id: "CID695170",
  vehicle_type: "Premier Sedan",
  route: { pickup: "Indira Gandhi International Airport",
           drop: "Gurgaon Cyber City" },
  avg_vtat: 2.84,
  cancellation: { by: "driver", reason: "Personal & Car related issues" }
})
7

Clear your two test documents, then import the real dataset. Run mongoimport from the repository root, in a normal terminal — not inside mongosh.

// in mongosh
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
8

Verify the import. These four numbers must match exactly before you leave the session.

db.rides.countDocuments()                              // 15000
db.rides.countDocuments({ status: "Completed" })       // 9343
db.rides.countDocuments({ booking_value: { $exists: false } })  // 4756
db.rides.findOne({ booking_id: "CNR3000142" })
9

Optional but recommended. mongoimport loads booked_at as a string because that is what JSON can express. Convert it to a real BSON Date so that Lectures 13–16 can do time arithmetic on it.

db.rides.updateMany(
  { booked_at: { $type: "string" } },
  [ { $set: { booked_at: { $toDate: "$booked_at" } } } ]
)

db.rides.findOne({}, { booked_at: 1, _id: 0 })   // now an ISODate

Sanity check on what you just loaded. 15,000 bookings; 9,343 completed (62.29%); average completed fare ₹235.73 against a total of ₹22,02,449; average distance 10.87 km; average driver rating 4.41; average VTAT 8.03 minutes. The busiest hour is 18:00 with 1,373 bookings, and the worst failure rate among the hours that actually carry volume is 17:00 at 39.92% — higher than the 18:00 peak's 37.14%, even though 18:00 takes 1,373 bookings to 17:00's 1,062. Top pickup points: Noida Sector 62 (1,209), Indira Gandhi International Airport (1,183), New Delhi Railway Station (1,179), Gurgaon Cyber City (1,178), Connaught Place (1,139). You will reproduce every one of these figures yourself in Lectures 13 and 14.

Explore in Compass Before You Leave

In Compass, open bdma.rides and click the Schema tab. Compass samples the collection and reports, for every field, its types and how often it appears. You will see booking_value present in only about 68% of documents. That is not an error — it is the dataset telling you the truth about failed bookings, and it is the hook for Lecture 13.

Key Takeaways

✓ What You Should Be Able to Say After This Session

  1. A document is the unit of storage, and it nests. Deployment → database → collection → document → field maps onto server → database → table → row → column, except that collections have no fixed columns and fields may hold whole structures.
  2. You write JSON; MongoDB stores BSON. Binary gives real types (ObjectId, Date, Decimal128, Int32/Int64), guaranteed field order, and length-prefixed traversal. Money belongs in Decimal128.
  3. Every document has an _id, and the default ObjectId is 12 bytes — 4-byte timestamp, 5-byte random, 3-byte counter. It is therefore roughly time-sortable and safe to generate client-side with no coordination.
  4. Flexible schema is not the absence of a schema — it is the deferral of the decision. $jsonSchema with validator is how a mature team re-tightens it, progressively, where the business actually needs guarantees.
  5. Embed what is read together, bounded, and changes together; reference what is large, unbounded, shared, or changes independently. When the tests disagree, access pattern wins.
  6. 16 MB is a hard document ceiling and an early-warning system. The unbounded array — all of a customer's bookings inside the customer document — is the anti-pattern it exists to catch.

→ Next: Lecture 13 — Querying Data with MongoDB

With 15,000 rides loaded, we turn the collection into answers. The full CRUD surface, the operator families ($gt, $in, $exists, $elemMatch and the rest), dot notation for nested fields, projection, sorting and paging, and the update operators that change data in place. The lab is eight business questions you must answer in mongosh. Arrive with the dataset imported and verified at 15,000 documents.