文档数据库服务属于NoSQL数据库,提供了可扩展的高性能数据解决方案,与关系型数据库(例如MySQL、SQLServer、Oracle)一样,在数据库设计、语句优化、索引创建等方面都会影响数据库的使用性能。
下面从不同维度,给出提升文档数据库服务实例使用性能的建议:
查询操作的性能提升方式
- 使用索引:文档数据库服务中的索引和关系型数据库中的索引类似,都可以加速数据的查询和排序操作。在MongoDB中,我们可以通过 createIndex 方法来创建索引。
- 合理设计文档结构:文档数据库服务中的数据结构应该遵循“嵌套”的原则,即尽量将关联的数据放在同一个文档中。这样可以减少数据查询时的I/O操作,提高查询效率。
- 避免全局扫描:当查询条件无法使用索引时,查询操作可能会变得很慢。因此,我们应该尽可能避免全局扫描,可以通过分页查询、索引合并等方式来优化查询。
- 增加缓存:通过使用缓存技术,可以减少查询操作对数据库的访问,提高查询效率。MongoDB中可以使用TTL索引来实现数据缓存。
- 使用聚合查询:聚合查询可以对文档进行分组、统计、排序等操作,可以更方便地处理复杂的查询需求。MongoDB中提供了聚合框架来实现这些操作。
常用的文档数据库服务索引
文档数据库服务支持丰富的索引机制,合理使用索引能够加快数据检索速度并降低资源消耗。
单字段索引
比如 coll表中有如下格式的文档:
{ "_id" : ObjectId("64bde367d89fa22ccaa281e9"), "score" : 98, "name" : "zhang san" }
{ "_id" : ObjectId("64bde367d89fa22ccaa281ea"), "score" : 88, "name" : "li si" }
随着业务的增长,文档数逐渐变多后,如果想要按照 score 进行排序并提取 top 10 的文档就显得比较困难,因为涉及到全表扫描和内存排序。
此时可以按照 score 字段创建索引来加速查询,使用 mongo shell 创建索引的命令如下:
db.coll.createIndex({score:1})
命令中包含的 score 字段表示索引的 key, 1 表示升序,-1 表示降序。文档数据库服务会按照 score 字段创建索引,底层实现为 b-tree 形式。用户通过 score 字段进行等值查询或者排序时,文档数据库服务会自动选择索引进行加速。
用户也可以在连接上实例后,通过下面的命令查看目前表中包含哪些索引:
db.coll.getIndexes()
多字段复合索引
假设 coll 表中有如下文档:
{ "_id" : ObjectId("64bde367d89fa22ccaa281e9"), "score" : 98, "name" : "zhang san" }
{ "_id" : ObjectId("64bde367d89fa22ccaa281ea"), "score" : 88, "name" : "li si" }
{ "_id" : ObjectId("64bde73ad89fa22ccaa281eb"), "score" : 98, "name" : "wang wu" }
{ "_id" : ObjectId("64bde73ad89fa22ccaa281ec"), "score" : 98, "name" : "ma liu" }
如果希望查询 score=98 的所有文档,并按照 name 字段进行升序排序。
此时可以对 score 和 name 字段创建复合索引,使用 mongo shell 创建复合索引的命令如下:
db.coll.createIndex({score:1, name:1})
复合索引比单字段索引能够处理的场景更多,比如上述复合索引既可以处理 score+name 联合查询的场景,也能处理只按照 score 等值查询和排序的场景。
但是使用复合索引也有一些注意事项:
- 复合索引遵循前缀匹配原则。比如 {score: 1, name: 1} 复合索引能处理按照 score 查询的场景,但是不能处理按照 name 查询的场景。因此在建索引时需要特别注意字段顺序。
- 字段越多,索引的存储空间越大。比如用户只有按照 score 字段进行查询和排序的场景,就没有必要创建复合索引了。
Hash 索引
如果只涉及到对单个字段的等值查询,可以考虑创建 hash 索引,使用 mongo shell 创建 hash 索引的方式如下:
db.coll.createIndex({score: "hashed"})
Hash 索引底层也是使用 btree 存储,但是区别在于 btree 的 key 是 hash 之后的值。
对于分片版本的文档数据库实例,如果按照 hash 方式创建分片表,则每个 shard server 上会自动创建基于分片键的 hash 索引。
Hash 索引有一些限制:
- 不能指定唯一属性。
- 不能进行范围扫描。
如果要创建索引的字段很长,由于 hash 索引是按照的 64bit 的hash 值创建索引,可以在一定程度上节省索引的存储空间。
索引属性
在创建索引时可以指定属性完成更高级的功能,常用的配置属性有:
- TTL 。实现数据自动过期删除。比如有 coll 表中存放了系统运行日志,其中包含了 createTime 指定了日志生成时间,则可以按照这个字段创建 TTL 索引自动删除 1 个小时以前的数据:db.coll.createIndex( { "createTime": 1 }, { expireAfterSeconds: 3600 } )。
- Unique。指定字段的唯一性。如果重复插入该字段值相同的数据会报错失败,指定方式为 db.coll.createIndex( { "name": 1 }, { unique: true } )。
- Partial。只对满足条件的文档创建索引。比如只对分数大于 80 的文档创建索引:db.coll2.createIndex({score:1, name:1}, {partialFilterExpression: {score: {$gt: 80}} })。
慢查询定位和处理
用户可以在控制台上单机实例名称进入慢日志展示页面,查看实例最近的慢请求。慢日志中一般会包含 docsExamined 等信息记录了本次请求扫描的文档数,可以结合日志信息和查询语句来判断是否缺少合适的索引。
用户也可以使用 mongo shell 连接到文档数据库服务实例,然后执行 explain() 观察查询计划是否复合预期。例如:
db.coll.find({xxx:xxx}).explain()
如果缺少索引,可以执行前文所述的命令进行索引创建,然后重新执行查询命令观察执行计划是否正常。
清理不必要的索引
文档数据库服务中每个索引都使用独立的文件存储,如果数据库中有太多不使用或者功能重复的索引,会在一定程度上浪费磁盘空间,并且影响数据库的写入性能。
对于功能重复的索引,比如前文提到的 {"score": 1, "name": 1} 复合索引就能覆盖 {"score": 1} 索引的功能。
用户可以在使用 mongo shell 连接上文档数据库实例之后,执行如下命令进行检查和清理。
- 查看某个表的所有索引总大小:
db.coll.totalIndexSize()
- 查看每个索引的大小:
db.coll.stats().indexSizes
- 查看每个索引的字段和属性信息:
db.coll.getIndexes()
- 查看索引的创建时间,使用次数等信息:
db.coll.aggregate([{$indexStats:{}}])
- 清理索引:
db.coll.dropIndex({a: 1})