MongoDB Mastery
MongoDB Mastery lessons / Module 3: BSON Data Types

BSON types, ObjectId and type ordering

Level 1: Beginner18 min

Types decide how values compare, sort and index. Learn the important BSON types, how ObjectId works, and how MongoDB orders mixed types.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • 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

TypeShellNotes
Double3.14Default number type in the shell and in JSON input
32-bit intNumberInt(5)Counters, small integers
64-bit intNumberLong("9007199254740993")Large integers; pass as a string to avoid JS precision loss
Decimal128NumberDecimal("19.99")Exact decimal arithmetic: money, rates
String"text"UTF-8
DateISODate("2026-01-31T10:00:00Z")64-bit milliseconds since epoch, always UTC
ObjectIdObjectId()12 bytes, default _id
Boolean, Null, Array, Object
Binary / UUIDUUID()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.

ObjectId carries a timestamp
const id = ObjectId()
id
id.getTimestamp()
db.orders.findOne({}, { _id: 1, createdAt: 1 })
Dates are real dates
db.orders.find({ createdAt: { $gte: ISODate("2026-09-01"), $lt: ISODate("2026-10-01") } }).count()
db.orders.findOne({}, { createdAt: 1 }).createdAt.getUTCFullYear()
Query by type
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.
Floating point vs exact decimals
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.

Playground noteIn this in-browser engine ObjectIds are stored as 24-character hex strings and printed as ObjectId('...'). Everything else in this lesson behaves as on a real server.

Avoid / preferDo this, not that

Avoid
db.payments.insertOne({ amount: 19.99 })
Prefer
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.

Avoid
db.events.insertOne({ ts: "2026-10-08T10:00:00Z" })
Prefer
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
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.