KevsRobots Learning Platform
By Kevin McAleer, 9 Minutes
Page last updated October 18, 2026

One of the best things about MongoDB is that a document doesn’t have to be flat. Our parts already have a document inside a document (location: { shelf: "A", bin: 1 }) and lists of values (tags, used_in). A document inside another document is called a sub-document or embedded document.
So far we’ve mostly looked at these from the outside. In this lesson we’ll reach inside them - finding everything on shelf D, every part that runs on 5V, and which shop has a Pico W in stock.
Starting fresh: lessons 08 and 09 changed some parts, so let’s reset. Run
db.parts.drop()and re-run theinsertManyfrom lesson 05. The outputs below assume the original 13 parts.
To reach a field inside a sub-document, join the names with a dot. "location.shelf" means “the shelf field inside location”. What’s on shelf D?
db.parts.find({ "location.shelf": "D" }, { name: 1, location: 1, _id: 0 })
[
{ name: 'N20 Gear Motor', location: { shelf: 'D', bin: 1 } },
{ name: 'L298N Motor Driver', location: { shelf: 'D', bin: 2 } },
{ name: 'DRV8833 Motor Driver', location: { shelf: 'D', bin: 3 } },
{ name: 'PCA9685 Servo Driver', location: { shelf: 'D', bin: 4 } }
]
The quotes around "location.shelf" are required. Leave them out and mongosh gets confused by the dot:
Uncaught:
SyntaxError: Unexpected token, expected "," (1:24)
> 1 | db.parts.find({ location.shelf: "D" })
| ^
Dot notation works with everything we’ve learned so far - operators, projection and sorting. Which sensors reach 300cm or more?
db.parts.find({ "specs.range_cm": { $gte: 300 } }, { name: 1, "specs.range_cm": 1, _id: 0 })
[ { name: 'HC-SR04 Ultrasonic Sensor', specs: { range_cm: 400 } } ]
See how the projection "specs.range_cm": 1 brought back just that one field from inside specs. And here’s shelf D sorted by bin, highest first:
db.parts.find({ "location.shelf": "D" }, { name: 1, "location.bin": 1, _id: 0 }).sort({ "location.bin": -1 })
[
{ name: 'PCA9685 Servo Driver', location: { bin: 4 } },
{ name: 'DRV8833 Motor Driver', location: { bin: 3 } },
{ name: 'L298N Motor Driver', location: { bin: 2 } },
{ name: 'N20 Gear Motor', location: { bin: 1 } }
]
You can match a whole sub-document:
db.parts.find({ location: { shelf: "A", bin: 1 } }, { name: 1, _id: 0 })
[ { name: 'SG90 Micro Servo' } ]
But swap the order to { bin: 1, shelf: "A" } and you get nothing. Matching a whole sub-document needs the exact same fields in the exact same order. Dot notation doesn’t care about order, so prefer { "location.shelf": "A", "location.bin": 1 }.
In lesson 06 we saw that { tags: "5v" } finds any part with 5v somewhere in its list. Here are three more tools for lists.
$all matches when the list contains every value you give, in any order:
db.parts.find({ tags: { $all: ["sensor", "distance"] } }, { name: 1, tags: 1, _id: 0 })
[
{
name: 'HC-SR04 Ultrasonic Sensor',
tags: [ 'sensor', 'distance', '5v' ]
},
{
name: 'VL53L0X Time-of-Flight Sensor',
tags: [ 'sensor', 'distance', 'i2c', '3v3' ]
}
]
Compare that with $in from lesson 07, which matches if the list contains any of the values.
$size matches a list of an exact length. Parts with an empty used_in aren’t in any project yet:
db.parts.find({ used_in: { $size: 0 } }, { name: 1, _id: 0 })
[
{ name: 'VL53L0X Time-of-Flight Sensor' },
{ name: 'WS2812B LED Strip 1m' }
]
And the part every robot needs:
db.parts.find({ used_in: { $size: 3 } }, { name: 1, used_in: 1, _id: 0 })
[
{
name: 'M3 Screws (pack of 100)',
used_in: [ 'SMARS', 'BurgerBot', 'Robot Arm' ]
}
]
You can also pick an item by its position, counting from zero. { "tags.0": "sensor" } finds parts whose first tag is sensor.
Lists can hold sub-documents too. Let’s record where we can buy the Pico W and the SG90, with each shop’s price and whether it’s in stock:
db.parts.updateOne(
{ name: "Raspberry Pi Pico W" },
{ $set: { suppliers: [
{ shop: "Pimoroni", price: 6.30, in_stock: true },
{ shop: "The Pi Hut", price: 6.60, in_stock: false }
] } }
)
db.parts.updateOne(
{ name: "SG90 Micro Servo" },
{ $set: { suppliers: [
{ shop: "Pimoroni", price: 3.00, in_stock: false },
{ shop: "The Pi Hut", price: 2.40, in_stock: true }
] } }
)
Dot notation reaches into every item in the list. Who sells through The Pi Hut?
db.parts.find({ "suppliers.shop": "The Pi Hut" }, { name: 1, _id: 0 })
[ { name: 'SG90 Micro Servo' }, { name: 'Raspberry Pi Pico W' } ]
Now a trickier question: which parts can I buy in stock at Pimoroni?
db.parts.find({ "suppliers.shop": "Pimoroni", "suppliers.in_stock": true }, { name: 1, _id: 0 })
[ { name: 'SG90 Micro Servo' }, { name: 'Raspberry Pi Pico W' } ]
That’s wrong! Pimoroni is out of SG90s. MongoDB checked each condition separately: “does some supplier say Pimoroni?” (yes) and “is some supplier in stock?” (yes, The Pi Hut). The two conditions matched different items.
$elemMatch fixes this. It says “one single item in the list must match all of these”:
db.parts.find(
{ suppliers: { $elemMatch: { shop: "Pimoroni", in_stock: true } } },
{ name: 1, _id: 0 }
)
[ { name: 'Raspberry Pi Pico W' } ]
That’s the right answer. Whenever you have two or more conditions about the same item in a list, reach for $elemMatch.
Dot notation works in updates too. We’ve moved the MG996R to bin 3:
db.parts.updateOne({ name: "MG996R Servo" }, { $set: { "location.bin": 3 } })
db.parts.findOne({ name: "MG996R Servo" }, { name: 1, location: 1, _id: 0 })
{ name: 'MG996R Servo', location: { shelf: 'A', bin: 3 } }
Only bin changed - shelf is untouched. Watch what happens if we leave the dot out and set the whole sub-document:
db.parts.updateOne({ name: "MG996R Servo" }, { $set: { location: { bin: 2 } } })
db.parts.findOne({ name: "MG996R Servo" }, { name: 1, location: 1, _id: 0 })
{ name: 'MG996R Servo', location: { bin: 2 } }
The shelf has vanished, because we replaced all of location. Let’s put it back with { $set: { location: { shelf: "A", bin: 2 } } }.
The Pi Hut now has Pico Ws in stock at £6.50. We need to change one item inside the suppliers list - but which one? The positional operator $ means “the item that matched my filter”:
db.parts.updateOne(
{ name: "Raspberry Pi Pico W", "suppliers.shop": "The Pi Hut" },
{ $set: { "suppliers.$.price": 6.50, "suppliers.$.in_stock": true } }
)
db.parts.findOne({ name: "Raspberry Pi Pico W" }, { name: 1, suppliers: 1, _id: 0 })
{
name: 'Raspberry Pi Pico W',
suppliers: [
{ shop: 'Pimoroni', price: 6.3, in_stock: true },
{ shop: 'The Pi Hut', price: 6.5, in_stock: true }
]
}
The filter found The Pi Hut’s entry, and suppliers.$ pointed the update at it. Pimoroni’s entry wasn’t touched. $push and $pull from lesson 08 work on these lists too - { $pull: { suppliers: { shop: "Kitronik" } } } removes a whole supplier entry.
After this lesson, the Pico W and SG90 have suppliers lists, and everything else matches the seed data.
specs.voltage is exactly "3.3V".motor driver and 5v.$ to set that supplier’s in_stock to true.Problem: SyntaxError: Unexpected token, expected "," on a query with a dot in it.
Solution: Put quotes around the field name: "location.shelf".
Why: Without quotes, the shell reads the dot as JavaScript rather than part of a field name.
Problem: The positional operator did not find the match needed from the query.
Solution: Include the array field in the filter, e.g. "suppliers.shop": "The Pi Hut".
Why: $ points at the item your filter matched. If the filter doesn’t mention the list, there’s nothing for it to point at.
Problem: Failed to parse $size. Expected a number
Solution: $size only takes an exact number, so { $size: { $gt: 1 } } won’t work. Use { "used_in.1": { $exists: true } } to mean “has at least two items”.
Why: $size is a simple length check. Checking whether position 1 exists is a handy workaround.
Problem: A query on two conditions in an array of sub-documents returns too much.
Solution: Wrap the conditions in $elemMatch.
Why: Without it, each condition can be satisfied by a different item in the list.
"location.shelf") reaches inside sub-documents - always in quotes$all needs every value in the list; $size checks the list’s length$elemMatch when several conditions must all be true of the same list item$set with dot notation changes one nested field; without it you replace the whole sub-document$ updates the list item your filter matchedNext, we’ll step back and think about designing our data - what to embed, what to keep in a separate collection, and how to stop typos getting in.
You can use the arrows ← → on your keyboard to navigate between lessons.
Comments