MongoDB索引
索引是一种用来快速查询数据的数据结构。B+Tree 就是一种常用的数据库索引数据结构,
MongoDB采用B+Tree 做索引 ,索引创建在colletions上。MongoDB不使用索引的查
询,先扫描所有的文档,再匹配符合条件的文档。 使用索引的查询,通过索引找到文档,
使用索引能够极大的提升查询效率。
索引的分类
按照索引包含的字段数量,可以分为单键索引和组合索引(或复合索引)。
按照索引字段的类型,可以分为主键索引和非主键索引。
按照索引节点与物理记录的对应方式来分,可以分为聚簇索引和非聚簇索引,其
中聚簇索引是指索引节点上直接包含了数据记录,而后者则仅仅包含一个指向数据记
录的指针。
按照索引的特性不同,又可以分为唯一索引、稀疏索引、文本索引、地理空间索 引等
与大多数数据库一样,MongoDB支持各种丰富的索引类型,包括单键索引、复合索 引,唯一索引等一些常用的结构。由于采用了灵活可变的文档类型,因此它也同样支持对嵌 套字段、数组进行索引。通过建立合适的索引,我们可以极大地提升数据的检索速度。在一些特殊应用场景,MongoDB还支持地理空间索引、文本检索索引、TTL索引等不同的特 性。
索引设计原则
1、每个查询原则上都需要创建对应索引
2、单个索引设计应考虑满足尽量多的查询
3、索引字段选择及顺序需要考虑查询覆盖率及选择性
4、对于更新及其频繁的字段上创建索引需慎重
5、对于数组索引需要慎重考虑未来元素个数
6、对于超长字符串类型字段上慎用索引
7、并发更新较高的单个集合上不宜创建过多索引
索引操作
创建索引
创建索引语法格式
db.collection.createIndex(keys, options)
Key 值为你要创建的索引字段,1 按升序创建索引, -1 按降序创建索引
可选参数列表如下:
Parameter | Type | Description |
2 | 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 | 对于文本索引 ,该参数指定了包含在文档中的字段名 ,语 |
# 创建索引后台执行
db.values.createIndex({open: 1, close: 1}, {background: true})
# 创建唯一索引
db.values.createIndex({title:1},{unique:true})
查看索引
#查看索引信息
db.books.getIndexes()
#查看索引键
db.books.getIndexKeys()
查看索引占用空间
db.collection.totalIndexSize([is_detail])
is_detail:可选参数,传入除0或false外的任意数据,都会显示该集合中每 个索引的大小及总大小。如果传入0或false则只显示该集合中所有索引的总大小。默 认值为false。
删除索引
#删除集合指定索引
db.col.dropIndex("索引名称")
#删除集合所有索引 不能删除主键索引
db.col.dropIndexes()
索引类型
单键索引
在某一个特定的字段上建立索引 mongoDB在ID上建立了唯一的单键索引,所以经常会使用
id来进行查询; 在索引字段上进行精确匹配、排序以及范围查找都会使用此索引
db.books.createIndex({title:1})
对内嵌文档字段创建索引:
复合索引
复合索引是多个字段组合而成的索引,其性质和单字段索引类似。但不同的是, 复合索
引中字段的顺序、字段的升降序对查询性能有直接的影响 ,因此在设计复合索引时则需要考
虑不同的查询场景。
db.books.createIndex({type:1,favCount:1})
多键索引
在数组的属性上建立索引。 针对这个数组的任意值的查询都会定位到这个文档,既多个索引
入口或者键值引用同一个文档
db.inventory.createIndex( { ratings: 1 } )
注意 :
多键索引很容易与复合索引产生混淆, 复合索引是多个字段的组合,而多键索引则仅仅是在
一个字段上出现了多键(multi key) 。而实质上,多键索引也可以出现在复合字段上
MongoDB并不支持一个复合索引中同时出现多个数组字段
地理空间索引(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"})
全文索引(Text Indexes)
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的文本索引功能存在诸多限制,而官方并未提供中文分词的功能 ,这使得该功能
的应用场景十分受限。
Hash索引(Hashed Indexes)
不同于传统的B-Tree索引,哈希索引使用hash函数来创建索引。 在索引字段上进行精确匹配,
但不支持范围查询,不支持多键hash ; Hash索引上的入口是均匀分布的,在分片集合中非常
有用
db.users.createIndex({username : 'hashed'})
通配符索引(Wildcard Indexes)
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": {
$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)
在现实场景中,唯一性是很常见的一种索引约束需求,重复的数据记录会带来许多处理上的
麻烦,比如订单的编号、用户的登录名等。通过建立唯一性索引,可以保证集合中文档的指
定字段拥有唯一值。
唯一性索引对于文档中缺失的字段,会使用null值代替 ,因此不允许存在多个文
档缺失索引字段的情况。
对于分片的集合,唯一性约束必须匹配分片规则。 换句话说,为了保证全局的唯
一性,分片键必须作为唯一性索引的前缀字段。
部分索引(Partial Indexes)
部分索引仅对满足指定过滤器表达式的文档进行索引。 通过在一个集合中为文档的一个
子集建立索引, 部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。 3.2 新
版功能
部分索引提供了稀疏索引功能的超集,应该优先于稀疏索引
db.restaurants.createIndex(
{ cuisine: 1, name: 1 },
{ partialFilterExpression: { rating: { $gt: 5 } } }
)
partialFilterExpression选项接受指定过滤条件的文档:
等式表达式(例如:field: value或使用$eq操作符)
$exists: true
$gt, $gte, $lt, $lte
$type
顶层的$and
# 符合条件,使用索引
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
# 不符合条件,不能使用索引
db.restaurants.find( { cuisine: "Italian" } )
唯一约束结合部分索引使用导致唯一约束失效的问题
注意:如果同时指定了 partialFilterExpression 和唯一约束,那么唯一约束只适用于满足筛选
器表达式的文档。 如果文档不满足筛选条件,那么带有惟一约束的部分索引不会阻止插入不
满足惟一约束的文档。
稀疏索引(Sparse Indexes)
索引的稀疏属性确保 索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段
的文档。
特性: 只对存在字段的文档进行索引(包括字段值为 null 的文档)
#不索引不包含xmpp_id字段的文档
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )
如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引 , 除非hint()明确指定索引。
同时具有稀疏性和唯一性的索引 可以防止集合中存在字段值重复的文档,但允许不包含 此索引字段的文档插入。
TTL索引(TTL Indexes)
在一般的应用系统中,并非所有的数据都需要永久存储。例如一些系统事件、用户消息等,
这些数据随着时间的推移,其重要程度逐渐降低。更重要的是,存储这些大量的历史数据需
要花费较高的成本,因此 项目中通常会对过期且不再使用的数据进行老化处理。
通常的做法如下:
方案一: 为每个数据记录一个时间戳,应用侧开启一个定时器,按时间戳定期删除过期的数
据。
方案二: 数据按日期进行分表,同一天的数据归档到同一张表,同样使用定时器删除过期的
表。
对于数据老化,MongoDB提供了一种更加便捷的做法:TTL(
Time To Live)索引。 TTL
索引需要声明在一个日期类型的字段中,TTL 索引是特殊的单字段索引 ,MongoDB 可以
使用它在一定时间或特定时钟时间后自动从集合中删除文档。
db.log_events.insertOne( {
"createdAt": new Date(),
"logEvent": 2,
"logMessage": "Success!"
} )
db.log_events.createIndex( { "createdAt": 1 }, { expireAfterSeconds: 20 })
最后被清理掉了,并没有查询到数据
使用约束
TTL索引的确可以减少开发的工作量,而且通过数据库自动清理的方式会更加高效、可靠,
但是在使用TTL索引时需要注意以下的限制:
TTL索引只能支持单个字段,并且必须是非_id字段。 TTL索引不能用于固定集合。
TTL索引无法保证及时的数据老化 ,MongoDB会通过后台的TTLMonitor定时器
来清理老化数据,默认的间隔时间是1分钟。当然如果在数据库负载过高的情况下,
TTL的行为则会进一步受到影响。
TTL索引对于数据的清理仅仅使用了remove命令,这种方式并不是很高效。 因
此TTL Monitor在运行期间对系统CPU、磁盘都会造成一定的压力。 相比之下,按日
期分表的方式操作会更加高效。
日志存储:
日期分表
固定集合
TTL索引
插入: writeConcern:{w:0}
索引使用建议
1.为每一个查询建立合适的索引
这个是针对于数据量较大比如说超过几十上百万(文档数目)数量级的集合。如果没有
索引MongoDB需要把所有的Document从盘上读到内存,这会对MongoDB服务器造成较
大的压力并影响到其他请求的执行。
2.创建合适的复合索引,不要依赖于交叉索引
如果你的查询会使用到多个字段,MongoDB有两个索引技术可以使用:交叉索引和复
合索引。 交叉索引就是针对每个字段单独建立一个单字段索引 , 然后在查询执行时候使用相 应的单字段索引进行索引交叉而得到查询结果。 交叉索引目前触发率较低,所以如果你有一
个多字段查询的时候,建议使用复合索引能够保证索引正常的使用。
3.复合索引字段顺序:匹配条件在前,范围条件在后(Equality First, Range After)
在创建复合索引时如果条件有匹配和范围之分,那么匹配条件(sport: “marathon”) 应该在复合索引的前面。
范围条件(age: <30)字段应该放在复合索引的后 面。
4.尽可能使用覆盖索引(Covered Index)
建议只返回需要的字段,同时,利用覆盖索引来提升性能。
5.建索引要在后台运行
在对一个集合创建索引时,该集合所在的数据库将不接受其他读写操作。 对大数据量的集合 建索引,建议使用后台运行选项 {background: true}
6.避免设计过长的数组索引
数组索引是多值的,在存储时需要使用更多的空间。如果索引的数组长度特别长,或者数组
的增长不受控制,则可能导致索引空间急剧膨胀。
explain执行计划详解
通常我们需要关心的问题:
查询是否使用了索引
索引是否减少了扫描的记录数量
是否存在低效的内存排序
MongoDB 提供了 explain 命令,它可以帮助我们评估指定查询模型(querymodel)的执行计 划, 根据实际情况进行调整,然后提高查询效率。
explain() 方法的形式如下 :
db.collection.find().explain(<verbose>)
模式名字 | 描述 |
queryPlanner | 执行计划的详细信息 ,包括查询计划、集合信息、查询条件、最佳执行划、查询方式和 MongoDB 服务信息等 |
exectionStats | 最佳执行计划的执行情况和被拒绝的计划等信息 |
allPlansExecution | 选择并执行最佳执行计划 ,并返回最佳执行计划和其他执行计划的执行情 况 |
queryPlanner
# 未创建title的索引
db.books.find({title:"book‐1"}).explain("queryPlanner")
字段名称 | 描述 |
plannerVersion | 执行计划的版本 |
namespace | 查询的集合 |
indexFilterSet | 是否使用索引 |
parsedQuery | 查询条件 |
winningPlan | 最佳执行计划 |
stage | 查询方式 |
filter | 过滤条件 |
direction | 查询顺序 |
rejectedPlans | 拒绝的执行计划 |
serverInfo | mongodb服务器信息 |
executionStats
executionStats 模式的返回信息中包含了 queryPlanner 模式的所有字段,并且还包含了
最佳执行计划的执行情况
#创建索引
db.books.createIndex({title:1})
db.books.find({title:"book‐1"}).explain("executionStats")
{
"createdCollectionAutomatically" : true,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.books.find({title:"book‐1"}).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "restaurant.books",
"indexFilterSet" : false,
"parsedQuery" : {
"title" : {
"$eq" : "book‐1"
}
},
"winningPlan" : {
"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" : 0,
"executionTimeMillis" : 0,
"totalKeysExamined" : 0,
"totalDocsExamined" : 0,
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 0,
"executionTimeMillisEstimate" : 0,
"works" : 1,
"advanced" : 0,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"docsExamined" : 0,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 0,
"executionTimeMillisEstimate" : 0,
"works" : 1,
"advanced" : 0,
"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" : 0,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0
}
}
},
"serverInfo" : {
"host" : "192.168.30.130",
"port" : 27017,
"version" : "4.4.9",
"gitVersion" : "b4048e19814bfebac717cf5a880076aa69aba481"
},
"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.executionTim eMillisEstimate | 检索文档获取数据的时间 |
executionStats.executionStages.inputStage.ex ecutionTimeMillisEstimate | 扫描获取数据的时间 |
executionStats.totalKeysExamined | 索引扫描次数 |
executionStats.totalDocsExamined | 文档扫描次数 |
executionStats.executionStages.isEOF | 是否到达 steam 结尾, 1 或者 true 代表已到达结 尾 |
executionStats.executionStages.works | 工作单元数 ,一个查询会分解成小的工作单元 |
executionStats.executionStages.advanced | 优先返回的结果数 |
executionStats.executionStages.docsExamine | 文档检查数 |
allPlansExecution返回的信息包含 executionStats 模式的内容,且包含
allPlansExecution:[]块
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)