好好学习,天天向上

MongoDB初识之查询

查询

find

使用find进行查询,返回一个集合中文档的子集,子集合的范围从0个文档到整个集合。 find ( query , fields , limit , skip, batchSize, options ) * query:查询器文档,决定了要返回哪些文档,这个参数说明了要执行的查询细节 1. 默认为{},表示匹配集合的全部内容。例如:> db.c.find() 2. 支持多个键/值对的方式将多个查询条件组合在一起。例如:> db.users.find({"username":"joe","age":27}) * fields:指定返回的键 1. 使用此参数的好处:节省传输的数据量,节省客户端解码文档的时间和内存消耗 2. 使用举例:> db.users.find({},{"username":1,"emails":1}) 3. 注意,"_id"这个键总是被返回,即便是没有指定也一样 4. 也可以用来剔除查询结果中的某个键/值对。例如:> db.users.find({},{"emails":0}) * limit * skip * batchSize * options

查询条件

比较查询

四种比较操作符:"\(lt"(<), "\)lte"(<=), "\(gt"(>), "\)gte"(>=)

1
2
# 查询在18~30岁(含)的用户
> db.users.find({"age":{"$gte":18,"$lte":30}})
条件操作符:"\(ne"(<>,用于所有类型的数据)
1
2
# 查询所有名字不为joe的用户
> db.users.find({"username":{"$ne":"joe"}})
### OR查询 两种方式 * "\)
in"用来查询一个键的多个值 * "$or"用来完成多个键值的任意给定值
1
2
3
4
5
6
7
8
# 在raffle中查找ticket_no在[725,542,390]中的文档
# "$in"还可以指定不同类型的条件和值
> db.raffle.find({"ticket_no":{"$in":[725,542,390]}})
# "$nin"将返回与数组中所有条件都不匹配的文档
# 下面这个语句就是查找ticket_no不在[725,542,390]中的文档
> db.raffle.find({"ticket_no":{"$nin":[725,542,390]}})
# "$or"接受一个包含所有可能条件的数组作为参数
> db.raffle.find({"$or":[{"ticket_no":725},{"winner":true}]})
注意:对于OR类型的查询,第一个条件尽可能匹配更多的文档,这样才最为有效

\(not "\)not":元条件句,可以用在任何其他条件之上。与正则表达式联合使用,可以用来查找那些与特定模式不符的文档。

条件句 V.S 修改器

  1. 条件句是内层文档的键,修改器则是外层文档的键
  2. 可以对一个键应用多个条件,但是不能对一个键应用多个更新修改器

特定于类型的查询

null

  1. null可以匹配自身
  2. 还可以匹配不存在。也就是说,这种匹配会返回缺少这个键的所有文档
    1
    2
    3
    # 下面这个语句会返回name值为null的文档
    # 还会返回没有键值name的所有文档
    > db.users.find({"name":null})
  3. 若只想匹配键值为null的文档,既要检查该键的值是否为null,还要通过"$exists"条件判定键值已存在
    1
    > db.c.find({"z":{"$in":[null],"$exists":true}})
    ### 正则表达式 MongoDB使用Perl兼容的正则表达式(PCRE)库来匹配正则表达式。
    1
    2
    3
    4
    5
    6
    7
    # 忽略大小写:查找所有名为Joe或者joe的用户
    > db.users.find({"username":/joe/i})
    # 匹配各种大小写组合形式的joe
    > db.users.find({"username":/joe?/i})
    # 还可以匹配自身
    > db.foo.insert({"bar":/baz/})
    > db.foo.find({"bar":/baz/})
    ### 查询数组
  • $all:匹配一组元素,元素的顺序无关紧要
    1
    2
    3
    4
    5
    6
    7
    > db.food.insert({"_id":1,"fruit":["apple","kumquat","orange"]})
    > db.food.insert({"_id":2,"fruit":["apple","kumquat","orange"]})
    > db.food.insert({"_id":3,"fruit":["cherry","banana","apple"]})
    # 查找既有"apple"又有"banana"的文档
    > db.food.find({"fruit":{$all:["apple","banana"]}})
    { "_id" : 1, "fruit" : [ "apple", "banana", "peach" ] }
    { "_id" : 3, "fruit" : [ "cherry", "banana", "apple" ] }
  • \(size:查询指定长度的数组
    1
    > db.food.find({"fruit":{"$size":3}})
    注意:"\)
    size"不能与其他查询子句组合,但是这种查询可以通过在文档中添加一个"size"键的方式来实现。
  • \(slice:返回数组的一个子集合
    1
    2
    3
    4
    5
    # 返回前10条评论。如果要返回后10条,使用-10即可
    > db.blog.posts.findOne({},{"comments":{"$slice":10}})
    # $slice接受偏移值和要返回的元素数量
    # 下面这个语句返回第24~33个元素
    > db.blog.posts.findOne({},{"comments":{"$slice":[23,10]}})
    注意:如无特别声明,使用\)
    slice会返回文档中所有键。

查询内嵌文档

两种方式: * 查询整个文档:与普通查询完全相同。要注意,这种查询与顺序是相关的。 * 只针对其键/值对进行查询

1
> db.people.find({"name.first":"joe","name.last":"Schemoe"})
说明:点表示法是查询文档区别于其他文档的主要特点,查询文档时使用表示“深入内嵌文档内部”。因此,也要求待插入的文档不能包含“.”

\(where查询 使用"\)where"子句,可以执行任意JavaScript作为查询的一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> db.foo.insert({"apple":1,"banana":6,"peach":3})
> db.foo.insert({"apple":8,"spinach":4,"watermelon":4})
# 比较文档中两个键的值是否相等
# 若函数返回true,文档就作为结果的一部分被返回;若为false,则不然
> 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"查询。因为它们在速度上比常规查询慢得多(每个文档都要从BSON转换成JavaScript对象,然后通过\)where表达式运行。而且还不能用索引)

游标

数据库使用游标返回find的执行结果

1
2
3
4
5
6
7
8
9
10
11
# 使用cursor变量保存结果
> var cursor = db.c.find()
# 使用游标的next方法来迭代结果,hasNext方法检查是否有后续结果存在
> while (cursor.hasNext()) {
... obj = cursor.next()
... // do stuff
... }
# 游标类实现了迭代器接口,可以在foreach循环中使用
> cursor.forEach(function(x) {
... print (x);
... });
### limit, skip和sort 最常用的查询选项是限制返回结果的数量(limit),忽略一定数量的结果(skip)并排序(sort)。所有这些选项一定要在查询被派发到服务器之前添加。
1
2
3
4
5
6
7
8
9
10
# 限制结果数量,limit指定上限
> db.c.find().limit(3)
# 略过前n个匹配的文档,返回如下的文档。若匹配的文档少于n个,则不会返回任何文档
> db.c.find().skip(3)
# sort用一个对象作为参数:一组键/值对。
# 键对应文档的键名,值代表排序的方向(1:升序;-1:降序)。
# 若指定了多个键,则按照多个键的顺序逐个排序
> db.c.find().sort({x:1,age:-1})
# 可以组合来用。例如:分页
> db.stock.find({"desc":"mp3"}).limit(50).skip(50).sort({"price":-1})
对于混合类型的键排序顺序是预先定义好的。从小到大,顺序如下: 1. 最小值 2. null 3. 数字(整型、长整型、双精度) 4. 字符串 5. 对象/文档 6. 数组 7. 二进制数据 8. 对象ID 9. 布尔型 10. 日期型 11. 时间戳 12. 正则表达式 13. 最大值

避免使用skip略过大量结果

当skip的数量非常多的时候,会变得很慢,所以要尽量避免 * 不用skip对结果分页:一般来讲,可以找到一种方法实现不用skip的分页。 * 随机选取文档:可以在插入文档时给每个文档都添加一个额外的随机键以避免skip

高级查询选项

两种查询: 1. 普通查询:> var cursor = db.foo.find({"foo":"bar"}) 2. 包装查询:> var cursor = db.foo.find({"foo":"bar"}).sort({"x":1})。这种查询实际上会被转化为db.foo.find({"$query":{"foo":"bar"}, "$orderby": {"x":1}}) 下面是一些有用选项: * $maxscan:integer 指定查询最多扫描的文档数量 * $min:document 查询的开始条件 * $max:document 查询的结束条件 * $hint:document 指定服务器使用哪个索引进行查询 * $explain:boolean 获取查询执行的细节(用到的索引、结果数量、耗时等),而并非真正执行查询 * $snapshot:boolean 确保查询的结果是在查询执行的一致快照

获取一致结果

当查找修改后存回去时,可能会出现由于文档体积增加而预留空间不足造成的移动。通常会将其挪至集合的末尾处。这样的情况下,游标有可能会返回那些已经被挪动的文档。 解决方法是对查询进行快照,也就是使用"$snapshot"选项。这个时候,查询是针对不变的集合视图运行的。

在服务端的游标

游标在服务端会消耗内存和其他资源:当游标遍历尽了结果以后或者客户端发来消息要求终止,数据库才会释放这些资源 导致游标终止(随后被清理)的一些情况: 1. 游标完成匹配结果的迭代时会清理自身; 2. 当游标在客户端已经不在作用域内了,驱动会向服务器发送专门的消息让其销毁游标; 3. 只要10分钟不使用游标,数据库游标就会自动销毁。

索引

索引是用来加速查询的。 当查询中仅使用一个键时,可以对该键建立索引,以提高查询速度。

1
2
3
4
5
# ensureIndex方法:创建索引
> db.users.ensureIndex({"username":1})
# 一组值为1(升序)或-1(降序)的键,表示索引创建的方向。
# 若索引只有一个键,则方向无关紧要
> db.posts.ensureIndex({"date":1, "username":-1})
MongoDB的查询优化器会重排查询项的顺序,以便利用索引。 创建索引的缺点是每次插入、更新和删除时都会产生额外的开销(数据库不但需要执行这些操作,还要讲这些数据在集合的索引中标记)。

  • 使用索引前三问
    1. 会做什么样的查询?其中哪些键需要索引?
    2. 每个键的索引方向是怎样的?
    3. 如何应对扩展?有没有种不同键的排列可以使常用数据更多地保留在内存中?
  • 为内嵌文档的键建立索引和为普通的键创建索引没有什么区别。而对内嵌文档的键索引与普通键索引并无差异,二者可以联合组成复合索引。
  • 由于做无索引排序时有内存限制的,因此,可以按照排序来索引以便让MongoDB按照顺序提取数据。
  • 索引名字
    1. 每个索引都有一个字符串类型的名字,来唯一标识索引
    2. 默认情况下,索引名类似keyname1_dir1_keyname2_dir2_..._keynameN_dirN,其中keynameX代表索引的键,dirX代表索引的方向(-1/1)
    3. 可以自定义索引名字,例如:> db.foo.ensureIndex({"a":1,"b":-1}, {"name":"alphabet"})

唯一索引

唯一索引可以确保集合的每一个文档的指定键都有唯一值

1
2
# 创建唯一索引:{"unique":true}
> db.users.ensureIndex({"username":1},{"unique":true})
* 记住,默认情况下,insert不检查文档是否插入过了。因此,可以使用安全插入。 * 对"_id"的索引是在创建普通集合时一通创建的。这个索引和普通唯一索引只有一点不同,就是不能删除。 * 插入时,若无对应键,索引会将其作为null存储。所以,如果对某个键建立了唯一索引,但插入了多个缺少该索引键的文档,则由于文档包含null值而导致插入失败。 * 当为已有集合创建索引时,若有些值已经重复了,则会创建索引失败。此时,可以使用dropDups选项保留发现的第一个文档,删除接下来的有重复值的文档。例如:> db.users.ensureIndex({"username":1},{"unique":true, "dropDups:true"}) * 创建复合唯一索引时,单个键的值可以相同,只要所有键的值组合起来不同即可。

explain和hint

  • explain:获得查询方面的信息。
    1
    2
    3
    # explain会返回一个文档
    # 文档中包含了查询使用的索引情况(若有),耗时及扫描文档数的统计信息。
    > db.users.find({"age":18}).sort({"username":1}).explain()
  • hint:当发现MongoDB用了非预期的索引,可以用hint强制使用某个索引
    1
    > db.c.find({"age":14, "username":/.*/}).hint({"username":1,"age":1})
    ## 索引管理 索引的元信息存储在每个数据库的system.indexes集合中。这个是个保留集合,不能对其插入或删除文档。操作只能通过ensureIndex或者dropIndexes进行。 system.namespaces集合含有索引的名字。里面包含了两种信息,集合本身和对应集合包含的索引。 注意:集合名和索引名加起来不能超过127字节。
    1
    2
    # 使用background选项让建立索引操作在后台完成,同时不阻塞正常请求
    > db.users.ensureIndex({"username":1},{"background":true})
    为已有文档创建索引比先创建索引再插入所有文档要快一点。
    1
    2
    3
    4
    5
    6
    # dropIndexes:删除索引
    # 通常,要查一下system.indexes集合来找出索引名
    > db.runCommand({"dropIndexes":"foo","index":"alphabet"})
    # 删除所有索引
    > db.runCommand({"dropIndexes":"users","index":"*"})
    # 这里注意,删除集合也会删除索引。
    ## 地理空间索引 地理空间索引:MongoDB为坐标平面查询提供的专门的索引
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 参数"2d":创建地理空间索引
    # "gps"键的值必须是某种形式的一对值:
    # 一个包含两个元素的数组或是包含两个键的内嵌文档
    > db.map.ensureIndex({"gps":"2d"})
    # 默认情况下,地理空间索引假设值的范围是-180~180
    # 可以通过ensureIndex选项来指定最大最小值
    > db.star.trek.ensureIndex({"light-years":"2d"},{"min":-1000,"max":1000})
    # 查询方法一:使用find命令+"$near"
    # 按照离点(40,-73)由近到远的方式将map集合的所有文档都返回
    > db.map.find({"gps":{"$near":[40,-73]}})
    # 查询方法二:使用geoNear命令
    # geoNear还会返回每个文档到查询点的距离。
    > db.runCommand({geoNear:"map",near:[40,-73]})
    # 创建复合地理空间索引
    > db.map.ensureIndex({"location":"2d","desc"1})
    # 查找
    > db.map.find({"location":{"$near":[-70,30]},"desc":"coffeeshop"})
    # 聚合 ## count:返回集合中文档的数量
    1
    2
    3
    4
    5
    # 没有传参时,返回总文档数
    > db.foo.count()
    # 可以传递查询,Mongo会计算查询结果的数量
    # 注意:增加查询条件会使得count变慢。因为这相当于先查询出来再计数。
    > db.foo.count({"x":1})
    ## distinct:找出给定键的所有不同的值
    1
    2
    # 使用distinct时必须指定集合和键
    > db.runCommand({"distinct":"users", "key":"age"})
    ## group 过程:选定分组所依据的键 -> 将集合依据选定键值的不同分成若干组 —> 聚合每一组内的文档 -> 产生一个结果文档
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    > db.runCommand({"group": {
    ... "ns":"stocks", # 指定要进行分组的集合
    ... "key":"day", # 指定文档分组依据的键
    ... "initial":{"time":0}, # 每一组reduce函数调用的初始时间,会作为初始文档传递给后续过程。每一组的所有成员都会使用这个累加器
    ... "$reduce":function(doc,prev) { # 每个文档都对应一次这个吊用。系统传递两个参数:当前文档和累加器文档
    ... if (doc.time > prev.time) {
    ... prev.price = doc.price;
    ... prev.time = doc.time;
    ... }
    ... }}})
    # "finalizer":完成器,用以精简从数据库传到用户的数据
    # "$keyf":定义分组函数
    ## MapReduce MapReduce会拆分问题,再将各个部分发送到不同的机器上,让每台机器都完成一部分。当所有机器都完成的时候,再把结果汇集起来形成最终完整的结果。

MapReduce的步骤: 1. 映射(map):将操作映射到集合中的每个文档。 2. 洗牌(shuffle):按照键分组,并将产生的键值组成列表放到对应的键中 3. 化简(reduce):把列表中的值化简成一个单值。接着进行洗牌,直到每个键的列表只有一个值为止,这个值就是最后结果。

使用MapReduce的代价,就是速度。

1
2
3
4
5
> map = function() {....} # 使用emit函数“返回”要处理的值
> reduce = function(key, emits) {....} # reduce应该能处理emit文档和其他reduce结果的各种组合
> mr = db.runCommand({"mapreduce":"foo", "map":map,"reduce": reduce})
# 对结果集合进行查询
> db[mr.result].find()

MapReduce命令的其他可选的键: * "finalize":函数 将reduce的结果发送给这个键,这是处理过程的最后一步 * "keeptemp":布尔 连接关闭时临时结果集合是否保存 * "output":字符串 结果集合的名字。设定该项隐含keeptemp:true * "query":文档 会在发往map函数前,先用指定条件过滤文档 * "sort":文档 在发往map前先给文档排序 * "limit":整数 发往map函数的文档数量的上限 * "scope":文档 JavaScript代码中要用到的变量 * "verbose":布尔 是否产生更详尽的服务器日志

请言小午吃个甜筒~~