一、索引规则
(1)索引可以大大减少要处理的文档数量,没有适当的索引,唯一满足条件的查询方式就是扫描全部文档,直到找到满足条件的查询。
(2)唯一的单键索引将会用来处理查询。对于包含多个键查询,包含这些键的复合索引是最好的解决方案
(3)如果有一个复合索引a-b,那么a上的单键索引就是多余的,b上的不多余
(4)复合索引的键值顺序很重要
example1:
db.products.find({'details.manufacturer':'Acme', 'pricing.sale':{$lt : 7500}})
如果details.manufacturer和pricing.sale各自有单键索引,需要单独遍历每个数据结构,找到它们的磁盘位置,计算交集。
如果建立了details.manufacturer和pricing.sale的复合索引,查询优化器需要找到索引中制造商manufacturer是Acme并且价格是7500的第一个入口。从那里开始,结果可以使用连续的扫描查找出来。
注意:(1)索引键值的顺序非常重要。如果我们定义的复合索引第一个值是price,第二个是manufacturer,那么查询效率就会非常低。键值必须按照出现的顺序比较,先找到7500,然后扫苗小于7500的文档判断是否是Acme公司生成的,假设有10000万个产品,所有价格低于10000,并且按价格均匀分布。这种情况需要扫描7500万个索引。
索引效率
如果某个集合包含10个索引,那么除了编写文档,每次插入都需要单独修改10个数据结构。这适用于任何写操作,无论是删除文档,还是因为空间不足挪动文档或者更新文档的索引键。对于读取密集型的应用,索引的成本是可以理解的。知道索引有成本,所以必须仔细选择。这意味着确保所有的索引都会被使用,不会有冗余。
第二个问题,即使所有的索引都建设得恰当,也可能无法加快查询,这会在索引和数据集没有加载到RAM的时候发生。
当使用默认的MMAPV1存储引擎时,使用系统调用mmap()方法,mongodb告诉os把所有的数据文件映射到内存中。但WiredTiger引擎使用了不同的方式,这一点上,包含所有文档,集合和索引的数据文件,被os加载和移除RAM,都按照4KB大小数据移动,这个数据块称为内存页。无论是否需要页上的数据,os都必须确保RAM中的数据页可用。如果没有,就会出现页面错误的异常,这会告诉内存管理器,要从磁盘加载数据到RAM里。
无论何时修改内存数据,比如写数据时,这些修改都会被os异步写入磁盘。写入时更快,因为直接操作内存,因此将磁盘的访问量降到最低。但是如果数据文件无法全部进入RAM就会出现页面错误。这意味着os将会频繁访问磁盘,大大降级读写速度。最坏的情况下,数据大小变得比RAM容量大很多。一种情形就是无论读写,数据都必须从磁盘或者向磁盘写入数据。这种情况有个专业的称呼:“颠簸”,它会严重导致性能变慢。
幸运的是,这种情形相对容易避免。至少我们可以确保索引加入RAM里,这也是为什么不创建不需要的索引的原因。设置的索引越多,就需要越多的RAM来维护这些索引。沿着相同的路线,每个索引应该只包含需要的键值。有时候也会需要3个键值的复合索引,但是需要知道它比单键索引需要更多的空间。创建一个或者2个字段索引,是为频繁地查询创建覆盖索引。覆盖索引是所有的查询都可以使用一个查询来满足查询的索引,让查询变得非常快。
多键索引
{name:"ww" , tags:["tools","soil"]}
如果在tags上创建索引,每个文档tags数组的值会出现在索引里,就意味着针对这些数组任意值在索引上的查询都会定位到文档上。这是多键索引背后的思想:多个索引入口或者键值,引用同一个文档。
哈希索引
db.tablename.createIndex({name:"hashed"})
限制:等值查询相似,不支持范围查询; 不支持多键哈希; 浮点数在哈希之前转换为整数,因此4.2和4.3有相同的哈希索引
为什么要使用:哈希索引的入口是均匀分布的。换句话说,当有键值数据不均匀分布时,哈希函数可以创建均匀性。‘Apple Pie’和“Artichoke Ravioli”在哈希索引中就不会相邻了。索引数据的位置已经变化了。它对于分片集合非常有用,分片索引决定文档分配到哪个片中。如果分片索引基于增长的值,比如mongo OIDs ,那么新创建的文档只会插入单个片中,除非索引是哈希的。
深入:除非显示设置,否则mongo文档会使用OID作为主键。这就是一组连续生成的OID;
...da9
...daa
...dab
注意这些值很相似,这是因为最重要的位是基于创建时间生成的,当新的文档使用这些id插入时,它们的索引入口会彼此相近。如果使用这些id来决定文档保存到哪个片(机器)中,那么这些文档很可能在同一个机器上。这些是非常有害的,当集合接受大量的写请求时会产生大量的负载压力,因为只有一台机器处理请求。
哈希索引通过均匀分散这些分档来解决这个问题,因此可以跨片或者跨机器存储。
构建索引
后台索引:
如果是在生产环境下无法停止数据库访问,就可以指定在后台构建索引。虽然构建索引还要占用写锁,但是此过程中允许其他用户读写数据库。如果应用给予mongodb的压力很大,后台索引构建就会影响性能,但是这些影响对于特定的环境是可以接受的。例如,如果知道构建索引可以在一个浏览最小的时期里完成。
db.values.createIndex({open:1,close:1}, {background: true})
离线索引
如果承受不了后台索引的压力。就需要离线索引。通常做法需要离线复制一个新的服务器节点,然后在此服务器上创建索引,并且允许此服务器复制主服务器数据。一旦更新完毕,就可以把此服务器作为主服务器,然后采用第二台离线服务器构建其索引的版本。这个策略假设复制oplog日志足够大,以避免脱机服务器在索引构建过程中丢失数据。
备份:
mongodump和mongorestore
碎片整理
如果应用对于数据库执行大量更新和删除操作,可能会产生许多索引碎片。B-树也会自己调正一些空间,但是这对于光删除空间还是不足的。索引碎片最大的问题是实际占用的空间远远大于数据需要的空间。索引碎片会导致使用更多的内存空间。这时候,我们可以考虑重建索引了。
可以通过删除并运行reIndex命令重新创建索引来实现,它会为集合重新创建所有的索引
db.values.reIndex()
重建索引要格外小心:此命令在重建期间会占用写入锁,导致mongo实例无法使用。重建索引最好脱机进行。
查询优化
> db.values.find({"stock_symbol":"GOOG"}).sort({date:-1}).limit(1)
{ "_id" : ObjectId("4d094f7ec96767d7a02a0af6"), "exchange" : "NASDAQ", "stock_symbol" : "GOOG", "date" : "2008-03-07", "open" : 428.88, "high" : 440, "low" : 426.24, "close" : 433.35, "volume" : 8071800, "adj close" : 433.35 }
看下mongo日志
2019-02-25T22:33:48.395+0800 I COMMAND [conn6] command stocks.values appName: "MongoDB Shell" command: find { find: "values", filter: { stock_symbol: "GOOG" }, limit: 1.0, singleBatch: false, sort: { date: -1.0 }, lsid: { id: UUID("cc615cdf-7a1c-4061-aa5a-3db7bfd044a2") }, $db: "stocks" } planSummary: COLLSCAN keysExamined:0 docsExamined:4308303 hasSortStage:1 cursorExhausted:1 numYields:33856 nreturned:1 reslen:279 locks:{ Global: { acquireCount: { r: 33857 } }, Database: { acquireCount: { r: 33857 } }, Collection: { acquireCount: { r: 33857 } } } protocol:op_msg 4524ms
执行花了4524ms
grep -E '[0-9]+ms' mongod.log
在启动mongo时,我们可以使用-slowms参数设置、指定。如果要记录超过50ms的操作,可以在启动mongo时使用--slowms 50参数
使用PROFILER分析器
use stocks; db.setProfilingLevel(2)
首先选择要监控的db,分析的范围通常是某个数据库。我们可以把分析级别设置为2。这是最详细的级别,它会告诉分析器记录每个读写操作。其它参数也可以使用。记录慢速操作耗时(100ms)要设置监控级别为1。要禁用分析器可以设置为0。分析器只会记录耗时超市的操作,传递毫秒作为第二个参数,如下所示:
use stocks; db.setProfilingLevel(1,50)
现在可以尝试查询。
> db.values.find({}).sort({close:-1}).limit(1)
{ "_id" : ObjectId("4d094f69c96767d7a01a110d"), "exchange" : "NASDAQ", "stock_symbol" : "BORD", "date" : "2000-09-25", "open" : 7500, "high" : 7500, "low" : 7500, "close" : 7500, "volume" : 0, "adj close" : 6679.94 }
监控结果
监控结果保存到一个特殊的盖子集合system.profile里,它存储在执行setProfilingLevel命令的数据库中。盖子集合具有固定的大小,而且数据写入的方式很特殊,一旦达到最大数量,新文档就会取代旧的文档。system.profile集合分配了128KB空间,因此要确保监控分析数据不会消耗太多的资源。
我们也可以作为盖子集合查询system.profile。例如,可以查找所有耗时超过150ms的操作
db.system.profile.find({millis:{$gt:150}})
因为盖子集合维护了自然的插入顺序,所以可以使用$natural操作符来排序,让最近的结果可以首先显示出来。
db.system.profile.find().sort({$natural:-1}).limit(5).pretty()
> db.system.profile.find().sort({$natural:-1}).limit(5).pretty()
{
"op" : "query",
"ns" : "stocks.values", //集合名字
"command" : {
"find" : "values",
"filter" : {
},
"limit" : 1,
"singleBatch" : false,
"sort" : {
"close" : -1
},
"lsid" : {
"id" : UUID("cc615cdf-7a1c-4061-aa5a-3db7bfd044a2")
},
"$db" : "stocks"
},
"keysExamined" : 0,
"docsExamined" : 4308303,
"hasSortStage" : true,
"cursorExhausted" : true,
"numYield" : 34255,
"nreturned" : 1,
"locks" : {
"Global" : {
"acquireCount" : {
"r" : NumberLong(34256)
}
},
"Database" : {
"acquireCount" : {
"r" : NumberLong(34256)
}
},
"Collection" : {
"acquireCount" : {
"r" : NumberLong(34256)
}
}
},
"responseLength" : 279,
"protocol" : "op_msg",
"millis" : 14214,
"planSummary" : "COLLSCAN",
"execStats" : {
"stage" : "SORT",
"nReturned" : 1,
"executionTimeMillisEstimate" : 12924,
"works" : 4308308,
"advanced" : 1,
"needTime" : 4308306,
"needYield" : 0,
"saveState" : 34255,
"restoreState" : 34255,
"isEOF" : 1,
"invalidates" : 0,
"sortPattern" : {
"close" : -1
},
"memUsage" : 182,
"memLimit" : 33554432,
"limitAmount" : 1,
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"nReturned" : 4308303,
"executionTimeMillisEstimate" : 10806,
"works" : 4308306,
"advanced" : 4308303,
"needTime" : 2,
"needYield" : 0,
"saveState" : 34255,
"restoreState" : 34255,
"isEOF" : 1,
"invalidates" : 0,
"inputStage" : {
"stage" : "COLLSCAN",
"nReturned" : 4308303,
"executionTimeMillisEstimate" : 2593,
"works" : 4308305,
"advanced" : 4308303,
"needTime" : 1,
"needYield" : 0,
"saveState" : 34255,
"restoreState" : 34255,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 4308303
}
}
},
"ts" : ISODate("2019-02-25T14:44:49.389Z"),
"client" : "127.0.0.1",
"appName" : "MongoDB Shell",
"allUsers" : [ ],
"user" : ""
}
explain
> db.inventory.find({}).sort({quantity:-1}).limit(1).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "stocks.inventory",
"indexFilterSet" : false,
"parsedQuery" : {
},
"winningPlan" : {
"stage" : "EOF"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 0,
"executionTimeMillis" : 0,
"totalKeysExamined" : 0,
"totalDocsExamined" : 0,
"executionStages" : {
"stage" : "EOF",
"nReturned" : 0,
"executionTimeMillisEstimate" : 0,
"works" : 1,
"advanced" : 0,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0
}
},
"serverInfo" : {
"host" : "DESKTOP-ALJ721J",
"port" : 27017,
"version" : "4.0.1",
"gitVersion" : "54f1582fc6eb01de4d4c42f26fc133e623f065fb"
},
"ok" : 1
}