一、数据库设计优化:范式化与反范式化 1、完全分离(范式化) { "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "如何使用MongoDB", "author" : [ ObjectId("144b5d83041c7dca84416"), ObjectId("144b5d83041c7dca84418"), ObjectId("144b5d83041c7dca84420"), ] } 将作者(comment) 的id数组作为一个字段添加到了图书中去。这样的设计方式是在非关系型数据库中常用的,也就是我们所说的范式化设计。在MongoDB中我们将与主键没有直接关系的图书单独提取到另一个集合,用存储主键的方式进行关联查询。当我们要查询文章和评论时需要先查询到所需的文章,再从文章中获取评论id,最后用获得的完整的文章及其评论。在这种情况下查询性能显然是不理想的。但当某位作者的信息需要修改时,范式化的维护优势就凸显出来了,我们无需考虑此作者关联的图书,直接进行修改此作者的字段即可 2、完全内嵌(反范式化) { "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "如何使用MongoDB", "author" : [ { "name" : "丁磊" "age" : 40, "nationality" : "china", }, { "name" : "马云" "age" : 49, "nationality" : "china", } ] } 将作者的字段完全嵌入到了图书中去,在查询的时候直接查询图书即可获得所对应作者的全部信息,但因一个作者可能有多本著作,当修改某位作者的信息时时,我们需要遍历所有图书以找到该作者,将其修改。 3、部分内嵌(折中方案) { "_id" : ObjectId("5124b5d86041c7dca81917"), "title" : "如何使用MongoDB", "author" : [ { "_id" : ObjectId("144b5d83041c7dca84416"), "name" : "丁磊" }, { "_id" : ObjectId("144b5d83041c7dca84418"), "name" : "马云" } ] } 将作者字段中的最常用的一部分提取出来。当我们只需要获得图书和作者名时,无需再次进入作者集合进行查询,仅在图书集合查询即可获得。 这种方式是一种相对折中的方式,既保证了查询效率,也保证的更新效率。但这样的方式显然要比前两种较难以掌握,难点在于需要与实际业务进行结合来寻找合适的提取字段。如同示例3所述,名字显然不是一个经常修改的字段,这样的字段如果提取出来是没问题的,但如果提取出来的字段是一个经常修改的字段(比如age)的话,我们依旧在更新这个字段时需要大范围的寻找并依此进行更新。 在上面三个示例中,第一个示例的更新效率是最高的,但查询效率是最低的,而第二个示例的查询效率最高,但更新效率最低。所以在实际的工作中我们需要根据自己实际的需要来设计表中的字段,以获得最高的效率。 二、填充因子 填充因子(padding factor)是MongoDB为文档的扩展而预留的增长空间,因为MongoDB的文档是以顺序表的方式存储的,每个文档之间会非常紧凑,如图所示。 因为文档的移动非常消耗性能,频繁的移动会大大增加系统的负担,在实际开发中最有可能会让文档体积变大的因素是数组,所以如果我们的文档会频繁修改并增大空间的话,则一定要充分考虑填充因子。 那么如果我们的文档是个常常会扩展的话,应该如何提高性能? 两种方案 1、增加初始分配空间。在集合的属性中包含一个 usePowerOf2Sizes 属性,当这个选项为true时,系统会将后续插入的文档,初始空间都分配为2的N次方。 这种分配机制适用于一个数据会频繁变更的集合使用,他会给每个文档留有更大的空间,但因此空间的分配不会像原来那样高效,如果你的集合在更新时不会频繁的出现移动现象,这种分配方式会导致写入速度相对变慢。 2、可以利用数据强行将初始分配空间扩大 db.book.insert({ "name" : "MongoDB", "publishing" : "清华大学出版社", "author" : "john" "tags" : [] "stuff" : "ggggggggggggggggggggggggggggggggggggg ggggggggggggggggggggggggggggggggggggg ggggggggggggggggggggggggggggggggggggg" }) 当我们对这个文档进行增长式修改时,只要将stuff字段删掉即可。当然,这个stuff字段随便你怎么起名,包括里边的填充字符当然也是可以随意添加的 三、索引 索引的原理是通过建立指定字段的B树,通过搜索B树来查找对应document的地址。这也就解释了如果需要查询超过一半的集合数据,直接遍历省去了搜索B树的过程,效率反而会高 1、隐式索引 >db.test.ensureIndex({"age": 1,"no": 1,"name": 1 }) //建立复合索引 隐式索引指的是如果我们想要排序的字段包含在已建立的复合索引中则无需重复建立索引。 >db.test.find().sort("age": 1,"no": 1) >db.test.find().sort("age": 1) 以上2个都会走索引 >db.test.find().sort("name": 1) 会走索引吗? 2、翻转索引 >db.test.ensureIndex({"age": 1}) 在排序查询时无需考虑索引列的方向,例如这个例子中我们在查询时可以将排序条件写为"{'age': 0}",依旧不会影响性能。 四、监控 mongodb可以通过profile来监控数据,进行优化 通过 >db.getProfilingLevel() 来监控 查看当前是否开启profile功能用命令: db.getProfilingLevel()返回level等级,值为0|1|2, 分别代表意思:0代表关闭,1代表记录慢命令,2代表全部。 开始profile功能为 >db.setProfilingLevel(level); 分片集群里执行报错 level为1的时候,慢命令默认值为100ms,更改为db.setProfilingLevel(level,slowms)如db.setProfilingLevel(1,50)这样就更改为50毫秒 也报错 查看当前的监控日志 >db.system.profile.find() 。 在单机环境执行是好的。 通过执行db.system.profile.find({millis:{$gt:500}})能够返回查询时间在500毫秒以上的查询命令。 结果: { "op" : "query", "ns" : "admin.system.profile", "command" : { "find" : "system.profile", "filter" : { }, "lsid" : { "id" : UUID("61380e47-ddc6-4112-af85-aec93b88be76") }, "$db" : "admin" }, "keysExamined" : 0, "docsExamined" : 0, "cursorExhausted" : true, "numYield" : 0, "nreturned" : 0, "locks" : { "Global" : { "acquireCount" : { "r" : NumberLong(1) } }, "Database" : { "acquireCount" : { "r" : NumberLong(1) } }, "Collection" : { "acquireCount" : { "r" : NumberLong(1) } } }, "responseLength" : 109, "protocol" : "op_msg", "millis" : 311, "planSummary" : "COLLSCAN", "execStats" : { "stage" : "COLLSCAN", "nReturned" : 0, "executionTimeMillisEstimate" : 0, "works" : 2, "advanced" : 0, "needTime" : 1, "needYield" : 0, "saveState" : 0, "restoreState" : 0, "isEOF" : 1, "invalidates" : 0, "direction" : "forward", "docsExamined" : 0 }, "ts" : ISODate("2019-05-28T02:50:21.620Z"), "client" : "127.0.0.1", "appName" : "MongoDB Shell", "allUsers" : [ ], "user" : "" } 解释: ts:命令执行时间 info:命令的内容 query:代表查询 order.order: 代表查询的库与集合 reslen:返回的结果集大小,byte数 nscanned:扫描记录数量 nquery:后面是查询条件 nreturned:返回记录数及用时 millis:所花时间 结果分析: 如果发现时间比较长,那么就需要作优化。 比如nscanned数很大,或者接近记录总数,那么可能没有用到索引查询。 reslen很大,有可能返回没必要的字段。 nreturned很大,那么有可能查询的时候没有加限制。 mongo可以通过db.serverStatus()查看mongod的运行状态 五、explain查看执行情况 说明没使用索引或没设置索引 六、配置优化 1、设置WiredTiger的cacheSizeGB 通过cacheSizeGB选项配置控制WiredTiger引擎使用内存的上限,默认配置在系统可用内存的60%左右。 如果一台机器上只部署一个mongod,mongod可以使用所有可用内存,则使用默认配置即可。 如果一台机器上部署多个mongod,或者mongod跟其他的一些进程一起部署,则需要根据分给mongod的内存配额来配置cacheSizeGB,按配额的60%左右配置即可。