MongoDB的聚合管道功能强大,其中$in聚合表达式专门用于判断一个值是否存在于数组中,返回布尔值true或false。不少刚接触聚合管道的同学容易把它和查询阶段的$in操作符搞混,导致写出的语句直接报错。本文将详细讲解$in在聚合管道中的正确用法,包括基本语法、常见使用场景以及容易踩到的坑,帮助你在实际项目中灵活运用这个表达式。

一、$in聚合表达式的基本语法与用法
$in作为聚合表达式时,语法格式为{ $in: [ <值>, <数组或数组引用> ] },它接收两个参数:第一个参数是要判断的值,第二个参数是目标数组。如果第一个参数存在于第二个数组中,表达式返回true,否则返回false。这个数组可以是字面量数组,也可以是文档中的某个数组字段的引用。
先来看一个最简单的例子,假设有一个用户集合users,其中hobbies是数组字段,我们想在$project阶段判断用户是否喜欢"reading"这个爱好:
db.users.aggregate([
{
$project: {
name: 1,
likesReading: { $in: ["reading", "$hobbies"] }
}
}
])
执行后,likesReading字段的值会是true或false,取决于hobbies数组中是否包含"reading"。这里要注意,第二个参数写的是"$hobbies",这是对文档字段的引用,加上$符号后聚合框架会自动解析为对应字段的值。
还有一个容易忽略的细节:如果引用的字段不是数组而是标量值(比如一个字符串),$in在MongoDB 4.4之前的版本会直接报错,而在较新版本中的行为也需要特别注意。因此使用前最好确保数据结构一致,或者在表达式前面用$isArray做一层判断保护,例如{ $cond: [{ $isArray: "$hobbies" }, { $in: ["reading", "$hobbies"] }, false] },这样即使字段缺失也不会导致整个聚合失败。
二、$in聚合表达式与查询操作符$in的区别
这是初学者最容易混淆的地方。MongoDB中存在两个名字都叫$in的东西,但它们的使用位置完全不同。一个是查询操作符,用在find方法或$match阶段的查询条件中,语法是{ field: { $in: [值1, 值2] } },作用是查询字段值等于数组中任意一个的文档,它不能用于聚合表达式上下文。
另一个就是我们本文讨论的聚合表达式$in,它只能出现在聚合管道的表达式位置,比如$project、$addFields、$group的accumulator之外的表达式位置、$cond的条件里等。两者一旦用错位置,MongoDB会直接抛出类似"The argument to $in must be an array"或者 unrecognized expression 的错误。
用一个对比来加深理解。下面的写法是查询操作符用法,作用是找出status为"active"或"pending"的用户:
// 查询操作符用法:正确
db.users.find({ status: { $in: ["active", "pending"] } })
db.users.aggregate([
{ $match: { status: { $in: ["active", "pending"] } } }
])
而下面这种写法就是错误的,把查询操作符的语法用在了$project的表达式位置:
// 错误示范:表达式位置不能这样写
db.users.aggregate([
{
$project: {
isActive: { status: { $in: ["active"] } } // 会报错
}
}
])
// 正确写法:使用聚合表达式$in
db.users.aggregate([
{
$project: {
isActive: { $in: ["$status", ["active", "pending"]] }
}
}
])
简单记忆的方法:查询操作符$in用于匹配文档、筛选数据,写在查询条件里;聚合表达式$in用于计算布尔结果,写在表达式里。判断当前场景需要的是筛选还是计算,就能快速确定该用哪一个。
三、结合$cond实现条件分支与实际业务场景
单独返回布尔值往往不够,实际业务中更常见的做法是把$in和$cond、$switch配合使用,根据判断结果生成不同的字段值。举个例子,电商系统中订单有多个标签tags,我们想根据订单是否包含"vip"标签来计算不同的折扣:
db.orders.aggregate([
{
$addFields: {
discount: {
$cond: {
if: { $in: ["vip", "$tags"] },
then: 0.8,
else: 0.95
}
}
}
},
{
$project: {
orderId: 1,
tags: 1,
discount: 1,
finalAmount: {
$multiply: ["$amount", "$discount"]
}
}
}
])
这段管道先通过$addFields添加折扣字段,vip用户打八折,普通用户打九五折,再通过$project计算出最终金额。这种写法把判断逻辑完全放在数据库端执行,减少了应用层的数据传输和二次处理,对于大批量数据处理来说性能优势明显。
另一个典型场景是在$match阶段配合$expr使用。普通$match中的查询操作符无法引用其他字段做比较,但借助$expr可以嵌入聚合表达式。比如要找出department字段值存在于allowedDepartments数组字段中的文档:
db.employees.aggregate([
{
$match: {
$expr: { $in: ["$department", "$allowedDepartments"] }
}
}
])
这种字段与字段之间的数组包含判断,在普通查询语法中很难直接表达,$in配合$expr就能轻松搞定。不过需要注意,$expr内部的比较无法有效利用索引,如果集合数据量非常大,建议先用普通查询条件缩小范围,再在后续阶段做$expr判断,避免全集合扫描带来的性能问题。
四、判断对象数组时的注意事项与替代方案
$in判断的是值是否在数组中,对于对象数组,它比较的是整个对象是否完全相等。比如数组[{ name: "a", score: 90 }],用{ $in: [{ name: "a", score: 90 }, "$records"] }可以匹配,但只要对象中任何一个字段不同就不算匹配。如果只想根据对象的某个字段判断,$in就不适用了,此时应该改用$map配合$in,或者使用$anyElementTrue加$map的组合:
db.students.aggregate([
{
$addFields: {
// 判断scores对象数组中是否存在subject为math的元素
hasMath: {
$gt: [
{
$size: {
$filter: {
input: "$scores",
as: "s",
cond: { $eq: ["$$s.subject", "math"] }
}
}
},
0
]
}
}
}
])
这里用$filter筛选出满足条件的元素,再用$size统计数量,大于0说明存在,从而实现了基于对象内部字段的判断。这种写法虽然比$in长一些,但表达力更强,可以支持任意复杂的匹配条件。
此外提醒两点常见的坑:第一,$in对类型是敏感的,数字1和字符串"1"不会匹配,遇到"明明值一样却返回false"的情况,先检查两边的类型是否一致;第二,如果目标数组为null或者字段不存在,表达式会报错而不是返回false,数据质量不稳定的集合建议先做好空值处理。掌握这些细节后,$in就能在你的聚合管道中稳定可靠地发挥作用了。