Talking to MongoDB: CRUD, filters, projections and nested documents
After this lesson you will write MongoDB's create, read, update and delete commands the way the exam expects them, build filters from the five operator families, mask and sort what find returns, and query sub-documents and arrays without falling into the type trap or the any-element trap.
Mark Jonhson — the exercise session spells him this way on every slide — has two mobile phones: +39 XXX 3456 778 with Wind, and +39 YYY 9874 123 with TIM. You ask MongoDB for the people with a Wind phone whose number is +39 YYY 9874 123. No such phone exists.
MongoDB returns Mark, and the session shows it on purpose (p. 1437). The exam of 6 September 2024 asks to "count the number of chefs who cooked at least one dish named “Octopus with Potatoes”, which cost 25.00€" — the same shape, two conditions on one element of an array, for 2 points.
Why does Mark come back, and what do you write when you mean "the same phone"? The answer needs filters, dotted paths, types and arrays, and it arrives in section 5.
Four verbs in a JavaScript shell
Every command of this lesson is typed into the shell that the lecture lists among MongoDB's processes: "Mongo – an interactive shell ( a client)", a "Fully functional JavaScript environment for use with a MongoDB" (p. 321). The other two processes of that slide, mongod and mongos, belong to lesson 16; today's shell is mongosh, typed the same way.
So a command is a JavaScript method call — db, the collection, the method — and its arguments are JavaScript objects. A key may go without quotes ({amenities: 1}, p. 1492) unless it contains a dot, and a missing brace is a syntax error before MongoDB sees anything.
CRUD is "Create – Read – Update – Delete" (p. 313). use smbud selects a database, which is created with its first document; a collection, too, appears with its first insert, and db.createCollection('people_collection') creates it empty. The session inserts with insertOne, or with insertMany and "a list of comma-separated documents" (p. 1421):
// adds two documents at once; MongoDB gives each an _id
db.people_collection.insertMany([
{"name": {"first": "Mike", "last": "Ruby"}, "age": 63},
{"name": {"first": "Anna", "last": "Verdi"}, "age": 29}
])
find(filter) returns every match, findOne(filter) "the first document that satisfies one or more conditions", countDocuments(filter) their number (p. 1424; lesson 13 introduced them). The lecture's find().pretty() (p. 314) only formats the output.
updateOne "collects the documents that satisfy one or more conditions defined in a filter and updates the first one found"; updateMany updates them all (p. 1425):
// sets Mike's age to 15; his name stays as it is
db.people_collection.updateOne(
{"age": 63},
{"$set": {"age": 15}}
)
The second argument must hold an update operator. $set rewrites the fields it names, adds missing ones and leaves the rest alone. Without an operator today's updateOne refuses to run ("Update document requires atomic operators"); to swap the whole document you call replaceOne(filter, newDocument), as the lecture's save did (p. 319).
An update operator: one field changes
A replacement: the whole document is swapped
$set — in the lecture's update (p. 319) and the session's updateOne (p. 1425) — rewrites the field it names and keeps the others. A replacement document becomes the whole document, so a field you did not repeat is gone: the lecture's save and today's replaceOne do this on purpose.deleteOne "deletes the first one found", deleteMany "all the collected documents" (p. 1426). The lecture adds db.people_collection.drop(), which removes a collection with its indexes, db.dropDatabase() and show dbs (p. 320).
// deletes Mike, whose age is now 15
db.people_collection.deleteOne(
{"age": 15}
)
The lecture prints the shell's older method names, the session the current ones — write those at the exam:
| Verb | Lecture (pp. 313–320) | Session (pp. 1421–1426) — write these |
|---|---|---|
| Create | insert({…}) | insertOne({…}), insertMany([{…}, {…}]) |
| Read | find().pretty() | find, findOne, countDocuments |
| Update | update(<select_criteria>,<updated_data>) | updateOne, updateMany with $set |
| Replace | save({_id: …, …}) | replaceOne(filter, document) (not on the slides) |
| Delete | remove({…}) | deleteOne, deleteMany |
| Drop | drop(), dropDatabase() | the same |
The new names say what the old ones hid: the old update changed the first match, the old remove every match.
Every read, update and delete starts from a filter; "One" acts on the first match, "Many" on all, and an update needs an operator such as $set.
A filter is a document
The lecture's first query with a condition (p. 314):
// the female students aged 20 or less
db.students.find({ "gender": "female", "age": {$lte:20} }).pretty()
Read it aloud. A field with a value is an equality; a field with a document applies the operator inside it, here $lte, "less than or equal"; all the fields must hold — an implicit AND, SQL's WHERE gender = 'female' AND age <= 20. "It’s pattern matching again!", the slide adds: you describe the documents you want, as Cypher's MATCH does (lesson 11).
The next slide maps SQL to MongoDB (p. 315). Two of its lines are wrong; the right-hand column gives the form to write.
| SQL statement | MongoDB, as printed | Write it like this |
|---|---|---|
SELECT * FROM table | db.collection.find() | the same |
SELECT * FROM table WHERE artist = 'Nirvana' | db.collection.find({Artist:"Nirvana"}) | find({"artist": "Nirvana"}) — field names are case-sensitive |
SELECT* FROM table ORDER BY Title | db.collection.find().sort(Title:1) | find().sort({"Title": 1}) |
DISTINCT | .distinct() | distinct("artist") — it takes the field name |
GROUP BY | .group() | the $group stage of aggregate(), lesson 15 |
>=, < | $gte, $lt | the same |
The session sorts the operators into five families (p. 1430), with an example of each on the slides that follow:
| Family | Operators | Example on the slides |
|---|---|---|
| Logical | $and, $or, $not, $nor | {"$or": [{"name.first": "Mark", "name.last": "Jonhson"}, {"age": 37}]} |
| Comparison | $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin | {"age": {"$nin": [40, 41, 42, 44]}} |
| Element | $exists, $type | {"birth_date": {"$type": "date"}} |
| Evaluation | $regex, $expr, $mod, $text, $where | {"name.first": {"$regex": "[A-Z][a-z]+"}} |
| Array | $all, $elemMatch, $size, $in | {"mobile_phone_numbers": {"$size": 2}} |
A comparison sits inside a field's value, {"age": {"$gt": 19}}; a logical operator sits at the top and takes an array of complete filters, each in its own braces. The nested example of p. 1432, braces balanced:
// aged 20 to 31, or not 40, 41, 42, 44: returns Mark (37)
db.people_collection.find({
"$or": [
{"$and": [
{"age": {"$gt": 19}},
{"age": {"$lte": 31}}
]},
{"age": {"$nin": [40, 41, 42, 44]}}
]
})
Two conditions on one field may also share a document, {"age": {"$gt": 19, "$lte": 31}}, and $not goes inside a field: {"age": {"$not": {"$gt": 30}}}. One definition on p. 1431 is wrong:
$exists asks whether a field is there at all — the check the exam of 21 January 2025 demands for an optional relationship: "be sure to check that at least one book inspired the movie (-0.5 PT otherwise)". $type asks for the type, {"birth_date": {"$type": "date"}} (p. 1433). $regex finds a pattern anywhere inside a string, case-sensitive: "[A-Z][a-z]+" matches "Mark" (p. 1434), and {"$regex": "Pier"} answers "includes the word “Pier”" (exam of 24 June 2024). $text needs a text index, $where runs JavaScript on each document (p. 1434).
A comparison goes inside a field; a logical operator takes an array of whole filters.
Masks, order and the top N
The second argument of find is a projection: "lists of key-value pairs made by the field name and a boolean value representing whether the field will be returned (1) or not (0). They can be seen as masks" (p. 1427). An inclusion mask keeps the listed fields — plus _id, unless you write "_id": 0; an exclusion mask drops them. The two kinds do not mix ("Cannot do exclusion on field address in inclusion projection").
// only the age of every person, without the _id
db.people_collection.find({}, {"age": 1, "_id": 0})
A dotted path keeps the nested shape: {"host.host_response_rate": 1}, the mask of Ex. 5 (p. 1496), returns {"_id": "10006546", "host": {"host_response_rate": 100}} on the listing of section 4. The slide also computes a "New (Temporary) Field", year — with one character missing:
The next slide fixes the order (p. 1428): find "can only perform filters and projections in that exact order". So find({"year": 1986}, {"year": {"$year": "$birth_date"}}) returns nothing — year does not exist when the filter runs. Filtering on a computed value needs the aggregation pipeline.
Sort and limit (p. 1429): sort "accepts a list of fields and their ordering, i.e. descending (-1) and ascending (1). The earlier a field is referenced, the more relevant it is for the ordering" — sort({"name.first": 1, "name.last": 1}) is ORDER BY first, last. limit(n) keeps the first n, so a top N is a descending sort and a limit — Ex. 4 (p. 1495):
// the 10 housings with the most bedrooms; shows bedrooms (and _id)
db.air_bnb_collection.find({},
{ "bedrooms": 1 }
).sort(
{ "bedrooms": -1 }
).limit(10)
On a find the two calls are settings of one query: .limit(2).sort(…) still sorts first. In the pipeline of lesson 15 the stages run in the order written, and $limit before $sort keeps arbitrary documents. Ties come back in no fixed order; add a second sort key if it matters.
find filters first and applies the mask after; sort decides the order, limit cuts.
Types are data: the AirBnB listing
The second half of the session queries the AirBnB sample dataset that MongoDB offers with its free Atlas clusters, imported as air_bnb_collection; pp. 1472–1474 show its first document, the "Ribeira Charming Duplex" in Porto. One listing is one tree: plain values, sub-documents (host, address, availability, review_scores, images), an array of values (amenities) and an array of sub-documents (reviews) — what the session calls a sub-collection. Ex. 1 (p. 1492) inspects them one by one:
// the first housing, showing only its amenities (and _id)
db.air_bnb_collection.findOne({ }, {amenities: 1})
On the slide, minimum_nights: "2" is printed in quotes and green, number_of_reviews: 51 without quotes and blue (p. 1472): a string and a number, as MongoDB's documentation of the dataset confirms. Hence the quotes of Ex. 2 (p. 1493), whose footnote returns the field "just to check that the answer is correct":
// housings whose minimum nights is the string "2"
db.air_bnb_collection.find(
{ "minimum_nights": { "$eq": "2" } },
{ "minimum_nights": 1 }
)
MongoDB compares a filter's value only with values of the same kind — strings with strings, numbers with numbers, dates with dates. "2" and 2 are different values, and {"$gt": "10"} against stored numbers returns nothing, without an error; within numbers the storage type does not matter (80 finds 80.00). At the exam the types are those you declared in your documental representation — the papers ask for "all attributes and their types" — and the queries must use them: counts and amounts as numbers, names and codes as strings, dates as ISODate("2020-03-17").
Ex. 3 asks for a number, and the session's solution returns something else:
Ex. 5 (p. 1496) reaches into a sub-document:
// housings whose host response rate is over 90; shows the rate
db.air_bnb_collection.find(
{ "host.host_response_rate": { "$gt": 90 } },
{ "host.host_response_rate": 1 }
)
The dotted key needs quotes, since host.host_response_rate is not a valid JavaScript key. Otherwise a sub-document behaves like plain fields — such conditions "are not subject to any particular behaviour change" (p. 1466) — and $gt is strict, so a host at exactly 90 stays out. A whole sub-document in a filter, {"address": {"city": "Milan"}}, must match exactly, every field in the same order: it finds nothing.
Arrays of values: contains, all of, any of
Ex. 6 and Ex. 7 (pp. 1497–1498) differ by one operator, $and against $or:
// housings that include a TV and an Iron; shows name and amenities
db.air_bnb_collection.find(
{
"$and": [
{ "amenities": "TV" },
{ "amenities": "Iron" }
],
},
{ "name": 1, "amenities": 1 }
)
On an array, {"amenities": "TV"} means "contains "TV"". Two such conditions need $and, because a JavaScript object cannot hold the key amenities twice (the trailing comma, by the way, is legal in the shell). The array family has shorter forms; on the widget's listings they return:
amenities; results on the widget's five listings, checked on a real server.| Filter | Meaning | Returned |
|---|---|---|
{"amenities": "TV"} | contains TV | Ribeira, Big Villa, Attic Room |
$and of TV and Iron (Ex. 6) | contains both | Big Villa, Attic Room |
{"amenities": {"$all": ["TV", "Iron"]}} | contains all of them, in any order | Big Villa, Attic Room |
$or of TV and Iron (Ex. 7) | contains at least one | Ribeira, Tiny Studio, Big Villa, Attic Room |
{"amenities": {"$in": ["TV", "Iron"]}} | the same as Ex. 7 | Ribeira, Tiny Studio, Big Villa, Attic Room |
{"amenities": ["TV", "Iron"]} | is exactly this array, in this order | Attic Room |
{"amenities": {"$size": 2}} | has exactly two elements | Tiny Studio, Attic Room |
The highlighted row is the trap: brackets ask for the whole array, not its contents. And $size takes a number only — {"$size": {"$gt": 1}} is refused.
"The mask {"name": 1} returns the names of the hosts." Ex. 6 and Ex. 7 ask for "the name of the hosts", but name is the housing's, "Ribeira Charming Duplex"; the host's is host.host_name, "Ana&Gonçalo" (p. 1473). At the exam the mask answers the question — {"host.host_name": 1, "_id": 0}; compare "Return only its Product_ID" (exam of 6 September 2024).
A value in a filter has a type — "2" is not 2 — and one value on an array of values means "contains".
Arrays of sub-documents: the same element or any element
Back to Mark (p. 1419): mobile_phone_numbers is a sub-collection. One dotted condition tests every element — "queries evaluating a single condition on the fields of the documents of an array (sub-collection) will return the main document if at least one of the documents in the array satisfies the condition" (p. 1436):
// people with at least one Wind phone: returns Mark
db.people_collection.find(
{"mobile_phone_numbers.service_provider": "Wind"}
)
Several conditions are "assessed individually on the array’s documents": "It doesn’t matter if one item matches all the conditions or if different items match different conditions" (p. 1437). That is the warm-up:
// returns Mark: Wind is found in one element, YYY in the other
db.people_collection.find(
{"mobile_phone_numbers.service_provider": "Wind",
"mobile_phone_numbers.phone_number": "+39 YYY 9874 123"}
)
To tie the conditions to one element, $elemMatch "matches documents containing an array field with at least one document that satisfies all the specified query criteria" (p. 1438):
// returns Mark: TIM and YYY sit in the same element
db.people_collection.find({
"mobile_phone_numbers": {
"$elemMatch": {
"service_provider": "TIM",
"phone_number": "+39 YYY 9874 123"
}
}
})
With Wind in place of TIM the same filter returns nothing.
Dotted paths: each condition on its own (p. 1437)
$elemMatch: all conditions on one element
$elemMatch reads along the rows — does one element have both? — and does not.Step by step, on Mark:
Condition A, provider "Wind": the path runs into an array, so MongoDB tries every element. Element 0 says Wind — A holds.
Condition B, number "+39 YYY 9874 123": element 0 no, element 1 yes — B holds, on another element.
Both conditions hold, so Mark is returned: nothing asked them to look at the same element (p. 1437).
$elemMatchswaps the loops: one element at a time, all the conditions on it. Element 0: Wind yes, YYY no; element 1: Wind no, YYY yes. No element passes both — Mark is not returned.With TIM and YYY (p. 1438) element 1 passes both, and both forms return Mark.
In logic the two filters differ only in the order of two quantifiers:
- \(A\)
- the array, e.g.
mobile_phone_numbers - \(c_i\)
- the conditions of the filter
- first line
- dotted paths: every condition finds some element, not necessarily the same one
- second line
$elemMatch: one element passes every condition
The second line implies the first, but not the reverse: $elemMatch never returns more documents, and with one condition the two coincide. The widget replays the session's pair on reviews: Ex. 14 asks for "at least one review with listing id 10006546 and reviewer id 11263097" (p. 1505, solved with $elemMatch), Ex. 15 for "at least one review with listing id 10006546 and one with reviewer name Bridget" (p. 1506, dotted paths).
So read the question first. "At least one X with A and B" — one element, both properties — is $elemMatch; "one X with A and one with B" is two dotted paths; a single condition is the same either way. The warm-up's chef question is the first kind, and it is the first exam question below.
Several dotted conditions on an array may be satisfied by different elements; $elemMatch demands one element that satisfies them all.
Indexes: fast reads, paid for by writes
Without an index, find reads every document and tests the filter on each. An index is a second structure kept sorted — "B+ tree indexes" (p. 322); for the session, "data structures that store a small portion of the collection's data set in an easy to traverse form, ordered by the value of the field" (p. 1422). A B+ tree reaches a value in a few steps and then reads its neighbours in order, so one index serves equality, ranges and sorting.
Page 322 lists the rest. "An index is automatically created on the _id field (the primary key)", which is what keeps every _id unique; "Users can create other indexes to improve query performance or to enforce Unique values for a particular field"; they are single-field or compound, and "Like SQL order of the fields in a compound index matters"; an indexed array gets "separate index entries for every element of the array" — a multikey index, in the current manual. The session's compound index (p. 1422):
// a compound index: last name descending, then first name, then age
db.people_collection.createIndex(
{"name.last": -1, "name.first": 1, "age": 1}
)
1 and -1 mean ascending and descending, as in sort. Why the order of the fields matters shows on the leaves of the tree:
A prefix of the index: one run (index scan)
Not a prefix: scattered (collection scan)
This is the prefix rule: the index serves name.last, name.last with name.first, or all three — a prefix of its fields — but not name.first or age alone. A sort on name.last can use it either way round, since the tree reads backwards too. A composite index in SQL behaves the same: hence "Like SQL".
Page 323 adds: "Sparse property of an index ensures that the index only contain entries for documents that have the indexed field", and "If an index is both unique and sparse – then the system will reject records that have a duplicate key value but allow records that do not have the indexed field defined". Why: a unique index that is not sparse files a missing field as null, so a second document without it is a duplicate null and is refused. The typical case is an optional e-mail, unique when present:
// e-mail optional, but unique when present
db.users.createIndex({"email": 1}, {"unique": true, "sparse": true})
The price is printed under the session's index, in green and red: "Faster Queries and Improved Read Performance but Slower Writes and Increased Storage" (p. 1422). Every insert and delete must also update every index, an update every index on a field it changes, and every index takes disk space — an indexed array of twenty values adds twenty entries per document. Index what your queries filter and sort on, not everything.
An index trades write speed and storage for read speed, and a compound index serves only the prefixes of its fields.
Exam-style questions
Five exercises in the form of the practical part, then two theory questions. Each exercise starts from the ER scenario of its paper and states the documental representation it assumes — the query depends on it, which is why every paper asks for the design first. No solutions of past papers are published: these model answers are ours, run on a real MongoDB server against small test collections. In the theory part of the real exam, wrong answers may carry a penalty, announced just before the exam begins.
Exam of 6 Sep 2024, exercise 3 — a restaurant. ER scenario: Person (Personal_ID, Name, Surname, Birth Date, Phone Number) with the children Owner, Customer, Waiter and Chef (Hiring Date); Head Chef (Head Chef Since) is a child of Chef. Dish (Dish_ID, Name, Price, Recipe, Description); Ingredient (Ingredient_ID, Name, Allergies (Yes/No)). Dish 1:N — Cook — 1:N Chef; Dish 1:N — Use — 1:N Ingredient; Chef 1:1 — Manage — 1:N Head Chef. N.B. "Dishes are cooked by more Chefs overall, but only one at a time."
Assumed documental representation (ours): one collection chef; a head chef is a chef document that also has head_chef_since; the dishes a chef cooked are embedded as a sub-collection, each with its ingredients: {"_id": "P1", "name": "Antonio", "hiring_date": ISODate("2019-04-01"), "dishes": [{"dish_id": "D1", "name": "Octopus with Potatoes", "price": 25.00, "recipe": "…", "description": "…", "ingredients": […]}]}.
Write a query to count the number of chefs who cooked at least one dish named “Octopus with Potatoes”, which cost 25.00€. Perform the query starting from the Chef Collection or Sub-Collection. (2 PT)
Show model answer
db.chef.countDocuments({
"dishes": {
"$elemMatch": {"name": "Octopus with Potatoes", "price": 25.00}
}
})
Both conditions describe the same dish, so they go into one $elemMatch on the sub-collection dishes; "count the number of chefs" asks for a number, so the method is countDocuments. The cost is the Price attribute, stored as a number.
This is the warm-up in a chef's apron. Written with two dotted paths, the filter would also count a chef who cooked an octopus at 30€ and some other dish at 25€: on our three test chefs the dotted form answers 2, $elemMatch answers 1. The price must be a number — 25.00 and 25 find the same value, the string "25.00" finds nothing.
Exam of 13 Jun 2025 (its header prints Jan. 21, 2025), exercise 3 — concert attendances and performances. ER scenario: Person (Personal ID, Name, Surname, Birth Date) with the children Maestro (Biography, Stage Name) and Musician (Biography), among others, (T, E). Orchestra (Name, Foundation Date, Biography); Instrument (Name, Type, Description). Maestro (1,1) — Direct — (1,1) Orchestra, with First Concert (Yes/No); Orchestra (10,N) — Compose — (1,2) Musician; Musician (1,N) — Play — (1,N) Instrument, with Since.
Assumed documental representation: the one of lesson 13 — one collection orchestra; the maestro is an embedded sub-document, the musicians a sub-collection, and each musician holds his instruments as a sub-collection: {"_id": "Orchestra Aurora", "maestro": {"Name": "Claudio", …}, "musicians": [{"Personal_ID": "P101", "Name": "Lorenzo", …, "instruments": [{"Name": "piano", …, "Since": …}]}]}.
Write a query to count the number of orchestras directed by directors named “Claudio” and including at least one member named “Lorenzo” who can play the piano (2 PT)
Show model answer
db.orchestra.countDocuments({
"maestro.Name": "Claudio",
"musicians": {
"$elemMatch": {"Name": "Lorenzo", "instruments.Name": "piano"}
}
})
The maestro is a sub-document, so a dotted path reaches his name. "At least one member named Lorenzo who can play the piano" is one musician with two properties: $elemMatch on musicians. Inside it, "instruments.Name": "piano" is a dotted path into that musician's own instruments, true if any of them is the piano.
The "directors" of the question are the maestros of Direct. Two dotted paths for the member would also count an orchestra where Lorenzo plays the cello and Fabiana the piano: on our three test orchestras the dotted form answers 2, the model answer 1.
Exam of 6 Jun 2024, exercise 3 — tracking natural disasters. ER scenario: Natural Disaster (ID, Start Time, Start Date, End Time, End Date, Disaster Description) with the children Earthquake (Magnitude, Mercalli Scale), Flooding (Water Height), Landslide (Landslide Class) and Tornado (EF Scale, 0 to 5), (T, E); Location (Postal Code, City, Country, Description). Location 0:N — Occurr — 1:N Natural Disaster; Cause, recursive on Natural Disaster, with Causal Description.
Assumed documental representation (ours): one collection natural_disaster; as the hierarchy is total and exclusive, each document is one child, with a type field and its own attributes; the locations of Occurr are embedded as a sub-collection, postal codes stored as strings: {"_id": "ND1", "type": "tornado", "ef_scale": 4, "start_date": …, "locations": [{"postal_code": "23563", "city": "…", "country": "Italy", "description": "…"}]}.
Write a query to return the list of Tornadoes with an EF Scale greater than 3 that affected at least one location whose Country is Italy and whose postal code is 23563 or that affected Milan. (2 PT)
Show model answer
db.natural_disaster.find({
"type": "tornado",
"ef_scale": {"$gt": 3},
"$or": [
{"locations": {
"$elemMatch": {"country": "Italy", "postal_code": "23563"}
}},
{"locations.city": "Milan"}
]
})
Three conditions joined by the implicit AND: the type, the EF scale and an $or of two location clauses. "At least one location whose Country is Italy and whose postal code is 23563" is one location with two properties, hence $elemMatch; "affected Milan" is a single condition, hence a dotted path.
Read the "or" carefully: it separates the two location clauses, not the EF scale. With two dotted paths the first clause would also accept a tornado that hit location 23563 in another country and some other place in Italy. The postal code is a string because the design says so; written as the number 23563, the first clause would find nothing.
Exam of 24 Jun 2024, exercise 3 — tracking Disney's digital products. ER scenario: Company Section (ID, Name, Description, Number of Employees); Digital Content (Title, Release Date, Duration, Production Cost, Minimum Age) with the children Series (Number of Episodes, …) and Movie (Sequence Number, Nominated for Award (Yes/No), …), (T, E), and Animated Movie a child of Movie, (P, O); Script (ID, Length, Duration, Title). Company Section 1:N — Produce — 1:N Digital Content, with Starting Date and End Date; Digital Content 1:N — Describe — 1:1 Script.
Assumed documental representation (ours): one collection digital_content with a type field ("series" or "movie"); the scripts are embedded as a sub-collection, Script being the N side of Describe; the producing sections are embedded as a sub-collection with the dates of Produce: {"_id": "The Pier Saga", "type": "series", "number_of_episodes": 10, "company_sections": [{"id": "908686", "name": "Digital Artist & Co", "starting_date": …, "end_date": …}], "scripts": [{"id": "S1", "length": …, "duration": …, "title": "Pier 39"}]}.
Write a query to count the number of series produced by a company section named “Digital Artist & Co” with ID “908686” and for which at least one script includes the word “Pier” in its title. (2 PT)
Show model answer
db.digital_content.countDocuments({
"type": "series",
"company_sections": {
"$elemMatch": {"name": "Digital Artist & Co", "id": "908686"}
},
"scripts.title": {"$regex": "Pier"}
})
The name and the ID describe one section, so they share one $elemMatch; the script condition is a single condition, so a dotted path is enough; "includes the word" is a $regex, which matches anywhere inside the title. The ID is written in quotes in the question and stored as a string.
Three families in one filter: comparison (the implicit $eq of "type": "series"), array ($elemMatch) and evaluation ($regex). $regex is case-sensitive and does not stop at word boundaries: "Pier" also matches "Pierce". If you want the word only, "\\bPier\\b" marks its boundaries — a refinement, worth one line of comment.
The session's AirBnB collection (the exercise is ours). ER scenario: Housing (ID, Name, Minimum Nights, Number of Reviews, Amenities (multi-valued)) and Host (Host_ID, Host Name, Response Rate); Housing (1,1) — Offered By — (1,N) Host; each housing has an Address with a Country. Documental representation: the session's air_bnb_collection — host and address are embedded sub-documents, amenities an array of strings, and minimum nights is stored as a string, as on p. 1472.
Write a query to return the host name and the number of reviews of the 5 housings with the highest number of reviews, among those located in Portugal, whose minimum nights is equal to 2 and that include Wifi.
Show model answer
db.air_bnb_collection.find(
{"address.country": "Portugal", "minimum_nights": "2",
"amenities": "Wifi"},
{"host.host_name": 1, "number_of_reviews": 1, "_id": 0}
).sort({"number_of_reviews": -1}).limit(5)
Three conditions in one filter, an implicit AND: a dotted path into the address, a string "2" because minimum nights is stored as a string, and "Wifi" on the array of amenities, meaning "contains". The mask keeps only what the question asks for and removes the _id; the descending sort followed by the limit gives the top 5.
On the widget's five listings this returns Ribeira (51 reviews) and River Loft (12), both hosted by Ana&Gonçalo. With the number 2 it would return Tiny Studio instead — the type trap. Forgetting "_id": 0 returns one field the question did not ask for.
True or false: in MongoDB, $nor returns the documents that do not match at least one of the query expressions in its array.
Show model answer
False. $nor selects the documents that fail every query expression in its array: {"$nor": [A, B]} means NOT (A OR B). A document that matches A but not B is not returned.
The slide's wording is the trap, so this is the kind of statement a true-or-false question can quote. A one-line check: Mark, aged 37, is not returned by {"$nor": [{"age": 37}, {"name.first": "Bob"}]}, although he fails one of the two.
Explain how MongoDB uses indexes: which index exists in every collection, why the order of the fields of a compound index matters, what happens when an array field is indexed, and what a unique and sparse index allows. What is the cost of an index?
Show model answer
Indexes are B+ tree structures that keep a small part of the data sorted by the indexed fields, so equality and range filters and sorts can be answered without scanning the whole collection. An index on _id, the primary key, is created automatically in every collection and keeps the _id values unique. A compound index is sorted by its first field, then by the second within equal values of the first, and so on: as in SQL, it serves queries on a prefix of its fields — for an index on (a, b, c): a; a and b; a, b and c — but not on b alone. Indexing a field that holds an array creates one index entry for every element of the array. A sparse index contains entries only for the documents that have the field; if it is also unique, duplicate values are rejected but any number of documents without the field are accepted, whereas a unique index that is not sparse would reject the second document without the field as a duplicate null. The cost: every insert and delete, and every update of an indexed field, must also update the indexes, so writes are slower, and the indexes take extra storage.
A complete answer covers five points: the automatic and unique _id index; the prefix rule with an example; one entry per array element; why unique needs sparse when the field is optional (the duplicate null); and the read/write trade-off that p. 1422 prints in green and red.
Check yourself
Mark's phones are [{number XXX, provider Wind}, {number YYY, provider TIM}], stored with the number first. Which filters return Mark? Select all that apply.
Dotted paths: every condition somewhere. $elemMatch: all the conditions on one element. A whole sub-document: an exact copy.
In the session's collection, minimum_nights is stored as a string in every listing — "2" for Ribeira. What does {"minimum_nights": 2} return?
Which fields of Mark does db.people_collection.find({}, {"name": 1}) return?
You run db.students.updateOne({"name": "nguyen"}, {"age": 20}) in today's shell. What happens?
With the index {"name.last": -1, "name.first": 1, "age": 1}, which query cannot use it, so that MongoDB falls back to a collection scan?
Big Villa's amenities are ["TV", "Iron", "Wifi"]. Which filters return it? Select all that apply.
A collection has a unique and sparse index on email. You insert two documents that have no email field. What happens?
Key take-aways
- The shell is JavaScript. The session's CRUD names are the ones to write:
insertOne/insertMany,find/findOne/countDocuments,updateOne/updateManywith an operator such as$set,deleteOne/deleteMany,drop; the lecture'sinsert,update,saveandremoveare the older names. - A filter is a document: a field with a value is an equality, a field with an operator document applies the operator, several fields are an AND. The operators come in five families — logical, comparison, element, evaluation, array — and
$normeans "none of them", whatever p. 1431 says. find(filter, projection)filters first and masks after: 1 keeps, 0 drops,_idstays unless you remove it, and the two kinds do not mix. A top N issortdescending, thenlimit; "the number of" iscountDocuments.- Values have types: "2" is not 2, numbers compare with numbers of any numeric type, dates are written
ISODate(…). A wrong type gives an empty result, not an error. - On an array of values, one value means "contains",
$allmeans all of them,$inany of them, brackets the exact array,$sizethe exact length. - On an array of sub-documents, dotted conditions may be satisfied by different elements;
$elemMatchneeds one element that satisfies them all. That is why Mark comes back in the warm-up, and why "at least one dish named X which cost Y" needs$elemMatch. - Indexes are B+ trees:
_idis always indexed and unique, a compound index serves only its prefixes, an array field gets one entry per element, unique and sparse together allow missing fields; reads get faster, writes slower, storage grows.
What to watch and read
-
MongoDB Crash Course
Web Dev Simplified
The shell's
insertOne,findwith filters and projections,sortandlimit,updateOnewith$setanddeleteMany, typed live. It uses the current method names — the ones of the session, not of the lecture's older slides. -
Mongo playground
feliixx, open source
A MongoDB server in the browser, no account: paste a few documents on the left and a query on the right. Type the widgets' five listings and check every filter of this lesson, the type trap and the dotted-against-
$elemMatchpair included. -
Query an Array of Embedded Documents
MongoDB manual
The three cases of pp. 1436–1438 on the manual's own inventory example: one condition, several conditions that "any combination of elements" may satisfy, and
$elemMatch. It also shows that a whole embedded document must match in field order. -
Sample AirBnB Listings Dataset
MongoDB Atlas documentation
The session's dataset, with the Ribeira document and its types: minimum_nights a string, bedrooms and number_of_reviews integers, price a decimal. Load it into a free cluster and run Ex. 1 to Ex. 7.
-
Indexes
MongoDB manual
The default
_idindex, compound and multikey indexes, the unique and sparse properties, and the read/write trade-off of pp. 322–323 and p. 1422, in the vendor's words.