How does MongoDB compare values of different BSON types when matching and sorting?
answer
- the engine converts nothing for you
- every value still needs a place in the sort
- one band holds four types at once
- strings rank above every number
- a missing field behaves like null
basics
~20 sMongoDB never coerces types: a number never equals a string. Across types it uses a fixed BSON order — MinKey, null, numbers, string, object, array, binData, ObjectId, boolean, date, timestamp, regex, MaxKey — with all numeric types compared by value.
solid answer
~50 sBSON is strongly typed and MongoDB does **no implicit conversion** during matching, so `{ qty: 100 }` does not match a document whose `qty` is the string `"100"`. When values of different types must nonetheless be ordered, MongoDB applies a fixed type ranking, lowest to highest: MinKey, null, numbers, string, object, array, binData, ObjectId, boolean, date, timestamp, regex, MaxKey. All four numeric types sit in one band and compare by numeric value, so an int, a long and a double interleave correctly. A missing field sorts as null. An array sorts by its smallest element ascending and its largest descending. In practice this matters when an import or a client-library change leaves one field holding mixed types: range queries then silently skip documents. Find them with `$type` and repair them with an update that uses an aggregation pipeline and `$toInt` or `$toDecimal`.
code
javascript · 3 linesdb.orders.insertMany([{ qty: 100 }, { qty: "100" }])
db.orders.find({ qty: { $gt: 50 } }) // returns only the numeric document
db.orders.find({ qty: { $type: "string" } }) // finds the straygo deeper
Remember that BSON types are strict: a query for the number 100 will not find the string "100", and there is no automatic conversion to rescue you.
Be able to state the cross-type ordering, explain that all numeric types share one band and compare by value, and describe how missing fields and arrays are positioned in a sort.
Show the diagnosis: a range query returning too few rows, $type to isolate the strays, a pipeline update with $toInt or $convert to repair, and a validator to stop it recurring.
Own type discipline as a contract question — where types are enforced (client boundary, validator, ingestion pipeline), and how you migrate a field's type across services without a window where queries silently miss data.
## No implicit coercion The single most important fact is negative: MongoDB does not convert types to make a comparison succeed. The query `{ qty: 100 }` matches documents where `qty` is the number 100 — as an int, a long, a double or a decimal — and does **not** match a document where `qty` is the string `"100"`. Nothing warns you; the document simply is not in the result. The same holds for a date compared with its string rendering, or a boolean compared with 1. This is what makes a schemaless store demand type discipline. A field whose type varies across documents is not merely untidy: it silently splits your data into partitions your queries can only reach one at a time. ## The cross-type ordering Comparison still has to be **total**, because indexes and `sort()` must place every value somewhere. MongoDB therefore ranks the BSON types, from lowest to highest: 1. MinKey (internal) 2. Null 3. Numbers (int, long, double, decimal) 4. String 5. Object (embedded document) 6. Array 7. BinData 8. ObjectId 9. Boolean 10. Date 11. Timestamp 12. Regular expression 13. MaxKey So in an ascending sort every number precedes every string, which precedes every embedded document, and so on. Two useful corollaries: `{ f: { $gt: 5 } }` cannot return a string, because strings rank above all numbers but the predicate is evaluated within the number band; and sorting a mixed-type field produces clean per-type blocks rather than an interleaving. ## Numbers are one band All four numeric types compare against each other **by value**: `NumberInt(5)`, `NumberLong(5)` and `5.0` are equal, and a range predicate spans them all. The comparison is performed exactly, which is why a `double` literal and a `Decimal128` holding the "same" decimal amount are *not* equal — the double's exact binary value differs from the decimal's. That is a numeric-precision consequence, not an exception to the type rule. ## Missing fields, null, and arrays For ordering purposes, a **missing field is treated as null**, so documents lacking the sort key cluster with the explicit nulls at the low end of an ascending sort. **Arrays** get special treatment. For an ascending sort, an array is represented by its **smallest** element; for a descending sort, by its **largest**. And in matching, a query predicate on a field holding an array succeeds if *any* element satisfies it, which is why `{ tags: "db" }` matches `{ tags: ["db", "nosql"] }` without any special operator. **Embedded documents** compare field by field in the order the fields appear in the BSON, comparing field names and then values. That means two documents with the same fields written in a different order are different values — a real trap when an embedded document is used as an `_id` or as an equality target. ## Diagnosing and repairing mixed types The `$type` operator is the audit tool. `{ field: { $type: "string" } }` isolates the string-typed strays; the alias `{ field: { $type: "number" } }` matches int, long, double and decimal together, so `{ field: { $not: { $type: "number" } } }` finds everything that is not numeric at all. `$type` also accepts an array of type names. Repair is an update that uses an aggregation pipeline (supported from MongoDB 4.2 onward), which lets the new value be computed from the old one: ```javascript db.orders.updateMany( { qty: { $type: "string" } }, [{ $set: { qty: { $toInt: "$qty" } } }] ) ``` For values that may not convert cleanly, `$convert` takes `onError` and `onNull` so a bad row can be routed to a sentinel instead of failing the whole batch. ## Preventing recurrence Once repaired, the way to keep a field single-typed is to declare it — a collection validator that pins `bsonType` rejects the next writer that sends a string. Type discipline at the boundary (wrapping values in the right constructor in application code) is the other half; the database can only reject what reaches it.
- Why can a range query like { qty: { $gt: 10 } } silently skip documents?Because documents whose qty is a string are not numbers, and the predicate is evaluated inside the number band. They are neither matched nor reported as an error. Audit with { qty: { $not: { $type: "number" } } }, repair with a pipeline update using $toInt, and pin the field's bsonType in a validator so it cannot recur.
- How does a query behave when the field holds an array?A predicate on an array field matches if any single element satisfies it, so { tags: "db" } matches { tags: ["db","nosql"] } with no special operator. For sorting, an array is represented by its smallest element ascending and its largest descending. Requiring several conditions on the same element needs $elemMatch.
- Do two embedded documents with the same fields in different order compare as equal?No. BSON compares embedded documents field by field in stored order, comparing names then values, so { a: 1, b: 2 } and { b: 2, a: 1 } are different values. This bites when an embedded document is used as an _id or as an exact equality target — always build such sub-documents in one fixed field order.
saying these in an interview costs you the question
- Assumes MongoDB casts "100" to 100 for a query
- Says mixed-type fields raise an error on query
- Thinks a missing field is excluded from a sort entirely
- Believes an int and a long compare as different values
- Expects field order in an embedded document to be irrelevant