7.索引

介绍

索引是一种用来快速查询数据的数据结构。B+Tree就是一种常用的数据库索引数据结构,MongoDB 采用B+Tree 做索引,索引创建在colletions上。MongoDB不使用索引的查询,先扫描所有的文档,再 匹配符合条件的文档。 使用索引的查询,通过索引找到文档,使用索引能够极大的提升查询效率。

MongoDB索引数据结构

B+Tree

索引的分类

  • 按照索引包含的字段数量,可以分为单键索引和组合索引(或复合索引)。
  • 按照索引字段的类型,可以分为主键索引和非主键索引。
  • 按照索引节点与物理记录的对应方式来分,可以分为聚簇索引和非聚簇索引,其中聚簇索引是指索 引节点上直接包含了数据记录,而后者则仅仅包含一个指向数据记录的指针。
  • 按照索引的特性不同,又可以分为唯一索引、稀疏索引、文本索引、地理空间索引等

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

索引操作

创建索引

db.collection.createIndex(keys, options)
  • Key值为你要创建的索引字段,1按升序创建索引,-1按降序创建索引可选
  • 参数列表如下:

image-20220321170745305

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

例子:

# 创建索引后台执行
db.values.createIndex({open: 1, close: 1}, {background: true})
# 创建唯一索引
db.values.createIndex({title:1},{unique:true})

查看索引

#查看索引信息
db.values.getIndexes()
#查看索引键
db.values.getIndexKeys()
#查看索引占用空间
#is_detail=0查看集合索引占用总空间
#is_detail!=0查看集合索引分别占用空间
db.values.totalIndexSize([is_detail]

删除索引

#删除集合指定索引
db.values.dropIndex("索引名称")
#删除集合所有索引
db.values.dropIndexes()

索引类型

单键索引(Single Field Indexes)

在某一个特定的字段上建立索引 mongoDB在ID上建立了唯一的单键索引,所以经常会使用id来进行查 询; 在索引字段上进行精确匹配、排序以及范围查找都会使用此索引

db.values.createIndex({col:1})
#创建一个索引
db.values.createIndex({col.name:1})
#创建一个内嵌文档字段索引

复合索引(Compound Index)

复合索引是多个字段组合而成的索引,其性质和单字段索引类似。但不同的是,复合索引中字段的顺序、字段的升降序对查询性能有直接的影响,因此在设计复合索引时则需要考虑不同的查询场景。

image-20220321175458720

db.values.createIndex({col1:1,col2:1})
#key:字段 value:1升序创建索引 -1降序创建索引

多键索引(Multikey Index)

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

创建数据

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 } )

地理空间索引(Geospatial Index)

在移动互联网时代,基于地理位置的检索(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 :{ #$near查询操作符,用于实现附近商家的检索,返回数据结果会按距离排序。
$geometry :{ #$geometry操作符用于指定一个GeoJSON格式的地理空间对象,type=Point表示地理坐标点,
coordinates则是用户当前所在的经纬度位置;
type : "Point" ,
coordinates : [ -73.88, 40.78 ]
} ,
$maxDistance:10000 #$maxDistance限定了最大距离,单位是米。
}
}
} )

全文索引(Text Indexes)

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

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"}})

Hash索引(Hashed Indexes)

不同于传统的B-Tree索引,哈希索引使用hash函数来创建索引。在索引字段上进行精确匹配,但不支持范 围查询,不支持多键hash; Hash索引上的入口是均匀分布的,在分片集合中非常有用;

db.users. createIndex({username : 'hashed'})

通配符索引(Wildcard Indexes)

MongoDB的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询 的加速。MongoDB 4.2 引入了通配符索引来支持对未知或任意字段的查询。

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

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" } )

注意事项:通配符索引不兼容的索引类型或属性

image-20220321194234474

通配符索引是稀疏的,不索引空字段。因此,通配符索引不能支持查询字段不存在的文档。

# 通配符索引不能支持以下查询,不会使用索引
db.products.find( {"product_attributes" : { $exists : false } } )
db.products.aggregate([
{ $match : { "product_attributes" : { $exists : false } } }
])

通配符索引为文档或数组的内容生成条目,而不是文档/数组本身。因此通配符索引不能支持精确 的文档/数组相等匹配。通配符索引可以支持查询字段等于空文档{}的情况。

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

索引属性

唯一索引(Unique Indexes)

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

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

部分索引(Partial Indexes)

部分索引仅对满足指定过滤器表达式的文档进行索引。通过在一个集合中为文档的一个子集建立索引, 部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。3.2新版功能。

部分索引提供了稀疏索引功能的超集,应该优先于稀疏索引。

db.restaurants.createIndex(
{ cuisine: 1, name: 1 },
{ partialFilterExpression: { rating: { $gt: 5 } } }
)
#创建索引,并且查询是rating>5,索引才生效

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

  • 等式表达式(例如:field: value或使用$eq操作符)
  • $exists: true
  • $gt, $gte, $lt, $lte
  • $type
  • 顶层的$and
# 符合条件rating>5了,使用索引
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
# 不符合条件没有rating查询条件,不能使用索引
db.restaurants.find( { cuisine: "Italian" } )

例子

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 },
{ partialFilterExpression: { 'rating.grade': { $eq: "A" } } }
)

测试

#索引生效
db.restaurants.find( { borough: "Bronx", 'rating.grade': "A" } )
#索引失效
db.restaurants.find( { borough: "Bronx", cuisine: "Bakery" } )

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

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

案例

users集合数据准备

db.users.insertMany( [
{ username: "david", age: 29 },
{ username: "amanda", age: 35 },
{ username: "rajiv", age: 57 }
] )

创建索引,指定username字段和部分过滤器表达式age: {$gte: 21}的唯一约束。

db.users.createIndex(
{ username: 1 },
{ unique: true, partialFilterExpression: { age: { $gte: 21 } } }
)
#创建一个username的唯一索引,并且只有age>=21才会创建索引

测试

下面有坑坑坑坑坑坑!!!!

索引防止了以下文档的插入,因为文档已经存在,且指定的用户名和年龄字段大于21:

db.users.insertMany( [
{ username: "david", age: 27 },
{ username: "amanda", age: 25 },
{ username: "rajiv", age: 32 }
] )
#插入失败,因为上面已经创建了相同的用户名,并且年龄>=27

但是,以下具有重复用户名的文档是允许的,因为唯一约束只适用于年龄大于或等于21岁的文档。

db.users.insertMany( [
{ username: "david", age: 20 },
{ username: "amanda" },
{ username: "rajiv", age: null }
] )
#插入成功,因为年龄条件为满足,但唯一索引username值重复了

稀疏索引(Sparse Indexes)

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

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

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

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

案例

数据准备

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

创建稀疏索引

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

测试

# 使用稀疏索引
db.scores.find( { score: { $lt: 90 } } )
# 即使排序是通过索引字段,MongoDB也不会选择稀疏索引来完成查询,以返回完整的结果
db.scores.find().sort( { score: -1 } )
# 要使用稀疏索引,使用hint()显式指定索引
db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )

同时具有稀疏性和唯一性的索引可以防止集合中存在字段值重复的文档,但允许不包含此索引字段的文 档插入。

案例

# 创建具有唯一约束的稀疏索引
db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )

测试

这个索引将允许插入具有唯一的分数字段值或不包含分数字段的文档。因此,给定scores集合中的现有 文档,索引允许以下插入操作:

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

索引不允许添加下列文件,因为已经存在评分为82和90的文件:

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

TTL索引(TTL Indexes)

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

通常的做法如下:

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

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

对于数据老化,MongoDB提供了一种更加便捷的做法:TTL(Time To Live)索引。TTL索引需要声明 在一个日期类型的字段中,TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时 钟时间后自动从集合中删除文档。

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

对集合创建TTL索引之后,MongoDB会在周期性运行的后台线程中对该集合进行检查及数据清理工作。 除了数据老化功能,TTL索引具有普通索引的功能,同样可以用于加速数据的查询。

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

修改过期时间

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

db.runCommand({collMod:"log_events",index:{keyPattern:
{createdAt:1},expireAfterSeconds:600}})

使用约束

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

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

隐藏索引(Hidden Indexes)

想删除索引,但又无法确定删除后果,先假删除一下

隐藏索引对查询规划器不可见,不能用于支持查询。通过对规划器隐藏索引,用户可以在不实际删除索 引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已 删除的索引。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 },
{ hidden: true }
);

查看索引信息

db.scores.getIndexes()
#发现有个字段hidden:true

测试

# 不使用索引
db.scores.find({userid:"abby"}).explain()
#取消隐藏索引(取消假删索引,恢复它)
db.scores.unhideIndex( { userid: 1} );
#使用索引
db.scores.find({userid:"abby"}).explain()

索引使用建议

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

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

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

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

    #查找所有年龄小于30岁的深圳市马拉松运动员
    db.athelets.find({sport: "marathon", location: "sz", age: {$lt: 30}}})
    #创建复合索引
    db.athelets.createIndex({sport:1, location:1, age:1})
    
  • 复合索引字段顺序:匹配条件在前,范围条件在后(Equality First, Range After)

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

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

  • 建索引要在后台运行

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

Explain执行计划

我们可以通过执行计划来判断查询语句的效率,根据实际情况进行调整,然后提高查询效率。
可以使用如下方法:

db.collection.find().explain(<verbose>)
#非管道查询explain
db.collection.explain(<verbose>).aggregate()
#非、管道查询explain

verbose 参数表示执行计划的输出模式。有三种:queryPlanner,executionStats,allPlanExecution

image-20220321210450155

queryPlanner

返回结果字段解释

image-20220321210758521

executionStats

executionStats 模式的返回信息中包含了 queryPlanner 模式的所有字段,并且还包含了最佳执行计划的执行情况

image-20220321210849738

allPlansExecution

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

“allPlansExecution” : [ ] 块信息格式如下:

image-20220321211000056

stage状态

image-20220321211108938

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

  • COLLSCAN(全表扫描)
  • SORT(使用sort但是无index)
  • 不合理的SKIP
  • SUBPLA(未用到index的$or)
  • COUNTSCAN(不使用index进行count)
posted @ 2022-03-24 10:58  RollBack2010  阅读(46)  评论(0编辑  收藏  举报