【MongoDB】查询条件运算符:$expr 和 $regex 详解,以及为什么$where和$expr难以使用索引

发布于:2025-08-09 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、$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+ 所有版本支持
典型场景 比较同一文档中的多个字段 搜索、模糊匹配

五、最佳实践建议

  1. 谨慎使用 $expr

    • 对于简单查询,优先使用常规查询操作符
    • 只在需要字段间比较或复杂逻辑时使用 $expr
  2. 优化 $regex 查询

    • 避免前导通配符(如 ^ 开头可以使用索引)
    • 考虑使用文本索引替代复杂正则表达式
    • 对常用模式考虑预计算字段
  3. 测试查询性能

    • 使用 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)

业务注释

  1. $ifNull 确保字段存在,避免空值报错
  2. $multiply 实现金额百分比计算
  3. $hour + $or 组合实现时间段过滤
  4. 最后排序并限制结果数量,便于风险团队优先处理大额交易

二、电商领域:动态定价监控

场景:找出定价低于成本价或异常折扣的商品(当前价<成本价 或 折扣>70%)

db.products.find({
  $expr: {
    $or: [
      // 情况1:当前售价低于成本价
      { $lt: ["$currentPrice", "$costPrice"] },
      // 情况2:折扣力度超过70%(需先计算折扣率)
      {
        $gt: [
          { 
            $divide: [
              { $subtract: ["$originalPrice", "$currentPrice"] },
              "$originalPrice"
            ]
          },
          0.7
        ]
      }
    ]
  },
  status: "active" // 只查询上架商品
})

业务注释

  1. $subtract 计算原价与现价的差额
  2. $divide 计算折扣百分比
  3. 组合使用 $or 覆盖两种异常情况
  4. 额外添加常规查询条件(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
    }
  }
])

业务注释

  1. 使用聚合管道结合 $match$expr 实现复杂过滤
  2. 日期字段比较需转换为毫秒数计算
  3. $project 阶段将毫秒差转换为易读的天数
  4. 多层嵌套的表达式展示 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"] } // 只检查特定部门
})

业务注释

  1. 使用 $let 定义临时变量简化复杂表达式
  2. $switch 实现职级与薪资上限的映射
  3. 多层嵌套的算术运算演示 MongoDB 的表达能力
  4. 最终与常规查询条件组合使用

五、物联网(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天未维护
})

业务注释

  1. 统计学方法应用于设备监测(平均值+3标准差)
  2. $add$multiply 组合计算阈值
  3. 结合 $where 实现更复杂的脚本判断(注意性能影响)
  4. 适合大规模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代码,逐行判断条件是否成立。这种逐文档扫描的方式本质上是“全表扫描”,索引无法直接加速这个过程。
  • 索引失效场景:即使字段pricecost有索引,$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(最终价格),且amountdiscount有单字段索引。

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(全表扫描),即使amountdiscount有索引,也不会被使用。
  • 原因:MongoDB需要计算每个文档的amount - discount结果,再与finalPrice比较,这个过程无法通过索引加速。

三、如何避免性能问题?

  1. 优先使用字段直接查询:能用普通查询条件(如{ amount: { $gt: 1000 } })实现的逻辑,就避免使用$where$expr

  2. 预计算结果字段:若业务频繁需要字段间运算(如finalPrice = amount - discount),可在文档中新增一个finalPrice字段,存储预计算结果,并为该字段创建索引:

    // 新增预计算字段后,用普通查询(可使用索引)
    db.orders.find({ finalPrice: { $gt: 500 } }) // 假设为finalPrice创建了索引
    
  3. 限制$where/$expr的使用场景:仅在其他方法无法实现时使用,且需配合过滤条件缩小范围(如先通过status: "paid"过滤,再用$expr处理):

    // 先通过索引字段过滤,减少$expr处理的文档数量
    db.orders.find({
      status: "paid", // 假设status有索引,先过滤出部分文档
      $expr: { $gt: ["$finalPrice", { $multiply: ["$amount", 0.8] }] }
    })
    

总结

$where$expr的灵活性是以牺牲性能为代价的——它们的动态计算逻辑与索引基于“字段原始值有序存储”的设计理念冲突,因此通常无法利用索引。在实际开发中,应优先通过索引友好的查询方式实现业务逻辑,仅在必要时谨慎使用这两个操作符,并做好性能测试。


网站公告

今日签到

点亮在社区的每一天
去签到