← back

Aggregation
Expressions

FILE  19_aggregation_expressions
TOPIC  Arithmetic · String · Array · Conditional · Type Conversion · Null Propagation
LEVEL  Foundation
01
What are Expressions
Values, field references, and operator expressions
concept

In the aggregation pipeline, an expression is any value-producing construct used inside a stage. Expressions can be literals, field references, or operator expressions. They are evaluated per-document and can be nested arbitrarily.

Expression Types

// Literal value — just a constant
"hello"
42
true

// Field reference — prefixed with $
"$price"          // value of the price field
"$address.city"   // dot notation for nested fields

// Operator expression — { $operator: argOrArgs }
{ $add: ["$price", 10] }                     // arithmetic
{ $concat: ["$first", " ", "$last"] }        // string
{ $cond: ["$active", "yes", "no"] }          // conditional

// Nested expressions
{ $multiply: [{ $add: ["$price", 5] }, 1.1] }
// Inner expression is evaluated first, result passed to outer

$expr — Bridge to find() Context

$expr allows aggregation expression operators to be used inside a find() query or a $match stage where only query operators are normally allowed.

// In a regular find() — compare two fields in same doc
db.orders.find({ $expr: { $gt: ["$revenue", "$cost"] } })

// In $match — use computed values as filter condition
db.orders.aggregate([
  { $match: { $expr: { $gt: [{ $size: "$items" }, 5] } } }
])
NOTE
Expressions are available in: $project, $addFields/$set, $match (via $expr), $group accumulators, $sort (via $meta), $filter, $map, $reduce, and $cond. Essentially, anywhere you provide a value in the pipeline.
02
Arithmetic Operators
$add · $subtract · $multiply · $divide · $mod · rounding · powers
math

Basic Arithmetic

// $add — sum of values (also adds dates + milliseconds)
{ $add: ["$price", "$tax"] }          // price + tax
{ $add: ["$price", 5, "$fee"] }       // price + 5 + fee (variadic)

// $subtract — exactly two operands
{ $subtract: ["$total", "$discount"] }       // total - discount
{ $subtract: ["$endDate", "$startDate"] }    // date subtraction → milliseconds

// $multiply — variadic
{ $multiply: ["$price", "$qty"] }
{ $multiply: ["$price", 0.9] }        // 10% discount

// $divide — exactly two operands; divide by zero → null (see section 07)
{ $divide: ["$totalRevenue", "$orderCount"] }
{ $divide: ["$bytes", 1048576] }      // bytes to MB

// $mod — remainder; sign follows dividend (not divisor)
{ $mod: ["$count", 2] }               // 0 if even, 1 if odd
{ $mod: [-7, 3] }                     // = -1 (sign follows -7)

Rounding Functions

// $abs — absolute value
{ $abs: -5.3 }    // 5.3

// $ceil — smallest integer >= input
{ $ceil: 4.1 }    // 5
{ $ceil: -4.9 }   // -4  (toward zero, not away)

// $floor — largest integer <= input
{ $floor: 4.9 }   // 4
{ $floor: -4.1 }  // -5  (away from zero)

// $trunc — truncate decimal part (toward zero)
{ $trunc: [4.9,  0] }   // 4   — differs from $floor on negatives
{ $trunc: [-4.9, 0] }   // -4  — $floor would give -5
{ $trunc: [3.14159, 2] } // 3.14 — second arg = decimal places

// $round — round to N decimal places
{ $round: [2.5, 0] }    // 2  — banker's rounding (round half to even)
{ $round: [3.5, 0] }    // 4  — banker's rounding
{ $round: [3.14159, 2] } // 3.14
WARN
$round uses banker's rounding (round half to nearest even), not the common "round half up". $round: [2.5, 0] → 2 (rounds to even), $round: [3.5, 0] → 4. This matters for financial calculations — use NumberDecimal / Decimal128 for monetary values.

Power, Root, Logarithm

// $sqrt — square root; fails on negative (see section 07)
{ $sqrt: 25 }     // 5
{ $sqrt: "$variance" }

// $pow — base to the power of exponent
{ $pow: [2, 10] }              // 1024
{ $pow: ["$radius", 2] }       // radius squared

// $exp — e^x (Euler's number to the power x)
{ $exp: 1 }   // 2.718281828...

// $ln — natural log (base e); fails on 0 or negative
{ $ln: "$value" }

// $log — log base N; $log: [value, base]
{ $log: [1000, 10] }    // 3  (log₁₀ 1000)

// $log10 — shorthand for log base 10
{ $log10: 1000 }        // 3

Arithmetic Operator Quick Reference

OperatorArgsNotes
$addArray (variadic)Also adds ms to dates
$subtract[a, b]Date subtraction → ms
$multiplyArray (variadic)
$divide[dividend, divisor]Divide by zero → null
$mod[dividend, divisor]Sign follows dividend
$absNumber
$ceilNumberToward +∞
$floorNumberToward -∞
$trunc[num, places?]Toward zero; differs from $floor on negatives
$round[num, places?]Banker's rounding
$sqrtNumberErrors on negative
$pow[base, exp]
$log[value, base]Errors on 0 or negative
$log10NumberShorthand log base 10
03
String Operators
$concat · $toUpper · $toLower · $substrCP · $strcasecmp
strings

$concat

// Concatenates strings — variadic array of string expressions
{ $concat: ["$firstName", " ", "$lastName"] }
{ $concat: ["Order-", { $toString: "$orderId" }] }

// NULL POISONING — if ANY argument is null/missing, result is null
{ $concat: ["Hello", null, "World"] }  // → null (not "HelloWorld")
{ $concat: ["Hello", "$missingField"] }  // → null

// Guard with $ifNull to avoid null poisoning
{ $concat: [{ $ifNull: ["$middleName", ""] }, " ", "$lastName"] }
// If middleName is null/missing, uses "" — concat succeeds

$toUpper and $toLower

// Convert case — null/missing input returns "" (empty string, NOT null)
{ $toUpper: "$category" }        // "mongodb" → "MONGODB"
{ $toLower: "$title" }           // "Hello World" → "hello world"
{ $toUpper: null }               // → ""  (not null — a quirk!)
{ $toUpper: "$missingField" }    // → ""  (missing treated as null → "")
NOTE
$toUpper and $toLower return "" (empty string) on null or missing input — unlike most operators which propagate null. This is intentional for display formatting contexts but can mask missing data.

$substrCP — Unicode-Aware Substring

// $substrCP: [string, start, length] — code-point based (Unicode-safe)
{ $substrCP: ["$title", 0, 10] }    // first 10 characters
{ $substrCP: ["Hello World", 6, 5] } // "World" (start:6, length:5)
{ $substrCP: ["Hello", 2, -1] }     // "" — negative length = empty string

// Truncate with ellipsis
{
  $cond: {
    if:   { $gt: [{ $strLenCP: "$description" }, 100] },
    then: { $concat: [{ $substrCP: ["$description", 0, 97] }, "..."] },
    else: "$description"
  }
}

// $substrBytes exists but use $substrCP — handles multi-byte Unicode correctly
// A single emoji/kanji = 1 code point but 2-4 bytes — $substrBytes can split them

$strcasecmp — Case-Insensitive Comparison

// Returns: 0 (equal), 1 (first > second), -1 (first < second)
// NOT a boolean — uses numeric return like C strcmp
{ $strcasecmp: ["$status", "Active"] }   // 0 if equal (case-insensitive)

// Use inside $cond or $match/$expr
{
  $project: {
    isActive: { $eq: [{ $strcasecmp: ["$status", "active"] }, 0] }
  }
}

// Common mistake — treating it as boolean
{ $match: { $expr: { $strcasecmp: ["$type", "premium"] } } }
// WRONG — matches when result is non-zero (truthy), i.e., NOT equal!
// CORRECT:
{ $match: { $expr: { $eq: [{ $strcasecmp: ["$type", "premium"] }, 0] } } }

Other String Operators

OperatorSyntaxReturns
$strLenCP{ $strLenCP: "$field" }Number of Unicode code points
$strLenBytes{ $strLenBytes: "$field" }Byte length (UTF-8)
$split{ $split: ["$str", ","] }Array of substrings
$trim{ $trim: { input: "$str" } }Leading/trailing whitespace removed
$regexFind{ $regexFind: { input: "$str", regex: /pattern/ } }First match object (or null)
$regexFindAll{ $regexFindAll: { input: "$str", regex: /p/ } }Array of all match objects
04
Array Operators
$isArray · $size · $arrayElemAt · $concatArrays · $reverseArray · $filter
arrays

$isArray and $size

// $isArray — safe type guard; returns true/false, never throws
{ $isArray: "$tags" }          // true if tags is an array, false otherwise
{ $isArray: "literal_string" } // false

// $size — returns the number of elements in an array
{ $size: "$tags" }             // e.g., 3
{ $size: null }                // ERROR — $size throws on null/missing

// Safe pattern — guard $size with $cond + $isArray
{
  $cond: {
    if:   { $isArray: "$tags" },
    then: { $size: "$tags" },
    else: 0
  }
}

$arrayElemAt

// $arrayElemAt: [array, index] — zero-based; negative from end
// OUT-OF-RANGE index returns null (not an error)
{ $arrayElemAt: ["$items", 0] }    // first element
{ $arrayElemAt: ["$items", -1] }   // last element
{ $arrayElemAt: ["$items", 999] }  // null (out of range — no error)

// Get top score from a sorted scores array
db.students.aggregate([
  { $sort: { "scores": -1 } },
  { $project: { topScore: { $arrayElemAt: ["$scores", 0] } } }
])

$concatArrays

// Merge two or more arrays into one
{ $concatArrays: ["$arrayA", "$arrayB"] }
{ $concatArrays: ["$tags", ["new-tag"]] }   // append a literal element

// NULL POISONING — any null argument makes the whole result null
{ $concatArrays: [["a", "b"], null, ["c"]] }  // → null

// Guard with $ifNull
{ $concatArrays: [
  { $ifNull: ["$arrayA", []] },
  { $ifNull: ["$arrayB", []] }
] }

$reverseArray and Double-Reverse Pattern

// Reverse an array (null-safe — returns null on null input)
{ $reverseArray: "$items" }

// Get last N elements — double-reverse pattern
// Step 1: reverse  →  Step 2: $slice first N  →  Step 3: reverse back
db.col.aggregate([
  {
    $project: {
      lastThree: {
        $reverseArray: {
          $slice: [{ $reverseArray: "$items" }, 3]
        }
      }
    }
  }
])
// No $slice from-end equivalent — this pattern is the idiomatic solution

$filter — Remove Array Elements

// Filter elements matching a condition — returns new array
{
  $filter: {
    input: "$scores",
    as:    "s",
    cond:  { $gte: ["$$s", 60] }   // $$var references element variable
  }
}
// Returns only scores >= 60; original array unchanged

// Filter array of objects
{
  $filter: {
    input: "$items",
    as:    "item",
    cond:  { $eq: ["$$item.status", "active"] }
  }
}

$map — Transform Array Elements

// Apply an expression to each element — returns transformed array
{
  $map: {
    input: "$prices",
    as:    "p",
    in:    { $multiply: ["$$p", 1.1] }   // add 10% to each price
  }
}

// Extract a field from each embedded document
{
  $map: {
    input: "$orders",
    as:    "o",
    in:    "$$o.total"                    // extract just the total field
  }
}
05
Conditional Operators
$cond · $ifNull · $switch
conditional

$cond — If-Then-Else

// Object form (preferred — more readable)
{
  $cond: {
    if:   condition,
    then: valueIfTrue,
    else: valueIfFalse
  }
}

// Array form — [ condition, then, else ]
{ $cond: [ { $gte: ["$score", 90] }, "A", "B" ] }

// Classify score into grade
{
  $project: {
    grade: {
      $cond: {
        if:   { $gte: ["$score", 90] },
        then: "A",
        else: {
          $cond: {
            if:   { $gte: ["$score", 75] },
            then: "B",
            else: "C"
          }
        }
      }
    }
  }
}
// Nested $cond works; consider $switch for 3+ branches

$switch — Multi-Branch Conditional

// Cleaner than deeply nested $cond for multiple branches
{
  $switch: {
    branches: [
      { case: { $gte: ["$score", 90] }, then: "A" },
      { case: { $gte: ["$score", 75] }, then: "B" },
      { case: { $gte: ["$score", 60] }, then: "C" }
    ],
    default: "F"   // optional — throws error if omitted and no branch matches
  }
}
WARN
If $switch has no default and no branch matches, it throws an error. Always include a default unless you are certain every document will match a branch.

$ifNull — Null Coalescing

// Return first non-null value from the list
{ $ifNull: ["$optionalField", "default-value"] }
{ $ifNull: ["$field", 0] }           // treat missing number as 0
{ $ifNull: ["$name", "Anonymous"] }

// Chain multiple fallbacks
{ $ifNull: ["$preferredName", "$firstName", "Unknown"] }
// Returns preferredName if not null, else firstName, else "Unknown"

// Common use: safe defaults before $concat to avoid null poisoning
{ $concat: [
  { $ifNull: ["$title", ""] }, " — ",
  { $ifNull: ["$author", "Unknown Author"] }
] }
06
Type Conversion & Date
$toString · $toInt · $toDouble · $toDate · date extraction
types

Type Conversion Operators

// Convert number to string for concatenation
{ $toString: "$orderId" }       // 42 → "42"
{ $toString: "$price" }         // 9.99 → "9.99"

// Convert string to number
{ $toInt:    "$qtyString" }     // "10" → 10 (integer)
{ $toDouble: "$priceString" }   // "9.99" → 9.99
{ $toLong:   "$id" }            // → Int64
{ $toDecimal: "$amount" }       // → Decimal128 (high precision)

// $convert — explicit with onError and onNull handling
{
  $convert: {
    input:   "$userAge",
    to:      "int",
    onError: -1,     // return -1 if conversion fails
    onNull:  0       // return 0 if null/missing
  }
}
NOTE
The specific conversion operators ($toInt, $toString, etc.) error if the conversion fails. Use $convert with onError/onNull for safer handling of potentially invalid data.

Date Extraction Operators

// Extract parts from a Date field
{ $year:        "$createdAt" }    // 2024
{ $month:       "$createdAt" }    // 1-12
{ $dayOfMonth:  "$createdAt" }    // 1-31
{ $dayOfWeek:   "$createdAt" }    // 1=Sunday, 7=Saturday
{ $hour:        "$createdAt" }    // 0-23
{ $minute:      "$createdAt" }    // 0-59
{ $second:      "$createdAt" }    // 0-59 (60 for leap seconds)

// $dateToString — format a date as a string
{ $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }
// 2024-03-15

// Available format specifiers: %Y (year), %m (month), %d (day),
// %H (hour), %M (minute), %S (second), %L (ms), %j (day of year)

// Group by year-month — common reporting pattern
db.orders.aggregate([
  {
    $group: {
      _id: {
        year:  { $year:  "$createdAt" },
        month: { $month: "$createdAt" }
      },
      total: { $sum: "$amount" }
    }
  },
  { $sort: { "_id.year": 1, "_id.month": 1 } }
])
07
Null Propagation & Fail-Fast
How expressions handle null, missing, and invalid inputs
gotchas

Understanding how expressions treat null and missing fields prevents silent incorrect results and unexpected pipeline errors.

The Null Propagation Rule

Most operators follow the null propagation rule: if any input is null or refers to a missing field, the output is null (not an error). The exception is operators with fail-fast behavior.

// Null propagation examples
{ $add:      [5, null] }             // → null
{ $multiply: [10, "$missingField"] } // → null (missing = null)
{ $subtract: [null, 3] }            // → null
{ $toUpper:  null }                  // → "" (exception! see section 03)
{ $concat:   ["a", null, "b"] }      // → null (null poisoning)

Fail-Fast Operators (Throw Errors)

// These throw an error — do NOT silently return null
{ $divide: [10, 0] }         // Error: division by zero
{ $sqrt:   [-4] }            // Error: sqrt of negative number
{ $log:    [0] }             // Error: log of zero
{ $log:    [-5] }            // Error: log of negative number
{ $size:   null }            // Error: $size requires array, not null
{ $arrayElemAt: [null, 0] }  // Error: requires array, not null
DANGER
A single document with a zero divisor field or a null where $size expects an array will cause the entire pipeline to fail with an error. Always guard with $cond or $ifNull when operating on fields that might be zero, null, or missing.

Guard Patterns

// Safe division — avoid divide-by-zero
{
  $cond: {
    if:   { $ne: ["$count", 0] },
    then: { $divide: ["$total", "$count"] },
    else: null
  }
}

// Safe $size — avoid error on non-array
{
  $cond: {
    if:   { $isArray: "$tags" },
    then: { $size: "$tags" },
    else: 0
  }
}

// Safe $sqrt — avoid error on negative
{
  $cond: {
    if:   { $gte: ["$variance", 0] },
    then: { $sqrt: "$variance" },
    else: null
  }
}

Null vs Missing Field

ScenarioExpression seesTypical result
Field exists, value = nullnullnull (propagated)
Field missing from documentnullnull (treated as null)
Field = 0 (number zero)0Valid — but 0 as divisor errors
Field = "" (empty string)""Valid string
Field = [] (empty array)[]$size → 0; $arrayElemAt → null