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)进行处理,每个阶段对数据进行特定的操作。
- 数据流经多个阶段组成的管道
- 每个阶段处理输入文档并输出结果文档
- 一个阶段的输出作为下一个阶段的输入
- 管道可以包含一个或多个阶段
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 中提高查询性能的关键机制。合理使用索引可以显著提升查询速度,而不当的索引则可能导致性能下降
- 索引是特殊的数据结构,存储集合中部分数据的有序表示
- 使用 B-tree 数据结构(默认)或哈希数据结构
- 通过减少全集合扫描(COLLSCAN)来提高查询效率
- 以空间换时间,需要额外的存储空间和维护开销
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")
查看 winningPlan
和 executionStats
, 其中关键指标:
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)
避免全集合扫描
- 确保查询使用索引
- 避免正则表达式前缀为通配符
分页优化
// 避免使用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 })