Module 2 · Lecture 13

Querying Data with MongoDB

The full CRUD surface, the operator families, and the discipline that keeps updateMany from ruining your afternoon

⏱ 90 minutes 🎯 CLO3 📍 Prerequisites: Lecture 12 — bdma.rides loaded and verified at 15,000 documents 🔧 Lab: Eight graded business questions in mongosh

Contents

  1. Learning Objectives
  2. Session Timeline
  3. The CRUD Surface
  4. Query Operators, Grouped Properly
  5. Nested Fields and Dot Notation
  6. Shaping Results: Projection, Sort, Limit, Skip
  7. Updating Data in Place
  8. Counting, and the Blast-Radius Rule
  9. Hands-On — Eight Business Questions
  10. Key Takeaways

Learning Objectives

Session Timeline

TimeSegmentActivity
0–6 minCheck-InEveryone runs db.rides.countDocuments() — it must say 15000
6–18 minCRUD SurfaceThe twelve methods, what each returns, and the singular/plural distinction
18–34 minQuery OperatorsComparison, logical, element, evaluation, array — with live examples
34–44 minDot Notationroute.pickup, ratings.driver, and the missing-field problem
44–56 minShaping ResultsProjection, sort, limit, skip; why deep paging is slow
56–70 minUpdate Operators$set, $inc, array operators, upsert, and the blast-radius warning
70–86 minLabEight business questions, answered in mongosh and submitted
86–90 minWrap-UpDebrief the two hardest queries; Lecture 14 preview

Section 1: The CRUD Surface

Everything you can do to data falls under four verbs. MongoDB exposes each as a pair — one method that touches a single document and one that touches many — and that pairing is the most important safety feature in the API.

VerbSingleMultipleReturns
CreateinsertOne()insertMany()insertedId / insertedIds
ReadfindOne()find()A document / a cursor
UpdateupdateOne(), replaceOne()updateMany()matchedCount, modifiedCount, upsertedId
DeletedeleteOne()deleteMany()deletedCount

Create

db.rides.insertOne({
  booking_id: "CNR9000001",
  booked_at: new Date("2024-12-31T22:15:00Z"),
  date: "2024-12-31",
  status: "Completed",
  customer_id: "CID482910",
  vehicle_type: "Go Sedan",
  route: { pickup: "Connaught Place",
           drop: "Indira Gandhi International Airport" },
  avg_vtat: 5.2, avg_ctat: 28.4,
  booking_value: 389.00, ride_distance: 16.2,
  ratings: { driver: 4.7, customer: 4.9 },
  payment_method: "UPI"
})

db.rides.insertMany([
  { booking_id: "CNR9000002", status: "No Driver Found",
    customer_id: "CID101010", vehicle_type: "Auto",
    route: { pickup: "Nehru Place", drop: "Saket" } },
  { booking_id: "CNR9000003", status: "Cancelled by Customer",
    customer_id: "CID101010", vehicle_type: "Bike",
    route: { pickup: "Saket", drop: "Hauz Khas" }, avg_vtat: 11.3,
    cancellation: { by: "customer", reason: "Driver is not moving towards pickup location" } }
])

Ordered vs unordered inserts. By default insertMany is ordered: it stops at the first failure and leaves the remainder uninserted. Pass { ordered: false } to attempt every document and report the failures at the end. For a nightly bulk load of partner data, unordered is usually what you want — one malformed record should not block 40,000 good ones.

Read

findOne() returns a document — or null. find() returns a cursor, a lazy pointer that fetches results in batches as you iterate. That distinction matters: find() on a million-document match does not pull a million documents into memory.

db.rides.findOne({ booking_id: "CNR3000142" })      // one document or null
db.rides.find({ status: "Completed" })               // a cursor
db.rides.find()                                      // every document
db.rides.find({ vehicle_type: "Auto", payment_method: "Cash" })
// Comma-separated conditions are an implicit AND.

In mongosh, a cursor you do not consume prints the first 20 documents and offers it for the next batch. Use .toArray() to materialise everything, or .count()-style helpers when you only want the number.

Update and Delete

db.rides.updateOne(
  { booking_id: "CNR9000001" },
  { $set: { "ratings.customer": 5.0 } }
)

db.rides.replaceOne(
  { booking_id: "CNR9000002" },
  { booking_id: "CNR9000002", status: "Cancelled by Customer",
    customer_id: "CID101010", vehicle_type: "Auto",
    route: { pickup: "Nehru Place", drop: "Saket" } }
)

db.rides.deleteOne({ booking_id: "CNR9000003" })
db.rides.deleteMany({ booking_id: { $regex: "^CNR9" } })

updateOne vs replaceOne — the difference costs data. updateOne with $set changes only the fields you name and leaves the rest untouched. replaceOne discards the entire existing document (except _id) and substitutes what you supplied. If you forget one field in a replaceOne, that field is gone. Prefer updateOne with $set unless you genuinely mean to overwrite the whole record.

Section 2: Query Operators, Grouped Properly

A MongoDB query is a document that describes the documents you want. Everything beyond exact equality is expressed with operators, which always begin with $. Learning them as five families rather than one long list is the difference between recall and recognition.

Family 1 — Comparison

OperatorMeaningExample against rides
$eqEquals (implicit when you write a bare value){ status: { $eq: "Completed" } }
$neNot equal — also matches documents missing the field{ payment_method: { $ne: "Cash" } }
$gtGreater than{ booking_value: { $gt: 500 } }
$gteGreater than or equal{ ride_distance: { $gte: 20 } }
$ltLess than{ "ratings.driver": { $lt: 3 } }
$lteLess than or equal{ avg_vtat: { $lte: 3 } }
$inMatches any value in a list{ vehicle_type: { $in: ["Auto", "Bike", "eBike"] } }
$ninMatches none of the values in a list{ payment_method: { $nin: ["Cash"] } }
// Premium rides: fare above ₹500 on a long trip
db.rides.find({ booking_value: { $gt: 500 }, ride_distance: { $gte: 20 } })

// The two-wheeler and three-wheeler product lines
db.rides.find({ vehicle_type: { $in: ["Auto", "Bike", "eBike"] } })

// A fare band — note that both bounds live in one operator document
db.rides.find({ booking_value: { $gte: 200, $lte: 400 } })

The $ne trap, and it bites on our data. { payment_method: { $ne: "Cash" } } matches every document where payment_method is something other than Cash plus every document where the field does not exist at all — all 4,756 bookings that never completed. If you mean "completed rides paid by something other than cash", you must say so: { status: "Completed", payment_method: { $ne: "Cash" } }. In MongoDB, a missing field is not equal to anything, which means it is "not equal" to everything.

Family 2 — Logical

OperatorMeaningWhen You Actually Need It
$andAll conditions trueOnly when two conditions apply to the same field; otherwise commas already mean AND
$orAny condition trueConstantly — different fields, either one
$notInverts a single operator expressionNegating a $regex or a range
$norNone of the conditions trueExclusion lists: "neither cancelled nor incomplete"
// Any failed booking — three different statuses count as failure
db.rides.find({
  $or: [
    { status: "Cancelled by Driver" },
    { status: "Cancelled by Customer" },
    { status: "No Driver Found" }
  ]
})
// Cleaner, and it uses an index better:
db.rides.find({ status: { $in: ["Cancelled by Driver",
                                "Cancelled by Customer",
                                "No Driver Found"] } })

// Where $and is genuinely required: two conditions on booking_value
// that cannot both live in one operator document
db.rides.find({
  $and: [
    { booking_value: { $gt: 100 } },
    { $or: [ { vehicle_type: "Uber XL" }, { ride_distance: { $gt: 25 } } ] }
  ]
})

// Neither cancelled nor incomplete
db.rides.find({
  $nor: [
    { status: "Cancelled by Driver" },
    { status: "Cancelled by Customer" },
    { status: "Incomplete" }
  ]
})

// Everything that does not start at the airport — $not inverts the regex
db.rides.find({ "route.pickup": { $not: /^Indira Gandhi/ } })

Family 3 — Element

These two operators exist because of exactly the situation our dataset models: fields that are legitimately absent.

OperatorMeaningBusiness Question It Answers
$existsDoes the field exist on this document at all?"Which bookings produced no revenue?"
$typeIs the field of this BSON type?"Did any loader write fares as strings?"
// Bookings that generated no revenue at all — 4,756 of them
db.rides.countDocuments({ booking_value: { $exists: false } })

// Bookings that produced a fare — 10,244 (9,343 completed + 901 incomplete)
db.rides.countDocuments({ booking_value: { $exists: true } })

// Rated rides only — ratings exist on completed bookings alone
db.rides.countDocuments({ ratings: { $exists: true } })        // 9343

// The demand-failure signal: no driver was ever assigned,
// so not even a vehicle arrival time was recorded
db.rides.countDocuments({ avg_vtat: { $exists: false } })      // 1036

// Data-quality audit: has anything written a fare as a string?
db.rides.countDocuments({ booking_value: { $type: "string" } }) // 0

Why this is a business skill, not a syntax trick. In a relational table these would all be NULL, and NULL is ambiguous: it conflates "did not happen", "not applicable", and "we failed to record it". Our schema distinguishes them structurally. avg_vtat missing means no driver was ever assigned — a supply failure. booking_value missing but avg_vtat present means a driver was assigned and then the trip collapsed — a conversion failure. Those two numbers go to different teams with different fixes.

Family 4 — Evaluation

The one you will use is $regex, for pattern matching on strings.

// Every pickup in Noida, case-insensitive
db.rides.find({ "route.pickup": { $regex: "Noida", $options: "i" } })

// Equivalent shorthand using a JavaScript literal
db.rides.find({ "route.pickup": /Noida/i })

// Anchored to the start — this one can use an index
db.rides.find({ "route.drop": { $regex: "^Gurgaon" } })

// Either end of the trip touches the airport. The dataset stores the
// full name "Indira Gandhi International Airport", so match on a substring.
db.rides.find({ $or: [ { "route.pickup": /Airport/ },
                       { "route.drop": /Airport/ } ] })

Performance warning. A regex anchored at the start of the string (/^Gurgaon/) can use a standard index, because the index is sorted and the engine can seek to the right prefix. An unanchored regex (/Noida/) or a case-insensitive one cannot — it must examine every value in the collection. Fine on 15,000 documents; a visible outage on 15 million. For real text search, use a text index or Atlas Search rather than a regex.

Family 5 — Array

Our rides documents do not contain arrays, so we demonstrate these on a realistic extension: a tags array and a fare_breakdown array of sub-documents that a surge-pricing feature would add.

OperatorMeaning
$allThe array contains all of these values, in any order
$elemMatchOne single element satisfies all the stated conditions
$sizeThe array has exactly this many elements
// Seed a few tagged documents so the operators have something to match
db.rides.updateMany(
  { "route.pickup": "Indira Gandhi International Airport", status: "Completed" },
  { $set: { tags: ["airport", "premium"] } }
)

// Bare value: matches if the array CONTAINS it
db.rides.find({ tags: "airport" })

// $all: must contain both
db.rides.find({ tags: { $all: ["airport", "premium"] } })

// $size: exactly two tags
db.rides.find({ tags: { $size: 2 } })

// $elemMatch on an array of sub-documents — the crucial one
db.rides.find({
  fare_breakdown: { $elemMatch: { component: "surge", amount: { $gt: 50 } } }
})

Why $elemMatch exists. Written without it, { "fare_breakdown.component": "surge", "fare_breakdown.amount": { $gt: 50 } } matches a document where some element is a surge line and some element has an amount above 50 — not necessarily the same element. A ₹60 base fare plus a ₹20 surge would match, which is wrong. $elemMatch forces both conditions onto one element. This is the array equivalent of the $ne trap: the query runs, returns results, and quietly answers a different question.

Section 3: Nested Fields and Dot Notation

Two of our most useful fields are sub-documents. Reaching inside them uses dot notation, and the path must be quoted — route.pickup is not a valid JavaScript identifier, so "route.pickup" it is.

db.rides.find({ "route.pickup": "Indira Gandhi International Airport" })
db.rides.find({ "route.drop": "Aerocity" })
db.rides.find({ "ratings.driver": { $lt: 3 } })
db.rides.find({ "ratings.driver": { $gte: 4.8 }, "ratings.customer": { $gte: 4.8 } })
db.rides.find({ "cancellation.by": "driver" })
db.rides.find({ "cancellation.reason": "Personal & Car related issues" })
db.rides.find({ "incomplete.reason": "Vehicle Breakdown" })

// The airport corridor, both directions
db.rides.find({
  $or: [
    { "route.pickup": "Indira Gandhi International Airport",
      "route.drop":   "Gurgaon Cyber City" },
    { "route.pickup": "Gurgaon Cyber City",
      "route.drop":   "Indira Gandhi International Airport" }
  ]
})

Dot notation is not the same as matching the whole sub-document.

db.rides.find({ route: { pickup: "Indira Gandhi International Airport" } }) matches only documents whose route sub-document contains exactly and only the field pickup with that value. Since every route also has a drop, this returns zero results — silently.

db.rides.find({ "route.pickup": "Indira Gandhi International Airport" }) matches 1,183 documents, which is what you meant. Whole-document matching is also order-sensitive: { pickup, drop } does not match { drop, pickup }. Use dot notation unless you are deliberately matching an entire embedded object.

Section 4: Shaping Results — Projection, Sort, Limit, Skip

Projection — Ask Only for What You Need

The second argument to find() controls which fields come back. 1 includes, 0 excludes, and you may not mix the two — except for _id, which can always be suppressed.

// Include only these three fields (plus _id, which comes by default)
db.rides.find(
  { status: "Completed" },
  { booking_id: 1, booking_value: 1, "route.pickup": 1 }
)

// Drop _id as well — the common reporting form
db.rides.find(
  { status: "Completed" },
  { _id: 0, booking_id: 1, booking_value: 1, ride_distance: 1 }
)

// Exclusion style: everything EXCEPT these
db.rides.find({ status: "Completed" }, { ratings: 0, avg_vtat: 0, avg_ctat: 0 })

// INVALID — cannot mix inclusion and exclusion
// db.rides.find({}, { booking_id: 1, ratings: 0 })

Projection is a cost control, not a cosmetic. Every field you return is read from disk, serialised, pushed across the network, and parsed by the client. Returning 14 fields to a dashboard that renders 3 wastes roughly four-fifths of that work on every request. On a metered cloud plan, network egress is a line item on the invoice.

Sorting, Limiting, Paging

// The ten most expensive completed rides
db.rides.find(
  { status: "Completed" },
  { _id: 0, booking_id: 1, booking_value: 1, "route.pickup": 1, "route.drop": 1 }
).sort({ booking_value: -1 }).limit(10)

// 1 = ascending, -1 = descending; multiple keys sort left to right
db.rides.find({ status: "Completed" })
  .sort({ vehicle_type: 1, booking_value: -1 })
  .limit(20)

// Page 3 of a 20-per-page listing
db.rides.find({ status: "Completed" })
  .sort({ booked_at: -1 })
  .skip(40)
  .limit(20)

Order of operations. Regardless of the order you chain them, the server applies sort → skip → limit. You cannot limit first and then sort the survivors — which is exactly what you want, because "top ten by fare" must sort the whole matching set before taking ten.

Why Deep skip Is Slow

skip(n) does not jump. The server must still produce the first n results in sorted order and then throw them away. Page 2 discards 20 documents and nobody notices. Page 500 discards 10,000 documents to return 20 — the same work as scanning half the collection, repeated on every page load.

PageQueryDocuments Processed to Return 20
1.limit(20)20
10.skip(180).limit(20)200
100.skip(1980).limit(20)2,000
500.skip(9980).limit(20)10,000

The fix — range pagination (the "infinite scroll" pattern). Instead of counting pages, remember the last value you saw and ask for everything after it. The work is constant per page regardless of depth, which is why every high-traffic feed is built this way and why "jump to page 500" quietly disappeared from modern interfaces.

// Page 1
db.rides.find({ status: "Completed" }).sort({ booked_at: -1 }).limit(20)

// Page 2 — pass the last booked_at you displayed
db.rides.find({ status: "Completed",
                booked_at: { $lt: ISODate("2024-11-18T09:14:00Z") } })
  .sort({ booked_at: -1 }).limit(20)

Section 5: Updating Data in Place

Never read a document, change it in your application, and write the whole thing back. That pattern loses any change another process made in between. Use update operators, which modify fields on the server atomically.

OperatorEffectRealistic Use on rides
$setSets a field, creating it if absentCorrect a mis-recorded drop point
$unsetRemoves a field entirelyPurge a rating after a fraud investigation
$incAdds a number (negative to subtract)Apply a ₹25 goodwill adjustment
$mulMultiplies a numeric fieldApply a 1.2× surge to a fare band
$renameRenames a fieldMigrate avg_ctat to trip_duration_min
$pushAppends to an array (duplicates allowed)Add an audit entry
$addToSetAppends only if not already presentAdd a tag without creating duplicates
$pullRemoves all matching elements from an arrayRetire a deprecated tag
// $set — correct one booking's drop point
db.rides.updateOne(
  { booking_id: "CNR3000142" },
  { $set: { "route.drop": "Aerocity Metro Station" } }
)

// $set creates the field if it does not exist
db.rides.updateMany(
  { status: "Completed", booking_value: { $gt: 600 } },
  { $set: { segment: "premium" } }
)

// $unset — remove a disputed rating
db.rides.updateOne(
  { booking_id: "CNR3000142" },
  { $unset: { "ratings.customer": "" } }       // the value is ignored
)

// $inc — a ₹25 goodwill credit on one ride; negative values subtract
db.rides.updateOne({ booking_id: "CNR3000142" }, { $inc: { booking_value: -25 } })

// $mul — apply a 1.2x evening surge to one corridor
db.rides.updateMany(
  { status: "Completed",
    "route.pickup": "Indira Gandhi International Airport" },
  { $mul: { booking_value: 1.2 } }
)

// $rename — a schema migration, run once
db.rides.updateMany({}, { $rename: { "avg_ctat": "trip_duration_min" } })

// Array operators
db.rides.updateOne({ booking_id: "CNR3000142" },
  { $push: { tags: "airport" } })                   // appends, duplicates allowed
db.rides.updateOne({ booking_id: "CNR3000142" },
  { $addToSet: { tags: "airport" } })               // no-op if already present
db.rides.updateOne({ booking_id: "CNR3000142" },
  { $pull: { tags: "premium" } })                   // removes every match

upsert — Update If Present, Insert If Not

A third option on any update. If the filter matches nothing, MongoDB creates a document from the filter plus the update. This is how daily rollups and counters are maintained without a read-then-decide round trip.

db.daily_summary.updateOne(
  { date: "2024-03-14", vehicle_type: "Go Sedan" },   // the identity
  { $inc: { bookings: 1, revenue: 412.50 } },          // the change
  { upsert: true }                                     // create if absent
)
// First call  -> inserts { date, vehicle_type, bookings: 1, revenue: 412.50 }
// Every later -> increments the existing document
// Response includes upsertedId when it inserted, null when it updated.

Upsert's sharp edge: the inserted document is built from the equality terms of your filter. A filter containing { booking_value: { $gt: 500 } } contributes nothing to the new document, so you can upsert a record that does not satisfy your own filter. Keep upsert filters to plain equality on the fields that define identity.

Section 6: Counting, and the Blast-Radius Rule

countDocuments() vs estimatedDocumentCount()

countDocuments(filter)estimatedDocumentCount()
Accepts a filterYesNo — whole collection only
AccuracyExact, alwaysApproximate
How it worksScans the matching documents or index entriesReads a cached metadata figure
CostProportional to the match sizeConstant — effectively free
Use forAnything a human will act on; any filtered count"Roughly how big is this collection?" on a status page
db.rides.countDocuments()                                   // 15000, exact
db.rides.estimatedDocumentCount()                           // ~15000, instant
db.rides.countDocuments({ status: "Completed" })            // 9343
db.rides.countDocuments({ payment_method: "UPI" })           // 4613
db.rides.countDocuments({ "route.pickup": "Noida Sector 62" }) // 1209

The honest trade-off: estimatedDocumentCount reads a figure the storage engine maintains for its own purposes. After an unclean shutdown that figure can drift. Never put it behind a number a finance team will reconcile; it is for dashboards where "about 15,000" is a perfectly good answer delivered in a millisecond.

The Blast-Radius Rule

An empty filter is not a mistake the database will catch.

db.rides.deleteMany({}) deletes all 15,000 documents. It returns { acknowledged: true, deletedCount: 15000 } and exits successfully, because that is precisely what you asked for. db.rides.updateMany({}, { $set: { status: "Completed" } }) rewrites the status of every booking in the collection.

There is no confirmation prompt, no ROLLBACK, and no undo. MongoDB has multi-document transactions, but an autocommitted updateMany is not inside one.

Three habits, in order of value:

1

Write the filter as a find first. Run db.rides.find(FILTER).limit(5) and db.rides.countDocuments(FILTER). Look at the number. Then change find to updateMany and keep the identical filter.

2

Reach for the singular form by default. If you believe exactly one document should change, use updateOne. Should your filter turn out to be wrong, you have damaged one record instead of fifteen thousand.

3

Read the result object. matchedCount tells you how many documents the filter found; modifiedCount tells you how many actually changed. If matchedCount is far larger than you expected, stop and investigate before running the next command.

{ acknowledged: true, matchedCount: 1183, modifiedCount: 1183,
  upsertedId: null }

Section 7: Hands-On — Eight Business Questions

Work in mongosh against bdma.rides. For each question, submit the query you ran and the result. Marks are for the query; the number is how you check yourself. Several answers are verifiable against the figures from Lecture 12.

1

How healthy is the funnel? Return the total number of bookings and the number completed, and state the completion rate.

db.rides.countDocuments()                          // expect 15000
db.rides.countDocuments({ status: "Completed" })   // expect 9343 (62.29%)
2

How much demand do we fail outright? Count bookings where no driver was ever assigned — use the absence of avg_vtat, not the status string, and explain in one line why both give the same answer here.

db.rides.countDocuments({ avg_vtat: { $exists: false } })      // expect 1036
db.rides.countDocuments({ status: "No Driver Found" })         // expect 1036
3

What are our ten highest-value trips? Completed rides only. Return booking ID, fare, distance, and both ends of the route — and nothing else.

db.rides.find(
  { status: "Completed" },
  { _id: 0, booking_id: 1, booking_value: 1, ride_distance: 1,
    "route.pickup": 1, "route.drop": 1 }
).sort({ booking_value: -1 }).limit(10)
4

Is the airport corridor a premium segment? Count completed rides starting at the airport with a fare above ₹400, then compare that with all completed rides above ₹400. Note the pickup is stored as its full name, not "IGI".

db.rides.countDocuments({
  status: "Completed",
  "route.pickup": "Indira Gandhi International Airport",
  booking_value: { $gt: 400 }
})
db.rides.countDocuments({ status: "Completed", booking_value: { $gt: 400 } })
5

Where is service quality failing? Find completed rides rated below 3.0 by the customer's driver score, showing the route and the fare. These are the rides a quality team should listen to.

db.rides.find(
  { status: "Completed", "ratings.driver": { $lt: 3 } },
  { _id: 0, booking_id: 1, "ratings.driver": 1, "route.pickup": 1,
    "route.drop": 1, booking_value: 1, vehicle_type: 1 }
).sort({ "ratings.driver": 1 }).limit(20)

db.rides.countDocuments({ status: "Completed", "ratings.driver": { $lt: 3 } })
6

How big is the cash-handling problem? Count completed rides paid in cash, then count completed rides paid by any digital method. Write your second query two ways — once with $ne and once with $in — and explain which you would ship.

db.rides.countDocuments({ status: "Completed", payment_method: "Cash" })
db.rides.countDocuments({ status: "Completed",
                          payment_method: { $ne: "Cash" } })
db.rides.countDocuments({ status: "Completed", payment_method:
  { $in: ["UPI", "Uber Wallet", "Credit Card", "Debit Card"] } })
// Both are correct here only because status pins us to completed rides.
// Drop that filter and $ne silently sweeps in 4,756 fare-less bookings.
7

Why do drivers cancel? List the distinct driver-cancellation reasons, then count the single most common one.

db.rides.distinct("cancellation.reason", { "cancellation.by": "driver" })
db.rides.countDocuments({ "cancellation.by": "driver" })      // expect 2698
db.rides.countDocuments({
  "cancellation.reason": "Personal & Car related issues"
})
8

Tag and then clean up. Mark every completed ride over ₹600 as segment: "premium", confirm the count, then remove the field again. Run the countDocuments before the updateMany and compare it with matchedCount.

db.rides.countDocuments({ status: "Completed", booking_value: { $gt: 600 } })

db.rides.updateMany(
  { status: "Completed", booking_value: { $gt: 600 } },
  { $set: { segment: "premium" } }
)
// check: matchedCount must equal the count above

db.rides.countDocuments({ segment: "premium" })

db.rides.updateMany({ segment: { $exists: true } }, { $unset: { segment: "" } })
db.rides.countDocuments({ segment: { $exists: true } })        // expect 0

Stretch question for the quick finishers. Using only what is in this lecture, work out how much of the ₹22,02,449 completed revenue came through UPI. You can count the bookings with countDocuments, but you cannot sum the fares — find has no SUM. That wall is deliberate, and climbing it is what the aggregation pipeline in Lecture 14 is for.

Key Takeaways

✓ What You Should Be Able to Say After This Session

  1. CRUD comes in singular/plural pairs, and that pairing is a safety feature. find() returns a lazy cursor, not a list; replaceOne overwrites the whole document while updateOne with $set touches only named fields.
  2. Learn operators as five families — comparison ($eq $ne $gt $gte $lt $lte $in $nin), logical ($and $or $not $nor), element ($exists $type), evaluation ($regex), array ($all $elemMatch $size) — and the right one is usually obvious from the business question.
  3. A missing field is not equal to anything, so it is "not equal" to everything. $ne sweeps in documents lacking the field; $exists is how you ask the question you actually meant.
  4. Dot notation reaches into sub-documents; whole-object matching does not. { "route.pickup": "Indira Gandhi International Airport" } finds 1,183 rides. { route: { pickup: "Indira Gandhi International Airport" } } finds none, silently.
  5. Projection is cost control and deep skip is a trap. Return only the fields you render; page with a range filter on the sort key, not with skip(9980).
  6. An empty filter is a valid filter. updateMany({}, …) and deleteMany({}) hit every document, with no prompt and no undo. Prove the filter with find and countDocuments first; read matchedCount afterwards.

→ Next: Lecture 14 — The Aggregation Framework

find retrieves documents; it cannot compute. To produce "average fare by vehicle type", "revenue per payment method", or "failure rate by hour of day" you need the aggregation pipeline — $match, $group, $sort, $project, $unwind and $lookup composed in stages. That is where ₹22,02,449 and the 10.87 km average stop being numbers we told you and start being numbers you derive.

It is also where you meet the first real analytical trap. Rank the 24 hours of the day by failure rate and 03:00 comes top at 43.48% — on 46 bookings. That is a small-sample artifact, and acting on it sends an operations team chasing noise at three in the morning. Put a { $match: { bookings: { $gte: 1000 } } } after the $group and the real answer appears: among the hours that actually carry volume, 17:00 is worst 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. A rate without a denominator is not an insight.