一、$expr 运算符
$expr
允许你在查询语句中使用聚合表达式,实现字段间的比较和复杂条件查询。
注解:
$expr
主要用于比较同一文档中不同字段的值,或者使用聚合表达式进行条件判断。
基本语法
db.collection.find({
$expr: {
<聚合表达式>
}
})
常见使用场景
1. 比较同一文档中的两个字段
// 查找库存量小于销售量的产品
db.products.find({
$expr: { $lt: ["$stock", "$sales"] }
})
2. 使用条件逻辑
// 查找折扣价大于原价50%的产品
db.products.find({
$expr: { $gt: ["$discountPrice", { $multiply: ["$price", 0.5] }] }
})
$expr 常用操作符
操作符 | 描述 | 示例 |
---|---|---|
$eq |
等于 | $eq: ["$field1", "$field2"] |
$ne |
不等于 | $ne: ["$field1", "value"] |
$gt |
大于 | $gt: ["$field1", 100] |
$lt |
小于 | $lt: ["$field1", "$field2"] |
$and |
逻辑与 | $and: [expr1, expr2] |
$or |
逻辑或 | $or: [expr1, expr2] |
$concat |
字符串连接 | $concat: ["$fname", " ", "$lname"] |
$substr |
字符串截取 | $substr: ["$name", 0, 3] |
注意:使用
$expr
时,字段名前需要加$
符号,表示引用字段值而非字面量。
二、$regex 运算符
$regex
提供正则表达式功能,用于模式匹配字符串查询。
注解:正则表达式是强大的文本匹配工具,
$regex
让你能在 MongoDB 中利用这一功能。
基本语法
db.collection.find({
field: { $regex: /pattern/, $options: 'options' }
})
// 或者
db.collection.find({
field: { $regex: 'pattern', $options: 'options' }
})
常用选项($options)
选项 | 描述 |
---|---|
i |
不区分大小写 |
m |
多行匹配 |
x |
忽略空白字符 |
s |
允许点字符(.)匹配所有字符 |
使用示例
1. 简单匹配
// 查找名字以"Jo"开头的用户(不区分大小写)
db.users.find({
name: { $regex: /^Jo/i }
})
2. 包含特定模式
// 查找邮箱包含"example.com"或"sample.com"的用户
db.users.find({
email: { $regex: /(example|sample)\.com/ }
})
3. 使用字符串而非正则字面量
// 查找描述中包含"mongodb"的产品(不区分大小写)
db.products.find({
description: {
$regex: "mongodb",
$options: 'i'
}
})
性能考虑
重要提示:正则表达式查询通常无法使用索引,尤其是:
- 当使用通配符开头(如
/^abc/
可以使用索引,但/abc$/
或/.*abc/
则不行)- 使用复杂正则表达式时
三、$expr 和 $regex 结合使用
你可以组合这两个运算符实现更复杂的查询:
// 查找全名(firstname + lastname)包含"Smith"且邮箱与用户名相同的用户
db.users.find({
$expr: {
$and: [
{ $regexMatch: { input: { $concat: ["$firstname", " ", "$lastname"] }, regex: "Smith" } },
{ $eq: ["$email", "$username"] }
]
}
})
注意:MongoDB 4.2+ 引入了
$regexMatch
等聚合运算符,使正则表达式在$expr
中使用更方便。
四、总结对比
特性 | $expr |
$regex |
---|---|---|
主要用途 | 字段间比较和复杂逻辑 | 文本模式匹配 |
语法 | 使用聚合表达式 | 使用正则表达式 |
性能 | 可以使用索引(取决于表达式) | 有限索引支持 |
版本 | 需要 MongoDB 3.6+ | 所有版本支持 |
典型场景 | 比较同一文档中的多个字段 | 搜索、模糊匹配 |
五、最佳实践建议
谨慎使用
$expr
:- 对于简单查询,优先使用常规查询操作符
- 只在需要字段间比较或复杂逻辑时使用
$expr
优化
$regex
查询:- 避免前导通配符(如
^
开头可以使用索引) - 考虑使用文本索引替代复杂正则表达式
- 对常用模式考虑预计算字段
- 避免前导通配符(如
测试查询性能:
- 使用
explain()
方法分析查询执行计划 - 对大集合进行性能测试
- 使用
通过案例学习$expr用法
一、金融领域:风险交易检测
场景:检测异常大额交易(单笔超过账户日均余额30%的交易)
db.transactions.find({
$expr: {
$and: [
// 确保amount和dailyBalance字段存在
{ $ifNull: ["$amount", false] },
{ $ifNull: ["$dailyBalance", false] },
// 主条件:交易金额 > 日均余额的30%
{
$gt: [
"$amount",
{ $multiply: ["$dailyBalance", 0.3] } // 计算日均余额的30%
]
},
// 附加条件:交易时间在非工作时间(晚上8点到早上6点)
{
$or: [
{ $lt: [{ $hour: "$transactionTime" }, 6] },
{ $gt: [{ $hour: "$transactionTime" }, 20] }
]
}
]
}
}).sort({ amount: -1 }).limit(100)
业务注释:
$ifNull
确保字段存在,避免空值报错$multiply
实现金额百分比计算$hour
+$or
组合实现时间段过滤- 最后排序并限制结果数量,便于风险团队优先处理大额交易
二、电商领域:动态定价监控
场景:找出定价低于成本价或异常折扣的商品(当前价<成本价 或 折扣>70%)
db.products.find({
$expr: {
$or: [
// 情况1:当前售价低于成本价
{ $lt: ["$currentPrice", "$costPrice"] },
// 情况2:折扣力度超过70%(需先计算折扣率)
{
$gt: [
{
$divide: [
{ $subtract: ["$originalPrice", "$currentPrice"] },
"$originalPrice"
]
},
0.7
]
}
]
},
status: "active" // 只查询上架商品
})
业务注释:
$subtract
计算原价与现价的差额$divide
计算折扣百分比- 组合使用
$or
覆盖两种异常情况- 额外添加常规查询条件(status)与
$expr
配合使用
三、物流领域:时效性分析
场景:查找实际配送时间超过承诺时间2倍以上的订单
db.orders.aggregate([
{
$match: {
$expr: {
$and: [
// 确保必要字段存在且已完成配送
{ $gt: ["$actualDeliveryDate", null] },
{ $gt: ["$promisedDeliveryDate", null] },
{ $eq: ["$status", "delivered"] },
// 计算时间差(毫秒)
{
$gt: [
{ $subtract: ["$actualDeliveryDate", "$orderDate"] },
{
$multiply: [
{ $subtract: ["$promisedDeliveryDate", "$orderDate"] },
2
]
}
]
}
]
}
}
},
{
$project: {
// 计算超时天数(展示用)
delayDays: {
$divide: [
{
$subtract: [
{ $subtract: ["$actualDeliveryDate", "$orderDate"] },
{ $subtract: ["$promisedDeliveryDate", "$orderDate"] }
]
},
1000 * 60 * 60 * 24 // 毫秒转天数
]
},
orderId: 1,
customerId: 1
}
}
])
业务注释:
- 使用聚合管道结合
$match
和$expr
实现复杂过滤- 日期字段比较需转换为毫秒数计算
$project
阶段将毫秒差转换为易读的天数- 多层嵌套的表达式展示 MongoDB 强大的计算能力
四、人力资源:薪资合规审计
场景:检测薪资异常(当前薪资<入职薪资 或 涨幅超过职级上限)
db.employees.find({
$expr: {
$or: [
// 异常情况1:当前薪资低于入职薪资
{ $lt: ["$currentSalary", "$startingSalary"] },
// 异常情况2:薪资涨幅超过职级允许上限
{
$let: {
vars: {
// 计算实际涨幅百分比
actualIncrease: {
$divide: [
{ $subtract: ["$currentSalary", "$startingSalary"] },
"$startingSalary"
]
},
// 获取该职级的允许最大涨幅
maxAllowed: {
$switch: {
branches: [
{ case: { $eq: ["$grade", "P1"] }, then: 0.3 },
{ case: { $eq: ["$grade", "P2"] }, then: 0.4 },
{ case: { $eq: ["$grade", "P3"] }, then: 0.5 }
],
default: 0.2
}
}
},
in: {
$gt: ["$$actualIncrease", "$$maxAllowed"]
}
}
}
]
},
department: { $in: ["Engineering", "Product"] } // 只检查特定部门
})
业务注释:
- 使用
$let
定义临时变量简化复杂表达式$switch
实现职级与薪资上限的映射- 多层嵌套的算术运算演示 MongoDB 的表达能力
- 最终与常规查询条件组合使用
五、物联网(IoT):设备异常监测
场景:找出传感器读数异常的设备(当前值超过3个标准差)
db.deviceReadings.find({
$expr: {
$gt: [
"$currentValue",
{
$add: [
"$avgValue",
{ $multiply: ["$stdDev", 3] } // 计算3个标准差范围
]
}
]
},
$where: "new Date() - this.lastMaintenanceDate > 1000*60*60*24*30" // 超过30天未维护
})
业务注释:
- 统计学方法应用于设备监测(平均值+3标准差)
$add
和$multiply
组合计算阈值- 结合
$where
实现更复杂的脚本判断(注意性能影响)- 适合大规模IoT设备的异常检测场景
六、进阶技巧:性能优化方案
优化建议1:为 $expr
常用字段组合创建复合索引
// 为物流案例创建优化索引
db.orders.createIndex({
status: 1,
actualDeliveryDate: 1,
promisedDeliveryDate: 1
})
// 为金融交易案例创建优化索引
db.transactions.createIndex({
transactionTime: 1,
amount: -1
})
优化建议2:使用 $redact
替代复杂 $expr
// 人力资源案例的替代方案
db.employees.aggregate([
{
$redact: {
$cond: {
if: {
$or: [
{ $lt: ["$currentSalary", "$startingSalary"] },
{ $gt: ["$salaryIncreaseRatio", "$grade.maxAllowedRatio"] }
]
},
then: "$$KEEP",
else: "$$PRUNE"
}
}
}
])
一、为什么$where
和$expr
难以使用索引?
1. $where
:使用JavaScript表达式查询
$where
允许通过JavaScript代码定义查询条件(例如判断字段间的关系),示例:
// 查询"价格高于成本2倍"的商品
db.products.find({ $where: "this.price > this.cost * 2" })
无法有效使用索引的原因:
- 执行机制:
$where
会对集合中的每个文档执行JavaScript代码,逐行判断条件是否成立。这种逐文档扫描的方式本质上是“全表扫描”,索引无法直接加速这个过程。 - 索引失效场景:即使字段
price
和cost
有索引,$where
也无法利用它们,因为索引是基于字段值的有序结构,而$where
的逻辑是动态计算的(依赖两个字段的运算结果)。
2. $expr
:使用聚合表达式查询
$expr
允许在查询中使用聚合管道的表达式(如$gt
、$add
等),支持字段间的比较,示例:
// 与上面$where等效的$expr查询
db.products.find({ $expr: { $gt: ["$price", { $multiply: ["$cost", 2] }] } })
通常无法使用索引的原因:
- 表达式的动态性:
$expr
支持复杂运算(如字段间的加减乘除、逻辑组合),这些运算结果是动态生成的,而索引是基于字段原始值的有序结构,无法直接匹配运算后的结果。 - 例外情况:若
$expr
中仅使用单个字段的简单判断(如$expr: { $eq: ["$status", "active"] }
),MongoDB可能会尝试使用该字段的索引。但只要涉及多个字段的运算或函数处理(如$substr
、$year
),索引就会失效。
二、实例对比:索引生效 vs 失效
假设有orders
集合,包含字段amount
(金额)、discount
(折扣)、finalPrice
(最终价格),且amount
和discount
有单字段索引。
1. 索引生效的查询(无$where
/$expr
)
// 查询金额大于1000的订单(使用amount索引)
db.orders.find({ amount: { $gt: 1000 } })
- 执行计划显示
IXSCAN
(索引扫描),直接利用amount
索引定位符合条件的文档。
2. 索引失效的查询(使用$where
/$expr
)
// 用$where查询"最终价格 = 金额 - 折扣"的订单
db.orders.find({ $where: "this.finalPrice === this.amount - this.discount" })
// 用$expr查询同样逻辑
db.orders.find({ $expr: { $eq: ["$finalPrice", { $subtract: ["$amount", "$discount"] }] } })
- 执行计划显示
COLLSCAN
(全表扫描),即使amount
和discount
有索引,也不会被使用。 - 原因:MongoDB需要计算每个文档的
amount - discount
结果,再与finalPrice
比较,这个过程无法通过索引加速。
三、如何避免性能问题?
优先使用字段直接查询:能用普通查询条件(如
{ amount: { $gt: 1000 } }
)实现的逻辑,就避免使用$where
或$expr
。预计算结果字段:若业务频繁需要字段间运算(如
finalPrice = amount - discount
),可在文档中新增一个finalPrice
字段,存储预计算结果,并为该字段创建索引:// 新增预计算字段后,用普通查询(可使用索引) db.orders.find({ finalPrice: { $gt: 500 } }) // 假设为finalPrice创建了索引
限制
$where
/$expr
的使用场景:仅在其他方法无法实现时使用,且需配合过滤条件缩小范围(如先通过status: "paid"
过滤,再用$expr
处理):// 先通过索引字段过滤,减少$expr处理的文档数量 db.orders.find({ status: "paid", // 假设status有索引,先过滤出部分文档 $expr: { $gt: ["$finalPrice", { $multiply: ["$amount", 0.8] }] } })
总结
$where
和$expr
的灵活性是以牺牲性能为代价的——它们的动态计算逻辑与索引基于“字段原始值有序存储”的设计理念冲突,因此通常无法利用索引。在实际开发中,应优先通过索引友好的查询方式实现业务逻辑,仅在必要时谨慎使用这两个操作符,并做好性能测试。