56、MongoDB查询操作:查询过滤器、投影、排序、分页(skip/limit)、正则查询
MongoDB 的查询,说白了就是跟数据库要数据。但怎么要得准、要得快,这里头门道不少。我最早用 MongoDB 时,上来就是 find({}) 一把梭,结果数据量一上来,客户端直接卡死。嗯,后来才老老实实把查询过滤器、投影、排序、分页这些基本功练扎实了。
今天咱们就把 MongoDB 查询的五个核心操作捋一遍。你想想看,一个查询从发出到返回结果,中间经历了什么?其实就是:先过滤出符合条件的文档,再决定要哪些字段,然后排个序,最后切一段出来。正则查询则是过滤器的特殊形式,专门对付模糊匹配。
5.1 查询过滤器:精准定位数据
查询过滤器就是 find() 的第一个参数。它告诉 MongoDB:我只想要这些文档。
最基本的用法是等值匹配:
db.users.find({ "status": "active" })
但实际项目中,条件往往更复杂。比如我要查年龄在 18 到 30 之间、并且状态不是 "banned" 的用户:
db.users.find({
"age": { "$gte": 18, "$lte": 30 },
"status": { "$ne": "banned" }
})
这里 $gte 是大于等于,$lte 是小于等于,$ne 是不等于。MongoDB 提供了丰富的比较操作符:$gt、$lt、$in、$nin 等等。
$exists 这种低选择性条件写在前面,结果索引根本用不上。
还有一个常用的操作符是 $elemMatch,专门对付数组字段。比如查一个订单里包含某个商品:
db.orders.find({
"items": {
"$elemMatch": {
"product_id": "p1001",
"quantity": { "$gte": 2 }
}
}
})
5.2 投影:只取需要的字段
投影是 find() 的第二个参数。为什么要用投影?说白了,就是省带宽、省内存。你想想看,一个文档有 50 个字段,你只需要其中 3 个,把整个文档传回来多浪费。
投影的写法很简单:
db.users.find(
{ "status": "active" },
{ "name": 1, "email": 1, "_id": 0 }
)
这里 1 表示包含该字段,0 表示排除。注意 _id 默认是包含的,如果你不需要它,必须显式排除。
_id。比如 { "name": 1, "age": 0 } 会报错。要么全包含,要么全排除,这是 MongoDB 的硬性规定。
投影还能配合 $slice 操作符,只返回数组的一部分。比如只取评论的前 5 条:
db.articles.find(
{ "title": "MongoDB 入门" },
{ "comments": { "$slice": 5 } }
)
5.3 排序:让数据有序呈现
排序用 sort() 方法。1 表示升序,-1 表示降序。
db.users.find({ "status": "active" })
.sort({ "created_at": -1, "name": 1 })
这条查询先按创建时间降序排,时间相同的再按名字升序排。排序字段的顺序很重要,它直接影响索引的使用效率。
我个人习惯,只要数据量超过几千条,排序字段一定要建索引。否则 MongoDB 就得在内存里做排序,一旦超过 32MB 的限制,它会直接报错。
status 过滤并按 created_at 排序,就应该建一个 { "status": 1, "created_at": -1 } 的索引。
5.4 分页:skip 和 limit 的配合
分页是 Web 应用里最常见的需求。MongoDB 用 skip() 跳过指定数量的文档,用 limit() 限制返回数量。
// 第 1 页,每页 20 条
db.users.find({ "status": "active" })
.sort({ "created_at": -1 })
.limit(20)
.skip(0)
// 第 2 页
db.users.find({ "status": "active" })
.sort({ "created_at": -1 })
.limit(20)
.skip(20)
看起来很简单对吧?但这里有个大坑:skip 的性能问题。skip 的实现方式是先扫描并丢弃前 N 条文档,再返回后面的。当页码很大时,比如 skip(10000),MongoDB 得扫描 10020 条文档,然后扔掉前 10000 条。这效率,你想想看。
_id 或时间戳做范围查询,才彻底解决这个问题。
基于游标的分页示例:
// 第一页
db.users.find({ "status": "active" })
.sort({ "_id": 1 })
.limit(20)
// 第二页,传入上一页最后一个文档的 _id
db.users.find({
"status": "active",
"_id": { "$gt": last_id }
}).sort({ "_id": 1 }).limit(20)
这种方式无论翻到多少页,性能都稳定。唯一的代价是客户端需要记住上一页的最后一个 _id。
5.5 正则查询:模糊匹配的利器
正则查询用 $regex 操作符。比如查所有名字里带 "张" 的用户:
db.users.find({ "name": { "$regex": "张", "$options": "i" } })
$options 里的 i 表示忽略大小写。其他常用选项还有 m(多行匹配)、x(忽略空白字符)。
正则查询虽然方便,但性能是个大问题。因为它通常无法使用索引。除非你的正则表达式是前缀匹配,比如 ^张,MongoDB 才能利用索引进行范围扫描。
正则查询还支持直接写 JavaScript 正则表达式:
db.users.find({ "name": /^张/i })
这两种写法效果一样,但 $regex 写法更灵活,可以动态拼接模式串。
知识体系总览
下面这张图把 MongoDB 查询的五个核心操作串起来了。你可以看到,一个完整的查询流程就是:先过滤,再投影,然后排序,最后分页。正则查询是过滤器的特殊形式。
综合示例:一个完整的查询
最后,咱们把今天学的所有操作串起来,写一个实际场景的查询。假设我们要查一个电商平台的订单:
- 只查已支付的订单
- 金额在 100 到 1000 之间
- 收货地址在 "北京" 的(模糊匹配)
- 只需要订单号、金额、收货人姓名
- 按支付时间降序排列
- 每页 10 条,取第 3 页
db.orders.find(
{
"status": "paid",
"total": { "$gte": 100, "$lte": 1000 },
"address.city": { "$regex": "北京", "$options": "i" }
},
{
"order_no": 1,
"total": 1,
"recipient.name": 1,
"_id": 0
}
).sort({ "paid_at": -1 })
.skip(20)
.limit(10)
这个查询把过滤器、投影、排序、分页、正则查询全用上了。你想想看,如果不用投影,每个订单文档可能带着几十个字段回来,网络传输量至少翻 5 倍。如果不用分页,一次返回几千条数据,客户端渲染都得卡半天。
嗯,MongoDB 查询的精髓就在这五个操作里。把它们用好,你的查询就能做到又快又准。下次遇到慢查询,别急着怪数据库,先看看你的过滤器有没有走索引,投影有没有省掉多余字段,分页有没有用 skip 翻到天荒地老。
公众号:蓝海资料掘金营,微信 deep3321