mongo——索引三

一、索引规则

(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
}

发布了43 篇原创文章 · 获赞 37 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_28119741/article/details/87907920