第五章:查询操作进阶

最后更新: 2024-01-01 作者: MongoDB Team
页面目录

第五章:查询操作进阶

深入理解 MongoDB 的高级查询技巧和最佳实践

5.1 投影操作符

包含和排除字段

// 只返回特定字段
db.users.find(
  { status: "active" },
  { username: 1, email: 1 }
)

// 排除 _id
db.users.find(
  { status: "active" },
  { _id: 0, username: 1, email: 1 }
)

// 排除多个字段
db.articles.find(
  {},
  { content: 0, raw_content: 0, _id: 0 }
)

数组元素投影

// 返回数组的前 N 个元素
db.articles.find(
  {},
  { title: 1, tags: { $slice: 3 } }  // 只返回前 3 个标签
)

// 返回数组的最后 N 个元素
db.articles.find(
  {},
  { comments: { $slice: -5 } }  // 返回最后 5 条评论
)

// $elemMatch - 返回匹配的第一个数组元素
db.scores.find(
  { scores: { $gte: 90 } },
  { username: 1, scores: { $elemMatch: { $gte: 90 } } }
)

投影修饰符

// 添加计算字段(需要聚合管道)
// 使用 $expr 在某些场景
db.users.find({
  $expr: {
    $gt: [{ $strLenCP: "$username" }, 10]
  }
})

// 投影中使用条件
db.products.find(
  { category: "Electronics" },
  {
    name: 1,
    price: 1,
    discounted: {
      $cond: {
        if: { $gt: ["$price", 1000] },
        then: true,
        else: false
      }
    }
  }
)

5.2 正则表达式查询

基本正则查询

// 匹配用户名以 "admin" 开头
db.users.find({ username: /^admin/ })

// 匹配邮箱以 .com 结尾
db.users.find({ email: /\.com$/i })  // i 表示不区分大小写

// 包含特定字符串
db.products.find({ name: /手机/ })

// 精确匹配(整个字段)
db.users.find({ username: /^alice$/i })

正则选项

选项 说明
i 不区分大小写
m 多行模式
x 扩展模式(忽略空白和 # 注释)
s 点号匹配换行符
// 多行匹配
db.logs.find({ message: /^Error/m })

// 组合使用
db.users.find({ username: /^(admin|root)/im })

使用 $regex 操作符

// $regex - 正则表达式
db.users.find({ username: { $regex: "^admin", $options: "i" } })

// $not - 反向匹配
db.users.find({ username: { $not: /^admin/i } })

// 正则 + $in
db.users.find({
  email: { $regex: /\.(com|org|net)$/, $options: "i" }
})

5.3 Null 和缺失字段处理

查询 null 值

// 查询字段值为 null 的文档
db.users.find({ phone: null })

// 查询字段不存在的文档
db.users.find({ phone: { $exists: false } })

// 查询字段值为 null 或不存在的文档
db.users.find({
  $or: [
    { phone: null },
    { phone: { $exists: false } }
  ]
})

使用 $type 判断

// 查询字段类型为 null 的文档
db.users.find({ phone: { $type: "null" } })

// 查询字段类型不是 null 的文档
db.users.find({ phone: { $not: { $type: "null" } } })

// $type 支持的类型
// "double", "string", "object", "array", "binData", "objectId", "bool", "date", "null"

5.4 文档关系处理

子查询和聚合

// 查询用户的最新订单
db.orders.aggregate([
  { $sort: { created_at: -1 } },
  { $group: {
      _id: "$user_id",
      latest_order: { $first: "$$ROOT" }
    }
  }
])

手动引用(Denormalization)

// 用户集合
db.users.findOne({ _id: ObjectId("...") })

// 查询用户的所有订单
db.orders.find({ user_id: ObjectId("...") })

// 获取订单关联的用户信息
const order = db.orders.findOne({ _id: ObjectId("...") })
const user = db.users.findOne({ _id: order.user_id })

DBRef

// 使用 DBRef 存储引用
{
  _id: ObjectId("..."),
  type: "order",
  user: {
    $ref: "users",
    $id: ObjectId("..."),
    $db: "ecommerce"
  }
}

// 解析 DBRef
const order = db.orders.findOne({ _id: ObjectId("...") })
const user = order.user // 返回关联的文档

5.5 分页查询

基于游标的分页

// 第一页
const page1 = db.articles.find({ status: "published" })
  .sort({ published_at: -1 })
  .limit(10)
  .toArray()

// 第二页
const page2 = db.articles.find({ status: "published" })
  .sort({ published_at: -1 })
  .skip(10)
  .limit(10)
  .toArray()

// 第三页
const page3 = db.articles.find({ status: "published" })
  .sort({ published_at: -1 })
  .skip(20)
  .limit(10)
  .toArray()

基于键的分页(更高效)

// 上一页最后一条记录的字段值
const lastItem = {
  published_at: ISODate("2024-01-15"),
  _id: ObjectId("...")
}

// 下一页
const nextPage = db.articles.find({
  $or: [
    { published_at: { $lt: lastItem.published_at } },
    {
      published_at: lastItem.published_at,
      _id: { $lt: lastItem._id }
    }
  ]
}).sort({ published_at: -1, _id: -1 }).limit(10)

获取总页数

// 获取总记录数
const totalCount = db.articles.countDocuments({ status: "published" })
const pageSize = 10
const totalPages = Math.ceil(totalCount / pageSize)

// 返回分页元数据
{
  page: 1,
  pageSize: 10,
  totalCount: totalCount,
  totalPages: totalPages,
  hasNextPage: page < totalPages,
  hasPrevPage: page > 1
}

5.6 文本搜索

创建文本索引

// 创建文本索引
db.articles.createIndex({ title: "text", content: "text", tags: "text" })

// 创建带权重的文本索引
db.articles.createIndex(
  {
    title: "text",
    content: "text",
    tags: "text",
    description: "text"
  },
  {
    weights: {
      title: 10,
      tags: 5,
      description: 3,
      content: 1
    },
    default_language: "chinese",
    name: "article_text_index"
  }
)

文本搜索查询

// 基本文本搜索
db.articles.find({ $text: { $search: "MongoDB 教程" } })

// 文本搜索带相关性评分
db.articles.find(
  { $text: { $search: "database nosql" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })

// 获取相关性分数
db.articles.find(
  { $text: { $search: "MongoDB 安装" } },
  {
    title: 1,
    score: { $meta: "textScore" }
  }
).sort({ score: { $meta: "textScore" } })

// 短语搜索(精确匹配)
db.articles.find({ $text: { $search: "\"MongoDB 教程\"" } })

// 排除词语
db.articles.find({ $text: { $search: "数据库 -Redis" } })

文本搜索选项

// 设置文本搜索语言
db.articles.find(
  { $text: { $search: "tutorial" } },
  { language: "english" }
)

// 设置文本搜索权重
db.runCommand({
  text: "articles",
  search: "MongoDB",
  projection: { title: 1 },
  limit: 5
})

5.7 地理空间查询

创建地理空间索引

// 2dsphere 索引(球面几何)
db.locations.createIndex({ location: "2dsphere" })

// 2d 索引(平面几何)
db.locations.createIndex({ coordinates: "2d" })

地理位置数据类型

// GeoJSON Point
{
  location: {
    type: "Point",
    coordinates: [longitude, latitude]
  }
}

// GeoJSON LineString
{
  route: {
    type: "LineString",
    coordinates: [
      [longitude1, latitude1],
      [longitude2, latitude2]
    ]
  }
}

// GeoJSON Polygon
{
  area: {
    type: "Polygon",
    coordinates: [[
      [longitude1, latitude1],
      [longitude2, latitude2],
      [longitude3, latitude3],
      [longitude1, latitude1]
    ]]
  }
}

地理空间查询

// 查询点附近的文档(单位:米)
db.locations.find({
  location: {
    $nearSphere: {
      $geometry: {
        type: "Point",
        coordinates: [116.4074, 39.9042]
      },
      $maxDistance: 5000  // 5 公里
    }
  }
})

// 查询包含某点的多边形
db.areas.find({
  area: {
    $geoIntersects: {
      $geometry: {
        type: "Point",
        coordinates: [116.4074, 39.9042]
      }
    }
  }
})

// 查询在某区域内的文档
db.locations.find({
  location: {
    $geoWithin: {
      $geometry: {
        type: "Polygon",
        coordinates: [[...]]
      }
    }
  }
})

5.8 Bitwise 查询

// 查询字段值在某些位为 1 或 0
db.flags.find({ permissions: { $bitsAllSet: 0x0101 } })

// 查询字段值在任何指定位为 1
db.flags.find({ permissions: { $bitsAnySet: 0x0101 } })

// 查询字段值在所有指定位为 0
db.flags.find({ permissions: { $bitsAllClear: 0x0101 } })

// 查询字段值在任何指定位为 0
db.flags.find({ permissions: { $bitsAnyClear: 0x0101 } })

5.9 表达式查询

$expr(允许在查询中使用聚合表达式)

// 比较同一文档中的两个字段
db.orders.find({
  $expr: { $gt: ["$total_amount", "$discount_amount"] }
})

// 使用聚合操作符
db.sales.find({
  $expr: {
    $gt: [
      { $divide: ["$revenue", "$cost"] },
      1.5
    ]
  }
})

// 自定义表达式
db.employees.find({
  $expr: {
    $and: [
      { $eq: [{ $mod: ["$salary", 1000] }, 0] },
      { $gte: ["$salary", 5000] }
    ]
  }
})

💡 实践提示

  1. 避免正则前缀通配:正则表达式开头使用 ^ 可以利用索引
  2. 优先使用文本索引:对于全文搜索,创建文本索引比正则更高效
  3. 地理空间查询需要索引:必须先创建 2dsphere 或 2d 索引
  4. 分页优化:使用基于键的分页替代 skip,避免大量 skip 导致的性能问题
  5. 投影优化:只查询需要的字段,减少网络传输和内存占用

📚 继续学习