当前位置: 首页 > news >正文

MongoDB05 - MongoDB 查询进阶

MongoDB 查询进阶

文章目录

  • MongoDB 查询进阶
    • 一:查询运算符
      • 1:比较运算符
      • 2:逻辑运算符
      • 3:元素运算符
      • 4:数组运算符
      • 5:评估运算符
    • 二:聚合框架
      • 1:核心聚合阶段
        • 1.1:数据初筛阶段
        • 1.2:数据变形阶段
        • 1.3:分组计算阶段
        • 1.4:连接和关联阶段
        • 1.5:结果处理阶段
      • 2:聚合中的各种表达式
        • 2.1:算术表达式
        • 2.2:比较表达式
        • 2.3:条件表达式
        • 2.4:日期表达式
        • 2.5:字符串表达式
        • 2.6:数组表达式
        • 2.7:累加器(用于$group)
      • 3:举几个完整的例子
    • 三:MongoDB索引
      • 1:索引类型
        • 1.1:单字段索引
        • 1.2:复合索引
        • 1.3:多键索引
        • 1.4:地图空间索引
        • 1.5:文本索引
        • 1.6:哈希索引
        • 1.7:通配符索引(4.2+)
        • 1.8:唯一索引
        • 1.9:稀疏索引
        • 1.10:TTL索引
        • 1.11:部分索引
        • 1.12:隐藏索引(4.4+)
        • 1.13:索引属性和选项
      • 2:索引管理相关命令
        • 2.1:创建索引
        • 2.2:查看索引
        • 2.3:删除索引
        • 2.4:重建索引
        • 2.5:索引大小信息
      • 3:索引使用策略
      • 4:explain索引性能分析
      • 5:特殊场景下的索引策略
    • 四:文本搜索
      • 1:创建文本索引
      • 2:文本搜索查询
        • 2.1:基本文本搜索
        • 2.2:搜索选项
        • 2.3:结果排序与评分
      • 3:文本搜索高级特性
        • 3.1:通配符文本索引
        • 3.2:聚合框架中的文本搜索
        • 3.3:文本索引与其他查询结合
      • 4:文本索引限制
      • 5:实际应用示例
        • 5.1:电商产品搜索
        • 5.2:新闻文章搜索系统
      • 6:和搜索引擎的对比
    • 五:查询优化技巧

一:查询运算符

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", // 其他的为Otheroutput: { 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字段是否是>=100then: "A", // 如果满足if的条件返回Aelse: "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",// 将商品的单价 * 数量求和作为totalSpenttotalSpent: { $sum: { $multiply: ["$items.price", "$items.quantity"] } },// total的平均值就是avgOrderavgOrder: { $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 })

相关文章:

  • 极限平衡法和应力状态法无限坡模型安全系数计算
  • 阿里云-接入SLS日志
  • Wpf布局之Border控件!
  • ​扣子Coze飞书多维表插件-创建数据表
  • GPT,GPT-2,GPT-3 论文精读笔记
  • mapstate
  • 打通Dify与AI工具生态:将Workflow转为MCP工具的实践
  • 养老保险交得越久越好
  • 【ad-hoc】# P12414 「YLLOI-R1-T3」一路向北|普及+
  • 《弦论视角下前端架构:解构、重构与无限延伸的可能》
  • 商业秘密保护新焦点:企业如何守护核心经营信息?
  • 【硬核数学】2.1 升级你的线性代数:张量,深度学习的多维数据语言《从零构建机器学习、深度学习到LLM的数学认知》
  • STM32——MDK5编译和串口下载程序+启动模式
  • 信创背景下应用软件迁移解析:从政策解读到落地实践方案
  • 详细的说一下什么是Arduino?
  • 【硬核数学】2.5 “价值标尺”-损失函数:信息论如何设计深度学习的损失函数《从零构建机器学习、深度学习到LLM的数学认知》
  • OpenCV学习3
  • 《平行宇宙思维如何让前端错误处理无懈可击》
  • (七)集成学习
  • python 使用 pyenv 管理 python 版本