BSON types, ObjectId and type ordering
Types decide how values compare, sort and index. Learn the important BSON types, how ObjectId works, and how MongoDB orders mixed types.
- Choose the right numeric type (int, long, double, Decimal128)
- Explain the structure of an ObjectId
- Store dates as Date, not strings
- Query by type with $type
conceptThe concept
| Type | Shell | Notes |
|---|---|---|
| Double | 3.14 | Default number type in the shell and in JSON input |
| 32-bit int | NumberInt(5) | Counters, small integers |
| 64-bit int | NumberLong("9007199254740993") | Large integers; pass as a string to avoid JS precision loss |
| Decimal128 | NumberDecimal("19.99") | Exact decimal arithmetic: money, rates |
| String | "text" | UTF-8 |
| Date | ISODate("2026-01-31T10:00:00Z") | 64-bit milliseconds since epoch, always UTC |
| ObjectId | ObjectId() | 12 bytes, default _id |
| Boolean, Null, Array, Object | ||
| Binary / UUID | UUID() | Opaque bytes, UUIDs as subtype 4 |
ObjectId = 4-byte timestamp (seconds) + 5-byte random value per process + 3-byte incrementing counter. It is roughly time-ordered, globally unique without coordination, and you can recover the creation time with getTimestamp().
When values of different types are compared or sorted, MongoDB uses a fixed order: MinKey, Null, Numbers, Strings, Object, Array, BinData, ObjectId, Boolean, Date, Timestamp, Regex, MaxKey. Range operators such as $gt only match values of the same type class ("type bracketing").
whyWhy it matters
Storing a date as the string "31/01/2026" breaks range queries and sorting. Storing money as a double produces 0.1 + 0.2 = 0.30000000000000004. Mixed types in one field (numbers in some documents, strings in others) make queries silently miss data, because { price: { $gt: 10 } } does not match "15".
Use caseReal-world scenario
A payments team stores amount as Decimal128 and currency as a string, timestamps as Date, and uses the ObjectId timestamp of _id to show "created 3 minutes ago" without an extra field.
Try itExamples
Run and edit these in the interactive lesson.
const id = ObjectId()
id
id.getTimestamp()
db.orders.findOne({}, { _id: 1, createdAt: 1 })db.orders.find({ createdAt: { $gte: ISODate("2026-09-01"), $lt: ISODate("2026-10-01") } }).count()
db.orders.findOne({}, { createdAt: 1 }).createdAt.getUTCFullYear()db.lab_types.drop()
db.lab_types.insertMany([{ v: 1 }, { v: "1" }, { v: 1.5 }, { v: null }, { v: [1, 2] }, { v: new Date() }])
db.lab_types.find({ v: { $type: "string" } }, { _id: 0 })
db.lab_types.find({ v: { $type: "date" } }, { _id: 0 })
db.lab_types.find({ v: { $gt: 0 } }, { _id: 0 })$gt: 0 matches the numbers and the array (an element is 1 or 2) but not the string "1": type bracketing.0.1 + 0.2
db.orders.aggregate([{ $match: { status: "delivered" } }, { $group: { _id: null, revenue: { $sum: "$total" } } }, { $project: { _id: 0, revenue: 1, rounded: { $round: ["$revenue", 2] } } }])internalsUnder the hood
Every BSON element is a type byte, a field name and the value; documents and arrays are length-prefixed. Arrays are stored as documents with keys "0", "1", ... which is why items.0.qty works in dot notation. Index keys are encoded in a type-aware, order-preserving format (KeyString) so the B-tree can compare keys as raw bytes.
ObjectId('...'). Everything else in this lesson behaves as on a real server.Avoid / preferDo this, not that
db.payments.insertOne({ amount: 19.99 })db.payments.insertOne({ amount: NumberDecimal("19.99") })
// or integer cents: { amountCents: 1999 }A double cannot represent 19.99 exactly, so sums drift by fractions of a cent. Decimal128 or integer minor units keep money exact.
db.events.insertOne({ ts: "2026-10-08T10:00:00Z" })db.events.insertOne({ ts: new Date("2026-10-08T10:00:00Z") })Date strings sort as text, cannot use date operators, take more space, and a range query on Date values silently skips documents that hold strings (type bracketing).
PitfallsCommon mistakes
- Dates stored as strings, epoch seconds in some documents and Date in others.
- Money stored as double. Use Decimal128, or integers in minor units (cents).
- Passing a large NumberLong as a JS number, e.g.
NumberLong(9007199254740993), which loses precision before it reaches the server. - Assuming ObjectIds are strictly increasing. They are only roughly ordered across processes (the counter is per process).
LimitsWhen not to use it
Do not use Decimal128 for every number: it takes 16 bytes and arithmetic is slower. Use it for money and other exact decimals, doubles for measurements and integers for counts. Do not store UUIDs as 36-character strings either; UUID() stores them as 16-byte binary.
SecuritySecurity notes
Types matter for query injection. If an application passes a request body straight into a filter, an attacker can send { "password": { "$ne": null } } instead of a string and match every user. Check that values have the type you expect (or cast with String(x)) before they reach a query.
ChallengeTry it yourself
Count orders by the BSON type of their total field, so you would spot any totals stored as strings.
Show one solution
db.orders.aggregate([
{ $group: { _id: { $type: "$total" }, orders: { $sum: 1 } } },
{ $sort: { orders: -1 } }
])InterviewQuestions you should be able to answer
What is inside an ObjectId?
12 bytes: a 4-byte Unix timestamp in seconds, a 5-byte random value unique to the machine and process, and a 3-byte counter starting from a random value. It is generated client-side by the driver, so no coordination is needed.
Why might a range query miss documents that look like they should match?
Type bracketing: comparison operators only match the same BSON type class. If some documents store price as a string, { price: { $gt: 10 } } skips them. Find them with { price: { $type: "string" } } and fix the data, then add a validator.
QuizCheck your understanding
4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.