MongoDB学习专题(五)索引

1、索引介绍

索引是一种用来快速查询数据的数据结构。B+Tree就是一种常用的数据库索引数据结构,MongoDB采用B+Tree 做索引,索引创建在colletions上。

MongoDB不使用索引的查询,先扫描所有的文档,再匹配符合条件的文档。 使用索引的查询,通过索引找到文档,使用索引能够极大的提升查询效率。

MongoDB索引数据结构

WiredTiger数据文件在磁盘的存储结构(​​WiredTiger​​ 是 MongoDB 的默认存储引擎)

B+ Tree中的leaf page包含:一个页头(page header)、块头(block header)和真正的数据(key/value)

页头:定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息

块头:定义了此页的checksum、块在磁盘上的寻址位置等信息。

2、索引的分类

按照索引包含的字段数量 ,可以分为单键索引和组合索引(复合索引)。

按照索引字段类型 ,可以分为主键索引和非主键索引。

按照索引节点与物理记录的对应方式 来分,可以分为聚簇索引和非聚簇索引(聚簇索引是指索引节点上直接包含了数据记录,而非聚簇索引则仅仅包含一个指向数据记录的指针)

按照索引的特性不同,又可以分为唯一索引、稀疏索引、文本索引、地理空间索引等

由于采用了灵活可变的文档类型,因此它也同样支持对嵌套字段、数组 进行索引。通过建立合适的索引,我们可以极大地提升数据的检索速度。在一些特殊应用场景,MongoDB还支持地理空间索引、文本检索索引、TTL索引等不同的特性。

3、索引设计原则

1、每个查询原则上都需要创建对应索引

2、单个索引设计应考虑满足尽量多的查询

3、索引字段选择及顺序需要考虑查询覆盖率及选择性

4、对于更新及其频繁的字段上创建索引需慎重

5、对于数组索引需要慎重考虑未来元素个数

6、对于超长字符串类型字段上慎用索引

7、并发更新较高的单个集合上不宜创建过多索引

4、索引操作

1、创建索引

创建索引语法格式

复制代码
db.collection.createIndex(keys, options)

注意:3.0.0 版本前创建索引方法为 db.collection.ensureIndex()

Key 值为你要创建的索引字段,1 按升序创建索引, -1 按降序创建索引

可选参数列表如下:

|--------------------|---------------|---------------------------------------------------------------------------------------|
| Parameter | Type | Description |
| background | Boolean | 建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 "background" 可选参数。 "background" 默认值为false。 |
| unique | Boolean | 建立的索引是否唯一。指定为true创建唯一索引。默认值为false. |
| name | string | 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 |
| dropDups | Boolean | 3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false. |
| sparse | Boolean | 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档。默认值为 false. |
| expireAfterSeconds | integer | 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。 |
| v | index version | 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。 |
| weights | document | 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 |
| default_language | string | 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 |
| language_override | string | 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language. |

复制代码
# 创建索引后台执行
db.values.createIndex({open: 1, close: 1}, {background: true})

# 创建唯一索引
db.values.createIndex({title:1},{unique:true})

2、查看索引

查看索引信息

复制代码
db.books.getIndexes()

查看索引键

复制代码
db.books.getIndexKeys()

查看索引占用空间

复制代码
db.collection.totalIndexSize([is_detail])

is_detail:可选参数,传入除0或false外的任意数据都会显示该集合中每个索引的大小及总大小 。如果传入0或false则只显示该集合中所有索引的总大小。默认值为false。

3、删除索引

复制代码
#删除集合指定索引
db.col.dropIndex("索引名称")

#删除集合所有索引   不能删除主键索引
db.col.dropIndexes()

5、索引类型

1、单键索引

在某一个特定的字段上建立索引

mongoDB在ID上建立了唯一的单键索引,所以经常会使用id来进行查询;

在索引字段上进行精确匹配、排序以及范围查找都会使用此索引

复制代码
db.books.createIndex({title:1})

对内嵌文档字段创建索引:

2、复合索引

复合索引是多个字段组合而成的索引,其性质和单字段索引类似。

但不同的是,复合索引中字段的顺序、字段的升降序对查询性能有直接的影响,因此在设计复合索引时则需要考虑不同的查询场景。

  • type: 1→ 按 type字段升序(A-Z 或 小到大)

  • favCount: 1→ 按 favCount字段升序(小到大)

    db.books.createIndex({type:1,favCount:1})

3、多键索引

数组的属性上建立索引

针对这个数组的任意值的查询都会定位到这个文档,既多个索引入口或者键值引用同一个文档

案例:

准备inventory集合:

复制代码
db.inventory.insertMany([
{ _id: 5, type: "food", item: "aaa", ratings: [ 5, 8, 9 ] },
{ _id: 6, type: "food", item: "bbb", ratings: [ 5, 9 ] },
{ _id: 7, type: "food", item: "ccc", ratings: [ 9, 5, 8 ] },
{ _id: 8, type: "food", item: "ddd", ratings: [ 9, 5 ] },
{ _id: 9, type: "food", item: "eee", ratings: [ 5, 9, 5 ] }
])

创建多键索引

复制代码
db.inventory.createIndex( { ratings: 1 } )

多键索引很容易与复合索引产生混淆,复合索引是多个字段的组合,而多键索引则仅仅是在一个字段上出现了多键(multi key)。而实质上,多键索引也可以出现在复合字段上

复制代码
# 创建复合多值索引
db.inventory.createIndex( { item:1,ratings: 1 } )

注意: MongoDB并不支持一个复合索引中同时出现多个数组字段

嵌入文档的索引数组

复制代码
db.inventory.insertMany([
{
  _id: 1,
  item: "abc",
  stock: [
    { size: "S", color: "red", quantity: 25 },
    { size: "S", color: "blue", quantity: 10 },
    { size: "M", color: "blue", quantity: 50 }
  ]
},
{
  _id: 2,
  item: "def",
  stock: [
    { size: "S", color: "blue", quantity: 20 },
    { size: "M", color: "blue", quantity: 5 },
    { size: "M", color: "black", quantity: 10 },
    { size: "L", color: "red", quantity: 2 }
  ]
},
{
  _id: 3,
  item: "ijk",
  stock: [
    { size: "M", color: "blue", quantity: 15 },
    { size: "L", color: "blue", quantity: 100 },
    { size: "L", color: "red", quantity: 25 }
  ]
}
])

在包含嵌套对象的数组字段上创建多键索引

查询结果返回整个文档

复制代码
db.inventory.createIndex( { "stock.size": 1, "stock.quantity": 1 } )

db.inventory.find({"stock.size":"S","stock.quantity":{$gt:20}})

4、地理空间索引

在移动互联网时代,基于地理位置的检索(LBS)功能几乎是所有应用系统的标配。MongoDB为地理空间检索提供了非常方便的功能。地理空间索引(2dsphereindex)就是专门用于实现位置检索的一种特殊索引。

案例:MongoDB如何实现"查询附近商家"?

假设商家的数据模型如下:

复制代码
db.restaurant.insert({
    restaurantId: 0,
    restaurantName:"兰州牛肉面",
    location : {
        type: "Point",
        coordinates: [ -73.97, 40.77 ]
    }
})

创建一个2dsphere索引

复制代码
db.restaurant.createIndex({location : "2dsphere"})

查询附近10000米商家信息

复制代码
db.restaurant.find( { 
    location:{ 
        $near :{
            $geometry :{ 
                type : "Point" ,
                coordinates : [  -73.88, 40.78 ] 
            } ,
            $maxDistance:10000 
        } 
    } 
} )

(1)$near查询操作符:

用于实现附近商家的检索,返回数据结果会按距离排序。

(2)$geometry操作符:

用于指定一个GeoJSON格式的地理空间对象,type=Point表示地理坐标点,coordinates则是用户当前所在的经纬度位置;

(3)$maxDistance限定了最大距离,单位是米。

5、全文索引

MongoDB支持全文检索功能,可通过建立文本索引来实现简易的分词检索。

复制代码
db.reviews.createIndex( { comments: "text" } )

$text操作符可以在有text index的集合上执行文本检索

$text将会使用空格和标点符号 作为分隔符对检索字符串进行分词 , 并且对检索字符串中所有的分词结果进行一个逻辑上的 OR 操作。

全文索引能解决快速文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来快速查找,则可以针对博客内容建立文本索引。

案例

数据准备

复制代码
db.stores.insert(
   [
     { _id: 1, name: "Java Hut", description: "Coffee and cakes" },
     { _id: 2, name: "Burger Buns", description: "Gourmet hamburgers" },
     { _id: 3, name: "Coffee Shop", description: "Just coffee" },
     { _id: 4, name: "Clothes Clothes Clothes", description: "Discount clothing" },
     { _id: 5, name: "Java Shopping", description: "Indonesian goods" }
   ]
)

创建name和description的全文索引

复制代码
db.stores.createIndex({name: "text", description: "text"})

测试

通过$text操作符来查寻数据中所有包含"coffee","shop","java"列表中任何词语的商店

复制代码
db.stores.find({$text: {$search: "java coffee shop"}})

注意:MongoDB的文本索引功能存在诸多限制,而官方并未提供中文分词的功能,这使得该功能的应用场景十分受限。

6、Hash索引

不同于传统的B-Tree索引,哈希索引使用hash函数来创建索引。

在索引字段上进行精确匹配,但不支持范围查询,不支持多键hash;

Hash索引上的入口是均匀分布的,在分片集合中非常有用;

复制代码
db.users.createIndex({username : 'hashed'})

7、通配符索引

MongoDB的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询的加速。

  • 为文档中的每个字段(包括嵌套字段)单独创建索引条目​

  • ​为数组中的每个元素创建索引条目

案例

准备商品数据,不同商品属性不一样

复制代码
db.products.insert([
    {
      "product_name" : "Spy Coat",
      "product_attributes" : {
        "material" : [ "Tweed", "Wool", "Leather" ],
        "size" : {
          "length" : 72,
          "units" : "inches"
        }
      }
    },
    {
      "product_name" : "Spy Pen",
      "product_attributes" : {
         "colors" : [ "Blue", "Black" ],
         "secret_feature" : {
           "name" : "laser",
           "power" : "1000",
           "units" : "watts",
         }
      }
    },
    {
      "product_name" : "Spy Book"
    }
])

创建通配符索引

复制代码
db.products.createIndex( { "product_attributes.$**" : 1 } )

测试:

通配符索引可以支持任意单字段查询 product_attributes 或其嵌入字段

复制代码
db.products.find( { "product_attributes.size.length" : { $gt : 60 } } )
db.products.find( { "product_attributes.material" : "Leather" } )
db.products.find( { "product_attributes.secret_feature.name" : "laser" } )

注意事项

  • 通配符索引不兼容的索引类型或属性
  • 通配符索引是稀疏的,不索引空字段 。因此,通配符索引不能支持查询字段不存在的文档

    复制代码
    db.products.find( { "product_attributes.size.length" : { $gt : 60 } } )
    目标​​:查找 product_attributes.size.length大于 60 的产品。
    ​​问题​​:当前数据中 ​​没有 size.length字段​​,因此返回空结果。
  • 通配符索引为文档或数组的内容生成条目 ,而不是文档/数组本身。因此通配符索引不能支持精确的文档/数组相等匹配 。但可以支持查询字段等于空文档{} 的情况。

    复制代码
    #通配符索引不能支持以下查询:
    db.products.find({ "product_attributes.colors" : [ "Blue", "Black" ] } )
    
    db.products.aggregate([{
      $match : { "product_attributes.colors" : [ "Blue", "Black" ] } 
    }])

    简而言之,通配符索引擅长于查找嵌套字段或数组 元素,但不适合查找完全匹配的文档或数组。因为通配符索引不会存储文档的完整结构(如字段顺序、嵌套层级),因此无法匹配。

6、索引属性

1、唯一索引

唯一性是很常见的一种索引约束需求,重复的数据记录会带来许多处理上的麻烦,比如订单的编号、用户的登录名等。通过建立唯一性索引,可以保证集合中文档的指定字段拥有唯一值。

复制代码
# 创建唯一索引
db.values.createIndex({title:1},{unique:true})

# 复合索引支持唯一性约束
db.values.createIndex({title:1,type:1},{unique:true})

#多键索引支持唯一性约束
db.inventory.createIndex( { ratings: 1 },{unique:true} )
  • 唯一性索引对于文档中缺失的字段会使用null值代替,因此不允许存在多个文档缺失索引字段的情况。
  • 对于分片的集合,唯一性约束必须匹配分片规则。换句话说,为了保证全局的唯一性,分片键必须作为唯一性索引的前缀字段 。(分片键 是进行数据拆分的关键字段

2、部分索引

部分索引仅对满足指定过滤器表达式的文档进行索引。

通过在一个集合中为文档的一个子集建立索引,部分索引具有更低的存储需求和更低的索引创建和维护的性能成本

复制代码
db.restaurants.createIndex(
   { cuisine: 1, name: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } }
)

partialFilterExpression选项接受指定过滤条件的文档:

  • 等式表达式(例如:field: value或使用$eq操作符)

  • [exists: true](https://docs.mongodb.com/manual/reference/operator/query/exists/#mongodb-query-op.-exists "exists: true")

  • gt, gte, lt, lte

  • $type

  • 顶层的$and

    符合条件,使用索引

    db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )

    不符合条件,不能使用索引

    db.restaurants.find( { cuisine: "Italian" } )

案例1

restaurants集合数据

复制代码
db.restaurants.insert({
   "_id" : ObjectId("5641f6a7522545bc535b5dc9"),
   "address" : {
      "building" : "1007",
      "coord" : [
         -73.856077,
         40.848447
      ],
      "street" : "Morris Park Ave",
      "zipcode" : "10462"
   },
   "borough" : "Bronx",
   "cuisine" : "Bakery",
   "rating" : { "date" : ISODate("2014-03-03T00:00:00Z"),
                "grade" : "A",
                "score" : 2
              },
   "name" : "Morris Park Bake Shop",
   "restaurant_id" : "30075445"
})

创建索引

复制代码
db.restaurants.createIndex(
   { borough: 1, cuisine: 1 },    // 索引字段

    //仅索引 rating.grade字段值为 "A"的文档​​。
   { partialFilterExpression: { 'rating.grade': { $eq: "A" } } }
)

测试

复制代码
db.restaurants.find( { borough: "Bronx", 'rating.grade': "A" } )
db.restaurants.find( { borough: "Bronx", cuisine: "Bakery" } )

唯一约束结合部分索引使用导致唯一约束失效的问题

**注意:**如果同时指定了partialFilterExpression和唯一约束,那么唯一约束只适用于满足筛选器表达式的文档。如果文档不满足筛选条件,那么带有惟一约束的部分索引不会阻止插入不满足惟一约束的文档。

3、稀疏索引

索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档

特性: 只对存在字段的文档进行索引(包括字段值为null的文档)

复制代码
#不索引不包含xmpp_id字段的文档
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )

如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引,除非hint()明确指定索引。

稀疏索引和部分索引的什么区别就像是:稀疏索引会查出活跃用户的整个字段,但是部分索引就只会查出活跃的用户

4、TTL索引

在一般的应用系统中,并非所有的数据都需要永久存储。

例如一些系统事件、用户消息等,这些数据随着时间的推移,其重要程度逐渐降低。更重要的是,存储这些大量的历史数据需要花费较高的成本,因此项目中通常会对过期且不再使用的数据进行老化处理。

方案一:为每个数据记录一个时间戳,应用侧开启一个定时器,按时间戳定期删除过期的数据。

方案二:数据按日期进行分表,同一天的数据归档到同一张表,同样使用定时器删除过期的表。

对于数据老化,MongoDB提供了一种更加便捷的做法:TTL索引

TTL索引需要声明在一个日期类型的字段中,TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时钟时间后自动从集合中删除文档。

复制代码
# 创建 TTL 索引,TTL 值为3600秒
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )

创建TTL索引之后,MongoDB会在周期性运行的后台线程中对该集合进行检查及数据清理工作

除了数据老化功能,TTL索引具有普通索引的功能,同样可以用于加速数据的查询

TTL 索引不保证过期数据会在过期后立即被删除

文档过期和 MongoDB 从数据库中删除文档的时间之间可能存在延迟。删除过期文档的后台任务每 60 秒运行一次。因此,在文档到期和后台任务运行之间的时间段内,文档可能会保留在集合中。

案例

数据准备

复制代码
db.log_events.insertOne({
  "createdAt": new Date(),  // 当前时间
  "logEvent": 2,
  "logMessage": "Success!"
})

创建TTL索引

复制代码
db.log_events.createIndex(
  { "createdAt": 1 },       // 索引字段
  { expireAfterSeconds: 20 } // 文档存活时间(秒)
)

可变的过期时间

TTL索引在创建之后,仍然可以对过期时间进行修改。这需要使用collMod命令对索引的定义进行变更

复制代码
db.runCommand({
  collMod: "log_events",                  // 要修改的集合名
  index: {
    keyPattern: { createdAt: 1 },         // 目标索引字段
    expireAfterSeconds: 600               // 将过期时间改为600秒(10分钟)
  }
})

使用约束

TTL索引的确可以减少开发的工作量,而且通过数据库自动清理的方式会更加高效、可靠,但是在使用TTL索引时需要注意以下的限制:

  • TTL索引只能支持单个非_id字段
  • TTL索引不能用于固定集合
  • TTL索引无法保证及时的数据老化,MongoDB会通过后台的TTLMonitor定时器来清理老化数据,默认的间隔时间是1分钟。当然如果在数据库负载过高的情况下,TTL的行为则会进一步受到影响。
  • TTL索引对于数据的清理仅仅使用了remove 命令,这种方式并不是很高效。因此TTL Monitor在运行期间对系统CPU、磁盘都会造成一定的压力。相比之下,按日期分表的方式操作会更加高效。

日志存储:

  • 日期分表
  • 固定集合
  • TTL索引

插入: writeConcern:{w:0}

5、隐藏索引

隐藏索引对查询规划器不可见,不能用于支持查询。

通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。

如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。4.4新版功能。

复制代码
创建隐藏索引
db.restaurants.createIndex({ borough: 1 },{ hidden: true });

# 隐藏现有索引
db.restaurants.hideIndex( { borough: 1} );
db.restaurants.hideIndex( "索引名称" )

# 取消隐藏索引
db.restaurants.unhideIndex( { borough: 1} );
db.restaurants.unhideIndex( "索引名称" ); 

案例:

复制代码
db.scores.insertMany([
    {"userid" : "newbie"},
    {"userid" : "abby", "score" : 82},
    {"userid" : "nina", "score" : 90}
])

创建隐藏索引

复制代码
db.scores.createIndex(
  { userid: 1 },   // 索引字段:userid(升序)
  { hidden: true }  // 将索引标记为隐藏
)

查看索引信息

复制代码
db.scores.getIndexes()

解释一下上图:

复制代码
默认的id索引:
{ 
  "v": 2,              // 索引版本(MongoDB内部使用)
  "key": { "_id": 1 },  // 索引字段:_id(升序)
  "name": "_id_"       // 索引名称
}

用户创建的隐藏索引:
{ 
  "v": 2, 
  "key": { "userid": 1 },  // 索引字段:userid(升序)
  "name": "userid_1",      // 索引名称
  "hidden": true           // 标记为隐藏索引
}

6、索引使用建议

1.为每一个查询建立合适的索引

这个是针对于数据量较大比如说超过几十上百万(文档数目)数量级的集合。如果没有索引MongoDB需要把所有的Document从盘上读到内存,这会对MongoDB服务器造成较大的压力并影响到其他请求的执行。

2.创建合适的复合索引,不要依赖于交叉索引

如果你的查询会使用到多个字段,MongoDB有两个索引技术可以使用:交叉索引和复合索引。交叉索引就是针对每个字段单独建立一个单字段索引,然后在查询执行时候使用相应的单字段索引进行索引交叉而得到查询结果。交叉索引目前触发率较低,所以如果你有一个多字段查询的时候,建议使用复合索引能够保证索引正常的使用。

复制代码
#查找所有年龄小于30岁的深圳市马拉松运动员
db.athelets.find({sport: "marathon", location: "sz", age: {$lt: 30}}})

#创建复合索引
db.athelets.createIndex({sport:1, location:1, age:1})

3.复合索引字段顺序:匹配条件在前,范围条件在后(Equality First, Range After)

前面的例子,在创建复合索引时如果条件有匹配和范围之分,那么匹配条件(sport: "marathon") 应该在复合索引的前面。范围条件(age:

4.尽可能使用覆盖索引(Covered Index)

建议只返回需要的字段,同时,利用覆盖索引来提升性能。

5.建索引要在后台运行

在对一个集合创建索引时,该集合所在的数据库将不接受其他读写操作。对大数据量的集合建索引,建议使用后台运行选项 {background: true}

6.避免设计过长的数组索引

数组索引是多值的,在存储时需要使用更多的空间。如果索引的数组长度特别长,或者数组的增长不受控制,则可能导致索引空间急剧膨胀。

7、explain执行计划详解

通常我们使用explain需要关心的问题:

  • 查询是否使用了索引
  • 索引是否减少了扫描的记录数量
  • 是否存在低效的内存排序

explain()方法的形式如下:

复制代码
db.collection.find().explain(<verbose>)

verbose 可选参数,表示执行计划的输出模式,默认queryPlanner

|-------------------|-------------------------------------------------------|
| 模式名字 | 描述 |
| queryPlanner | 执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和 MongoDB 服务信息等 |
| exectionStats | 最佳执行计划的执行情况和被拒绝的计划等信息 |
| allPlansExecution | 选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况 |

特性 queryPlanner executionStats allPlansExecution
​是否执行查询​ ❌ 否 ✅ 是(仅最优计划) ✅ 是(所有候选计划)
​返回信息​ 理论上的最优计划 最优计划的实际执行统计 所有计划的实际执行统计
​性能开销​ 最低 中等 最高
​主要用途​ 快速检查索引使用 分析查询实际性能 深度优化复杂查询
​输出重点​ winningPlan阶段类型 扫描文档数/执行时间 各计划性能对比

1、queryPlanner

复制代码
# 未创建title的索引
db.books.find({title:"book-1"}).explain("queryPlanner")

扫描索引:

全表扫描:

|----------------|--------------|
| 字段名称 | 描述 |
| plannerVersion | 执行计划的版本 |
| namespace | 查询的集合 |
| indexFilterSet | 是否使用索引 |
| parsedQuery | 查询条件 |
| winningPlan | 最佳执行计划 |
| stage | 查询方式 |
| filter | 过滤条件 |
| direction | 查询顺序 |
| rejectedPlans | 拒绝的执行计划 |
| serverInfo | mongodb服务器信息 |

2、executionStats

复制代码
#创建索引
db.books.createIndex({title:1})

db.books.find({title:"book-1"}).explain("executionStats")

结果:

复制代码
test> db.books.find({title:"book-1"}).explain("executionStats")
{
  explainVersion: '1',
  queryPlanner: {
    namespace: 'test.books',
    parsedQuery: { title: { '$eq': 'book-1' } },
    indexFilterSet: false,
    queryHash: '79F2D0D9',
    planCacheShapeHash: '79F2D0D9',
    planCacheKey: 'E03CA69C',
    optimizationTimeMillis: 0,
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    prunedSimilarIndexes: false,
    winningPlan: {
      isCached: false,
      stage: 'FETCH',
      inputStage: {
        stage: 'IXSCAN',
        keyPattern: { title: 1 },
        indexName: 'title_1',
        isMultiKey: false,
        multiKeyPaths: { title: [] },
        isUnique: false,
        isSparse: false,
        isPartial: false,
        indexVersion: 2,
        direction: 'forward',
        indexBounds: { title: [ '["book-1", "book-1"]' ] }
      }
    },
    rejectedPlans: []
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 1,
    executionTimeMillis: 0,
    totalKeysExamined: 1,
    totalDocsExamined: 1,
    executionStages: {
      isCached: false,
      stage: 'FETCH',
      nReturned: 1,
      executionTimeMillisEstimate: 0,
      works: 2,
      advanced: 1,
      needTime: 0,
      needYield: 0,
      saveState: 0,
      restoreState: 0,
      isEOF: 1,
      docsExamined: 1,
      alreadyHasObj: 0,
      inputStage: {
        stage: 'IXSCAN',
        nReturned: 1,
        executionTimeMillisEstimate: 0,
        works: 2,
        advanced: 1,
        needTime: 0,
        needYield: 0,
        saveState: 0,
        restoreState: 0,
        isEOF: 1,
        keyPattern: { title: 1 },
        indexName: 'title_1',
        isMultiKey: false,
        multiKeyPaths: { title: [] },
        isUnique: false,
        isSparse: false,
        isPartial: false,
        indexVersion: 2,
        direction: 'forward',
        indexBounds: { title: [ '["book-1", "book-1"]' ] },
        keysExamined: 1,
        seeks: 1,
        dupsTested: 0,
        dupsDropped: 0
      }
    }
  },
  queryShapeHash: '4C0BD86DFC72FB6F57EE5B5C97A0AA4BADF66C58B3F100C249786C0C5BF6ABD6',
  command: { find: 'books', filter: { title: 'book-1' }, '$db': 'test' },
  serverInfo: {
    host: 'LAPTOP-7F69KB52',
    port: 27017,
    version: '8.0.12',
    gitVersion: 'b60fc6875b5fb4b63cc0dbbd8dda0d6d6277921a'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
    internalQueryFrameworkControl: 'trySbeRestricted',
    internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
  },
  ok: 1
}

|-----------------------------------------------------------------------|----------------------------------|
| 字段名称 | 描述 |
| winningPlan.inputStage | 用来描述子stage,并且为其父stage提供文档和索引关键字 |
| winningPlan.inputStage.stage | 子查询方式 |
| winningPlan.inputStage.keyPattern | 所扫描的index内容 |
| winningPlan.inputStage.indexName | 索引名 |
| winningPlan.inputStage.isMultiKey | 是否是Multikey。如果索引建立在array上,将是true |
| executionStats.executionSuccess | 是否执行成功 |
| executionStats.nReturned | 返回的个数 |
| executionStats.executionTimeMillis | 这条语句执行时间 |
| executionStats.executionStages.executionTimeMillisEstimate | 检索文档获取数据的时间 |
| executionStats.executionStages.inputStage.executionTimeMillisEstimate | 扫描获取数据的时间 |
| executionStats.totalKeysExamined | 索引扫描次数 |
| executionStats.totalDocsExamined | 文档扫描次数 |
| executionStats.executionStages.isEOF | 是否到达 steam 结尾,1 或者 true 代表已到达结尾 |
| executionStats.executionStages.works | 工作单元数,一个查询会分解成小的工作单元 |
| executionStats.executionStages.advanced | 优先返回的结果数 |
| executionStats.executionStages.docsExamined | 文档检查数 |

3、allPlansExecution

allPlansExecution返回的信息包含 executionStats 模式的内容,且包含allPlansExecution:[]块

复制代码
"allPlansExecution" : [
      {
         "nReturned" : <int>,
         "executionTimeMillisEstimate" : <int>,
         "totalKeysExamined" : <int>,
         "totalDocsExamined" :<int>,
         "executionStages" : {
            "stage" : <STAGEA>,
            "nReturned" : <int>,
            "executionTimeMillisEstimate" : <int>,
            ...
            }
         }
      },
      ...
   ]

4、stage状态

|-----------------|-------------------------------|
| 状态 | 描述 |
| COLLSCAN | 全表扫描 |
| IXSCAN | 索引扫描 |
| FETCH | 根据索引检索指定文档 |
| SHARD_MERGE | 将各个分片返回数据进行合并 |
| SORT | 在内存中进行了排序 |
| LIMIT | 使用limit限制返回数 |
| SKIP | 使用skip进行跳过 |
| IDHACK | 对_id进行查询 |
| SHARDING_FILTER | 通过mongos对分片数据进行查询 |
| COUNTSCAN | count不使用Index进行count时的stage返回 |
| COUNT_SCAN | count使用了Index进行count时的stage返回 |
| SUBPLA | 未使用到索引的$or查询的stage返回 |
| TEXT | 使用全文索引进行查询时候的stage返回 |
| PROJECTION | 限定返回字段时候stage的返回 |

执行计划的返回结果中尽量不要出现以下stage:

  • COLLSCAN(全表扫描)
  • SORT(使用sort但是无index)
  • 不合理的SKIP
  • SUBPLA(未用到index的$or)
  • COUNTSCAN(不使用index进行count)
相关推荐
Runing_WoNiu5 分钟前
最佳左前缀法则(Optimal Left-Prefix Rule)
数据库·mysql
二闹15 分钟前
面试官经常问的ArrayList 和 LinkedList的区别
后端
IvorySQL19 分钟前
如何使用 pg_rman 进行 PostgreSQL 的备份与恢复
数据库
五岁小孩吖38 分钟前
Go 踩过的坑之协程参数不能过大
后端
树獭叔叔40 分钟前
深入理解 Node.js 中的原型链
后端·node.js
雨绸缪40 分钟前
为什么 Java 在 2025 年仍然值得学习:开发人员的 25 年历程
java·后端·掘金·金石计划
Tapdata1 小时前
一文了解增量物化视图维护(IVM):原理、演化与实践落地
数据库
码间舞1 小时前
IndexDB适用于什么场景?如何使用IndexDB?
前端·javascript·数据库
lovebugs1 小时前
Kubernetes中高效获取Java应用JVM参数的终极指南
后端·docker·kubernetes
二闹1 小时前
Java中的随机数生成的方法
后端