← MongoDB Notes

Element
Operators

FILE 10_element_operators
TOPIC $exists · $type · Field presence · BSON type matching
LEVEL Foundation
01
Overview
Field presence and BSON type introspection
intro

Element operators query documents based on the presence or BSON type of a field, rather than its value. They are critical in MongoDB because its flexible, schema-less design means fields can be absent, null, or stored as the wrong type across different documents in the same collection.

OperatorWhat it checksCommon use
$existsWhether a field is present in the document (even if null)Finding incomplete or missing fields
$typeWhether a field's value is a specific BSON data typeData quality audits and type mismatch detection

Where These Operators Can Be Used

  • find() filter document
  • updateMany() and updateOne() filter
  • deleteMany() and deleteOne() filter
  • Aggregation $match stage
NOTE
MongoDB's flexible schema is a feature, not a bug — but it means that field presence and type consistency are YOUR responsibility to enforce. $exists and $type are your primary tools for detecting and fixing schema irregularities.
02
$exists
Field presence · null distinction · Missing vs explicit null
exists

$exists Syntax

// $exists: true — field must exist (value can be anything, even null)
db.users.find({ phone: { $exists: true } })

// $exists: false — field must be completely absent from the document
db.users.find({ phone: { $exists: false } })

CRITICAL: Three-Way Distinction for "Missing" Data

This is the most important thing to understand about $exists. There are three distinct states a field can be in relative to a document, and each requires a different query pattern.

TRAP
A null query like { phone: null } matches BOTH documents with a null value AND documents with no phone field at all. This often causes unintended results.
// Sample documents in collection:
// { _id: 1, name: "Alice", phone: "+91-9999" }    ← field exists, has value
// { _id: 2, name: "Bob",   phone: null }           ← field exists, value is null
// { _id: 3, name: "Cara" }                         ← field is completely absent

// QUERY 1: { phone: null }
// Matches: _id 2 (null value) AND _id 3 (missing field) — TWO docs
db.users.find({ phone: null })

// QUERY 2: { phone: { $exists: false } }
// Matches: ONLY _id 3 (field is absent) — one doc
db.users.find({ phone: { $exists: false } })

// QUERY 3: { phone: { $exists: true, $eq: null } }
// Matches: ONLY _id 2 (field exists AND is explicitly null) — one doc
db.users.find({ phone: { $exists: true, $eq: null } })
Query patternMatches null value?Matches missing field?
{ phone: null }YesYes
{ phone: { $exists: false } }NoYes
{ phone: { $exists: true, $eq: null } }YesNo
{ phone: { $exists: true } }YesNo

Use Case: Find Incomplete User Profiles

// Find users who haven't provided their age
db.users.find({ age: { $exists: false } })

// Find users missing any of the required profile fields
db.users.find({
  $or: [
    { age:     { $exists: false } },
    { email:   { $exists: false } },
    { phone:   { $exists: false } },
    { address: { $exists: false } }
  ]
})

// Count incomplete profiles
db.users.countDocuments({ phone: { $exists: false } })
03
$type
BSON type matching · Multiple types · Type mismatch fixing
type

$type Syntax

$type matches documents where the specified field is of a particular BSON data type. You can reference types by their string alias or their numeric BSON type number.

// Using string alias (preferred — readable)
db.products.find({ price: { $type: "double" } })

// Using BSON type number (also valid)
db.products.find({ price: { $type: 1 } })   // 1 = double

// Match multiple types — field can be int OR double
db.products.find({ price: { $type: ["int", "double"] } })

// Use "number" alias to match all numeric types at once
db.products.find({ price: { $type: "number" } })

Finding Type Mismatches (Data Quality)

In real-world collections, the same field may have been inserted as different types by different application versions or data pipelines. $type helps you find and fix these inconsistencies.

// Find products where price was accidentally stored as a string
db.products.find({ price: { $type: "string" } })

// Find documents where createdAt is not a Date type
db.orders.find({ createdAt: { $not: { $type: "date" } } })

// Find users where age was stored as string instead of int
db.users.find({ age: { $type: "string" } })

Matching Multiple Types

// Find docs where quantity is either int32 or int64
db.inventory.find({ quantity: { $type: ["int", "long"] } })

// Find docs where score is any numeric type
db.results.find({ score: { $type: "number" } })
// "number" is a special alias matching: double, int, long, decimal

Fix Type Mismatches: Find, ForEach, UpdateOne

// Step 1: Find all products with price stored as string
// Step 2: Parse the string to a number and update each document
db.products.find({ price: { $type: "string" } }).forEach(doc => {
  db.products.updateOne(
    { _id: doc._id },
    { $set: { price: parseFloat(doc.price) } }
  )
})

// Verify fix — should return 0 results now
db.products.countDocuments({ price: { $type: "string" } })
TIP
For large collections, prefer an aggregation pipeline with $merge or a bulkWrite operation over per-document updates in a forEach loop. The forEach approach is fine for small datasets or one-off fixes.
04
$type Alias Reference
All BSON types · String aliases · Type numbers
reference

Every value stored in a MongoDB document has a BSON type. Each type can be referenced in a $type query either by its string alias name or its numeric identifier.

Type NameAlias (string)NumberNotes
Double"double"164-bit IEEE 754 float
String"string"2UTF-8 string
Object"object"3Embedded document
Array"array"4Array of values
Binary Data"binData"5Binary / Buffer
ObjectId"objectId"712-byte unique ID
Boolean"bool"8true / false
Date"date"9UTC datetime (64-bit)
Null"null"10Explicit null value
Regular Expression"regex"11BSON regex pattern
Int32"int"1632-bit integer
Timestamp"timestamp"17Internal BSON timestamp
Int64"long"1864-bit integer
Decimal128"decimal"19High-precision decimal
MinKey"minKey"-1Lower bound sentinel
MaxKey"maxKey"127Upper bound sentinel

Special Alias: "number"

// "number" is a convenience alias matching all numeric BSON types:
// double (1), int (16), long (18), decimal (19)
db.products.find({ price: { $type: "number" } })

// Equivalent explicit multi-type match:
db.products.find({ price: { $type: ["double", "int", "long", "decimal"] } })
NOTE
MongoDB's BSON integers from the shell default to double unless explicitly cast with NumberInt(), NumberLong(), or NumberDecimal(). This means { qty: 5 } stores a double, while { qty: NumberInt(5) } stores an int32.
05
Practical Patterns
Incomplete profiles · Data audits · Schema migration
patterns

Pattern 1: Find Incomplete Profiles (Missing Required Fields)

// Find users missing any required field
db.users.find({
  $or: [
    { firstName: { $exists: false } },
    { lastName:  { $exists: false } },
    { email:     { $exists: false } },
    { dob:       { $exists: false } }
  ]
})

// Count users missing email specifically
db.users.countDocuments({ email: { $exists: false } })

// Tag all incomplete docs (add a flag field)
db.users.updateMany(
  { phone: { $exists: false } },
  { $set: { profileComplete: false } }
)

Pattern 2: Data Quality Audit (Find Wrong Types)

// Find products where price is not a number type
db.products.find({ price: { $not: { $type: "number" } } })

// Full audit — find any field stored as wrong type
db.orders.find({ amount: { $type: "string" } })
db.orders.find({ createdAt: { $not: { $type: "date" } } })
db.orders.find({ userId: { $not: { $type: "objectId" } } })

// Summarise type issues per field using aggregation
db.products.aggregate([
  { $match: { price: { $type: "string" } } },
  { $count: "stringPriceCount" }
])

Pattern 3: Find Documents with an Optional Field Present

// Find users who have provided an optional "bio" field
db.users.find({ bio: { $exists: true } })

// Find users who have a profile picture AND a bio
db.users.find({
  avatar:  { $exists: true },
  bio:     { $exists: true }
})

// Find users with a nested optional field present (dot notation)
db.users.find({ "address.city": { $exists: true } })

Pattern 4: Migration Helper — Find Old Schema Documents

// Old schema stored "name" as a single string field
// New schema splits into "firstName" and "lastName"
// Find all old-schema documents still using the old field
db.users.find({
  name:      { $exists: true },   // has old field
  firstName: { $exists: false }   // missing new field
})

// Migrate old-schema docs to new schema
db.users.find({ name: { $exists: true }, firstName: { $exists: false } })
  .forEach(doc => {
    const parts = doc.name.trim().split(" ")
    db.users.updateOne(
      { _id: doc._id },
      {
        $set: { firstName: parts[0], lastName: parts.slice(1).join(" ") },
        $unset: { name: "" }
      }
    )
  })

// Verify: no old-schema docs remain
db.users.countDocuments({ name: { $exists: true } })
06
Edge Cases
null vs missing · $type on arrays · Dot notation · Type aliases
gotchas

Edge Case 1: { field: null } vs { field: { $exists: false } }

This is the most critical distinction to memorise. The two queries have completely different semantics.

// { field: null } — matches NULL value AND missing field
// { field: { $exists: false } } — matches ONLY missing field

db.users.find({ phone: null })
// Matches: { phone: null }  AND  { }  (no phone field)

db.users.find({ phone: { $exists: false } })
// Matches: ONLY { }  (no phone field)
// Does NOT match: { phone: null }

db.users.find({ phone: { $exists: true, $type: "null" } })
// Matches: ONLY { phone: null }
// Does NOT match missing field

Edge Case 2: $type on an Array Field

$type: "array" checks whether the field value itself is an array — it does NOT check the types of elements inside the array.

// Find documents where tags field IS an array (vs a string)
db.posts.find({ tags: { $type: "array" } })
// Returns: { tags: ["mongodb", "nosql"] } ✓
// Does NOT return: { tags: "mongodb" }       ← string, not array

// To check element types inside an array, use $elemMatch with $type
db.posts.find({ tags: { $elemMatch: { $type: "string" } } })

Edge Case 3: "number" Alias Covers Four Types

// "number" matches: double, int, long, decimal — but NOT string
db.products.find({ price: { $type: "number" } })

// This will NOT match: { price: "29.99" }  ← stored as string
// This WILL match:
//   { price: 29.99 }       ← double
//   { price: NumberInt(30) }      ← int32
//   { price: NumberLong(30) }     ← int64
//   { price: NumberDecimal("29.99") }  ← decimal128

Edge Case 4: $exists with Dot Notation on Nested Fields

// Dot notation works seamlessly with $exists for nested fields
db.users.find({ "address.city": { $exists: true } })
// Matches docs where address.city exists, even if address itself is present

// Check for deeply nested optional fields
db.users.find({ "preferences.notifications.email": { $exists: true } })

// Combine $exists with $type on a nested field
db.orders.find({
  "shipping.estimatedDelivery": {
    $exists: true,
    $type: "date"
  }
})

Edge Case 5: Wrong Alias Throws an Error

ERROR
Using an unrecognised type alias in $type throws a BadValue error. Common mistakes include using "integer" instead of "int", "boolean" instead of "bool", or "objectid" (wrong case) instead of "objectId".
// WRONG aliases — all throw BadValue errors
db.col.find({ field: { $type: "integer" } })    // should be "int"
db.col.find({ field: { $type: "boolean" } })    // should be "bool"
db.col.find({ field: { $type: "objectid" } })   // should be "objectId"

// CORRECT aliases
db.col.find({ field: { $type: "int" } })
db.col.find({ field: { $type: "bool" } })
db.col.find({ field: { $type: "objectId" } })

Edge Case 6: Null is Its Own BSON Type (type 10)

// null is a distinct BSON type — not the same as "missing"
db.users.find({ middleName: { $type: "null" } })
// Matches: { middleName: null }
// Does NOT match: { }  (field absent — type 10 vs no type at all)

// Combine $exists + $type for precise null targeting
db.users.find({ middleName: { $exists: true, $type: "null" } })

Edge Case 7: Using $exists: false in updateMany — Safe Pattern

// Safely add a missing field with a default value to all affected docs
db.users.updateMany(
  { emailVerified: { $exists: false } },   // only docs missing the field
  { $set: { emailVerified: false } }        // add it with a safe default
)

// Verify — should return 0 after migration
db.users.countDocuments({ emailVerified: { $exists: false } })
TIP
The $exists: false + $set pattern is the standard MongoDB approach for backfilling a new required field across an existing collection without touching documents that already have it set.