二 查询
这里是本系列第二篇,对应书中第三章,主要讲各种find的技巧。
find
返回指定的键
为了节省流量和客户端解读开销等等,只返回指定的键。把需要的键放在第二位参数就好了。
把要的键的值设置为1则返回,设置为0则不返回。
查询条件
大小比较包括
- $lt 小于
- $lte 小于等于
- $gt
- $gte
- $ne
1 | db.data.find({'age': {'$lt': 24, '$gt': 18}}) |
or查询
$in
1 | db.data.find({'number': {'$in': [10, 20, 30]}}) |
$nin代表不在这里头。
1 | db.data.find({'number': {'$nin': [10, 20, 30]}}) |
$or
1 | db.data.find({'$or': [{'name': 'aa'}, {'age': 18}]) |
$not
null
会匹配值是null的,以及没有此键的。
若只要匹配值是null的,需要这么写
1 | db.data.find({'z': {'$in': [null], '$exsits': true}}) |
即是存在该字段并且为null。
使用正则表达式搜索
使用/符号包裹正则表达式,不用加引号了。
1 | db.data.find({'name': /abo/}) |
查询数组
如果键是数组名,则只要值是数组中任意一个元素,都可以查询出数组来。
1 | db.data.insert({'array': [1, 2, 3]}) |
$all
需要拥有所有元素,顺序无关紧要。
1 | db.data.find({'array': {'$all': [3,1,2,4]}}) |
$size
顾名思义查询数组长度。但是不节省性能。
建议自己维护一个size字段,每次插入的时候顺便用{'$inc': {'size': 1}}
来更新它。
slice
获取返回数列的一部分,用法有二:
{'$slice': [min, max]}
返回查询后数组中的min到max间的结果{'$slice': index}
返回索引为index的元素,当index为-1时返回最后一个
嵌入查询
假设数据如下
1 | { |
第一种查询条件
1 | {'name': {'firtst': 'ABO', 'last': 'Lin'}} |
这种方法的缺点是必须精确匹配,如果name中又加入一个middle,那么就搜索不出来了。为了可以模糊搜索,我们用嵌入查询。
第二种查询条件
1 | {'name': {'elemMatch': {'firtst': 'ABO', 'last': 'Lin'}}} |
这种查法是部分匹配即可。
where查询
直接使用一个js函数来查询。函数的第一个参数this为where过滤前的结果集合。
假设需要返回集合里有重复值的集合,可以这么写
1 | db.data.find({'$where', function(){ |
不仅可以传入一个function对象,传入字符串也是可以的。比如
1 | db.data.find({'$where': 'this.x + this.y == 10'}) // 直接写判断语句 |
游标
如果用一个变量来接住find返回的结果,那这个变量就变成了游标cursor。cursor的用法无非就那样
1 | while(cursor.hasNext()){ |
注意,当产生cursor的时候,并没有马上查询数据库。真正执行查询操作的时候在cursor.hasNext()之时。这样就使得我们可以对cursor执行复杂的链式操作了。譬如:
1 | var cursor = db.data.find().sort({'x': 1}).limit(1).skip(10) |
当执行第一次hasNext的时候,立即尝试从服务器获取前100个结果,或者4MB的数据中的较小者。当第一批数据被用完了之后,再次hasNext又会取回新的一批数据,如此往复直至游标耗尽。
后续操作
- limit 限制返回的结果个数
- skip 跳过前几个结果
- sort 排序,1是正序,-1是倒序
利用skip做分页
最简单的方法如下
1 | var page1 = db.data.find(condition).limit(100) |
但是其实大剂量的skip是非常慢的,所以需要避免。
可以用一个trick来避免分页。
- 对整个文档做一次降序排序
- 取前n个作为第一页,然后取其中最小者作为the_last
- 再次find,这次条件为要比the_last更小
- 再对3产生的cursor做一次降序排序,并limit设置n作为第二页
有个疑问
对所有文档做排序就不耗性能了么?
不过想了想由于cursor是分批次取数据的,所以这个排序相当于经典的「求最大的k个数」题目了。
顺便看这个网页复习了下。可以利用堆排序找出最大k个数。
随机选取文档
当然最常见的方法就是弄个随机数来skip(nRandom)然后limit(1)。但是计算总文档数目和skip这两个操作就是比较耗时的。
比较投机取巧的方法是每次插入文档的时候都带一个随机数。当需要随机的时候只要产生一个随机数,并作为条件去筛选文档即可。(卧槽这都可以?)偶尔会遇到往一个方向(比如大于我random出来的随机数)找不到,那就反个方向。
链式查询转换
实际上,执行db.data.find(condition).sort(sortDict)
的时候,是把条件转化成了db.data.find({'$query': condition, '$orderby': sortDict})
。除了常用的$query
、$orderby
以外,还有一些有用的高级选项比如:
- $maxscan 查询最多扫描的文档数量
- $min 查询开始条件
- $max
- $hint 使用哪个索引进行查询
- $explain 不是真正的做查询,只是为了获得耗时、结果数量等信息
- $snapshot 确保查询的是执行查询那一刻的时候的快照
保证结果一致性
假设我们需要对每个文档进行修改,并且是新增加内容。
1 | cursor = db.data.find(condition) |
由于数据库对每个文档预留的大小有限,当文档变大之后,可能它就无法再塞入到原本那个位置了,数据库会把它放到最后边去。这样造成的结果是,cursor遍历到的有可能是已经前面modify过的文档。
$snapshot就解决了这个问题。它在开始查询那一瞬间就保留了文档快照,这使得后面文档的位置不管怎么挪,都不再会影响查询结果。
游标的存在时间
游标在几种情况下会销毁并释放资源
- 遍历完成之后
- 游标超出客户端作用域,驱动向服务器发送专门的消息(估计这得要求语言有像c++一样超出作用域之后执行析构函数的特性)
- 10分钟不使用之后,当然这一机制也可以通过配置来关闭