โ† Home Lecture 13 ยท Querying Data with MongoDB
1 / 18 ๐Ÿ“„ Notes
Module 2 ยท Lecture 13

Querying Data with MongoDB

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

โฑ 90 minutes ๐ŸŽฏ CLO3 ๐Ÿ“ฆ Lab: 8 business questions
Check-In ยท 2 minutes

Everyone run this now

use bdma
db.rides.countDocuments()                          // must say 15000
db.rides.countDocuments({ status: "Completed" })   // must say 9343

If either number is wrong, re-run the mongoimport from Lecture 12 before we go any further. Every query today assumes these 15,000 Delhi NCR bookings.

Today: turn a collection into answers. Not today: computing totals and averages โ€” that needs the aggregation pipeline in Lecture 14.
The Four Verbs

CRUD comes in pairs โ€” and that is a safety feature

VerbSingleMultipleReturns
CreateinsertOne()insertMany()insertedId(s)
ReadfindOne()find()Document / cursor
UpdateupdateOne(), replaceOne()updateMany()matchedCount, modifiedCount
DeletedeleteOne()deleteMany()deletedCount
find() returns a lazy cursor, not a list. A million-document match does not pull a million documents into memory.
Create ยท Update

Two distinctions that cost data

updateOne vs replaceOne

$set changes only the fields you name. replaceOne discards the whole document except _id. Forget a field and it is gone.

Ordered vs unordered inserts

insertMany stops at the first failure by default. { ordered: false } attempts all and reports failures โ€” right for nightly bulk loads.

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

db.rides.insertMany([ /* ... */ ], { ordered: false })
Default to updateOne with $set unless you genuinely mean to overwrite the entire record.
Operators ยท Family 1

Comparison

OperatorAgainst rides
$eq $ne{ status: { $eq: "Completed" } }
$gt $gte{ booking_value: { $gt: 500 } }
$lt $lte{ "ratings.driver": { $lt: 3 } }
$in $nin{ vehicle_type: { $in: ["Auto","Bike","eBike"] } }
// A fare band โ€” both bounds in one operator document
db.rides.find({ booking_value: { $gte: 200, $lte: 400 } })

// Comma-separated conditions are an implicit AND
db.rides.find({ vehicle_type: "Auto", payment_method: "Cash" })
Average completed fare in this dataset: โ‚น235.73. So $gt: 500 is genuinely the premium tail, not an arbitrary number.
โš ๏ธ The Trap

$ne matches documents that lack the field

db.rides.find({ payment_method: { $ne: "Cash" } })

This returns every non-cash payment plus all 4,756 bookings that never completed โ€” they have no payment_method at all, and a missing field is not equal to anything, which makes it "not equal" to everything.

What you wrote

"Not cash" โ€” and got 4,756 phantom rides in a payments report.

What you meant

{ status: "Completed",
  payment_method:
    { $ne: "Cash" } }
The query ran. It returned results. It answered a different question โ€” the most expensive failure mode in analytics.
Operators ยท Family 2

Logical โ€” and when you actually need them

// Any failed booking
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"] } })

// $nor โ€” neither cancelled nor incomplete
db.rides.find({ $nor: [ { status: "Cancelled by Driver" },
                        { status: "Cancelled by Customer" },
                        { status: "Incomplete" } ] })
You rarely need $and โ€” commas already mean AND. Reach for it only when two conditions apply to the same field and cannot share one operator document.
Operators ยท Family 3

$exists โ€” because absence is data

db.rides.countDocuments({ booking_value: { $exists: false } })  // 4756
db.rides.countDocuments({ ratings:       { $exists: true  } })  // 9343
db.rides.countDocuments({ avg_vtat:      { $exists: false } })  // 1036
db.rides.countDocuments({ booking_value: { $type: "string" } }) //    0

Supply failure โ€” 1,036

No avg_vtat means no driver was ever assigned. This is a marketplace supply problem.

Conversion failure โ€” 3,720

avg_vtat present but no fare: a driver was assigned and the trip then collapsed. A reliability problem.

In SQL both are NULL and the distinction is lost. Here it is structural โ€” and the two numbers go to different teams with different fixes.
Operators ยท Families 4 & 5

Evaluation and array

// $regex โ€” anchored can use an index, unanchored cannot
db.rides.find({ "route.drop": { $regex: "^Gurgaon" } })   // indexable
db.rides.find({ "route.pickup": /Noida/i })               // full scan

// $all / $size / $elemMatch
db.rides.find({ tags: { $all: ["airport", "premium"] } })
db.rides.find({ tags: { $size: 2 } })
db.rides.find({ fare_breakdown:
  { $elemMatch: { component: "surge", amount: { $gt: 50 } } } })

Why $elemMatch exists

Without it, {"fare_breakdown.component":"surge", "fare_breakdown.amount":{$gt:50}} matches when some element is surge and some other element exceeds 50. A โ‚น60 base plus a โ‚น20 surge would match. Wrong answer, no error.

Fine on 15,000 documents; a visible outage on 15 million. For real text search use a text index or Atlas Search, not a regex.
Nested Fields

Dot notation โ€” and why the quotes matter

db.rides.find({ "route.pickup": "Indira Gandhi International Airport" })
db.rides.find({ "ratings.driver": { $lt: 3 } })
db.rides.find({ "cancellation.by": "driver" })
db.rides.find({ "incomplete.reason": "Vehicle Breakdown" })

โœ“ Dot notation โ€” 1,183 results

{ "route.pickup":
  "Indira Gandhi
   International Airport" }

โœ— Whole-object match โ€” 0 results

{ route: { pickup:
  "Indira Gandhi
   International Airport" } }
Whole-object matching requires an exact, order-sensitive match on the entire sub-document. Every route also has a drop, so this silently returns nothing.
Shaping Results

Projection is cost control

// 1 = include, 0 = exclude. Never mix โ€” except _id, always suppressible.
db.rides.find(
  { status: "Completed" },
  { _id: 0, booking_id: 1, booking_value: 1, "route.pickup": 1 }
)

// INVALID
// db.rides.find({}, { booking_id: 1, ratings: 0 })

Every field returned is read from disk, serialised, sent across the network, and parsed by the client. Returning 14 fields to a dashboard that renders 3 wastes four-fifths of that work on every single request.

On a metered cloud plan, network egress is a line item on the invoice. Projection is a cost decision, not a formatting one.
โš ๏ธ Sorting & The Paging Trap

sort runs first. skip does not jump.

// The ten most expensive completed rides. 1 = asc, -1 = desc.
db.rides.find({ status: "Completed" },
              { _id: 0, booking_id: 1, booking_value: 1 })
  .sort({ booking_value: -1 }).limit(10)
PageQueryDocs processed to return 20
1.limit(20)20
100.skip(1980).limit(20)2,000
500.skip(9980).limit(20)10,000
// Range pagination โ€” constant work at any depth
db.rides.find({ status: "Completed",
                booked_at: { $lt: ISODate("2024-11-18T09:14:00Z") } })
  .sort({ booked_at: -1 }).limit(20)
The server always applies sort โ†’ skip โ†’ limit. This is why high-traffic feeds are infinite scroll and "jump to page 500" quietly disappeared.
Writing

Update operators โ€” change it on the server

OperatorEffectUse on rides
$set $unsetSet / remove a fieldCorrect a drop point; purge a disputed rating
$inc $mulAdd / multiply a numberโ‚น25 goodwill credit; 1.2ร— surge
$renameRename a fieldavg_ctat โ†’ trip_duration_min
$push $addToSet $pullAppend / append-if-absent / removeMaintain a tags array
db.rides.updateOne({ booking_id: "CNR3000142" },
  { $inc: { booking_value: -25 } })

db.rides.updateOne({ booking_id: "CNR3000142" },
  { $addToSet: { tags: "airport" } })      // no-op if already there
Never read a document, edit it in your app, and write the whole thing back โ€” you silently discard whatever changed in between.
Writing

upsert โ€” update if present, insert if not

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

โš ๏ธ The sharp edge

The new document is built from the equality terms only. A filter containing { booking_value: { $gt: 500 } } contributes nothing โ€” so you can upsert a record that fails your own filter. Keep upsert filters to plain equality on identity fields.

This is how rollups and counters are maintained with no read-then-decide round trip.
Counting

Exact or instant โ€” pick one

countDocuments(filter)estimatedDocumentCount()
FilterYesNo โ€” whole collection
AccuracyExact, alwaysApproximate (cached metadata)
CostProportional to the matchConstant โ€” effectively free
Use forAnything a human acts on"Roughly how big is this?"
db.rides.countDocuments()                                      // 15000
db.rides.countDocuments({ payment_method: "UPI" })             //  4613
db.rides.countDocuments({ "route.pickup": "Noida Sector 62" }) //  1209
After an unclean shutdown the cached figure can drift. Never put it behind a number finance will reconcile.
โš ๏ธ Blast Radius

An empty filter is a valid filter

db.rides.deleteMany({})
// { acknowledged: true, deletedCount: 15000 }

db.rides.updateMany({}, { $set: { status: "Completed" } })
// every booking in the collection, rewritten

No confirmation prompt. No ROLLBACK. No undo. MongoDB has transactions โ€” an autocommitted updateMany is not inside one.

Three habits

1. Write the filter as a find + countDocuments first, then swap the verb.
2. Default to the singular form.
3. Read matchedCount afterwards.

What good looks like

{ acknowledged: true,
  matchedCount: 1183,
  modifiedCount: 1183,
  upsertedId: null }
If matchedCount is far larger than you expected โ€” stop. Investigate before the next command.
Lab ยท 16 minutes

Eight business questions

#QuestionSkill
1How healthy is the funnel? Total vs completed.countDocuments
2How much demand do we fail outright?$exists
3Our ten highest-value trips.Projection + sort + limit
4Is the airport corridor a premium segment?Dot notation + $gt
5Where is service quality failing?"ratings.driver" + $lt
6How big is the cash-handling problem?$ne vs $in
7Why do drivers cancel?distinct on a nested field
8Tag premium rides, verify, then clean up.$set / $unset + matchedCount
Submit the query and the result. Marks are for the query; the number is how you check yourself. Full detail in the notes.
Wrap-Up

Six things to carry forward

  1. CRUD comes in singular/plural pairs, and find() hands back a lazy cursor, not a list.
  2. Five operator families โ€” comparison, logical, element, evaluation, array. The business question tells you which.
  3. A missing field is "not equal" to everything. $ne sweeps in 4,756 phantom bookings; $exists asks what you meant.
  4. Dot notation reaches in; whole-object matching does not โ€” and fails silently with zero results.
  5. Projection is cost control; deep skip is a trap. Page with a range filter on the sort key.
  6. An empty filter is a valid filter. Prove it with find first; read matchedCount after.
Next โ€” Lecture 14: find retrieves, it cannot compute. The aggregation pipeline is where โ‚น22,02,449 and 10.87 km become numbers you derive โ€” and where ranking the 24 hours by failure rate puts 03:00 top at 43.48% on just 46 bookings. A rate without a denominator is noise.
โ† โ†’ or Space to navigate ยท F for fullscreen