4.4 $where查询
$where
可以进行任何的查询。比较常用的是比较文档中两个键的值是否相等。但是如果非必要,务必不要使用$where
进行查询。因为$where
查询要比常规查询慢很多,$where
查询会把每个文档从BSON转换为JavaScript对象,然后再进行查询,而且$where
不能使用索引。如果必须使用$where
,也尽量先用常规查询进行过滤再使用$where
来查询。
4.5 游标
游标可以保存一次查询的结果并提供逐条查看结果。例如:
var cursor = db.foo.find()
此时游标仅是保存了查询的构造,而并没有向服务器发起查询。在执行:
cursor.hasNext()
后,查询请求才会到达服务器,返回查询结果。接下来的cursor.next()
以及cursor.hasNext()
均是在本地执行的。
针对返回结果的限制的方法有limit
、skip
、sort
。limit
可以限制返回结果数量,即最多返回指定数量的文档;skip
可以略过指定数量的文档,如果结果收少于指定数量则不返回结果;sort
可以按照指定的键值大小进行文档的排序,例如:
db.c.find().sort({
username: 1,
age: -1
})
这表示针对username
进行升序排列,针对age
斤西瓜降序排列。这三种方法可以组合使用,链式写法即可。
此外,sort
如果对类型不一致的键值进行排序时是有指定类型循序的:(1)最小值;(2)null;(3)数字(整型、长整型、双精度);(4)字符串;(5)对象/文档;(6)数组;(7)二进制数据(8)对象ID;(9)布尔型;(10)日期型;(11)时间戳;(12)正则表达式;(13)最大值。
如果需要排序的结果集的大小超过32M,MongoDB就会报错,拒绝对过多的数据进行排序。
使用skip
跳过大量的文档会变得很慢,因为这些需要略过的文档也需要被找出,然后再抛弃这些文档。
高级查询选项:如find({"foo": "bar"}).sort({"x": 1})
在shell或者其它驱动程序中并不是直接发出{"foo": "bar"}
这样的查询请求,而是将它们封装进更大的查询文档中,像这样:
{
"$query": {"foo": "bar"},
"$orderby": {"x": 1}
}
此外还有一些其他的辅助查询操作,例如$maxscan
(指定本次查询中扫描文档的上限)、$min
(文档必须与索引的键完全匹配,并指定索引扫描的下边界)、$max
(文档必须与索引的键完全匹配,并指定索引扫描的下边界)等。
像前文所说,MongoDB中文档是顺序存储的,如果对某个文档进行修改而使其体积变大,MongoDB会将其移动到所有文档后面存储。由于游标同样是向后查询文档,如果在前面修改了某个文档导致其变大被移动到了后面,游标向后移动时会再次查询到该文档。解决这个问题需要对查询结果进行快照,这样查询就在_id
索引上遍历执行,保证每个文档只返回一次。使用快照会使查询变慢,所以应该只在必要的时候使用快照。
游标的生命周期:在服务端,游标占用内存等资源。当游标遍历了所有结果或者客户端发来信息要求终止,数据库会释放游标占用的资源。当然,还有一些其他情况会使得游标被删除,例如如果客户端的游标已经不在作用域内时,客户端会发出一个特殊的消息要求服务端销毁游标,或者10分钟内某个游标没有使用的话也会删除。
4.6 数据库命令
数据库命令,例如drop
,其实是一种特殊的查询,只不过服务端收到这类“查询”会进行特殊的逻辑处理。
5 索引
5.1 索引简介
索引可以根据给定字段组织数据,让MongoDB能够根据这些字段快速找到指定的文档。例如,为username
创建索引:
db.users.ensureIndex({"username": 1});
建立索引所耗时间根据机器性能以及文档整体大小的不同而不同。虽然建立索引之后查询几乎会瞬间完成,但是其他操作,包括插入、更新、删除等将会消耗更多时间,因为这些操作会修改集合的结构,从而导致需要修改建立的索引。因此,MongoDB也限制每个集合上最多有64个索引,一般来说,一个特定的集合上不应该有两个以上的索引。挑选合适的字段建立索引非常重要。如果某个字段是常用的查询字段,那么在这个字段上建立索引是一个好选择,反之则没有必要建立索引。
索引的值是按照一定的顺序排列的,因此索引键对文档进行排序非常快。但是如果查询时没有首先使用索引键进行排序时,索引并不会发挥作用。不过可以针对多个字段建立复合索引。建立复合索引时优先考虑需要排序的键,将其放在首位索引键。
索引的排序方式并不重要,因为MongoDB可以在索引的任意方向进行便遍历。
有一些查询操作完全无法使用索引,例如$where
、$exists
、$nin
查询只能进行全表扫描,而$ne
、$not
多数情况下也会进行全表扫描。
$or
查询实际上是两次查询,最后合并结果,这样的效率必然不如一次查询的效率高,因此尽可能使用$in
代替。此外,MongoDB一次查询只能使用一个索引/复合索引,但是由于$or
是两次独立的查询,所以它可以使用多个索引。
MongoDB可以针对嵌套文档的键建立索引,和建立普通的索引类似。例如文档:
{
"username": "sid",
"loc": {
"ip": "1.2.3.4",
"city": "Springfield",
"state": "NY"
}
}
给loc.city
建立索引:
db.users.ensureIndex({"loc.city": 1});
即可。但是注意和子文档loc
的索引进行区分。如果建立的索引是子文档的索引,那么只有如下的查询才会使用子文档索引:
db.users.find({
"loc": {
"ip": "123.234.111.222",
"city": "Shelbycille",
"state": "NY"
}
});
即必须与子文档字段顺序完全匹配的子文档查询才能使用子文档索引,而:
db.users.find({"loc.city": "Shelbycille"});
才会使用子文档的键的索引。
同样可以给数组元素建立索引,但是不能给数组本身建立索引。给数组元素建立索引即给数组的每一个元素建立索引,因此数组索引的代价要高于单值索引的代价。并且,一个索引中的数组字段最多只允许有一个,以防止多个数组建立索引之后索引条目爆炸式增长。
多键索引:在索引中,如果某个键是数组,那么这个索引会被标记为多键索引。多键索引会比非多键慢一些,因为可能会有多个索引条目指向同一个文档,因此MongoDB在返回结果集时需要先去重。
索引基数:就是指某个字段对应不同值的数量。例如性别可能只有两种取值,这种字段的基数就比较低;像用户名、住址这类字段可能每个值和其他的都不一样,这类字段的基数就很高;也有介于两者之间的,例如年龄。通常情况下,一个字段的基数越高,那么对这个字段建立的索引越有价值,因为这个索引可以快速将查询范围缩小到一个比较小的结果集,以供下一步操作快速完成。因此建立索引应该选择基数高的字段,或者在复合索引中将基数高的字段提前。
5.2 使用explain()和hint()
explain()
能够提供大量与查询相关的信息,其中返回的一些字段有助于发现查询速度慢的原因。以下列举一些字段:
(1)cursor
:表示游标的类型。可以看出本次查询是否使用了索引以及使用的什么索引。
(2)isMultiKey
:是否使用了多键索引。
(3)n
:本次查询返回的文档数量。
(4)nscannedObjects
:MongoDB按照索引指针去磁盘上查找文档的次数。如果查询包含的查询条件不是索引的一部分,或者要求返回的内容不在索引字段内,MongoDB就会从磁盘中读取原文档。
(5)nscanned
:如果使用了索引,这个字段表示查找过的索引条目,如果没有使用索引,那么这个字段表示查找的文档数量。
(6)scanAndOrder
:MongoDB是否对内存中的结果进行了排序。
(7)indexOnly
:MongoDB是否只是用了索引就完成了此次查询,即包括查找字段以及返回的结果集都包含在索引中,又称覆盖索引。
(8)nYields
:查询暂停次数,如果有写入请求,查询会周期性释放锁,以供写入执行。
(9)millis
:查询所耗毫秒数。
(10)indexBounds
:这个字段描述了索引的使用情况,给出了索引的遍历范围。
hint()
可以指定查询使用的索引。
查询优化器:与关系型数据库不同,当有多个索引可能适合本次查询时,MongoDB的查询优化器会并行执行这些查询,最早返回100个结果的索引就是“胜者”,并且这个索引会被缓存,在接下来的这个查询都会使用这个索引。如果建立了新的索引或者每执行1000次查询后,查询优化器就会重新评估查询计划。explain()
返回的allPlans
字段表示了本次查询尝试的每个查询计划。
5.3 何时不应该使用索引
如果数据集较小时,索引会十分高效。但是当结果集在原集合中所占比例越大,索引查找的速度就越慢。因为使用索引需要进行两次查找,最坏情况下索引查找的时间是全表查找的两倍。一般来说,当查询需返回30%的文档或更多时,就需要对比索引查找和全表查找的速度。
5.4 索引类型
唯一索引:唯一索引可以确保集合的每一个文档的指定键都有唯一值。例如:
db.users.ensureIndex({"username": 1}, {"unique": true});
这样可以防止有重复键的插入。但是如果有第一个没有该键的文档插入时,该键将被创建,值会置为null
,如果继续插入缺少该键的文档就会报错。
进行索引的所有字段都应当小于1024字节,否则这个条目将不会出现在索引中。如果唯一索引的字段超过8KB,唯一索引将会失效。
可以创建复合的唯一索引,复合唯一索引中,单个键的值可以重复,但是所有键的值的组合必须在集合中是唯一的。
稀疏索引:区别与关系型数据库中的稀疏索引,这里的稀疏索引是指如果文档包含指定字段,则为这些文档建立索引。如果和唯一索引一起使用,则这些字段的值必须唯一。
5.5 索引管理
索引的元信息保存在system.indexes
中,可以通过db.collectionName.getIndexes()
来查看集合的索引。
索引的标识形如keyname1_dir1_keyname2_dir2_..._keynameN_dirN
,索引标识也是有长度限制的。
如果需要删除索引,可以使用dropIndex
。