【MongoDB】查询条件运算符:$expr 和 $regex 详解,以及为什么$where和$expr难以使用索引
一、$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
的灵活性是以牺牲性能为代价的——它们的动态计算逻辑与索引基于“字段原始值有序存储”的设计理念冲突,因此通常无法利用索引。在实际开发中,应优先通过索引友好的查询方式实现业务逻辑,仅在必要时谨慎使用这两个操作符,并做好性能测试。