MongoDB05 - MongoDB 查询进阶

发布于:2025-07-02 ⋅ 阅读:(18) ⋅ 点赞:(0)

MongoDB 查询进阶

文章目录

一:查询运算符

1:比较运算符

  • $eq: 等于
  • $ne: 不等于
  • $gt: 大于
  • $gte: 大于等于
  • $lt: 小于
  • $lte: 小于等于
  • $in: 匹配数组中任意值
  • $nin: 不匹配数组中任意值
db.products.find(
    { 
        price: { $gt: 100, $lte: 500 }  // 100 < price <= 500
    }
)

db.users.find(
    { 
        age: { $in: [18, 20, 22] } // age是18或者20或者22
    }
)

2:逻辑运算符

  • $and: 逻辑与
  • $or: 逻辑或
  • $not: 逻辑非
  • $nor: 逻辑或非
db.users.find({
  // 年龄 < 18或者是学生
  $or: [
    { age: { $lt: 18 }},
    { isStudent: true }
  ]
})

3:元素运算符

  • $exists: 字段是否存在
  • $type: 字段类型检查
// 当前用户存在phone字段
db.users.find({ phone: { $exists: true } })
// 找出value是double类型的文档
db.data.find({ value: { $type: "double" } })

4:数组运算符

  • $all: 匹配包含所有指定元素的数组
  • $elemMatch: 匹配数组中满足所有条件的元素
  • $size: 匹配指定大小的数组
// 找到tags字段中既包含electronics,又包含sale的
db.products.find({ tags: { $all: ["electronics", "sale"] } })
// 找到成绩大于90并且attendance=1的所有的学生
db.classes.find({ students: { $elemMatch: { score: { $gt: 90 }, attendance: 1 } } })

5:评估运算符

  • $mod: 取模运算
  • $regex: 正则表达式匹配
  • $text: 文本搜索
  • $where: JavaScript表达式
// 找到name中符合指定正则的文档
db.products.find({ name: { $regex: /^smart/i } })

// 找到总数 > paid * 2的
db.orders.find({ $where: "this.total > this.paid * 2" })

二:聚合框架

MongoDB 的聚合框架(Aggregation Framework)是一个强大的数据处理工具,它允许你对集合中的文档进行复杂的转换和分析。

聚合框架基于管道(pipeline)概念,数据通过一系列阶段(stage)进行处理,每个阶段对数据进行特定的操作。

  1. 数据流经多个阶段组成的管道
  2. 每个阶段处理输入文档并输出结果文档
  3. 一个阶段的输出作为下一个阶段的输入
  4. 管道可以包含一个或多个阶段
db.collection.aggregate([
  { $stage1: { ... } },
  { $stage2: { ... } },
  ...
])

在这里插入图片描述

类似的概念,在ElasticSearch中也存在

在这里插入图片描述

1:核心聚合阶段

在这里插入图片描述

1.1:数据初筛阶段

$match:过滤文档,类似于find()

{ $match: { status: "A", qty: { $gt: 10 } } }

$limit:限制文档数量

{ $limit: 5 }

$skip:跳过指定数量的文档

{ $skip: 10 }
1.2:数据变形阶段

$project:选择、重命名或计算字段

// 选取name字段,计算total字段就是price和fee之和
{ $project: { name: 1, total: { $add: ["$price", "$fee"] } } }

$unwind:展开数组字段

// 展开tags数组字段
{ $unwind: "$tags" }

$addFields/$set:添加新字段(不改变现有字段)

{ $addFields: 
 	// 添加一个新的字段叫做total, 计算方式是price字段和tax字段之和
 	{ total: { $add: ["$price", "$tax"] } } 
}

$replaceRoot:替换文档根

// 文档根变成details字段
{ $replaceRoot: { newRoot: "$details" } }
1.3:分组计算阶段

$group:按指定表达式分组文档

{
    $group: {
        _id: "$category", // 依据什么分组?
        total: { $sum: "$amount" }, // 计算每一个分组的amount总和,记为total字段
        avg: { $avg: "$price" }, // 计算每一个分组的price的平均值,记为avg字段
        count: { $sum: 1 } // 获取每一个分组的文档个数,记为count字段
    }
}

$bucket:将文档分组到指定范围的桶中

{
    $bucket: {
        groupBy: "$price", // 分桶的依据是根据price字段
        boundaries: [0, 100, 200, 300], // 分桶边界,0~100一个桶,100~200一个桶,200~300一个桶,300以上一个桶
        default: "Other", // 其他的为Other
        output: { count: { $sum: 1 } } // 输出每一个桶的文档的个数,记为count
    }
}

$bucketAuto:自动确定桶边界

{
    $bucketAuto: {
        groupBy: "$price", // 分桶的依据是根据price字段
        buckets: 4 // 桶的数量是4
    } 
}
1.4:连接和关联阶段

$lookup:左外连接(4.0+支持子管道) -> left join

{
    $lookup: {
        from: "inventory", // 关联表的名称
        localField: "item", // 本表的连接字段名称
        foreignField: "sku", // 外键的名称
        as: "inventory_docs"  // 别名
    }
}

$graphLookup:递归查找(用于图数据)

{
    $graphLookup: {
        from: "employees",
        startWith: "$reportsTo",
        connectFromField: "reportsTo",
        connectToField: "name",
        as: "hierarchy"
    }
}
1.5:结果处理阶段

$sort:排序

{ 
    $sort: { 
        age: -1,  // -1表示倒叙
        name: 1   // 1表示正序
    } 
}

$count:计数

{ 
    $count: "total_documents" 
}

$facet:多管道并行处理

{
  $facet: {
    "priceStats": [
      { $match: { status: "A" } },
      { $group: { _id: null, avg: { $avg: "$price" } } }
    ],
    "qtyStats": [
      { $match: { status: "A" } },
      { $group: { _id: null, total: { $sum: "$qty" } } }
    ]
  }
}

$merge/$out:将结果写入集合

{
    $merge: { 
        into: "monthly_summary", 
        on: "_id", 
        whenMatched: "replace" 
    } 
}

2:聚合中的各种表达式

2.1:算术表达式
  • $add$subtract$multiply$divide$mod
  • $abs$ceil$floor$round$sqrt$pow
2.2:比较表达式
  • $cmp$eq$gt$gte$lt$lte$ne
2.3:条件表达式
  • $cond: if-then-else逻辑

    { 
        $cond: { 
            if: { $gte: ["$qty", 100] }, // qty字段是否是>=100
            then: "A", // 如果满足if的条件返回A
            else: "B"  // 否则返回B
        } 
    }
    
  • $ifNull: 处理null值

    { 
        $ifNull: ["$description", "No description"] 
    }
    
  • $switch: 多条件分支

    {
      $switch: {
        branches: [
          { case: { $lt: ["$score", 60] }, then: "F" }, // 情况1
          { case: { $lt: ["$score", 70] }, then: "D" } // 情况2
        ],
        default: "A" // 其他情况
      }
    }
    
2.4:日期表达式
  • $year$month$dayOfMonth$hour$minute$second

  • $dayOfWeek$dayOfYear$week$isoWeek

  • $dateToString: 格式化日期

    { 
        $dateToString: { 
            format: "%Y-%m-%d",  // 转换成为指定的格式
            date: "$orderDate" // 订单时间
        } 
    }
    
2.5:字符串表达式
  • $concat$substr$toLower$toUpper$trim
  • $split$strLenBytes$strLenCP
  • $indexOfBytes$indexOfCP
2.6:数组表达式
  • $arrayElemAt$concatArrays$filter$map
  • $reduce$size$slice$zip
  • $in: 检查元素是否在数组中
2.7:累加器(用于$group)
  • $sum$avg$first$last$max$min
  • $push$addToSet$stdDevPop$stdDevSamp

3:举几个完整的例子

db.orders.aggregate([
    // 数据初筛阶段 -> 先筛选出来状态已经完成的
    { $match: { status: "completed" } },
    // 对items数组进行拆解
    { $unwind: "$items" },
    // 对于顾客id进行分组
    { $group: {
        _id: "$customerId",
        // 将商品的单价 * 数量求和作为totalSpent
        totalSpent: { $sum: { $multiply: ["$items.price", "$items.quantity"] } },
        // total的平均值就是avgOrder
        avgOrder: { $avg: "$total" }
    }},
    // 结果处理阶段
    // 通过totalSpent字段倒排
    { $sort: { totalSpent: -1 } },
    { $limit: 10 }
])
/* 按年龄进行分组,并统计各组的数量(没有age字段的数据统计到一组) */
// select count(*) as count from cui group by(age) order by age asc;
db.cui.aggregate([
    // 1:通过$group基于age分组,通过$sum实现对各组+1的操作
    {$group: {_id:"$age", count: {$sum:1}}}, 
    // 2:基于前面的_id(原age字段)进行排序,1代表正序
    {$sort: {_id:1}}
]);

/* 按年龄进行分组,并得出每组最大的_id值 */
// select max(age) as max_id from cui group by(age) order by age desc
db.cui.aggregate([
    // 1:先基于age字段分组,并通过$max得到最大的id,存到max_id字段中
    {$group: {_id:"$age", max_id: {$max:"$_id"}}},
    // 2:按照前面的_id(原age字段)进行排序,-1代表倒序
    {$sort: {_id: -1}}
]);

/* 过滤掉食物为空的数据,并按食物等级分组,返回每组_id最大的姓名 */
db.cui.aggregate([
    // 1:通过$match操作符过滤food不存在的数据
    {$match: {food: {$exists:true}}}, // where food != null
    // 2:通过$sort操作符,基于_id字段进行倒排序
    {$sort: {_id: -1}}, // order by _id desc
    // 3:通过$group基于食物等级分组,并通过$max得到_id最大的数据,
    // 并通过$first拿到分组后第一条数据(_id最大)的name值
    {$group: {
        _id:"$food.grade", // 通过$group基于食物等级分组
        max_id: {$max:"$_id"}, // 通过$max得到_id最大的数据
        name:{$first: "$name"} // 拿到分组后第一条数据(_id最大)的name值
    }}, // max(food.grade) as max_id, first(name) as name group by food.grade
    // 4:最后通过$project操作符,只显示_id(原food.grade)、name字段
    {$project: {_id:"$_id", name:1}} // select food.grade, name
]);

/* 多字段分组:按食物等级、颜色字段分组,并求出每组的年龄总和 */
db.cui.aggregate([
    // 1:_id中写多个字段,代表按多字段分组
    // 2:接着通过$sum求和age字段
    {$group: {_id: {grade:"$food.grade", color:"$color"}, total_age: {$sum:"$age"}}}
]);

/* 分组后过滤:根据年龄分组,然后过滤掉数量小于3的组 */
// select count() as count from cui group by age having count() > 3;
db.cui.aggregate([
    // 1:先按年龄进行分组,并通过$sum:1对每组数量进行统计
    {$group: {_id: "$age", count: {$sum:1}}},
    // 2:通过$match操作符,保留数量>3的分组(过滤掉<=3的分组)
	{$match: {count: {$gt:3}}}
]);

/* 分组计算:根据颜色分组,求出每组的数量、最大/最小/平均年龄、所有姓名、首/尾的姓名 */
db.cui.aggregate([
    // 1:按颜色分组
    {$group: {_id: "$color", 
        // 计算每组数量
        count: {$sum:1}, 
        // 计算每组最大年龄
        max_age: {$max: "$age"},
        // 计算每组最小年龄
        min_age: {$min: "$age"},
        // 计算每组平均年龄
        avg_age: {$avg: "$age"},
        // 通过$push把每组的姓名放入到集合中
        names: {$push:"$name"},
        // 获取每组第一个熊猫的姓名
        first_name: {$first: "$name"},
        // 获取每组最后一个熊猫的姓名
        last_name: {$last: "$name"}}}
]);

/* 分组后保留原数据,并基于原_id排序,然后跳过前3条数据,截取5条数据 */
db.cu.aggregate([
    // 1:先基于age分组,并通过$$ROOT引用原数据,将其保存到数组中
    {$group: {_id: "$age", cui_list: {$push: "$$ROOT"}}},
    // 2:分解数组为一行行的数据, 使用index字段记录数组下标,preserveNullAndEmptyArrays可以保证不丢失数据
    {$unwind: {path: "$cui_list", includeArrayIndex:"index", preserveNullAndEmptyArrays: true}},
    // 3:基于分解后的_id字段进行排序,1代表升序
    {$sort: {"cui_list._id": 1}},
    // 4:通过$skip跳过前3条数据
    {$skip: 3},
    // 5:通过$limit获取5条数据
    {$limit: 5}
])

/* 根据年龄进行判断,大于3岁显示成年、否则显示未成年(输出姓名、结果) */
db.cui.aggregate([
    // 1:通过$project操作符来完成投影输出
    {$project: {
      // 不显示_id字段,将name字段重命名为:“姓名”
      _id:0, 
      姓名:"$name",
      // 通过$cond实现逻辑运算,如果年龄>=3,显示成年,否则显示未成年
      result: {
        $cond: { // 创建一个条件
          if: {$gte: ["$age", 3]}, // 条件
          then: "成年", // 条件成立的话result将展示这个
          else: "未成年" // 条件不成立的话result将展示这个
    }}}}
]);

三:MongoDB索引

索引是 MongoDB 中提高查询性能的关键机制。合理使用索引可以显著提升查询速度,而不当的索引则可能导致性能下降

  1. 索引是特殊的数据结构,存储集合中部分数据的有序表示
  2. 使用 B-tree 数据结构(默认)或哈希数据结构
  3. 通过减少全集合扫描(COLLSCAN)来提高查询效率
  4. 以空间换时间,需要额外的存储空间和维护开销

1:索引类型

1.1:单字段索引
db.collection.createIndex({ field: 1 })  // 升序
db.collection.createIndex({ field: -1 }) // 降序
  • 适用于单字段查询、排序
  • 方向(1/-1)对等值查询无影响,对范围查询和排序有影响
1.2:复合索引
db.collection.createIndex({ field1: 1, field2: -1 })
  • 遵循"最左前缀"原则,字段顺序至关重要:
    • 等值查询字段应放在前面
    • 范围查询/排序字段放在后面
  • 可以支持多个字段的排序
1.3:多键索引
db.collection.createIndex({ arrayField: 1 })
  • 自动为数组中的每个元素创建索引项
  • 不支持复合多键索引中的多个数组字段
  • 查询时使用 $elemMatch 可以高效利用多键索引
1.4:地图空间索引

2dsphere 索引(球面几何)

db.places.createIndex({ location: "2dsphere" })
  • 支持GeoJSON格式和传统坐标对
  • 支持的查询:
    • $near (附近点)
    • $geoWithin (几何形状内)
    • $geoIntersects (与几何形状相交)

2d索引,平面几何

db.places.createIndex({ location: "2d" })
  • 使用传统坐标对[longitude, latitude]
  • 支持平面距离计算
1.5:文本索引
db.articles.createIndex({ content: "text" })
  • 支持全文搜索
  • 还可以指定权重
db.articles.createIndex(
    { title: "text", content: "text" },
    { weights: { title: 10, content: 5 } } // 指定权重
)
  • 对应的查询语法如下
db.articles.find({ $text: { $search: "中华人民共和国" } })
1.6:哈希索引
db.collection.createIndex({ field: "hashed" })
  • 使用哈希函数计算字段值的哈希值
  • 仅支持等值查询,不支持范围查询
  • 常用于哈希分片键
1.7:通配符索引(4.2+)
db.collection.createIndex({ "$**": 1 }) // 所有字段
db.collection.createIndex({ "userMetadata.$**": 1 }) // 特定路径
  • 支持对未知或动态字段的查询
1.8:唯一索引
db.collection.createIndex({ field: 1 }, { unique: true })
  • 确保索引字段不包含重复值
  • 复合索引也可以设为唯一
  • null 值被视为重复值
1.9:稀疏索引
db.collection.createIndex({ field: 1 }, { sparse: true })
  • 仅索引包含索引字段的文档
  • 节省空间但可能导致不完整的查询结果
1.10:TTL索引
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
  • 自动删除过期文档
  • 必须是日期类型字段
  • 后台线程每分钟运行一次清理
1.11:部分索引
db.users.createIndex(
    { name: 1 },
    // 只对active状态的文档中的name进行正序索引
    { partialFilterExpression: { status: "active" } }
)
  • 只索引满足过滤条件的文档
  • 节省空间和维护成本
1.12:隐藏索引(4.4+)
db.collection.createIndex({ field: 1 }, { hidden: true })
  • 对查询规划器不可见
  • 用于测试索引移除的影响
1.13:索引属性和选项
  • background: 后台构建(生产环境不建议)
  • name: 指定索引名称
  • collation: 指定排序规则
  • expireAfterSeconds: TTL索引的过期时间
  • partialFilterExpression: 部分索引的过滤条件

2:索引管理相关命令

2.1:创建索引
db.collection.createIndex(keys, options)
2.2:查看索引
db.collection.getIndexes()
2.3:删除索引
db.collection.dropIndex("indexName")
db.collection.dropIndex({ field: 1 }) // 通过key删除
2.4:重建索引
db.collection.reIndex()
2.5:索引大小信息
db.collection.totalIndexSize()
db.collection.stats().indexSizes

3:索引使用策略

在这里插入图片描述

4:explain索引性能分析

db.collection.find(query).explain("executionStats")

查看 winningPlanexecutionStats, 其中关键指标:

  • stage: COLLSCAN(全表扫描) 或 IXSCAN(索引扫描)
  • nReturned: 返回文档数
  • executionTimeMillis: 执行时间
  • totalKeysExamined: 检查的索引键数
  • totalDocsExamined: 检查的文档数

索引效率评估

  • 理想情况:keysExamined ≈ nReturned
  • 糟糕情况:keysExamined ≫ nReturned
  • 全表扫描:docsExamined = collectionSize

5:特殊场景下的索引策略

在这里插入图片描述

四:文本搜索

MongoDB 提供强大的全文搜索功能,允许用户在字符串内容中执行文本查询。

  • 支持对字符串内容的全文搜索
  • 支持多种语言的分词和词干提取
  • 支持权重分配和评分
  • 每个集合只能创建一个文本索引(但可以包含多个字段)

1:创建文本索引

// 单字段文本索引
db.articles.createIndex({ content: "text" })

// 多字段复合文本索引
db.products.createIndex({
  title: "text",
  description: "text",
  tags: "text"
})

// 带权重的文本索引
db.articles.createIndex(
  { title: "text", abstract: "text", body: "text" },
  { weights: { title: 10, abstract: 5, body: 1 } }
)

2:文本搜索查询

2.1:基本文本搜索
// 简单搜索
db.articles.find({ $text: { $search: "mongodb tutorial" } })

// 排除特定词
db.articles.find({ $text: { $search: "mongodb -tutorial" } })

// 精确短语搜索
db.articles.find({ $text: { $search: "\"mongodb tutorial\"" } })
2.2:搜索选项
db.articles.find({
  $text: {
    $search: "database",
    $language: "en",       // 指定语言
    $caseSensitive: false, // 是否区分大小写(3.4+)
    $diacriticSensitive: false // 是否区分音标符号(3.4+)
  }
})
2.3:结果排序与评分
// 包含文本匹配评分
db.articles.find(
  { $text: { $search: "mongodb" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })

3:文本搜索高级特性

3.1:通配符文本索引
// 索引所有字符串字段(4.2+)
db.collection.createIndex({ "$**": "text" })
3.2:聚合框架中的文本搜索
db.articles.aggregate([
  { $match: { $text: { $search: "database" } } },
  { $sort: { score: { $meta: "textScore" } } },
  { $project: { title: 1, score: { $meta: "textScore" } } }
])
3.3:文本索引与其他查询结合
// 文本搜索与普通查询的组合
db.products.find({
  $text: { $search: "phone" },
  price: { $lt: 1000 },
  inStock: true
})

4:文本索引限制

在这里插入图片描述

5:实际应用示例

5.1:电商产品搜索
// 创建索引
db.products.createIndex({
  name: "text",
  description: "text",
  category: "text"
}, {
  weights: {
    name: 10,
    category: 5,
    description: 1
  }
})

// 执行搜索
db.products.find({
  $text: { $search: "smartphone -used" },
  price: { $lte: 500 },
  stock: { $gt: 0 }
}).sort({ score: { $meta: "textScore" } })
5.2:新闻文章搜索系统
// 创建带权重的索引
db.articles.createIndex(
  { headline: "text", body: "text", author: "text" },
  { weights: { headline: 10, author: 5, body: 1 } }
)

// 复杂搜索
db.articles.find({
  $text: { $search: "\"climate change\" -denier" },
  publishDate: { $gte: ISODate("2023-01-01") },
  category: { $in: ["science", "environment"] }
}).sort({
  publishDate: -1,
  score: { $meta: "textScore" }
}).limit(20)

6:和搜索引擎的对比

特性 MongoDB 文本搜索 专用搜索引擎(如Elasticsearch)
部署复杂度 简单(内置) 需要单独部署
功能丰富度 基础功能 高级功能(同义词、模糊搜索等)
性能 中等规模数据表现良好 大数据量和高并发下更优
一致性 实时一致 可能有延迟(取决于配置)
扩展性 依赖MongoDB扩展 独立扩展
学习曲线 简单(若已用MongoDB) 需要学习新系统

对于大多数应用的基本搜索需求,MongoDB的文本搜索功能已经足够。

但对于复杂的搜索需求(如复杂的同义词处理、词干提取、模糊搜索等),可能需要考虑集成专用搜索引擎。

五:查询优化技巧

使用投影减少返回数据

db.users.find({}, { name: 1, email: 1 })

使用游标批量处理大数据

const cursor = db.largeCollection.find().batchSize(1000)

避免全集合扫描

  1. 确保查询使用索引
  2. 避免正则表达式前缀为通配符

分页优化

// 避免使用skip进行深度分页
db.products.find(
    { _id: { $gt: lastSeenId } }
).limit(10)

使用$expr进行文档内比较

db.sales.find(
    { $expr: { $gt: ["$revenue", "$cost"] } }
)

数组查询优化

  • 对数组字段建立多键索引
  • 使用$elemMatch精确匹配数组元素

使用hint强制索引

db.orders
    .find({ status: "A" })
    .hint({ status: 1, date: -1 })

网站公告

今日签到

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