MongoDB的聚合管道提供了非常丰富的表达式运算符,但当查询逻辑变复杂时,一个表达式中往往需要多次引用同一段子表达式的结果。比如先根据某个字段的值做一次条件判断,再把判断结果用于多个分支计算。如果每次都重复写这段判断逻辑,语句会变得冗长且难以维护。$let表达式正是为解决这类问题而设计的,它允许在表达式内部定义一个或多个变量,把计算结果暂存起来,然后在后续表达式中直接通过变量名访问,相当于给聚合语句加了一层“局部作用域”。

$let的基本语法与执行原理
$let表达式的语法结构由两个核心字段组成:vars和in。vars是一个文档,用于定义变量,每个键是变量名,值是绑定给该变量的表达式;in则是使用这些变量的主表达式,最终返回的结果就是in表达式的计算值。整个结构写成JSON形式如下:
{
$let: {
vars: {
total: { $add: ["$price", "$tax"] },
discount: { $ifNull: ["$coupon", 0] }
},
in: { $subtract: ["$$total", "$$discount"] }
}
}这里有一个非常关键的细节需要注意:在in表达式中引用vars定义的变量时,必须使用双美元符号前缀,也就是$$变量名的写法。单美元符号$price表示引用文档的字段路径,而双美元符号表示引用变量,两者不能混淆。如果变量名与字段名相同,$$前缀就是区分它们的唯一依据。
从执行原理上看,MongoDB在计算$let时会先按vars中定义的顺序逐个求值并缓存结果,然后把这些结果作为变量代入in表达式继续计算。这意味着变量的计算只发生一次,即使in中多处引用同一个变量,也不会触发重复计算,这一点对性能是有实际帮助的。另外,vars中的变量定义支持嵌套,即后定义的变量可以引用先定义的变量,但反过来不行,因为求值是按顺序进行的。
在$project与$addFields中的实战用法
下面通过一个具体的例子来演示$let的实际应用。假设有一个存储商品订单的集合,每条文档包含商品单价、数量、税率和优惠券金额,现在要计算每个订单的实付金额。如果不使用变量,表达式可能需要多次写$multiply之类的运算,而用$let可以拆分成清晰的中间步骤:
db.orders.aggregate([
{
$addFields: {
payableAmount: {
$let: {
vars: {
subtotal: { $multiply: ["$unitPrice", "$quantity"] },
taxAmount: {
$multiply: [
{ $multiply: ["$unitPrice", "$quantity"] },
"$taxRate"
]
}
},
in: {
$subtract: [
{ $add: ["$$subtotal", "$$taxAmount"] },
{ $ifNull: ["$couponAmount", 0] }
]
}
}
}
}
}
])这个例子中,subtotal变量保存了小计金额,taxAmount保存税额,最后在in中统一计算实付金额。相比把所有运算压在一个巨型表达式里,这种写法的可读性明显更好,后续修改计算规则时只需要调整对应的变量定义即可。
需要注意的是,$let输出的结果类型完全由in表达式决定,vars中的变量只在$let内部可见,出了这个范围就无法访问了。如果希望在管道的后续阶段继续使用某个中间结果,应该把$let放在$addFields或$project中,让它成为文档的一个字段,这样才能跨越阶段边界传递数据。另外,变量命名要避免与MongoDB的系统变量冲突,比如ROOT、CURRENT、REMOVE、NOW这些都是保留的$$系统变量,自定义变量如果重名会覆盖系统变量的行为,容易引发难以排查的问题。
与$map、$filter等表达式配合的高级技巧
$let真正强大的地方在于与其他数组类表达式配合使用。在处理数组时,经常需要在遍历过程中依赖某个外部计算结果,这时把公共计算提取到$let变量中,可以让整个表达式结构更清晰。例如,统计订单中每个商品项的折后价格,折扣系数需要根据订单级别预先算好:
db.orders.aggregate([
{
$project: {
itemPrices: {
$let: {
vars: {
rate: {
$cond: {
if: { $gte: ["$orderAmount", 1000] },
then: 0.8,
else: 0.95
}
}
},
in: {
$map: {
input: "$items",
as: "item",
in: {
$multiply: [
"$$item.price",
"$$item.count",
"$$rate"
]
}
}
}
}
}
}
}
])这个查询里,折扣系数rate只计算一次,然后被$map内部的每个元素计算复用。如果不使用$let,要么把$cond判断写在$map内部导致重复计算,要么在管道前面额外加一个$addFields阶段。两种方案都可行,但当表达式只服务于单一计算目标时,$let的封装性更好,把中间变量限制在局部作用域内,不会污染文档结构。
再补充一个容易踩坑的场景:在$let的vars中引用字段时,如果字段不存在,表达式的行为取决于具体的运算符。例如$multiply遇到缺失字段会返回null,而不是报错。因此在定义变量时,养成使用$ifNull做默认值兜底的习惯很有必要。此外,从MongoDB 5.0版本开始,还可以使用$$ROOT等系统变量在$let内部访问完整文档,结合$mergeObjects等运算符,可以灵活地构造动态的输出结构。掌握$let之后,再回头看那些层层嵌套的聚合语句,往往都能通过变量提取的方式重构得简洁许多,这也是编写高质量聚合查询的基本功之一。
MongoDB聚合管道$let表达式变量定义修改时间:2026-09-15 14:23:01