0
点赞
收藏
分享

微信扫一扫

MongoDB索引

言午栩 2022-07-12 阅读 74

​索引​​就像书的目录,如果查找某内容在没有目录的帮助下,只能全篇查找翻阅,这导致效率非常的低下;如果在借助目录情况下,就能很快的定位具体内容所在区域,效率会直线提高。

索引简介

首先打开命令行,输入​​mongo​​​。默认mongodb会连接名为​​test​​的数据库。

➜  ~  mongo
MongoDB shell version: 2.4.9
connecting to: test
> show collections
>

可以使用​​show collections/tables​​查看数据库为空。

然后在mongodb shell执行如下代码

> for(var i=0;i<100000;i++) {
... db.users.insert({username:'user'+i})
... }
> show collections
system.indexes
users
>

再查看数据库发现多了​​system.indexes​​​ 和 ​​users​​​两个表,前者即所谓的​​索引​​​,后者为新建的数据库表。
这样​​​user​​​表中即有了​​10万​​条数据。

> db.users.find()
{ "_id" : ObjectId("5694d5da8fad9e319c5b43e4"), "username" : "user0" }
{ "_id" : ObjectId("5694d5da8fad9e319c5b43e5"), "username" : "user1" }
{ "_id" : ObjectId("5694d5da8fad9e319c5b43e6"), "username" : "user2" }
{ "_id" : ObjectId("5694d5da8fad9e319c5b43e7"), "username" : "user3" }
{ "_id" : ObjectId("5694d5da8fad9e319c5b43e8"), "username" : "user4" }
{ "_id" : ObjectId("5694d5da8fad9e319c5b43e9"), "username" : "user5" }

现在需要查找其中任意一条数据,比如

> db.users.find({username: 'user1234'})
{ "_id" : ObjectId("5694d5db8fad9e319c5b48b6"), "username" : "user1234" }

发现这条数据成功找到,但需要了解详细信息,需要加上​​explain​​方法

> db.users.find({username: 'user1234'}).explain()
{
"cursor" : "BasicCursor",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 100000,
"nscanned" : 100000,
"nscannedObjectsAllPlans" : 100000,
"nscannedAllPlans" : 100000,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 30,
"indexBounds" : {

},
"server" : "root:27017"
}

参数很多,目前我们只关注其中的​​"nscanned" : 100000​​​和​​"millis" : 30​​​这两项。
​​​nscanned​​​表示mongodb在完成这个查询过程中扫描的文档总数。可以发现,集合中的每个文档都被扫描了,并且总时间为30毫秒。
如果数据有1000万个,如果每次查询文档都遍历一遍。呃,时间也是相当可观。

对于此类查询,索引是一个非常好的解决方案。

> db.users.ensureIndex({"username": 1})

其中数字​​1​​​或​​-1​​​表示索引的排序方向,一般都可以。
然后再查找​​​user1234​

> db.users.ensureIndex({"username": 1})
> db.users.find({username: 'user1234'}).explain()
{
"cursor" : "BtreeCursor username_1",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"username" : [
[
"user1234",
"user1234"
]
]
},
"server" : "root:27017"
}

的确有点不可思议,查询在瞬间完成,因为通过索引只查找了一条数据,而不是100000条。

当然使用索引是也是有代价的:对于添加的每一条索引,每次写操作(插入、更新、删除)都将耗费更多的时间。这是因为,当数据发生变化时,不仅要更新文档,还要更新级集合上的所有索引。因此,mongodb限制每个集合最多有64个索引。通常,在一个特定的集合上,不应该拥有两个以上的索引。


小技巧


如果一个非常通用的查询,或者这个查询造成了性能瓶颈,那么在某字段(比如​​username​​)建立索引是非常好的选择。但只是给管理员用的查询(不太在意查询耗费时间),就不该对这个字段建立索引。

复合索引

索引的值是按一定顺序排列的,所以使用索引键对文档进行排序非常快。

db.users.find().sort({'age': 1, 'username': 1})

这里先根据age排序再根据username排序,所以username在这里发挥的作用并不大。为了优化这个排序,可能需要在age和username上建立索引。

db.users.ensureIndex({'age':1, 'username': 1})

这就建立了一个​​复合索引​​(建立在多个字段上的索引),如果查询条件包括多个键,这个索引就非常有用。

建立复合索引后,每个索引条目都包括一个​​age​​​字段和一个​​username​​​字段,并且指向文档在磁盘上的存储位置。
此时,age字段是严格升序排列的,如果age相等时再按照username升序排列。

查询方式

点查询(point query)

用于查询单个值(尽管包含这个值的文档可能有多个)

db.users.find({'age': 21}).sort({'username': -1})

因为我们已经建立好复合索引,一个​​age​​​一个​​username​​​,建立索引时使用的是升序排序(即数字​​1​​​),当使用点查询查找​​{age:21}​​​,假设仍然是10万条数据。可能年龄是21的很多人,因此会找到不只一条数据。然后​​sort({'username': -1})​​​会对这些数据进行逆序排序,本意是这样。但我们不要忘记建立索引时​​'username':1​​是升序(从小到大),如果想得到逆序只要对数据从最后一个索引开始,依次遍历即可得到想要的结果。


排序方向并不重要,mongodb可以从任意方向对索引进行遍历。


综上,复合索引在点查询这种情况非常高效,直接定位年龄,不需要对结果进行排序即可返回结果。

多值查询(multi-value-query)

db.users.find({'age': {"$gte": 21, "$lte": 30}})

查找多个值相匹配的文档。​​多值查询​​​也可以理解为多个​​点查询​​​。
如上,要查找年龄介于21到30之间。monogdb会使用索引的中的第一个键​​​"age"​​得到匹配的结果,而结果通常是按照索引顺序排列的。

db.users.find({'age': {"$gte": 21, "$lte": 30}}).sort({'username': 1})

与上一个类似,这次需要对结果排序。
在没有​​​sort​​​时,我们查询的结果首先是根据age等于21,age等于22..这样从小到大排序,当age等于21有多个时,在进行​​username​​​A-Z(0-9)这样排序。所以,​​sort({'username': 1})​​,要将所有结果通过名字升序排列,这次不得不先在内存中进行排序,然后返回。效率不如上一个高。

当然,在文档非常少的情况,排序也花费不了多少时间。
如果结果集很大,比如超过32MB,MongoDB会拒绝对如此多的数据进行排序工作。

还有另外一种解决方案

也可以建立另外一个索引​​{'username': 1, 'age': 1}​​​, 如果先对​​username​​​建立索引,如果再sort​​username​​,相当没有进行排序。但是需要在整个文档查找age等于21的帅哥美女,所以搜寻时间就长了。

  • 但哪个效率更高呢?
  • 如果建立多个索引,如何选择使用哪个呢?

效率高低是分情况的,如果在没有限制的情况下,不进行排序但需要搜索整个集合时间会远超过前者。但是在返回部分数据(比如​​limit(1000)​​),新的赢家就产生了。

>db.users.find({'age': {"$gte": 21, "$lte": 30}}).
sort({username': 1}).
limit(1000).
hint({'age': 1, 'username': 1})
explain()['millis']

2031ms

>db.users.find({'age': {"$gte": 21, "$lte": 30}}).
sort({username': 1}).
limit(1000).
hint({'username': 1, 'age': 1}).
explain()['millis']

181ms

其中可以使用​​hint​​​指定要使用的索引。
所以这种方式还是很有优势的。比如一般场景下,我们不会把所有的数据都取出来,只是去查询最近的,所以这种效率也会更高。

索引类型

单键索引

最普通索引,如

db.users.ensureIndex({'username': 1})

唯一索引

可以确保集合的每个文档的指定键都有唯一值。

db.users.ensureIndex({'username': 1, unique: true})

如果插入2个相同都叫张三的数据,第二次插入的则会失败。​​_id​​​即为唯一索引,并且不能删除。
这和使用mongoose框架很相似,比如在定义schema时,即可指定​​​unique: true​

company: { // 公司名称
type: String,
required: true,
unique: true
}

多键索引

如果某个键的值在文档中是一个​​数组​​​,那么这个索引就会被标记为​​多键索引​​​。
比如现在​​​members​​文档中随便添加有3条数据:

> db.members.find()
{ "_id" : ObjectId("1"), "tags" : [ "ame", "fear", "big" ] }
{ "_id" : ObjectId("2"), "tags" : [ "ame", "fear", "big", "chi" ] }
{ "_id" : ObjectId("3"), "tags" : [ "ame", "jr", "big", "chi" ] }

当我查找​​tags='jr'​​​数据时,db会查找所有文档,所以​​nscanned=3​​​,并且返回一条,此时​​n=1​​。

>db.members.find({tags: 'jr'}).explain()
{
"cursor" : "BasicCursor",
"isMultiKey" : false,
"n" : 1,
"nscanned" : 3,
}

然后建立索引

> db.members.ensureIndex({tags:1})

之后我们在对​​tags='jr'​​​进行查找,此时​​nscanned=1​​​,并且​​isMultiKey​​​由原来的​​false​​​变为​​true​​。所以可以说明,mongodb对数组做了多个键的索引,即把所有的数组元素都做了索引。

> db.members.find({tags: 'jr'}).explain()
{
"cursor" : "BtreeCursor tags_1",
"isMultiKey" : true,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
}

过期索引

是在一段时间后会过期的索引。索引过期后,相应的数据会被删除。适合存储一些在一段时间失效的数据比如用户的登录信息,存储的日志等。
和设置单键索引很类似,只是多个​​​expireAfterSeconds​​​参数,单位是​​秒​​。

db.collectionName.ensureIndex({key: 1}, {expireAfterSeconds: 10})

首先我们先建立一下索引,数据会在30秒后删除

> db.members.ensureIndex({time:1}, {expireAfterSeconds: 30})

插入数据

> db.members.insert({time: new Date()})

查询

> db.members.find()

{ "_id" : ObjectId("4"), "time" : ISODate("2016-01-16T12:27:20.171Z") }

30秒后再次查询,数据则消失了。


存储的值必须是ISODate时间类型(比如​​new Date()​​​),如果存储的非时间类型,则不会自动删除。
过期索引不能是复合索引。
删除的时间不精确,因为删除过程每60秒后台程序跑一次,而且删除也需要一些时间,存在误差。


稀疏索引

使用​​sparse​​可以创建稀疏索引和唯一索引

>db.users.ensureIndex({'email': 1}, {'unique': true, 'sparse': true})

下面来自官网的问候

Sparse Index with Unique Constraint(约束)

Consider a collection scores that contains the following documents:

{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }

You could create an index with a unique constraint and sparse filter on the score field using the following operation:

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

This index ​​would permit​​​ the insertion of documents that had unique values for the score field or did not include a score field.
所以索引会允许不同score的文档或根本没有score这个字段的文档插入成功。

As such, given the existing documents in the scores collection, the index permits the following insert operations:
以下插入成功:

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

However, the index ​​would not permit​​ the addition of the following documents since documents already exists with score value of 82 and 90:

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

索引管理

system.indexes集合中包含了每个索引的详细信息

db.system.indexes.find()

创建索引

Mongo shell

  • ensureIndex()
  • createIndex()

example

db.users.ensureIndex({'username': 1})

后台创建索引,这样数据库再创建索引的同时,仍然能够处理读写请求,可以指定​​background​​选项。

db.test.ensureIndex({"username":1},{"background":true})

Schema

var animalSchema = new Schema({
name: String,
type: String,
tags: { type: [String], index: true } // field level
});

animalSchema.index({ name: 1, type: -1 }); // schema level

在​​Schema​​中,官方不推荐在生成环境直接创建索引


When your application starts up, Mongoose automatically calls ensureIndex for each defined index in your schema. Mongoose will call ensureIndex for each index sequentially, and emit an 'index' event on the model when all the ensureIndex calls succeeded or when there was an error. While nice for development, ​​it is recommended this behavior be disabled in production since index creation can cause a ​​​​significant performance impact​​ . Disable the behavior by setting the autoIndex option of your schema to false, or globally on the connection by setting the option config.autoIndex to false.


2.​​getIndexes()​​查看索引

db.collectionName.getIndexes()
db.users.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"ns" : "test.users",
"name" : "_id_"
},
{
"v" : 1,
"key" : {
"username" : 1
},
"ns" : "test.users",
"name" : "username_1"
}
]

其中​​v​​字段只在内部使用,用于标识索引版本。

3.​​dropIndex​​删除索引

> db.users.dropIndex("username_1")
{ "nIndexesWas" : 2, "ok" : 1 }

 

> db.users.dropIndex({"username":1})



 


举报

相关推荐

0 条评论