1. 如何判断查询是否命中索引
游标调用explain函数会返回一个文档,用于描述当前查询的一些细节信息。
比如
01.
> db.blogs.find({
"comment.author"
:
"joe"
}).explain();
02.
{
03.
"cursor"
:
"BtreeCursor comment.author_1"
,
04.
"nscanned"
: 1,
05.
"nscannedObjects"
: 1,
06.
"n"
: 1,
07.
"millis"
: 70,
08.
"nYields"
: 0,
09.
"nChunkSkips"
: 0,
10.
"isMultiKey"
:
true
,
11.
"indexOnly"
:
false
,
12.
"indexBounds"
: {
13.
"comment.author"
: [
14.
[
15.
"joe"
,
16.
"joe"
17.
]
18.
]
19.
}
20.
}
1》 “cursor”:因为这个查询使用了索引,MongoDB中索引存储在B树结构中,所以这是也使用了BtreeCursor类型的游标。如果没有使用索引,游标的类型是BasicCursor。这个键还会给出你所使用的索引的名称,你通过这个名称可以查看当前数据库下的system.indexes集合(系统自动创建,由于存储索引信息,这个稍微会提到)来得到索引的详细信息。
2》 “nscanned”/“nscannedObjects”:表明当前这次查询一共扫描了集合中多少个文档,我们的目的是,让这个数值和返回文档的数量越接近越好。
3》 "n":当前查询返回的文档数量。
4》 “millis”:当前查询所需时间,毫秒数。
5》 “indexBounds”:当前查询具体使用的索引
01.
>
var
cursor = db.user.find({
"age"
:40,
"name"
:
"tim"
}).hint({
"age"
:1,
"name"
:1});
02.
> cursor.explain();
03.
{
04.
"cursor"
:
"BtreeCursor age_1_name_1"
,
05.
"nscanned"
: 1,
06.
"nscannedObjects"
: 1,
07.
"n"
: 1,
08.
"millis"
: 0,
09.
"nYields"
: 0,
10.
"nChunkSkips"
: 0,
11.
"isMultiKey"
:
false
,
12.
"indexOnly"
:
false
,
13.
"indexBounds"
: {
14.
"age"
: [
15.
[
16.
40,
17.
40
18.
]
19.
],
20.
"name"
: [
21.
[
22.
"tim"
,
23.
"tim"
24.
]
25.
]
26.
}
27.
}
28.
>
我们看,hint函数不同于explain函数,会返回游标,我们可以在游标上调用explain查看索引的使用情况!99%的情况,我们没有必要通过hint去强制使用某个索引,MongoDB的查询优化器非常智能,绝对能帮助我们使用最佳的索引去进行查询!
2.万能查询 $where
01.
> db.fruitprice.find({
"$where"
:
function
() {
02.
for
(
var
current
in
this
){
03.
for
(
var
other
in
this
){
04.
if
(current != other &&
this
[current] ==
this
[other]){
05.
return
true
;
06.
}
07.
}
08.
}
09.
return
false
;
10.
}});
11.
12.
//打印结果如下
13.
{
"_id"
: ObjectId(
"50226ba63becfacce6a22a5c"
),
"apple"
: 10,
"watermelon"
: 3,
"pear"
: 3 }
我们可以看出,使用"$where"其实就是写了一个javascript函数,MongoDB在查询时,会将每个文档转换成一个javascript对象,然后扔到这个函数中去执行,通过返回结果来判断其是否匹配!在实际使用中,尽量避免使用”$where" 条件操作符,因为其性能很差!在执行过程中,需要把每个档案转化为javascript对象!如果不可避免,则尽量这样写:find({”other“:”......“,......,“$where”:""}),即将"$where"放最后,作为结果调优,让常规查询作为前置过滤条件!这样能减少一些性能损失!
我们这里还可以发现,“$where”条件操作符也是作为外层文档的键使用
3.数据库命令
【命令的工作原理】
MongoDB中的命令其实是作为一种“特殊类型的查询”来实现的!这些查询是针对$cmd集合来执行(通过调用findOne函数)。runCmmand命令就是接受一个文档(注意该参数文档的键区分大小写!),来执行等价的查询操作。我们举个例子:通过执行命令和等价查询来删除一个集合
01.
> show collections;
02.
system.indexes
03.
test
04.
> db.runCommand({
"drop"
:
"test"
});
05.
{
06.
"nIndexesWas"
: 1,
07.
"msg"
:
"indexes dropped for collection"
,
08.
"ns"
:
"mylearndb.test"
,
09.
"ok"
: 1
10.
}
11.
> show collections;
12.
system.indexes
13.
> db.test.insert({
"x"
: 1});
14.
> show collections;
15.
system.indexes
16.
test
17.
> db.$cmd.findOne({
"drop"
:
"test"
});
18.
{
19.
"nIndexesWas"
: 1,
20.
"msg"
:
"indexes dropped for collection"
,
21.
"ns"
:
"mylearndb.test"
,
22.
"ok"
: 1
23.
}
24.
> show collections;
25.
system.indexes
26.
>
上面我们是通过执行命令和其对应的等价查询的方式删除同一个集合。当MongoDB服务器得到查询$cmd集合的请求时,会启动一套特殊的逻辑来处理!而不像别的集合那样来执行。几乎所有MongoDB的驱动程序都支持类似runCommand的执行命令的方式,也基本都支持其对应的等价查询这种执行方式。
在Shell中通过db.listCommands(),来看当前MongoDB支持的所有命令(同样可通过运行命令db.runCommand({"listCommands" : `1})来查询所有命令)。我们说几个常用的命令:
1》 db.runCommand({"buildInfo" : 1}):返回MongoDB服务器的版本号和服务器OS的相关信息。
2》 db.runCommand({"collStats" : 集合名}):返回该集合的统计信息,包括数据大小,已分配存储空间大小,索引的大小等。
3》 db.runCommand({"distinct" : 集合名, "key" : 键, "query" : 查询文档}):返回特定文档所有符合查询文档指定条件的文档的指定键的所有不同的值。
4》 db.runCommand({"dropDatabase" : 1}):清空当前数据库的信息,包括删除所有的集合和索引。
5》 db.runCommand({"isMaster" : 1}):检查本服务器是主服务器还是从服务器。
6》 db.runCommand({"ping" : 1}):检查服务器链接是否正常。即便服务器上锁,该命令也会立即返回。
7》 db.runCommand({"repaireDatabase" : 1}):对当前数据库进行修复并压缩,如果数据库特别大,这个命令会非常耗时。
8》 db.runCommand({"serverStatus" : 1}):查看这台服务器的管理统计信息。
我们前面提到了,某些命令必须在admin数据库(从权限角度看,是root数据库)下运行,我们再看两个这样的命令:
9》 db.runCommand({"renameCollection" : 集合名, "to":集合名}):对集合重命名,注意两个集合名都要是完整的集合命名空间,如foo.bar, 表示数据库foo下的集合bar。
10》db.runCommand({"listDatabases" : 1}):列出服务器上所有的数据库!
4.固定集合
前面提到的所有集合,都是动态创建的(向一个不存在的集合插入文档的同时会创建该集合)。这种集合会随着数据的增多而自动扩容。MongoDB同时还支持另外一种集合---固定集合。这种集合的特征就是,需要提前创建,并且大小固定!对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素!
在shell中,我们通过createCollection来显示创建一个固定集合:
1.
> db.createCollection(
"myFixedColl"
, {
"capped"
:
true
,
"size"
: 100000,
2.
...
"max"
: 100} );
3.
{
"ok"
: 1 }
4.
>
上面,我们创建了名称为myFixedColl的固定集合,其固定大小为100000字节,最多放置文档数目为100个。这里需要强调,可以不指定max,但必须指定size,当同时指定了size和max,当size没有达到上限时,以max来控制文档数量,当size达到上限时,以size为准!
我们还可以使用命令convertToCapped,将一个普通集合转化为一个固定集合:
1.
> db.runCommand({
"convertToCapped"
:
"testnew"
,
"size"
: 10000});
2.
{
"ok"
: 1 }
3.
>
上述命令将一个普通集合testnew转化为一个大小为10000字节的固定集合。
【固定集合的特性和使用限制】
固定集合本质上和普通集合不同,也为其赋予了很多特性。首先,默认情况下,固定集合没有索引,列"_id"上也不存在索引。其插入速度极快,直接在集合的尾部插入即可,有必要的话会进行自动覆盖,文档在集合中的存储顺序就是其插入顺序。对于查询,固定集合会按照插入顺序返回文档列表,速度也十分了得!
使用固定集合也有一些限制,如不能删除文档(自动淘汰不算),更新文档不能导致文档移动(即更新不能将原始文档尺寸一定范围,这个范围就是预留的文档补白大小),否则更新会失败!
【自然排序】
相对于其他集合的排序,固定集合有种特殊的排序方式:自然排序。自然顺序就是文档在磁盘上的存储顺序!因为固定集合的存储顺序就是插入文档的顺序,因此自然顺序就是插入顺序!
01.
> db.myFixedColl.insert({
"name"
:
"jimmy"
,
"age"
: 40,
"job"
:
"programmer"
});
02.
> db.myFixedColl.insert({
"name"
:
"tom"
,
"age"
: 60,
"job"
:
"manager"
});
03.
> db.myFixedColl.insert({
"name"
:
"tim"
,
"age"
: 50,
"job"
:
"jgs"
});
04.
> db.myFixedColl.find();
05.
{
"_id"
: ObjectId(
"5037324500275ae6127c6ada"
),
"name"
:
"jimmy"
,
"age"
: 40, "j
06.
ob
" : "
programmer" }
07.
{
"_id"
: ObjectId(
"5037325400275ae6127c6adb"
),
"name"
:
"tom"
,
"age"
: 60, "job
08.
" : "
manager" }
09.
{
"_id"
: ObjectId(
"5037326400275ae6127c6adc"
),
"name"
:
"tim"
,
"age"
: 50, "job
10.
" : "
jgs" }
11.
> db.myFixedColl.find().sort({
"$natural"
: 1});
12.
{
"_id"
: ObjectId(
"5037324500275ae6127c6ada"
),
"name"
:
"jimmy"
,
"age"
: 40, "j
13.
ob
" : "
programmer" }
14.
{
"_id"
: ObjectId(
"5037325400275ae6127c6adb"
),
"name"
:
"tom"
,
"age"
: 60, "job
15.
" : "
manager" }
16.
{
"_id"
: ObjectId(
"5037326400275ae6127c6adc"
),
"name"
:
"tim"
,
"age"
: 50, "job
17.
" : "
jgs" }
18.
> db.myFixedColl.find().sort({
"$natural"
: -1});
19.
{
"_id"
: ObjectId(
"5037326400275ae6127c6adc"
),
"name"
:
"tim"
,
"age"
: 50, "job
20.
" : "
jgs" }
21.
{
"_id"
: ObjectId(
"5037325400275ae6127c6adb"
),
"name"
:
"tom"
,
"age"
: 60, "job
22.
" : "
manager" }
23.
{
"_id"
: ObjectId(
"5037324500275ae6127c6ada"
),
"name"
:
"jimmy"
,
"age"
: 40, "j
24.
ob
" : "
programmer" }
25.
>
上述我们向一个固定集合中插入3条数据,直接调用find(),会按照插入顺序返回文档。对游标按自然正序({"$natural":1})处理后,返回的还是插入顺序,对游标按自然倒序({"$natural":-1})处理后,返回的就是插入的倒序。对于普通集合,因为其存储顺序与插入顺序没有必然的联系,所以自然排序在普通集合上使用没有多大意义!
5.服务器端脚本
在MongoDB的服务器端可以通过db.eval函数来执行javascript脚本,如我们可以定义一个javascript函数,然后通过db.eval在服务器端来运行!我们前面其实也接触过在服务器段运行一个预定义的javascript脚本的情况,如在$where查询,执行mapreduce任务等。我们先看看db.eval是如何使用的:
1.
> db.eval(
"return 1"
);
2.
1
3.
> db.eval(
"function(){return 9 + 8}"
);
4.
17
5.
> db.eval(
"function(x,y,z,k){return x*y + z*k}"
, 2,5, 8, 10);
6.
90
7.
>
对于db.eval,如果接受的javascript串,不需要什么参数,就可以直接书写!需要传递参数,就可以定义成函数,然后在调用时,eval函数从第二个参数起就分别代表需要传递给脚本的参数!
每个MongoDB数据库都会有一个特殊的结合system.js,用来存在javascript变量和脚本,这些变量和脚本可以在db.eval调用、$where子句、mapreduce任务中直接使用:
01.
> use mylearndb;
02.
switched to db mylearndb
03.
> db.system.js.insert({
"_id"
:
"x"
,
"value"
:11});
04.
> db.system.js.insert({
"_id"
:
"y"
,
"value"
:10});
05.
> db.system.js.insert({
"_id"
:
"z"
,
"value"
:9});
06.
> db.system.js.insert({
"_id"
:
"k"
,
"value"
:7});
07.
> db.eval(
"return x*y + z*k"
);
08.
173
09.
>
上例中,我们往system.js中保存了几个变量,然后可以在db.eval中直接使用!我们同样可以将函数保存在system.js中,对于system.js中的文档,"_id"表示的是变量名或函数名,“value”表示的是变量值或函数定义!对于一些常用的函数,我们可以将其存储在system.js集合中,这样可以进行将某些业务逻辑进行封装统一管理,也减少了网络传输时间(感觉这个非常像关系型数据库的存储过程!)。
使用服务器端脚本,如果需要客户端传递参数,需要严格处理这些参数,增强安全性!防止类似于关系型数据库中的脚本注入式攻击的出现!
6.DBRef
其就像关系型数据库中的外键的概念,让一个文档引用另外一个或多个其他文档。
DBRef的使用形式是一个内嵌文档,和其他内嵌文档的结构也一致。但这个内嵌文档有一些必选键,"$ref":指示一个集合,"$id":具体指示一条文档。如果该文档引用的文档在其他数据库中,这个内嵌文档也支持一个可选键:"$db":指示一个数据库!注意这个内嵌文档中键的顺序不能改变:第一个必须是"$ref",紧接着是"$id",然后是可选的"$db"。我们看一个例子,有两个集合,users和notes,用户(user)可以创建笔记(note),在notes集合中,每条笔记会引用一个用户:
01.
> db.users.find();
02.
{
"_id"
:
"01"
,
"name"
:
"jimmy"
,
"email"
:
"[email protected]"
,
"age"
: 23 }
03.
{
"_id"
:
"02"
,
"name"
:
"tim"
,
"email"
:
"[email protected]"
,
"age"
: 33 }
04.
> db.notes.find();
05.
{
"_id"
:
"001"
,
"content"
:
"Mongo is fun"
,
"references"
: {
"$ref"
:
"users"
,
"$id"
:
"01"
} }
06.
{
"_id"
:
"002"
,
"content"
:
"Mongo is too hard to learn"
,
"references"
: {
"$ref"
:
"users"
,
"$id"
:
"02"
} }
07.
>
var
note = db.notes.findOne({
"_id"
:
"001"
});
08.
> printjson(db[note.references.$ref].findOne({
"_id"
:note.references.$id}));
09.
{
"_id"
:
"01"
,
"name"
:
"jimmy"
,
"email"
:
"[email protected]"
,
"age"
: 23 }
10.
>
通过DBRef的使用,我们可以在应用层将一个文档引用的其他文档得到!但如果都是在应用层如上处理,我们何必要使用DBRef呢?我们完全可以自定义一种更轻量级的引用方式,何必要写成内嵌文档的格式,而且还有必选键?
选择使用DBRef的一个原因是,各类数据库驱动对DBRef的一些内置的支持,部分数据库驱动甚至将DBRef作为一种特殊的类型来使用!还有,如果我们要引用的文档来自另一个数据库中,DBRef这种方式也算一种比较紧凑的方式了,可以考虑直接使用。
我们上面提到的GridFS中,就有文档引用这种情况,集合fs.chunks中的文档会引用fs.files中的文档,其没有采用DBRef这种方式,而是直接使用了键“files_id”来进行最直接的引用。采用这种方式的原因是,被引用的文档的集合在同一个数据库中并且固定,这种情况下,我们没必要使用DBRef这种稍显复杂的方式。
7.Gridfs
GridFS是一个建立在普通MongoDB基础上的轻量级文件存储系统。GridFS的一个基本思想就是将大的文件分成很多块,每块作为一个单独的文档进行存储(MongoDB本身支持在文档中存储二进制数据)。除了这些分拆的数据文档外,还有一个单独的文档用于记录各个存储块的相关信息和文件的元数据信息。那如何使用GridFS呢?
使用GridFS的方法最简单的就是利用mongofiles实用程序(就是一个GridFS的客户端)。mongofiles内置在MongoDB的发布版中,可以通过它在GridFS中上传、下载、列示、查找、删除文件。其对应的操作命令是:put,get、list、search、delete,我们分别演示一下:
01.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles put E:\mongodb\mongofiles\file1.txt
02.
connected to: 127.0.0.1
03.
added file: { _id: ObjectId(
'503829eefb3029a75e1ec5ac'
), filename:
"E:\mongodb\mongofiles\file1.txt"
, chunkSize: 262144, uploadDate:
new
Date(13458580
04.
31228), md5:
"3eb9198f3d7013e253faf411be510cf6"
, length: 169 }
05.
done!
06.
07.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles list
08.
connected to: 127.0.0.1
09.
E:\mongodb\mongofiles\file1.txt 169
10.
11.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles get E:\mongodb\mongofiles\file1.txt
12.
connected to: 127.0.0.1
13.
done write to: E:\mongodb\mongofiles\file1.txt
14.
15.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles search file1
16.
connected to: 127.0.0.1
17.
E:\mongodb\mongofiles\file1.txt 169
18.
19.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles
delete
E:\mongodb\mongofiles\file1.txt
20.
connected to: 127.0.0.1
21.
done!
22.
23.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles list
24.
connected to: 127.0.0.1
25.
26.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>
上面演示的就是使用mongofiles命令,但我们看到put命令将文件存入到数据库中,默认使用当前的文件名作为文件在GridFS中的文件名,get操作默认将文件写入到文件名对应的本地磁盘系统中(如果原始文件还存在,则覆盖)。我们可以通过选项--local(或简写-l)来改变这种行为:(可以通过mongofiles --help查看更多选项)
01.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles --local E:\mongodb\mongofiles\file1.txt put file1.txt
02.
connected to: 127.0.0.1
03.
added file: { _id: ObjectId(
'50382cdff741bb4fae624d60'
), filename:
"file1.txt"
, chunkSize: 262144, uploadDate:
new
Date(1345858783427), md5: "3eb9198f
04.
3d7013e253faf411be510cf6", length: 169 }
05.
done!
06.
07.
E:\mongodb\mongodb-win32-x86_64-2.0.6\bin>mongofiles --local D:\test\file2.txt get file1.txt
08.
connected to: 127.0.0.1
09.
done write to: D:\test\file2.txt
上面,我们将本地E盘的一个文件file1.txt存入到GridFS中,重新命名为file1.txt,然后又将这个文件下载保存为本地D盘的file2.txt文件。这里需要注意,通过mongofiles下载文件到特定目录中,这个目录必须存在,否则下载失败!
上面提到了GridFS存储大文件是分块进行的,GridFS默认是将这些存储块信息的文档放置在fs.chunks集合中,将文件的元数据文档放置在fs.files集合中。这两个集合都在test数据库中,我们可以看一下:
01.
> use test
02.
switched to db test
03.
> db.fs.files.find();
04.
{
"_id"
: ObjectId(
"50382cdff741bb4fae624d60"
),
"filename"
:
"file1.txt"
,
"chunkSize"
: 262144,
"uploadDate"
: ISODate(
"2012-08-25T01:39:43.427Z"
), "m
05.
d5
" : "
3eb9198f3d7013e253faf411be510cf6
", "
length" : 169 }
06.
> db.fs.chunks.find();
07.
{
"_id"
: ObjectId(
"50382cdf79f437ddd142a048"
),
"files_id"
: ObjectId(
"50382cdff741bb4fae624d60"
),
"n"
: 0,
"data"
: BinData(0,"sLK+srXYt73I+MCtyPi12b
08.
eiy821vcj2tam3ycj3tcS/p7fIyPe1xMj2tdi3vcj4t9LI9rXYt73I9rWpt8nI97fJtbnI+LfJZA0KsKLLubbZt6LLzbfJyPdzYWRmYXNkZg0KDQphc2RmYXNkZg0KDQrI9rXYt73I9rWpt6LLzbW9
09.
ICANCg0KDQrI9rWpt6jI/bj20NDStc7byL66zbj2yMu5/szYDQoNCg==") }
10.
>
知道了这两个集合,其实我们在shell中就可以查询目前GridFS中都存储了哪些文件!上述两个集合,有些键需要注意,在集合fs.chunks中,有一个键"files_id"其值就是其对应的文件元数据文档在集合fs.files中的“_id”的值;键“n”就是这个块在大文件分成的所有块中排第几个;键“data”就是这个块的数据了。集合fs.files中有一个键"md5",表示整个文件对应的md5的值,用户可以通过这个值来校验文件数据的完整性!
GridFS底层利用的就是MongoDB来存储大文件,会直接使用业已建立好的复制和分片机制来保证文件的可用性。