😁 作者简介:一名大四的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:MongoDB数据库
👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气
🔥前言:
这里是关于MongoDB中查询语句find的使用,其中对于特定类型的查询非常特别,还有游标的使用可以加快我们的查询速度,这是我的学习MongoDB笔记,希望可以帮助到大家,欢迎大家的补充和纠正
文章目录
第4章 查询
4.1 find简介
4.1.1 指定要返回的键
概念:find方法中的第二个参数就是投影条件,投影条件是一个文档,其中键是要包含或排除的字段,对应的值为 1 表示包含,0 表示排除。默认情况下,如果不指定投影条件,MongoDB 会返回文档中的所有字段
案例:
- 默认情况下,如果不指定投影条件,MongoDB 会返回文档中的所有字段
> db.users.find({}, {"username" : 1, "email" : 1})
{
"_id" : ObjectId("4ba0f0dfd22aa494fd523620"),
"username" : "joe",
"email" : "joe@example.com"
}
2.可能在文档中有很多键,而你不希望结果中包含 “fatal_weakness” 键:
> db.users.find({}, {"fatal_weakness" : 0})
3.要将“_id“键从返回结果剔除
> db.users.find({}, {"username" : 1, "_id" : 0})
{
"username" : "joe"
}
4.3 特定类型查询
4.3.3 查询数组之$slice操作符
❗注意点:除非特别指定,否则在使用 “$slice” 时会返回文档中的所有键。这与其他的键指定符不同,后者不会返回未指定的键。例如,有这样一个关于博客文章的文档:
例子:
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
},
{
"name" : "bob",
"email" : "bob@example.com",
"content" : "good post."
}
]
}
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -1}})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "bob",
"email" : "bob@example.com",
"content" : "good post."
}
]
}
即使title和content没有显式地被包含在键指定符中,但它们依然被返回了
4.3.5 查询数组之数组与范围查询的相互作用
文档中的标量(非数组元素)必须与查询条件中的每一条子句相匹配。如果使用 {“x” : {“
g
t
"
:
10
,
"
gt" : 10, "
gt":10,"lt” : 20}} 进行查询,那么 “x” 必须同时满足大于 10 且小于 20。然而,如果文档中的 “x” 字段是一个数组,那么当 “x” 键的某一个元素与查询条件的任意一条语句相匹配(查询条件中的每条语句可以匹配不同的数组元素)时,此文档也会被返回。
接下来我们可以来看一个例子:
现在有一个文档
{"x" : 5}
{"x" : 15}
{"x" : 25}
{"x" : [5, 25]}
如果想找出 “x” 的值在 10 和 20 之间的所有文档,那么你可能会本能地构建这样的查询,即 db.test.find({“x” : {“ g t " : 10 , " gt" : 10, " gt":10,"lt” : 20}}),然后期望它会返回一个文档:{“x” : 15}。然而,当实际运行时,我们得到了两个文档,如下所示:
> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}})
{"x" : 15}
{"x" : [5, 25]}
5 和 25 都不在 10 和 20 之间,但由于 25 与查询条件中的第一个子句("x"的值大于 10)相匹配,5 与查询条件中的第二个子句(“x” 的值小于 20)相匹配,因此这个文档会被返回。
😐 这样就使得针对数组的范围查询基本失去了作用:一个范围会匹配任何多元素数组,有几种方法可以获得预期的行为
1.可以使用
e
l
e
m
M
a
t
h
强制
M
o
n
g
o
D
B
将这两个子句与单个数组元素进行比较,不过
elemMath强制MongoDB将这两个子句与单个数组元素进行比较,不过
elemMath强制MongoDB将这两个子句与单个数组元素进行比较,不过elemMath不会匹配非数组元素
> db.test.find({"x" : {"$elemMatch" : {"$gt" : 10, "$lt" : 20}}})
> // 没有结果
文档 {“x” : 15} 不再与查询条件匹配了,因为它的 “x” 字段不是一个数组。也就是说,你应该有充分的理由在一个字段中混合数组和标量值,而这在很多场景中并不需要。对于这样的情况,“ e l e m M a t c h " 为数组元素的范围查询提供了一个很好的解决方案。 ∗ ∗ 2. 如果在要查询的字段上有索引(参见第 5 章),那么可以使用 m i n 和 m a x 将查询条件遍历的索引范围限制为 " elemMatch" 为数组元素的范围查询提供了一个很好的解决方案。 **2.如果在要查询的字段上有索引(参见第 5 章),那么可以使用 min 和 max 将查询条件遍历的索引范围限制为 " elemMatch"为数组元素的范围查询提供了一个很好的解决方案。∗∗2.如果在要查询的字段上有索引(参见第5章),那么可以使用min和max将查询条件遍历的索引范围限制为"gt” 和 “$lt” 的值**
> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}).min({"x" : 10}).max({"x" : 20})
{"x" : 15}
现在,这条查询语句只会遍历值在 10 和 20 之间的索引,不会与值为 5 和 25的这两个条目进行比较。但是,只有在要查询的字段上存在索引时,才能使用min 和 max,并且必须将索引的所有字段传递给 min 和 max
🍁在查询可能包含数组的文档的范围时,使用 min 和 max 通常是一个好主意。在整个索引范围内对数组使用 “
g
t
"
/
"
gt"/"
gt"/"lt” 进行查询是非常低效的。它基本上接受任何值,因此会搜索每个索引项,而不仅仅是索引范围内的值。
4.4 $where查询
概念: 允许你在查询中执行任意的 JavaScript 代码
例子:
> db.foo.find({"$where" : function () {
... for (var current in this) {
... for (var other in this) {
... if (current != other && this[current] == this[other]) {
... return true;
... }
... }
... }
... return false;
... }});
注意:除非绝对必要,否则不应该使用 “$where” 查询:它们比常规查询慢得多
4.5 游标
概念:游标(Cursor)是用于遍历查询结果的对象,你执行查询时,MongoDB 返回一个游标,它指向查询结果的初始位置。通过游标,你可以逐步获取结果集中的文档,而不需要一次性将整个结果集加载到内存中。
案例:
> for(i=0; i<100; i++) {
... db.collection.insertOne({x : i});
... }
> var cursor = db.collection.find();
> while (cursor.hasNext()) {
... obj = cursor.next();
... // 执行任务
... }
//还有一种用法,游标cursor类还实现了Js的迭代器接口,因此可以使用forEach
> var cursor = db.people.find();
> cursor.forEach(function(x) {
... print(x.name);
... });
adam
matt
zak
上述案例中,find方法就返回一个游标,cursor.hasNext() 会检查是否有后续结果存在,而 cursor.next() 用来对其进行获取。
游标的工作机制:
- **查询请求:**当客户端发起一个查询请求,MongoDB服务器接受请求
- 返回游标:服务器会返回一个包含初始查询结果的游标给客户端,这是,并不会立刻返回所有匹配的文档,而是返回一个包含部分结果的游标对象,这个部分结果一般来说是100个结果或者4MB的数据(两者中较小者)
- 分批获取:客户端可以使用游标的方法(如next)来逐批获取文档。当客户端请求下一批文档时,服务器会在需要时继续执行查询,获取更多的文档,并返回给客户端。
- **游标生命周期:**获取的过程会一直持续,知道游标耗尽或者结果被全部返回
4.5.1 limit,skip和sort
- limit:会限制返回结果的数量,传入的参数只能是正数,负数会报错
- skip:会跳过指定数量的文档,然后返回剩下的文档,参数也是正数
- sort:会接受一个对象作为参数,这个对象是一组键–值对,键对应文档的键名,值对应排序的方向。排序方向可以是 1(升序)或 -1(降序)。如果指定了多个键,则结果会按照这些键被指定的顺序进行排序。例如,要按照 “username” 升序及 “age” 降序排列
案例:实现分页,假设你正在运营一个在线商店,有人想搜索 mp3。如果想每页返回 50 个结果并按照价格从高到低排序
> db.stock.find({"desc" : "mp3"}).limit(50).sort({"price" : -1})
//如果顾客单机下一页
> db.stock.find({"desc" : "mp3"}).limit(50).skip(50).sort({"price" : -1})
然而,略过大量的结果会导致性能问题
比较顺序
MongoDB 对于类型的比较有一个层次结构。有时一个键的值可能有多种类型:整型和布尔型,或者字符串和 null。如果对混合类型的键进行排序,那么会有一个预定义的排序顺序。从最小值到最大值,顺序如下。
- 最小值
- null
- 数字(整型、长整型、双精度浮点型、小数型)
- 字符串
- 对象/文档
- 数组
- 二进制数据
- 对象 ID
- 布尔型
- 日期
- 时间戳
- 正则表达式
- 最大值
4.5.2 避免略过大量结果
使用 skip 来略过少量的文档是可以的。但对于结果非常多的情况,skip 会非常慢,因为需要先找到被略过的结果,然后再丢弃这些数据。大多数数据库会在索引中保存更多的元数据以处理 skip,但 MongoDB 目前还不支持这样做,所以应该避免略过大量的数据
1.不使用skip对结果进行分页
最简单的方式就是使用limit和skip互相配合,用偏移量来进行返回:
> // 不要这么做:略过大量数据会非常慢
> var page1 = db.foo.find(criteria).limit(100)
> var page2 = db.foo.find(criteria).skip(100).limit(100)
> var page3 = db.foo.find(criteria).skip(200).limit(100)
...
使用skip查询很多结果时,会非常慢,所以我们有一种不使用skip来进行分页的方法,就是使用排序和条件查询
假设要按照date**降序显式文档,**可以使用以下方式获取第一页:
> var page1 = db.foo.find().sort({"date" : -1}).limit(100)
然后,假设日期是唯一的,可以使用最后一个文档的 “date” 值作为获取下一页的查询条件:
var latest = null;
// 显示第1页
while (page1.hasNext()) {
latest = page1.next();
display(latest);
}
// 获取下一页
var page2 = db.foo.find({"date" : {"$lt" : latest.date}});
page2.sort({"date" : -1}).limit(100);
2.查询一个随机文档
我们首先也会翻译使用skip来随机跳过数据来获取
> // 不要这样做
> var total = db.foo.count()
> var random = Math.floor(Math.random()*total)
> db.foo.find().skip(random).limit(1)
以这种方式获取随机元素实际上非常低效:必须先计算总数(如果使用查询条件,这会是一个昂贵的操作),并且略过大量的元素也会非常耗时
我们还有一种比较好的方法,不过需要提前计划,在文档中设置random键
> db.people.insertOne({"name" : "joe", "random" : Math.random()})
> db.people.insertOne({"name" : "john", "random" : Math.random()})
> db.people.insertOne({"name" : "jim", "random" : Math.random()})
这样,我们就可以使用正常的条件查询来查询,而不用使用skip
> var random = Math.random()
> result = db.people.findOne({"random" : {"$gt" : random}})
random 可能会大于集合中的任何 “random” 值,并且不会返回任何结果。可以简单地从另一个方向返回文档以避免这种情况:
> if (result == null) {
... result = db.people.findOne({"random" : {"$lte" : random}})
... }
如果集合中没有任何文档,那么这种方式会返回 null,这也是合理的。
4.5.3 游标生命周期
在服务器,游标会占用内存和资源,所以我们需要释放游标,有以下几种情况会释放游标
- 当游标遍历完结果之后,它会清除自身
- 当游标超出客户端的作用域,驱动程序会向数据库发送一条特殊的消息,让数据库知道它可以“杀死”该游标
- 用户没有遍历完所有结果而且游标仍在作用域内,如果 10 分钟没有被使用的话,数据库游标也将自动“销毁”。
有时候,我们需要一个游标维持很长时间,这种情况下,我们应该怎么办呢?
许多驱动程序实现了一个称为 immortal 的函数,或者类似的机制,它告诉数据库不要让游标超时。
如果关闭了游标超时,则必须遍历完所有结果或主动将其销毁以确保游标被关闭。否则,它会一直占用数据库的资源,直到服务器重新启动