MongoDB性能篇 - 索引,explain执行计划,优化器profile,性能监控mongosniff
一、索引
MongoDB 提供了多样性的索引支持,索引信息被保存在system.indexes 中,且默认总是为_id创建索引,它的索引使用基本和MySQL 等关系型数据库一样。其实可以这样说说,索引是凌驾于数据存储系统之上的另一层系统,所以各种结构迥异的存储都有相同或相似的索引实现及使用接口并不足为奇。
1.基础索引
在字段age 上创建索引,1(升序);-1(降序):
db.users.ensureIndex({age:1})
_id 是创建表的时候自动创建的索引,此索引是不能够删除的。当系统已有大量数据时,创建索引就是个非常耗时的活,我们可以在后台执行,只需指定“backgroud:true”即可。
db.t3.ensureIndex({age:1} , {backgroud:true})
2.文档索引
索引可以任何类型的字段,甚至文档:
db.factories.insert( { name: "wwl", addr: { city: "Beijing", state: "BJ" } } );
//在addr 列上创建索引
db.factories.ensureIndex( { addr : 1 } );
//下面这个查询将会用到我们刚刚建立的索引
db.factories.find( { addr: { city: "Beijing", state: "BJ" } } );
//但是下面这个查询将不会用到索引,因为查询的顺序跟索引建立的顺序不一样
db.factories.find( { addr: { state: "BJ" , city: "Beijing"} } );
3. 组合索引
跟其它数据库产品一样,MongoDB 也是有组合索引的,下面我们将在addr.city 和addr.state上建立组合索引。当创建组合索引时,字段后面的1 表示升序,-1 表示降序,是用1 还是用-1 主要是跟排序的时候或指定范围内查询 的时候有关的。另外,升序和降序的顺序不同都会产生不同的索引
db.factories.ensureIndex( { "addr.city" : 1, "addr.state" : 1 } );
// 下面的查询都用到了这个索引
db.factories.find( { "addr.city" : "Beijing", "addr.state" : "BJ" } );
db.factories.find( { "addr.city" : "Beijing" } );
db.factories.find().sort( { "addr.city" : 1, "addr.state" : 1 } );
db.factories.find().sort( { "addr.city" : 1 } )
但 db.factories.find( { "addr.state" : "BJ" } ); 是没用上索引的。
注:只要查找的条件字段在索引中有,顺序最好与索引中一致,但如果索引中的第一个字段没有出现在条件中,则不会用索引
4. 唯一索引
只需在ensureIndex 命令中指定”unique:true”即可创建唯一索引。
db.t4.ensureIndex({firstname: 1, lastname: 1}, {unique: true});
在缺省情况下创建的索引均不是唯一索引。下面的示例将创建唯一索引,如:
> db.test.ensureIndex({"userid":1},{"unique":true})
如果再次插入userid重复的文档时,MongoDB将报错,以提示插入重复键,如:
> db.test.insert({"userid":5})
> db.test.insert({"userid":5})
E11000 duplicate key error index: test.test.$userid_1 dup key: { : 5.0 }
如果插入的文档中不包含userid键,那么该文档中该键的值为null,如果多次插入类似的文档,MongoDB将会报出同样的错误,如:
> db.test.insert({"userid1":5})
> db.test.insert({"userid1":5})
E11000 duplicate key error index: test.test.$userid_1 dup key: { : null }
如果在创建唯一索引时已经存在了重复项,我们可以通过下面的命令帮助我们在创建唯一索引时消除重复文档,仅保留发现的第一个文档,如:
--先删除刚刚创建的唯一索引。
> db.test.dropIndex({"userid":1})
--创建唯一索引,并消除重复数据。
> db.test.ensureIndex({"userid":1},{"unique":true,"dropDups":true})
--查询结果确认,重复的键确实在创建索引时已经被删除。
> db.test.find()
{ "_id" : ObjectId("4fe823c180144abd15acd52e"), "userid" : 5 }
我们同样可以创建复合唯一索引,即保证复合键值唯一即可。如:
> db.test.ensureIndex({"userid":1,"age":1},{"unique":true})
5.强制使用索引
hint 命令可以强制使用某个索引。
db.t5.find({age:{$lt:30}}).hint({name:1, age:1}).explain()
6.删除索引
//删除t3 表中的所有索引
db.t3.dropIndexes()
//删除t4 表中的firstname 索引
db.t4.dropIndex({firstname: 1})
//删除指定名字的索引
db.t4.dropIndexes("索引名")
如需删除名称为index1的索引:
db.t4.dropIndex(‘index’)
如需删除t4中的所有索引时:
db.t4.dropIndex(‘*’) _id索引不会删除
7 地理空间索引
目前网络上LBS(location based service)越来越流行,有一个应用就是查询你所在位置附件的某些场所。为了提升这种查询的速度(查询不同于上面单维度,需要搜索两个维度),MongoDB为坐标平面查询提供了专门的索引,即称作地理空间索引。
由于建立地理空间索引的键的值必须是一对值:一个包含两个数值的数组或包含两个键的内嵌文档(内嵌文档的键的名称无所谓),如:{“gps”:[123,134]},{“gps”:{“x”:123,“y”:134}},{“gps”:{“latitude”:123,“longitude”:134}}。这些文档的键“gps”,我们都可以再上面建立地理空间索引:
> db.shopstreet.find();
{ "_id" : ObjectId("502673678a84caa12e8070be"), "desc" : "coffeehouse", "gps" : [ 100, 120 ] }
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673a08a84caa12e8070c1"), "desc" : "coffee buck", "gps" : [ 106, 113 ] }
{ "_id" : ObjectId("502674028a84caa12e8070c2"), "desc" : "nike shop", "gps" : [ 109, 111 ] }
> db.shopstreet.ensureIndex({"gps" : "2d"});
>
建立地理空间索引同样调用ensureIndex方法,{"gps" : "2d"},以前建立索引键的值为1,或-1,地理空间索引的值固定为"2d"。地理空间索引默认值的范围为(-180~180)(对于经纬度很适合),但我们在创建索引时可以指定其值的范围:
> db.shopstreet.ensureIndex({"gps" : "2d"},{"min":-1000,"max":1000});
>
上面我们创建的地理空间索引值的范围为-1000~1000。
地理空间查询可以通过find或使用数据库命令。这里我们需要使用“$near”查询操作符,如下:
[javascript] view plaincopy
> db.shopstreet.find({"gps":{"$near" : [110,130]}});
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673678a84caa12e8070be"), "desc" : "coffeehouse", "gps" : [ 100, 120 ] }
{ "_id" : ObjectId("502673a08a84caa12e8070c1"), "desc" : "coffee buck", "gps" : [ 106, 113 ] }
{ "_id" : ObjectId("502674028a84caa12e8070c2"), "desc" : "nike shop", "gps" : [ 109, 111 ] }
>
这个查询会按点(110,130)来查询文档,由远及近将符合条件的文档返回,如果没有在游标上使用limit函数,默认会返回100条文档。通常我们会利用limit限制前几个最靠近的目标文档即可!我们可以使用数据库命令完成上述查询:
> db.shopstreet.ensureIndex({"gps" : "2d"},{"min":-1000,"max":1000});
> db.shopstreet.find({"gps":{"$near" : [110,130]}});
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673678a84caa12e8070be"), "desc" : "coffeehouse", "gps" : [ 100, 120 ] }
{ "_id" : ObjectId("502673a08a84caa12e8070c1"), "desc" : "coffee buck", "gps" : [ 106, 113 ] }
{ "_id" : ObjectId("502674028a84caa12e8070c2"), "desc" : "nike shop", "gps" : [ 109, 111 ] }
> db.runCommand({"geoNear":"shopstreet", "near":[110,130], "num":3});
{
"ns" : "mylearndb.shopstreet",
"near" : "1111000111111000000111111000000111111000000111111000",
"results" : [
{
"dis" : 0,
"obj" : {
"_id" : ObjectId("502673738a84caa12e8070bf"),
"desc" : "coffeebar",
"gps" : [
110,
130
]
}
},
{
"dis" : 7,
"obj" : {
"_id" : ObjectId("502673838a84caa12e8070c0"),
"desc" : "coffee king",
"gps" : [
110,
123
]
}
},
{
"dis" : 14.142135623730951,
"obj" : {
"_id" : ObjectId("502673678a84caa12e8070be"),
"desc" : "coffeehouse",
"gps" : [
100,
120
]
}
}
],
"stats" : {
"time" : 0,
"btreelocs" : 0,
"nscanned" : 5,
"objectsLoaded" : 4,
"avgDistance" : 7.04737854124365,
"maxDistance" : 14.142152482638018
},
"ok" : 1
}
>
这个命令接受一个文档,文档中键"geoNear"指明查询的集合,键"near"指明查询的基准坐标值,键"num"指定返回的结果数量!然后执行后返回如上结果,这个命令同时还会返回每个返回文档距查询点的距离,这个距离的数据单位就是你数据的单位,如上述数据位经纬度,键“dis”后面的距离数值就是经纬度!
MongoDB不但能找到靠近一个点的文档,还能找到指定形状内的文档!使用查询操作符"$within"即可,同时通过MongoDB提供的查询操作符指定形状,如“$box”可以指定矩形,“$center”可以指定圆形:
> db.shopstreet.find();
{ "_id" : ObjectId("502673678a84caa12e8070be"), "desc" : "coffeehouse", "gps" : [ 100, 120 ] }
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673a08a84caa12e8070c1"), "desc" : "coffee buck", "gps" : [ 106, 113 ] }
{ "_id" : ObjectId("502674028a84caa12e8070c2"), "desc" : "nike shop", "gps" : [ 109, 111 ] }
> db.shopstreet.find({"gps" : {"$within" : {"$box":[[109,130],[110,120]]}}});
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
通过{"$box":[[109,130],[110,120]]}指定一个矩形,其左下角和右上角坐标!“$within”指定查询在这个范围内的点。
> db.shopstreet.find();
{ "_id" : ObjectId("502673678a84caa12e8070be"), "desc" : "coffeehouse", "gps" : [ 100, 120 ] }
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673a08a84caa12e8070c1"), "desc" : "coffee buck", "gps" : [ 106, 113 ] }
{ "_id" : ObjectId("502674028a84caa12e8070c2"), "desc" : "nike shop", "gps" : [ 109, 111 ] }
> db.shopstreet.find({"gps" : {"$within" : {"$center":[[110,130],10]}}});
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
>
上述是通过指定一个圆形来查询同样的点,{"$center":[[110,130],10]},指定了圆形的圆心坐标和半径
8 复合地理空间索引
通常我们查找一个位置,不会只是通过坐标去定位,还会添加其他条件,我们构建索引时也可以用上:
> db.shopstreet.find();
{ "_id" : ObjectId("502673678a84caa12e8070be"), "desc" : "coffeehouse", "gps" : [ 100, 120 ] }
{ "_id" : ObjectId("502673738a84caa12e8070bf"), "desc" : "coffeebar", "gps" : [ 110, 130 ] }
{ "_id" : ObjectId("502673838a84caa12e8070c0"), "desc" : "coffee king", "gps" : [ 110, 123 ] }
{ "_id" : ObjectId("502673a08a84caa12e8070c1"), "desc" : "coffee buck", "gps" : [ 106, 113 ] }
{ "_id" : ObjectId("502674028a84caa12e8070c2"), "desc" : "nike shop", "gps" : [ 109, 111 ] }
> db.showstreet.ensureIndex({"gps":"2d", "desc":1});
>
上述就创建了一个复合地理空间索引,这个索引更符合实际需要!
上述就是MongoDB中索引的使用,索引是数据库查询提升效率的利器,对于任何数据库都是如此!我们应该好好掌握!
9 Sparse索引(稀疏索引)
Sparse index解决索引文件过大的问题,有时候我们要索引的某个属性并非是所有记录都有,普通的索引是将所有的记录都包含进来,而sparse索引则仅包含含有这个属性的记录,它不会对该项值为空的行作索引。这样就大大减小了某些列的索引大小。目前的限制是,sparse index只能包含一个属性。比如:在Mail中有个标签属性labels,这个属性是唯一的,且有值的情况也不多,这种情况就最适合用sparse索引了,创建索引的命令为:
db.Mail.ensureIndex({labels:1},{sparse:true})
10 Covered 索引(覆盖索引)
如果你查找的值正好是在索引中,则可以直接返回索引中存的值,而不用到数据文件中查找。(这个在传统关系型数据库中也有实现),不过,必须满足以下条件:
Ø 必须提供准备的返回字段,以便可以直接从索引库中查询
Ø 必须明确地排除使用_id字段{_id:0}
即返回的字段就在索引中
当用explain时,当indexOnly=true,表示有用到covered index:
20 查看索引
getindexes可以查看索引,db.集合名.getindexes()
30 正则表达式在索引中的使用
正则表达式可以灵活地匹配查询条件,如果希望正则表达式能命中索引,就要注意了:
Mongodb能为前缀型的正则表达式命中索引,比如:需要查询Mail中user以z开头的:
/^z/
如果有user索引,这种查询很高效
但其他的即使有索引,也不会命中索引,比说:需要查询Mail中的user中含有z的:
/.*z.*/
/^.*z.*/
这种查询是不会命中到索引的,当数据量很大,速度很慢
总之,^后的条件必须明确,不能^.* ^[a-z]之类开头的
50 索引分析
a) 索引命中:
假设索引为:{a:1,b:1,c:1,…,z:1}:
实际上我们也可以使用:{a:1},{a:1,b:1},{a:1,b:1,c:1}…等索引的。
但是使用{b:1}、{a:1,c:1}等索引的查询是会被优化的,只有使用索引前部的查询才能使用该索引。
Mongodb的查询优化器会重排查询项的顺序,以便命中索引,比如:查询{x:’a’,y:’b’}的时候,如果已有了{y:1,x:1}的索引,mongodb也会自己找到并命中的。
创建索引的缺点是每次插入、更新与删除时都会产生额外的开销,这是因为数据库不但需要执行这些操作,还是处理索引,因些,要尽量可能少创建索引。每个集合默认的最大索引个数为64个
注:只要查找的条件字段在索引中有,顺序最好与索引中一致,但如果索引中的第一个字段没有出现在条件中,则不会用索引
60 为排序创建索引
随着集合的增长,如果查询中有用到排序时,就要创建索引了。如果对没有索引的键用sort,mongodb需要将所有的数据提到内存中进行排序,这个是很影响性能的。
特别是复合索引,将排序键放在第一位是个非常好的策略。
90 实例讲解
以MA的获取邮件列表为例,索引为:
{user:1,folderId:1,sendTime:-1,read:1,sourceSystem:1,importantFlag:1},{name:'folder_list_index'}
需要用到的查询条件组合(都需要以sendTime降序排列):
user:1,folderId:1,read:1,sourceSystem:1,importantFlag:1
user:1,folderId:1, read:1,sourceSystem:1
user:1,folderId:1,,read:1
user,folderId, sourceSystem:1,importantFlag:1
user,folderId, sourceSystem:1
注意:
用到排序时,sendTime必须还放在某个固定条件之前,比如:
大的范围为:user/folderId,sendTime则需放在此后,这样user/folderId对应的记录已经都以sendTime:-1的形式存放在索引库中(此时,用sort与不用sort的效果是相同的),即使后面再加其他的条件,sort时,也不用在内存中进行排序,这样与hint结合使用,每次查询都会很快。
二、explain执行计划
MongoDB 提供了一个 explain 命令让我们获知系统如何处理查询请求。利用 explain 命令,我们可以很好地观察系统如何使用索引来加快检索,同时可以针对性优化索引。
db.t5.ensureIndex({name:1})
db.t5.ensureIndex({age:1})
db.t5.find({age:{$gt:45}}, {name:1}).explain()
{
"cursor" : "BtreeCursor age_1",
"nscanned" : 0,
"nscannedObjects" : 0,
"n" : 0,
"millis" : 0,
"nYields" : 0,
"nChunkSkips" : 0,
"isMultiKey" : false,
"indexOnly" : false,
"indexBounds" : {
"age" : [
[45,1.7976931348623157e+308]
]
}
}
字段说明:
cursor: 返回游标类型(BasicCursor 或 BtreeCursor)
nscanned: 被扫描的文档数量
n: 返回的文档数量
millis: 耗时(毫秒)
indexBounds: 所使用的索引
isMultiKey:是否使用了多键索引
scanAndOrder:是否在内存中对结果集进行了排序
indexOnly:是否只使用索引就能完成查询(覆盖索引)
三、优化器profile
在MySQL 中,慢查询日志是经常作为我们优化数据库的依据,那在MongoDB 中是否有类似的功能呢?答案是肯定的,那就是MongoDB Database Profiler。
1.开启profiling功能
有两种方式可以控制 Profiling 的开关和级别,第一种是直接在启动参数里直接进行设置。启动MongoDB 时加上–profile=级别 即可。也可以在客户端调用db.setProfilingLevel(级别) 命令来实时配置,Profiler 信息保存在system.profile 中。我们可以通过db.getProfilingLevel()命令来获取当前的Profile 级别,类似如下操作:
db.setProfilingLevel(2);
上面profile 的级别可以取0,1,2 三个值,他们表示的意义如下:
0 – 不开启
1 – 记录慢命令 (默认为>100ms)
2 – 记录所有命令
Profile 记录在级别1 时会记录慢命令,那么这个慢的定义是什么?上面我们说到其默认为100ms,当然有默认就有设置,其设置方法和级别一样有两种,一种是通过添加 –slowms 启动参数配置。第二种是调用db.setProfilingLevel 时加上第二个参数:
db.setProfilingLevel( level , slowms )
db.setProfilingLevel( 1 , 10 );
2.查询 Profiling 记录
与MySQL 的慢查询日志不同,MongoDB Profile 记录是直接存在系统db 里的,记录位置system.profile ,所以,我们只要查询这个Collection 的记录就可以获取到我们的 Profile 记录了。列出执行时间长于某一限度(5ms)的 Profile 记录:
db.system.profile.find( { millis : { $gt : 5 } } )
MongoDB Shell 还提供了一个比较简洁的命令show profile,可列出最近5 条执行时间超过1ms 的 Profile 记录。
查询最新的一条可以使用如下命令
db.system.profile.find().sort({$natural:-1}).limit(1)
常用性能优化方案
创建索引
限定返回结果数
只查询使用到的字段
采用capped collection
采用Server Side Code Execution
使用Hint,强制使用索引
采用Profiling
尽量少用低效率的操作符 $exists,$ne,$not,$nin
复合索引中一般精确匹配的放前面,范围的放后面
$or可以对每个字句使用索引,因为$or实际是执行多次查询再合并结果
性能监控工具
1. mongosniff
此工具可以从底层监控到底有哪些命令发送给了MongoDB 去执行,从中就可以进行分析:以root 身份执行:
$./mongosniff --source NET lo
然后其会监控位到本地以localhost 监听默认27017 端口的MongoDB 的所有包请求。
注:本人机器上的该命令不能用,报错 error while loading shared libraries: libpcap.so.0.9: cannot open shared object file: No such file or directory
原因:缺少了 libpcap库,使用yum install libpcap-devel安装后还是不行报同样的错误原来是因为yum安装的libpcap-devel 1.0.0 版本, 要创建一个软连接:
ln -s /usr/lib64/libpcap.so.1.0.0 /usr/lib64/libpcap.so.0.9,随后以root身份启动就可以了!
如果libpcap.so.1.0.0 不是这个版本的改成对应的版本即可,装完后可在该目录下看到是那个版本
该问题解决后 /usr/local/mongodb/bin/mongosniff --source NET lo 37017 可以运行,但不一会还是报错
terminate called after throwing an instance of 'mongo::AssertionException'
what(): assertion src/mongo/db/dbmessage.cpp:81
Aborted
2.Mongostat
此工具可以快速的查看某组运行中的MongoDB 实例的统计信息 字段说明:
insert: 每秒插入量
query: 每秒查询量
update: 每秒更新量
delete: 每秒删除量
locked: 锁定量
qr | qw: 客户端查询排队长度(读|写)
ar | aw: 活跃客户端量(读|写)
conn: 连接数
time: 当前时间
它每秒钟刷新一次状态值,提供良好的可读性,通过这些参数可以观察到一个整体的性能情况。
3. db.serverStatus
这个命令是最常用也是最基础的查看实例运行状态的命令之一。
4.db.stats
db.stats 查看数据库状态信息。