第五章:查询操作进阶
最后更新: 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] }
]
}
})
💡 实践提示
- 避免正则前缀通配:正则表达式开头使用
^可以利用索引 - 优先使用文本索引:对于全文搜索,创建文本索引比正则更高效
- 地理空间查询需要索引:必须先创建 2dsphere 或 2d 索引
- 分页优化:使用基于键的分页替代 skip,避免大量 skip 导致的性能问题
- 投影优化:只查询需要的字段,减少网络传输和内存占用
📚 继续学习