MongoDB高阶特性:索引

一、为什么使用B-Tree

一)B树和B+树

  • B树的树内存储数据,因此查询单条数据的时候,B树的查询效率不固定,最好的情况是O(1)。我们可以认为在做单一数据查询的时候,使用B树平均性能更好。但是,由于B树中各节点之间没有指针相邻,因此B树不适合做一些数据遍历操作。
  • B+树的数据只出现在叶子节点上,因此在查询单条数据的时候,查询速度非常稳定。因此,在做单一数据的查询上,其平均性能并不如B树。但是,B+树的叶子节点上有指针进行相连,因此在做数据遍历的时候,只需要对叶子节点进行遍历即可,这个特性使得B+树非常适合做范围查询。

因此,我们可以做一个推论:

没准是Mysql中数据遍历操作比较多,所以用B+树作为索引结构。而Mongodb是做单一查询比较多,数据遍历操作比较少,所以用B树作为索引结构。

那么为什么Mysql做数据遍历操作多?而Mongodb做数据遍历操作少呢? 因为Mysql是关系型数据库,而Mongodb是非关系型数据。
那为什么关系型数据库,做数据遍历操作多?而非关系型数据库,做数据遍历操作少呢?

二)关系型VS非关系型

假设,我们此时有两个逻辑实体:学生(Student)和班级(Class),这两个逻辑实体之间是一对多的关系。毕竟一个班级有多个学生,一个学生只能属于一个班级。

1、关系型数据库

我们在关系型数据库中,考虑的是用几张表来表示这二者之间的实体关系。常见的无外乎是,一对一关系,用一张表就行。一对多关系,用两张表。多对多关系,用三张表。那我们,此时要查 cname 为 1班 的班级,有多少学生怎么办?
假设 cname 这列,我们建了索引!执行SQL,如下所示!

SELECT * FROM t_student t1, (SELECT cid FROM t_class WHERE cname = '1班') t2 WHERE t1.cid = t2.cid

而这,就涉及到了数据遍历操作!
因为但凡做这种关联查询,你躲不开join操作的!既然涉及到了join操作,无外乎从一个表中取一个数据,去另一个表中逐行匹配,如果索引结构是B+树,叶子节点上是有指针的,能够极大的提高这种一行一行的匹配速度!
有的人或许会抬杠说,如果我先执行:

SELECT cid FROM t_class WHERE cname = '1班'

获得cid后,再去循环执行:

SELECT * FROM t_student WHERE cid = ...

就可以避开join操作呀?
对此,我想说。你确实避开了join操作,但是你数据遍历操作还是没避开。你还是需要在student的这张表的叶子节点上,一遍又一遍的遍历!
那在非关系型数据库中,我们如何查询 cname 为 1班 的班级,有多少学生?

2、非关系型数据库

执行两次查询去获得结果!一次去class集合查,获得id后再去student集合查。
确实,这么设计是可以的,我没说不行。只是不符合非关系型数据库的设计初衷。在MongoDB中,根本不推荐这么设计。虽然,Mongodb中有一个$lookup操作,可以做join查询。但是理想情况下,这个$lookup操作应该不会经常使用,如果你需要经常使用它,那么你就使用了错误的数据存储了(数据库):如果你有相关联的数据,应该使用关系型数据库(SQL)。
假设 name 这列,我们建了索引!
我只需执行一次语句:

db.class.find({name:'1班'})

这样就能查询出自己想要的结果。
而这,就是一种单一数据查询!毕竟你不需要去逐行匹配,不涉及遍历操作,幸运的情况下,有可能一次IO就能够得到你想要的结果。
因此,由于关系型数据库和非关系型数据的设计方式上的不同。导致在关系型数据中,遍历操作比较常见,因此采用B+树作为索引,比较合适。而在非关系型数据库中,单一查询比较常见,因此采用B树作为索引,比较合适。

二、MongoDB索引类型

众所周知,MongoDB默认会为插入的文档生成_id字段(如果应用本身没有指定该字段),_id是文档唯一的标识,为了保证能根据文档id快递查询文档,MongoDB默认会为集合创建_id字段的索引(默认索引)。

db.person.getIndexes() // 查询集合的索引信息
[
    {
        "ns" : "test.person",  // 集合名
        "v" : 1,               // 索引版本
        "key" : {              // 索引的字段及排序方向
            "_id" : 1           // 根据_id字段升序索引
        },
        "name" : "_id_"        // 索引的名称
    }
]

除此之外,MongoDB还支持多种类型的索引,包括单值索引、复合索引、多key(多值)索引、文本索引、哈希索引、地理位置索引等,每种类型的索引有不同的使用场合。

1、单值索引

db.person.createIndex({age: 1}) 

上述语句针对age创建了单字段索引,其能加速对age字段的各种查询请求,是最常见的索引形式,MongoDB默认创建的id索引也是这种类型。{age: 1} 代表升序索引,也可以通过{age: -1}来指定降序索引,对于单字段索引,升序/降序效果是一样的。

2、复合索引

复合索引是Single Field Index的升级版本,它针对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档按第二个字段排序,依次类推,如下针对age, name这2个字段创建一个复合索引。

db.person.createIndex({age: 1, name: 1}) 

上述索引对应的数据组织类似下表,与{age: 1}索引不同的是,当age字段相同时,再根据name字段进行排序,所以pos5对应的文档排在pos3之前。
AGE,NAME 位置信息
18,adam pos5
18,jack pos3
19,jack pos1
20,rose pos2
21,tony pos4

复合索引能满足的查询场景比单字段索引更丰富,不光能满足多个字段组合起来的查询,比如db.person.find({age: 18, name: "jack"}),也能满足所有能匹配符合索引前缀的查询,这里{age: 1}即为{age: 1, name: 1}的前缀,所以类似db.person.find( {age: 18} )的查询也能通过该索引来加速;但db.person.find({name: "jack"})则无法使用该复合索引。如果经常需要根据『name字段』以及『name和age字段组合』来查询,则应该创建如下的复合索引

db.person.createIndex({name: 1, age: 1})

这里有个需要注意的地方:
对应上面的复合索引db.person.createIndex({name: 1, age: 1}) ,查询语句的索引使用顺序db.person.find({name: "adam ",age: 18})与db.person.find({age: 18,name: "adam"})相同,都会使用到复合索引db.person.createIndex( {name: 1, age: 1} ),因为MongoDB底层会对查询语句进行优化重新排列顺序(mysql也是同样的原理)。
除了查询的需求能够影响索引的顺序,字段的值分布也是一个重要的考量因素,即使person集合所有的查询都是『name和age字段组合』(指定特定的name和age),字段的顺序也是有影响的。
age字段的取值很有限,即拥有相同age字段的文档会有很多;而name字段的取值则丰富很多,拥有相同name字段的文档很少;显然先按name字段查找,再在相同name的文档里查找age字段更为高效。

3、多key(多值)索引

当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引,比如person表加入一个habbit字段(数组)用于描述兴趣爱好,需要查询有相同兴趣爱好的人就可以利用habbit字段的多key索引。

{"name": "jack", "age": 19, habbit: ["football", "runnning"]}
db.person.createIndex({habbit: 1})  // 自动创建多key索引
db.person.find({habbit: "football"})

4、文本索引

能解决快速文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来快速查找,则可以针对博客内容建立文本索引。text索引可以包括其值为字符串或字符串元素数组的任何字段。
文本索引,顾名思义就是用于搜索文本的,可以用于搜索所有的value,也可以搜索指定的field对应的value。只要field对应value是string,或者对应的value是array且array中的元素是string,那么文本索引都可以索引该field
注意:一个集合最多只能有一个文本索引。
要创建text索引,请使用该 db.collection.createIndex()方法。要索引包含字符串或字符串元素数组的字段,请包含该字段并"text"在索引文档中指定字符串文字。
如以下示例所示:

db.collection.createIndex({keys:"text"})

也可以创建多个字段text,例如

db.collection.createIndex({subject:"text",comments:"text"})

举例,有两条记录

{_id:5908df789dfd1fd5884fd84f7df4,statement:MongoDB is the worst}
{_id:5908dfgfh587hgf15f4hf54hf418,statement:MongoDB is the best}

给statement字段创建文本索引:

db.collection.createIndex({statement:"text"})

查询

db.collection.find({$text:{$search:"MongoDB best"}})

查询出来的结果集是:

{_id:5908df789dfd1fd5884fd84f7df4,statement:MongoDB is the worst}
{_id:5908dfgfh587hgf15f4hf54hf418,statement:MongoDB is the best}

这是因为文本索引在查询时,每个单词之间的分隔是"或者",所以上面的查询语句的意思是:查询包含MongoDB或者best的记录数。
在多个字段上创建文本索引时,还可以使用通配符说明符($**)。使用通配符文本索引,MongoDB会为包含集合中每个文档的字符串数据的每个字段编制索引。
例如:

db.collection.createIndex({"$**","text"})

此索引允许在具有字符串内容的所有字段上进行文本搜索。如果不清楚要包含在文本索引中的哪些字段或用于临时查询
通配符文本索引可以是复合索引的一部分。例如,以下内容在字段nane和通配符说明符上创建复合索引,例如:

db.collection.createIndex({name:1,"$**","text"})

排序操作无法从text索引获取排序顺序,即使是复合文本索引也是如此; 即排序操作不能使用文本索引中的排序。

5、哈希索引

是指按照某个字段的hash值来建立索引,目前主要用于MongoDB Sharded Cluster的Hash分片,hash索引只能满足字段完全匹配的查询,不能满足范围查询等。

6、地理位置索引

能很好的解决O2O的应用场景,比如『查找附近的美食』、『查找某个区域内的车站』等。
这样的查询也变得越来越流行:要找到离当前位置近期的N个场所,如要找到给定经纬度坐标周围近期的咖啡馆。
MongoDB为坐标平面提供了专门的索引。称作:地理空间索引
createIndex创建索引,只是參数不是1或-1,而是"2d"

db.stus.createIndex({"gps" : "2d"})

"gps"必需是某种形式的一对值:一个包括两个元素的数组或是包括两个键的内嵌文档;键值名能够随意。例如以下:

{ "gps" : [ 0, 100 ] }
{ "gps" : { "x" : -30, "y" : 30 } }
{ "gps" : { "latitude" : -180, "longitude" : 180 } }

默认情况下地理空间的范围是-180~180(经纬度)。要想用其他值,能够通过选项指定最大最小值:

db.stus.createIndex({"gps" : "2d"}, {"min" : -1000, "max" : 1000})

这样就创建了一个2000见方的索引。

6.1 地理空间查询方法

使用$near
返回离[40, -73]近期的10个文档

db.map.find({"gps" : {"$near" : [40, -73]}}).limit(10)
或是:
db.runCommand({geoNear : "map", near : [40, -73], num : 10});

6.2 查找指定开头内的文档

即将原来的 $near 换成 $within
$ within形状參数文档
搜索矩形内的文档:使用"$box"

db.map.find({"gps" : {"$ within" : {"$box" : [[10, 20], [15, 30]]}}})

搜索圆形内的文档:使用"$center"

db.map.find({"gps" : {"$ within" : {"$center" : [[12, 25], 5]}}})

6.3 复合地理空间索引

应用常常要找的东西不仅仅是一个地点。比如。用户要找出周围全部的咖啡店或披萨店。将地理空间索引与普通索引组合起来就能够满足这样的需求。
比如。要查询"location"和"desc",就能够这样创建索引:

db.stus.createIndex({"gps" : "2d", "name" : 1})

然后就可能非常快找到近期的咖啡馆了

db.stus.find({"gps" : {"$near" : [10, 19]}, "name" : "咖啡店"}).limit(1)

注意:创建一个关键词组对于用户自己定义查找非常有帮助。

7、唯一索引 (unique index)

保证索引对应的字段不会出现相同的值,比如_id索引就是唯一索引

db.stus.createIndex({ name: 1 }, { unique: true })

8、TTL索引

自动过期或删除文档。TTL索引允许你指定一个时间段,文档将在该时间段之后自动删除。这对于处理需要自动清理或过期的数据非常有用,例如会话数据、日志、缓存等

# 在插入时的3600秒(1小时)之后自动过期并被删除文档
db.stus.createIndex({ "name": 1 }, { expireAfterSeconds: 3600 })

9、部分索引 (partial index)

只针对符合某个特定条件的文档建立索引,这可以减小索引的大小,提高性能,特别是在处理大型数据集合时。

# name:创建索引的字段
# partialFilterExpression 是一个用于筛选文档的条件,只有 "anotherField" 等于 "someValue" 的文档才会被索引。
db.stus.createIndex({ "name": 1 },{partialFilterExpression: { "anotherField": { $eq: "someValue" } }})

10、稀疏索引(sparse index)

只针对存在索引字段的文档建立索引,用于处理包含大量空或缺失字段的文档的集合,这可以减小索引大小,提高性能,适用于包含空字段的文档

db.yourCollection.createIndex({ "yourField": 1 },{ sparse: true })

三、执行查询计划

一)explain()

索引已经建立了,但查询还是很慢怎么破?这时就得深入的分析下索引的使用情况了,可通过查看下详细的查询计划来决定如何优化。通过执行计划可以看出如下问题

根据某个/些字段查询,但没有建立索引
根据某个/些字段查询,但建立了多个索引,执行查询时没有使用预期的索引。

建立索引前,db.person.find( {age: 18} )语句执行COLLSCAN,即全表扫描。

db.person.find({age: 18}).explain()
{
    "queryPlanner" : {
        ......
        "winningPlan" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "age" : {
                    "$eq" : 18
                }
            },
            "direction" : "forward"
        },
        ......
    },
    ......
}

建立索引后,通过查询计划可以看出,先进行IXSCAN,然后FETCH,读取出满足条件的文档。

db.person.find({age: 18}).explain()
{
    "queryPlanner" : {
        .......
        "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "age" : 1
                },
                ......
            }
        },
        ......
    },
    ......
}

注意事项
既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,数据库在运行时也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引。

  • 第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。
  • 另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:Index Selectivity = Cardinality / #T
常见慢查询:
1.不等于和不包含查询
2.通配符在前面的模糊查询, like ‘%xxx’
3.无索引的count 查询 和 排序(复合索引顺序不匹配)
4.多个范围查询(范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引)
5.skip跳过过多的行数(优化方案:我们第一页可以用db.article.find().limit(articles_of_each_page),并记录最后一片文章的_id(或者其他排序值),之后查询db.article.find({_id:{$lt:_id_stored}}).limit(articles_of_each_page)来查找下一页或者类似的,上一页的文章,可以避免大量计数.)

二)执行计划三种模式

1、queryPlanner

db.stus.find({"name": "孙悟空3"}).explain("queryPlanner")

queryPlanner是explain的默认模式,是查询执行计划的初始阶段,并不会去真正进行操作语句的执行,它生成多个可能的查询计划并选择其中一个最有希望的计划,以后在executionStats中执行。这一阶段主要关注索引选择、查询优化和计划选择。
当你需要了解MongoDB如何选择执行查询的计划,以及是否使用了适当的索引时,可以查看queryPlanner输出。

  • 1)namespace:操作语句所针对的表
  • 2)indexFilterSet:布尔值,该操作是否指定indexFilter
  • 3)winningPlan:查询优化器对该query返回的最优执行计划的详细内容
  • 4)winningPlan.stage:操作阶段的策略
  • 5)winningPlan.inputStage/winningPlan.inputStages:子操作阶段
  • 6)winningPlan.inputStage.stage:子操作阶段的策略
  • 7)winningPlan.inputStage.keyPattern:扫描的index内容
  • 8)winningPlan.inputStage.indexName:winning plan所选用的index
  • 9)winningPlan.inputStage.isMultiKey:是否是Multikey,此处返回是false,如果索引建立在array上,此处将是true
  • 10)winningPlan.inputStage.direction:此query的查询顺序,与sort有关,sort:1为forward,sort:-1为backward
  • 11)winningPlan.inputStage.indexBounds:winningplan所扫描的索引范围
  • 12)rejectePlans:其他的执行计划(不会被采用,因为不是最优的。最优的是winningPlan,采用的也是winningPlan)

2、executionStats

db.stus.find({"name": "孙悟空3"}).explain("executionStats")

executionStats 阶段是在实际执行查询操作后生成的,提供了查询操作的详细性能数据。
当你需要深入了解查询操作的性能状况,包括实际执行时间和资源消耗时,可以查看executionStats输出。

  • 1)executionSuccess:是否执行成功
  • 2)nReturned:查询的返回条数
  • 3)executionTimeMillis:整体执行时间
  • 4)totalKeysExamined:索引扫描次数
  • 5)totalDocsExamined:document扫描文档数目
  • 6)executionStages:执行阶段的全部详情
  • 7)executionStages.works
  • 8)executionStages.advanced
  • 9)executionStages.needTime
  • 10)executionStages.needYield:存储层产生锁的次数
  • 11)executionStages.isEOF:指定执行阶段是否结束
  • 12)executionStages.inputStage.keysExamined:执行过程中扫描索引数据的次数
  • 13)executionStages.inputStage.docsExamined:执行阶段扫描文档数据的次数

3、allPlansExecution

该模式是前两种模式的更细化,会包括上述两种模式的所有信息。即按照最佳的执行计划列出统计信息,还会列出一些候选的执行计划。

三)IndexFilter

1、概念

IndexFilter决定了查询优化器对于某一类型的查询将如何使用index,indexFilter仅影响查询优化器对于该类查询可以尝试用哪些index的执行计划分析,查询优化器还是根据分析情况选择最优计划。

2、IndexFilter的创建

可以通过如下命令为某一个collection建立indexFilter

db.runCommand(
   {
      planCacheSetFilter: <collection>,
      query: <query>,
      sort: <sort>,
      projection: <projection>,
      indexes: [ <index1>, <index2>, ...]
   }
)
db.runCommand(
   {
      planCacheSetFilter: "orders",
      query: { status: "A" },
      indexes: [
         { cust_id: 1, status: 1 },
         { status: 1, order_date: -1 }
      ]
   }
)

以上针对orders表建立了一个indexFilter,indexFilter指定了对于orders表只有status条件(仅对status进行查询,无sort等)的查询的indexes,故下图的查询语句的查询优化器仅仅会从{cust_id:1,status:1}和{status:1,order_date:-1}中进行winning plan的选择

db.orders.find( { status: "D" } )
db.orders.find( { status: "P" } )

3、IndexFilter的列表

可以通过如下命令展示某一个collecton的所有indexFilter

db.runCommand( { planCacheListFilters: <collection> } )

4、IndexFilter的删除

可以通过如下命令对IndexFilter进行删除

db.runCommand(
   {
      planCacheClearFilters: <collection>,
      query: <query pattern>,
      sort: <sort specification>,
      projection: <projection specification>
   }
)

四)Stage

如queryPlanner.winningPlan.stage和queryPlanner.winningPlan.inputStage等。

  • COLLSCAN:全表扫描
  • IXSCAN:索引扫描
  • FETCH:根据索引去检索指定document
  • SHARD_MERGE:将各个分片返回数据进行merge
  • SORT:表明在内存中进行了排序(与老版本的scanAndOrder:true一致)
  • LIMIT:使用limit限制返回数
  • SKIP:使用skip进行跳过
  • IDHACK:针对_id进行查询
  • SHARDING_FILTER:通过mongos对分片数据进行查询
  • COUNT:利用db.coll.explain().count()之类进行count运算
  • COUNTSCAN:count不使用用Index进行count时的stage返回
  • COUNT_SCAN:count使用了Index进行count时的stage返回
  • SUBPLA:未使用到索引的$or查询的stage返回
  • TEXT:使用全文索引进行查询时候的stage返回
  • PROJECTION:限定返回字段时候stage的返回

五)结果分析

1、executionTimeMillis分析

executionStats模式中,有3个执行时间:

  • executionTimeMillis:该query的整体查询时间
  • executionStages.executionTimeMillis:该查询根据index去检索document获取数据的时间
  • executionStages.inputStage.executionTimeMillis:该查询扫描index行所用时间
    这三个值越小,该查询的性能最好

2、nReturned、totalKeysExamined与totalDocsExamined分析

这三个返回值分别代表该条查询返回的条目、索引扫描条目和根据索引去检索文档的扫描条目。这些都直观的影响到executionTimeMillis值的大小。
对于一个查询, 我们最理想的状态是:

  • 1)索引覆盖
    nReturned=totalKeysExamined & totalDocsExamined=0
    即仅仅使用到了index,无需文档扫描,这是最理想状态。
  • 2)正常使用索引
    nReturned=totalKeysExamined=totalDocsExamined
    即正常index利用,无多余index扫描与文档扫描。

如果有sort的时候,为了使得sort不在内存中进行,我们可以在保证nReturned=totalDocsExamined的基础上,totalKeysExamined可以大于totalDocsExamined与nReturned,因为量级较大的时候内存排序非常消耗性能。

3、Stage状态分析

Stage类型会影响到totalKeysExamined和totalDocsExamined,从而影响executionTimeMillis值的大小。

3.1 对于普通查询,我们最希望看到的组合有这些
  • Fetch+IDHACK
  • Fetch+ixscan
  • Limit+(Fetch+ixscan)
  • PROJECTION+ixscan
  • SHARDING_FILTER+ixscan
3.2 不希望看到包含如下的stage
  • COLLSCAN(全表扫)
  • SORT(使用sort但是无index)
  • 不合理的SKIP
  • SUBPLA(未用到index的$or)
3.3 对于count查询,希望看到的有
  • COUNT_SCAN
    不希望看到的有:
  • COUNTSCAN
    当查询覆盖精确匹配,范围查询与排序的时候,{精确匹配字段,排序字段,范围查询字段}这样的索引排序会更为高效。

4、执行计划说明(截图)

当我们查询条件为两个时,如:

db.commits.find({"eId":"5913cd0ee727e70007a109f2","sId":"5913c700434a8b00077256fe"}).explain("executionStats")
图片描述

因前面已经给eId创建了索引,所以本次查询范围从18248条记录,降到了80条记录,而sId没有创建索引,所有我们从80条记录里得到了符合eId,sId这两个条件的38条记录,那么我们现在给sId也创建索引

db.commits.createIndex({sId:1})

当查询的两个条件sId,eId都创建了索引后,再执行

db.commits.find({"eId":"5913cd0ee727e70007a109f2","sId":"5913c700434a8b00077256fe"}).explain("executionStats")
图片描述

四、慢查询

一)获取当前监控状态

db.getProfilingStatus() 

二)设置监控

db.setProfilingLevel(level,options)
// Level:0:关闭日志收集 1:收集大于slowms的日志 2:收集所有日志
// Options:若是整数, 则赋值给slowms;若是对象 , 则是slowms : 阈值 Default: 100, 单位毫秒(milliseconds),sampleRate : 采样率 Default: 1.0 , eg. 1 记录所有慢查询 , 0.5 记录50%的慢查询

三)获取慢查询

// 按执行时间排序获取前20条
db.system.profile.find().sort( { millis : -1 } ).limit(20).pretty();
// 获取最近20条执行数据
db.system.profile.find().limit(20).sort( { ts : -1 } ).pretty();
// 获取大于100ms的查询的20条数据
db.system.profile.find( { millis : { $gt : 100 } } ).limit(20).pretty()
// 获取时间范围段
db.system.profile.find({ts : {$gt: new ISODate("2020-06-09T03:00:00Z"),$lt: new ISODate("2020-06-09T03:40:00Z")}}).limit(20).pretty()
注意: 分析可能会影响性能并与系统日志共享设置。在生产部署上配置和启用探查器之前,请仔细考虑所有性能和安全隐患。

四)监控技巧

在测试/生产环境中,一个数据库可能会被多个应用使用,生成的慢查询都会记录在一起,若能清晰区分各自应用都慢查询,能准确区分到各自应用进行调优/调试
在这里使用是appName区分各自应用,跟SQL Server的Application Name是一样道理。
假设现在我想监控我本地调试的所有Mongodb查询

1、写入超过200毫秒的查询

db.setProfilingLevel(0)      # 禁止日志记录
db.system.profile.drop()     # 删除system.profile集合
db.createCollection("system.profile", { capped: true, size: 1024 * 1024 * 4 })    # 创建一个新system.profile集合,创建一个4MB大小的日志收集集合
db.setProfilingLevel(1, 200)   # 重新启用分析

2、修改连接字符串

mongodb://<username>:<password>@127.0.0.1:27017/<database>?appName=wilson-debug

3、查询监控

db.system.profile.find({ appName: 'wilson-debug', 
ts: { $gt: new Date(ISODate().getTime() - 1000 * 60) } }) //一分钟内执行的语句
.sort({ ts: 1})
.limit(20)
.pretty()

五、正确建立索引

在没有建立索引的情况下,对Mongodb数据表进行查询操作的时候,需要把数据都加载到内存。当数据的数量达到几十万乃至上百万的时候,这样的加载过程会对系统造成较大的冲击,并影响到其他请求的处理过程。
索引是对数据库表中一列或多列的值进行排序的一种结构,建立索引以后,对索引字段进行查询时,仅会加载索引数据,并能提高查询速度。

一)建立合适的索引

为每一个查询建立合适的索引。
组合索引是创建的索引由多个字段组成,例如:

db.test.createIndex({"username":1, "age":-1}) #1是按升序排列,-1是按降序排列 

交叉索引是每个字段单独建立索引,但是在查询的时候组合查找,例如:

db.test.createIndex({"username":1}) 
db.test.createIndex({"age":-1}) 
db.test.find({"username":"kaka", "age": 30}) 

交叉索引的查询效率较低,在使用时,当查询使用到多个字段的时候,尽量使用组合索引,而不是交叉索引。

二)组合索引的字段排列顺序

当我们的组合索引内容包含匹配条件以及范围条件的时候,比如包含用户名(匹配条件)以及年龄(范围条件),那么匹配条件应该放在范围条件之前。
比如需要查询:

db.test.find({"username":"kaka", "age": {$gt: 10}}) 

那么组合索引应该这样创建:

db.test.createIndex({"username":1, "age":-1}) 

三)查询时尽可能仅查询出索引字段

有时候仅需要查询少部分的字段内容,而且这部分内容刚好都建立了索引,那么尽可能只查询出这些索引内容,需要用到的字段显式声明(_id字段需要显式忽略!)。因为这些数据需要把原始数据文档从磁盘读入内存,造成一定的损耗。
比如说我们的表有三个字段:

username, age, mobile 

索引是这样建立的:

db.test.createIndex({"username":1,"age":-1}) 

我们仅需要查到某个用户的年龄(age),那可以这样写:

db.test.find({"username":"kaka"}, {"_id":0, "age":1}) 

注意到上面的语句,我们除了"age":1外,还加了"_id":0,因为默认情况下,_id都是会被一并查询出来的,当不需要_id的时候记得直接忽略,避免不必要的磁盘操作。

四)对现有的数据大表建立索引的时候,采用后台运行方式

在对数据集合建立索引的过程中,数据库会停止该集合的所有读写操作,因此如果建立索引的数据量大,建立过程慢的情况下,建议采用后台运行的方式,避免影响正常业务流程。

db.test.createIndex({"username":1,"age":-1},{"background":true}) #默认情况下background是false。
posted @ 2023-10-05 17:30  yifanSJ  阅读(72)  评论(0编辑  收藏  举报