MongoDB指南---13、索引类型、索引管理

上一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引
下一篇文章:MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引

创建索引时可以指定一些选项,使用不同选项建立的索引会有不同的行为。接下来的小节会介绍常见的索引变种,更高级的索引类型和特殊选项会在下一章介绍。

唯一索引

唯一索引可以确保集合的每一个文档的指定键都有唯一值。例如,如果想保证同不文档的"username"键拥有不同的值,创建一个唯一索引就好了:

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

如果试图向上面的集合中插入如下文档:

> db.users.insert({username: "bob"})
> db.users.insert({username: "bob"})
E11000 duplicate key error index: test.users.$username_1 dup key: { : "bob" }

如果检查这个集合,会发现只有第一个"bob"被保存进来了。发现有重复的键时抛出异常会影响效率,所以可以使用唯一索引来应对偶尔可能会出现的键重复问题,而不是在运行时对重复的键进行过滤。
有一个唯一索引可能你已经比较熟悉了,就是"_id"索引,这个索引会在创建集合时自动创建。这就是一个正常的唯一索引(但它不能被删除,而其他唯一索引是可以删除的)。

如果一个文档没有对应的键,索引会将其作为null存储。所以,如果对某个键建立了唯一索引,但插入了多个缺少该索引键的文档,由于集合已经存在一个该索引键的值为null的文档而导致插入失败。5.4.2节会详细介绍相关内容。

有些情况下,一个值可能无法被索引。索引储桶(index bucket)的大小是有限制的,如果某个索引条目超出了它的限制,那么这个条目就不会包含在索引里。这样会造成一些困惑,因为使用这个索引进行查询时会有一个文档凭空消失不见了。所有的字段都必须小于1024字节,才能包含到索引里。如果一个文档的字段由于太大不能包含在索引里,MongoDB不会返回任何错误或者警告。也就是说,超出8 KB大小的键不会受到唯一索引的约束:可以插入多个同样的8 KB长的字符串。

1. 复合唯一索引

也可以创建复合的唯一索引。创建复合唯一索引时,单个键的值可以相同,但所有键的组合值必须是唯一的。
例如,如果有一个{"username" : 1, "age" : 1}上的唯一索引,下面的插入是合法的:

db.users.insert({"username" : "bob"})
db.users.insert({"username" : "bob", "age" : 23})
db.users.insert({"username" : "fred", "age" : 23})
然而,如果试图再次插入这三个文档中的任意一个,都会导致键重复异常。
GirdFS是MongoDB中存储大文件的标准方式(详见6.5节),其中就用到了复合唯一索引。存储文件内容的集合有一个{"files_id" : 1, "n" : 1}上的复合唯一索引,因此文档的某一部分看起来可能会是下面这个样子:
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 1}
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 2}
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 3}
{"files_id" : ObjectId("4b23c3ca7525f35f94b60a2d"), "n" : 4}
注意,所有"files_id"的值都相同,但是"n"的值不同。

2. 去除重复

在已有的集合上创建唯一索引时可能会失败,因为集合中可能已经存在重复值了:

> db.users.ensureIndex({"age" : 1}, {"unique" : true})
E11000 duplicate key error index: test.users.$age_1 dup key: { : 12 }

通常需要先对已有的数据进行处理(可以使用聚合框架),找出重复的数据,想办法处理。
在极少数情况下,可能希望直接删除重复的值。创建索引时使用"dropDups"选项,如果遇到重复的值,第一个会被保留,之后的重复文档都会被删除。

> db.people.ensureIndex({"username" : 1}, {"unique" : true, "dropDups" : true})

"dropDups"会强制性建立唯一索引,但是这个方式太粗暴了:你无法控制哪些文档被保留哪些文档被删除(如果有文档被删除的话,MongoDB也不会给出提示说哪些文档被删除了)。对于比较重要的数据,千万不要使用"dropDups"。

 稀疏索引

前面的小节已经讲过,唯一索引会把null看做值,所以无法将多个缺少唯一索引中的键的文档插入到集合中。然而,在有些情况下,你可能希望唯一索引只对包含相应键的文档生效。如果有一个可能存在也可能不存在的字段,但是当它存在时,它必须是唯一的,这时就可以将unique和sparse选项组合在一起使用。

MongoDB中的稀疏索引(sparse index)与关系型数据库中的稀疏索引是完全不同的概念。基本上来说,MongoDB中的稀疏索引只是不需要将每个文档都作为索引条目。

使用sparse选项就可以创建稀疏索引。例如,如果有一个可选的email地址字段,但是,如果提供了这个字段,那么它的值必须是唯一的:

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

稀疏索引不必是唯一的。只要去掉unique选项,就可以创建一个非唯一的稀疏索引。
根据是否使用稀疏索引,同一个查询的返回结果可能会不同。假如有这样一个集合,其中的大部分文档都有一个"x"字段,但是有些没有:

> db.foo.find()
{ "_id" : 0 }
{ "_id" : 1, "x" : 1 }
{ "_id" : 2, "x" : 2 }
{ "_id" : 3, "x" : 3 }

当在"x"上执行查询时,它会返回相匹配的文档:

> db.foo.find({"x" : {"$ne" : 2}})
{ "_id" : 0 }
{ "_id" : 1, "x" : 1 }
{ "_id" : 3, "x" : 3 }

如果在"x"上创建一个稀疏索引,"_id"为0的文档就不会包含在索引中。如果再次在"x"上查询,MongoDB就会使用这个稀疏索引,{"_id" : 0}的这个文档就不会被返回了:

> db.foo.find({"x" : {"$ne" : 2}})
{ "_id" : 1, "x" : 1 }
{ "_id" : 3, "x" : 3 }

如果需要得到那些不包含"x"字段的文档,可以使用hint()强制进行全表扫描。

索引管理

如前面的小节所述,可以使用ensuerIndex函数创建新的索引。对于一个集合,每个索引只需要创建一次。如果重复创建相同的索引,是没有任何作用的。
所有的数据库索引信息都存储在system.indexes集合中。这是一个保留集合,不能在其中插入或者删除文档。只能通过ensureIndex或者dropIndexes对其进行操作。
创建一个索引之后,就可以在system.indexes中看到它的元信息。可以执行db.collectionName.getIndexes()来查看给定集合上的所有索引信息:

> db.foo.getIndexes()
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "ns" : "test.foo",
        "name" : "_id_"
    },
    {
        "v" : 1,
        "key" : {
            "y" : 1
        },
        "ns" : "test.foo",
        "name" : "y_1"
    },
    {
        "v" : 1,
        "key" : {
            "x" : 1,
            "y" : 1
        },
        "ns" : "test.foo",
        "name" : "x_1_y_1"
    }
]

这里面最重要的字段是"key"和"name"。这里的键可以用在hint、max、min以及其他所有需要指定索引的地方。在这里,索引的顺序很重要:{"x" : 1, "y" : 1}上的索引与{"y" : 1, "x" : 1}上的索引不同。对于很多的索引操作(比如dropIndex),这里的索引名称都可以被当作标识符使用。但是这里不会指明索引是否是多键索引。
"v"字段只在内部使用,用于标识索引版本。如果你的索引不包含"v" : 1这样的字段,说明你的索引是以一种效率比较低的旧方式存储的。将MongoDB升级到至少2.0版本,删除并重建这些索引,就可以把索引的存储方式升级到新的格式了。

 标识索引

集合中的每一个索引都有一个名称,用于唯一标识这个索引,也可以用于服务器端来删除或者操作索引。索引名称的默认形式是key name1_dir1_keyname2_dir2_..._keynameN_dirN,其中keynameX是索引的键,dirX是索引的方向(1或者-1)。如果索引中包含两个以上的键,这种命名方式就显得比较笨重了,好在可以在ensureIndex中指定索引的名称:

> db.foo.ensureIndex({"a" : 1, "b" : 1, "c" : 1, ..., "z" : 1},
... {"name" : "alphabet"})

索引名称的长度是有限制的,所以新建复杂索引时可能需要自定义索引名称。调用getLastError就可以知道索引是否成功创建,或者失败的原因。

 修改索引

随着应用不断增长变化,你会发现数据或者查询已经发生了改变,原来的索引也不那么好用了。这时可以使用dropIndex命令删除不再需要的索引:

> db.people.dropIndex("x_1_y_1")
{ "nIndexesWas" : 3, "ok" : 1 }

用索引描述信息里"name"字段的值来指定需要删除的索引。
新建索引是一件既费时又浪费资源的事情。默认情况下,MongoDB会尽可能快地创建索引,阻塞所有对数据库的读请求和写请求,一直到索引创建完成。如果希望数据库在创建索引的同时仍然能够处理读写请求,可以在创建索引时指定background选项。这样在创建索引时,如果有新的数据库请求需要处理,创建索引的过程就会暂停一下,但是仍然会对应用程序性能有比较大的影响。后台创建索引比前台创建索引慢得多。
在已有的文档上创建索引会比新创建索引再插入文档快一点。

上一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引
下一篇文章:MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引

相关推荐