资讯专栏INFORMATION COLUMN

MongoDB入门之索引篇

jsbintask / 2144人阅读

摘要:排序方向并不重要,可以从任意方向对索引进行遍历。其中可以使用指定要使用的索引。即为唯一索引,并且不能删除。索引过期后,相应的数据会被删除。

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

索引简介

首先打开命令行,输入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.indexesusers两个表,前者即所谓的索引,后者为新建的数据库表。
这样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有多个时,在进行usernameA-Z(0-9)这样排序。所以,sort({"username": 1}),要将所有结果通过名字升序排列,这次不得不先在内存中进行排序,然后返回。效率不如上一个高。

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

还有另外一种解决方案

也可以建立另外一个索引{"username": 1, "age": 1}, 如果先对username建立索引,如果再sortusername,相当没有进行排序。但是需要在整个文档查找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})

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/18804.html

相关文章

  • 数据库收集 - 收藏集 - 掘金

    摘要:前言在使用加载数据数据库常见的优化操作后端掘金一索引将放第一位,不用说,这种优化方式我们一直都在悄悄使用,那便是主键索引。 Redis 内存压缩实战 - 后端 - 掘金在讨论Redis内存压缩的时候,我们需要了解一下几个Redis的相关知识。 压缩列表 ziplist Redis的ziplist是用一段连续的内存来存储列表数据的一个数据结构,它的结构示例如下图 zlbytes: 记录整...

    Little_XM 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<