MongoDB索引

422 阅读8分钟

MongoDB索引

简述

索引可以提高查询和排序的效率。如果没有索引,MongoDB必须执行全集合扫描,比如:扫描一个collection中的每一个document,用来做查询匹配。如果在查询中有合适的索引,MongoDB就可以使用这些索引来做扫描,从而减少文档的数量

  1. 默认的_id索引

    MongoDB在每一个Document中都有一个_id字段,默认是通过ObjectId来生成的。_id 索引防止客户端插入两个具有相同 _id 字段值的文档。不能删除该索引。如果不使用MongoDB的ObjectId,就必须保证该值不会重复。

  2. 创建索引

    使用 db.collection.createIndex()创建索引

    db.collection.createIndex( <key and index type specification>, <options> )
    # 在name字段中创建索引,逆序
    db.collection.createIndex( { name: -1 } )
    

    索引名字

    默认的索引的名字是,索引字段 + _ + 排序方向。比如{ item : 1, quantity: -1 },默认的索引的名字是item_1_quantity_-1,也可以在创建索引的时候指定索引名字如下:

    db.products.createIndex(
      { item: 1, quantity: -1 } ,
      { name: "query for inventory" }
    )
    
  3. 查看索引

    db.products.getIndexes()
    

索引种类

  • 普通索引

    在document中的单个字段上面创建索引,按照升序或者降序的方式。

  • 复合索引

    在document中的多个字段上面创建索引,按照升序或者降序的方式。复合索引中索引字段的排序方式可以决定索引是否支持排序操作。

  • 多key索引

    在document中的数组类型字段上面创建的索引,该索引会自动为数组中的每个字段建立索引值,多key索引支持在查询的数组字段中的单个值或者多个值。MongoDB会自动的判断是否要创建多key索引,而不需要显示的指定索引类型(创建索引如果该字段是数组,就是多key索引,否则就不是)。

  • 地理空间索引

    支持对地理空间坐标数据的高效查询,MongoDB 提供了两个特殊的索引: 2d indexes2dsphere indexes

  • 文本索引

    支持在字符内容中的搜索。

  • hash索引

    为了支持基于散列的分片,MongoDB 提供了一个散列索引类型,它对字段值的散列进行索引。这些索引在其范围内具有更随机的值分布,但只支持相等匹配,不支持基于范围的查询。

索引属性

唯一索引(unique_index)

索引值是唯一的。通过unique关键词来指定

db.members.createIndex( { "user_id": 1 }, { unique: true } )

部分索引(Partial Index)

只对满足条件的值来建立索引,不会对文档中所有的数据建立索引。在创建索引的时候使用partialFilterExpression来指定筛选条件,如下:

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

上面的例子中,只会对rating大于5索引。

partialFilterExpression中能用的过滤表达式如下:

  • 等于运算符(field:valuie或者$eq运算符)
  • 存在($exists)
  • 比较运算符$gt, $gte, $lt, $lte expressions
  • $type
  • $and

要想使用部分索引,必须要在它的索引范围内做操作,否则就用不了。

比如,下面的查询

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

会用到rating字段上的部分索引,上面的查询rating是在部分索引范围中的。

下面的查询就不会用到部分索引,它的查询条件不是部分索引用的数据。(就是对于这个字段的查询条件必须是该字段上的部分索引中的子集)

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

部分索引和唯一性约束

部分索引只索引集合中满足指定筛选器表达式的文档。就算上面有唯一性约束,也只是在满足筛选条件中使用。别的不管。如下:有下面的数据

{ "_id" : ObjectId("56424f1efa0358a27fa1f99a"), "username" : "david", "age" : 29 }
{ "_id" : ObjectId("56424f37fa0358a27fa1f99b"), "username" : "amanda", "age" : 35 }
{ "_id" : ObjectId("56424fe2fa0358a27fa1f99c"), "username" : "rajiv", "age" : 57 }

建立部分索引

db.users.createIndex(
   { username: 1 },
   { unique: true, partialFilterExpression: { age: { $gte: 21 } } }
)

部分索引会阻止下面的插入操作。它是满足部分索引。

db.users.insert( { username: "david", age: 27 } )
db.users.insert( { username: "amanda", age: 25 } )
db.users.insert( { username: "rajiv", age: 32 } )

但是下面就不会

db.users.insert( { username: "david", age: 20 } )
db.users.insert( { username: "amanda" } )
db.users.insert( { username: "rajiv", age: null } )

稀疏索引(Sparse Indexes)

稀疏索引会跳过document中没有索引字段,或者索引字段为null的文档,不会在这些文档上面建立索引。

创建稀疏索引,通过sparse关键词来指定

db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )

索引只会索引包含xmpp_id字段的文档

和上面的部分索引一样,如果稀疏索引会导致排序和查询的结果不完整,那MongoDB也不会主动的使用它。除非用Hint()来指定,例子如下:

# 在collection中插入一条数据
db.collection.insertOne( { _id: 1, y: 1 } );
# 在collection的x字段上建立了一个稀疏索引
db.collection.createIndex( { x: 1 }, { sparse: true } );
# 指定用稀疏索引,结果就是0,因为稀疏索引上面没有对第一条数据建立索引。
db.collection.find().hint( { x: 1 } ).count();

结果如下:

图片.png

要想正确的count,就不要使用hint来指定索引。

稀疏索引和唯一性约束

和部分索引一样,唯一性只对稀疏索引上的数据有效。例子如下:

数据:

db.scores.insertMany( [
  { "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" },
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 },
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }
] )

创建稀疏索引,增加唯一性:

db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )

插入正常数据:

db.scores.insertMany( [
   { "userid": "AAAAAAA", "score": 43 },
   { "userid": "BBBBBBB", "score": 34 },
   { "userid": "CCCCCCC" },
   { "userid": "DDDDDDD" }
] )

score,43,34并没有重复,下面的两条数据没有score字段,不会添加到稀疏索引里面。

插入错误数据

db.scores.insertMany( [
   { "userid": "AAAAAAA", "score": 82 },
   { "userid": "BBBBBBB", "score": 90 }
] )

触发稀疏索引唯一性校验,82和90都是之前存在的。

稀疏索引和不完整的结果

数据:

db.scores.insertMany([
{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" },
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 },
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }
])

在score字段上创建稀疏索引:

db.scores.createIndex( { score: 1 } , { sparse: true } )

在score字段上做排序:

db.scores.find().sort( { score: -1 } )

结果如下:

图片.png

newbie没有score字段,排在最后。如果使用score字段上的稀疏索引,会导致结果不完整。所以,MongoDB就没有使用他,除非用Hint指定。结果如下:

db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )

图片.png

只有两条数据,newbie因为没有score字段,稀疏索引上没有他的数据。

TTL索引(TTL Indexes)

TTL索引是带有时间限制的,单一字段的索引。在创建索引的时候需要指定过期时间,到期就会将数据删除。TTL索引的字段必须是时间属性,在创建索引的时候通过expireAfterSeconds指定。

db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )

过期时间

过期时间是索引字段的值 + 指定过期时间 = 过期时间。

不是立即删除,后台有60s扫描一次。并且这个时间有可能会变化,因为服务器的负载的原因,文档过期和MongoDB从数据库中删除数据是存在延迟的

索引字段是数组,并且有多个值,MongoDB会使用最早的值来计算这条document的过期时间。

索引字段没有时间属性,TTL会忽略掉,document不会过期。

如果document中没有索引字段,TTL会忽略掉,document不会过期。

_id字段不支持TTL索引,不能在限制大小的collection中删除文档。

TTL索引创建之后是只能通过调用collMod 命令来修改,不能在同一个字段上再次通过createIndex来修改时间,并且不能将一个非TTL的索引转换为TTL的索引。

隐藏索引(Hidden Indexes)

隐藏索引功能是一样的,MongoDB也在维护,但只是在读数据的时候不能使用。通过隐藏索引,可以评估删除该索引所造成的影响。通过指定hidden属性

db.addresses.createIndex(
   { borough: 1 },
   { hidden: true }
);

可以通过

# 可以通过查看
db.scores.getIndexes();

图片.png

非隐藏索引变为隐藏索引

# 指定索引
db.restaurants.hideIndex( { borough: 1, ratings: 1 } ); 
# 指定索引名字
db.restaurants.hideIndex( "borough_1_ratings_1" );  

隐藏索引变为非隐藏索引

# 指定索引
db.restaurants.unhideIndex( { borough: 1, city: 1 } ); 
# 指定索引名字
db.restaurants.unhideIndex( "borough_1_ratings_1" ); 

上述的验证,可以通过 getIndexes() 来查看

除了不能给优化器用之外(就是不用这个索引,但是还是正常的维护),别的行为和非隐藏索引都是一样的。

  • 如果隐藏的是唯一索引,该唯一索引的限制还存在
  • 如果隐藏的是TTL的索引,TTL的作用还在
  • 隐藏的索引通过listIndexes(属于runCommand命令中的)和getIndexes可以查看
  • 隐藏索引只是不起作用,但是索引还是正常的维护,所以,在一些统计命令里面(db.collection.stats()),还是可以看到的。

约束

索引和排序规则

MongoDB中可以指定索引的比较规则,在创建索引的时候通过collation指定。如果不指定,就是普通的创建的索引的方式,默认的排序规则是二进制 如下:

# 创建索引,指定比较规则为fr
db.myColl.createIndex( { category: 1 }, { collation: { locale: "fr" } } )

在使用的时候也需要指定要使用哪种排序规则,如果和指定排序规则的索引一致,就可以使用

db.myColl.find( { category: "cafe" } ).collation( { locale: "fr" } )

下面的例子,在查询的时候没有指定排序规则,就不能用上面创建的索引了

# 默认采用的是二进制比较规则
db.myColl.find( { category: "cafe" } )

相关的文档可以看

Collation-官方文档


关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。