MongoDB索引(一) --- 入门篇:学习使用MongoDB数据库索引

这个系列文章会分为两篇来写:

第一篇:入门篇,学习使用MongoDB数据库索引

第二篇:进阶篇,研究数据库索引原理--B/B+树的基本原理

1. 准备工作

在学习使用MongoDB数据库索引之前,有一些准备工作要做,之后的探索都是基于这些准备工作。

首先需要建立一个数据库和一些集合,这里我就选用一个国内手机号归属地的库,大约32W条记录,数据量不大,不过做一些基本的分析是够了。

首先我们建立一个数据库,叫做db_phone,然后导入测试数据。测试数据就是一些手机号归属地的信息。单个文档长这个样子:

1 {
2     "_id": ObjectId("57bd12ba085bed84151ca203"),
3     "prefix": "1898852",
4     "province": "广东",
5     "city": "佛山",
6     "isp": "中国电信"
7 }

2. 学会分析MongoDB的查询

默认情况下,每个MongoDB文档都有一个_id字段,这个字段是唯一的。系统能保证在单台机器上这个字段是唯一的(而且是递增),有兴趣的同学可以去看看_id的生成方式。

(1) 用_id字段作为查询条件

在MongoDB shell中,利用一些查询语句来对数据库进行查询,比如想要找到刚才那个文档,可以执行:

1 db.phonehomes.find({_id:ObjectId("57bd12ba085bed84151ca203")})

我们利用explain()来分析这个查询:

1 db.phonehomes.find({_id:ObjectId("57bd12ba085bed84151ca203")}).explain()

这个查询分析不会返回找到的文档,而是返回该查询的分析文档:

 1 {
 2     "cursor": "IDCursor",
 3     "n": 1,
 4     "nscannedObjects": 1,
 5     "nscanned": 1,
 6     "indexOnly": false,
 7     "millis": 0,
 8     "indexBounds": {
 9         "_id": [[ObjectId("57bd12ba085bed84151ca203"), ObjectId("57bd12ba085bed84151ca203")]]
10     },
11     "server": "zhangjinyideMac-Pro.local:27017"
12 }

解释一些比较重要的几个字段:

1. "cursor" : "IDCursor"

cursor的本意是游标,在这里它表示用的是什么索引,或者没用索引。没用索引就是全表扫描了,后面会看到。这里的cursor是"IDCursor",这是_id特有的一个索引。默认情况下,数据库会为_id创建索引,因此在查询中如果用_id作为查询条件,效率是非常高的。

2. "n" : 1

返回文档的个数。这个查询本身只返回了一个文档(因为_id是不能重复的)。

3. "nscannedObjects" : 1

实际查询的文档数。

4. "nscanned" : 1

表示使用索引扫描的文档数,如果没有索引,这个值是整个集合的所有文档数。

5. "indexOnly" : false

表示是否只有索引即可完成查询,当查询的字段都存在一个索引中并且返回的字段也在同一索引中即为true。如果执行:

1 db.phonehomes.find({_id:ObjectId("57bd12ba085bed84151ca203")}, {"_id": 1}).explain()

则indexOnly会为true。

6. "millis" : 0

查询耗时,单位毫秒,为0说明这个查询太快了。由于索引会被加载到内存中,直接利用内存中的索引是非常高效的,可能只用到了纳秒级别的时间(1ms = 1000000ns),因此就显示为0了。

7. "indexBounds"

索引的使用情况,即文档中key的上下界。

(2) 用未被索引的prefix字段作为查询条件

接下来我们使用一个没有索引的字段:prefix,查询语句如下:

1 db.phonehomes.find({prefix: ‘1899950‘}).explain()

返回结果:

 1 {
 2     "cursor": "BasicCursor",
 3     "isMultiKey": false,
 4     "n": 1,
 5     "nscannedObjects": 327664,
 6     "nscanned": 327664,
 7     "nscannedObjectsAllPlans": 327664,
 8     "nscannedAllPlans": 327664,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 2559,
12     "nChunkSkips": 0,
13     "millis": 92,
14     "server": "zhangjinyideMac-Pro.local:27017",
15     "filterSet": false
16 }

这次的字段比较多,还是看来一些重要的(有些和之前查询完全重复就不列举了):

1. "cursor" : "BasicCursor"

查询使用索引的信息,为"BasicCursor"表示未使用索引,即全表扫描了。

2. "n" : 1

返回的文档数为1。

3. "nscannedObjectsAllPlans" : 327664

所有查询计划的查询文档数。

4. "nscannedAllPlans" : 327664

所有查询计划的查询文档数。

5. "scanAndOrder" : false

是否对返回的结果排序,当直接使用索引的顺序返回结果时其值为false。如果使用了sort(),则为true。

6. "nYields" : 2559

表示查询暂停的次数。这是由于mongoDB的其他操作使得查询暂停,使得这次查询放弃了读锁以等待写操作的执行。

7. "nChunkSkips" : 0

表示的略过的文档数量,当在分片系统中正在进行的块移动时会发生。

8. "filterSet" : false

表示是否应用了索引过滤。

需要特别说明的是,上面3个重要的文档数量指标的关系为:nscanned >= nscannedObjects >= n,也就是扫描数(也可以说是索引条目) >= 查询数(通过索引到硬盘上查询的文档数) >= 返回数(匹配查询条件的文档数)。

可以看到由于prefix字段没有索引,导致了全表扫描。当文档数量很小(只有32W条)时,耗时不大(92ms),不过一旦文档数量非常大,查询耗时就会增长到一个无法忍受的程度。

(3) 用有索引的prefix字段作为查询条件

为prefix字段增加索引:

1 db.phonehomes.ensureIndex({"prefix": 1})

成功建立索引后,执行之前那个查询语句:

1 db.phonehomes.find({prefix: ‘1899950‘}).explain()

返回结果:

 1 {
 2     "cursor": "BtreeCursor prefix_1",
 3     "isMultiKey": false,
 4     "n": 1,
 5     "nscannedObjects": 1,
 6     "nscanned": 1,
 7     "nscannedObjectsAllPlans": 1,
 8     "nscannedAllPlans": 1,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 0,
12     "nChunkSkips": 0,
13     "millis": 0,
14     "indexBounds": {
15         "prefix": [["1899950", "1899950"]]
16     },
17     "server": "zhangjinyideMac-Pro.local:27017",
18     "filterSet": false,
19     // 略去一部分暂时不讨论的内容
20 }

重点看下"cursor"字段:

1 "cursor" : "BtreeCursor prefix_1"

这个查询使用了一个prefix的索引。由于索引的使用,使得这个查询变得非常高效,从以下这几个字段可以很明显地看出:

"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"millis" : 0,

(4) 有多个单独索引的情况

执行查询:

1 db.phonehomes.find({province: ‘福建‘, ‘isp‘: ‘中国电信‘}).explain()

返回结果:

 1 {
 2     "cursor": "BasicCursor",
 3     "isMultiKey": false,
 4     "n": 2667,
 5     "nscannedObjects": 327664,
 6     "nscanned": 327664,
 7     "nscannedObjectsAllPlans": 327664,
 8     "nscannedAllPlans": 327664,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 2559,
12     "nChunkSkips": 0,
13     "millis": 138,
14     "server": "zhangjinyideMac-Pro.local:27017",
15     "filterSet": false,
16     // 略去一部分暂时不讨论的内容
17 }

可以看到是全表扫描。

先给"isp"字段加索引:

1 db.phonehomes.ensureIndex({"isp": 1})

再执行一次:

1 db.phonehomes.find({province: ‘福建‘, ‘isp‘: ‘中国电信‘}).explain()

返回结果:

 1 {
 2     "cursor": "BtreeCursor isp_1",
 3     "isMultiKey": false,
 4     "n": 2667,
 5     "nscannedObjects": 59548,
 6     "nscanned": 59548,
 7     "nscannedObjectsAllPlans": 59548,
 8     "nscannedAllPlans": 59548,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 465,
12     "nChunkSkips": 0,
13     "millis": 64,
14     "indexBounds": {
15         "isp": [["中国电信", "中国电信"]]
16     },
17     "server": "zhangjinyideMac-Pro.local:27017",
18     "filterSet": false,
19     // 略去一部分暂时不讨论的内容
20 }

发现"cursor"为"BtreeCursor isp_1",这个查询用到了isp的索引,扫描了59548个文档。

为了进一步提高查询效率,可以再对"province"字段建立索引:

1 db.phonehomes.ensureIndex({"province": 1})

再次执行:

1 db.phonehomes.find({province: ‘福建‘, ‘isp‘: ‘中国电信‘}).explain()

返回结果:

 1 {
 2     "cursor": "BtreeCursor province_1",
 3     "isMultiKey": false,
 4     "n": 2667,
 5     "nscannedObjects": 10223,
 6     "nscanned": 10223,
 7     "nscannedObjectsAllPlans": 10324,
 8     "nscannedAllPlans": 10425,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 81,
12     "nChunkSkips": 0,
13     "millis": 13,
14     "indexBounds": {
15         "province": [["福建", "福建"]]
16     },
17     "server": "zhangjinyideMac-Pro.local:27017",
18     "filterSet": false,
19     // 略去一部分暂时不讨论的内容
20 }

可以发现一个有意思的现象,我们同时拥有province和isp字段的单独索引,但是这个查询用了province的索引而不使用isp的索引。同时,扫描的文档数只有10223个,这比使用isp索引扫描的59548个文档要少。

使用province索引效率高于使用isp索引的原因是,这个集合中的包含的省份数为31个(部分地区未收入),isp为4个(中国移动、中国联通、中国电信和虚拟运营商),因此province对文档的区分度大于isp。

这两种情况的具体过程如下:

1. 使用isp索引

先用isp索引,获取到isp为"中国电信"的文档(59548个),然后再对这部分文档做扫描,筛选出province为"福建"的所有文档(2667个)。

2. 使用province索引

先用province索引,获取到province为"福建"的文档(10223个),然后再对这部分文档做扫描,筛选出isp为"中国电信"的所有文档(2667个)。

对比一下就知道,用isp索引要比用province索引多扫描4W+个文档(这里忽略了用索引筛选文档的代价,因为这个代价相比扫描大量文档要小得多)。

MongoDB会自动province索引的原因,个人猜测是MongoDB在真正执行查询时会现有一个预执行阶段,会先分析这个查询使用哪个索引最高效。

(5) 使用联合索引

刚才都是用单独索引,现在要介绍联合索引。顾名思义,联合索引使用多个字段作为索引。

我们先把刚才建的索引删除:

1 db.phonehomes.dropIndex({"province":1})
2 db.phonehomes.dropIndex({"isp":1})

建立一个province和isp的联合索引:

1 db.phonehomes.ensureIndex({"province": 1, "isp": 1})

再次执行刚才那个查询:

1 db.phonehomes.find({province: ‘福建‘, ‘isp‘: ‘中国电信‘}).explain()

返回结果:

 1 {
 2     "cursor": "BtreeCursor province_1_isp_1",
 3     "isMultiKey": false,
 4     "n": 2667,
 5     "nscannedObjects": 2667,
 6     "nscanned": 2667,
 7     "nscannedObjectsAllPlans": 2667,
 8     "nscannedAllPlans": 2667,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 20,
12     "nChunkSkips": 0,
13     "millis": 3,
14     "indexBounds": {
15         "province": [["福建", "福建"]],
16         "isp": [["中国电信", "中国电信"]]
17     },
18     "server": "zhangjinyideMac-Pro.local:27017",
19     "filterSet": false,
20     // 略去一部分暂时不讨论的内容
21 }

建立了province和isp的联合索引后,查询分析的"cursor"为"BtreeCursor province_1_isp_1",即使用了这个联合索引,其他数据也表现了此索引在这个查询上的高效:

"n" : 2667,
"nscannedObjects" : 2667,
"nscanned" : 2667,
"nscannedObjectsAllPlans" : 2667,
"nscannedAllPlans" : 2667,

就算改变查询条件的顺序也没关系,数据库会进行查询优化自动选择索引:

1 db.phonehomes.find({‘isp‘: ‘中国电信‘, province: ‘福建‘}).explain()

返回结果:

 1 {
 2     "cursor": "BtreeCursor province_1_isp_1",
 3     "isMultiKey": false,
 4     "n": 2667,
 5     "nscannedObjects": 2667,
 6     "nscanned": 2667,
 7     "nscannedObjectsAllPlans": 2667,
 8     "nscannedAllPlans": 2667,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 20,
12     "nChunkSkips": 0,
13     "millis": 2,
14     "indexBounds": {
15         "province": [["福建", "福建"]],
16         "isp": [["中国电信", "中国电信"]]
17     },
18     "server": "zhangjinyideMac-Pro.local:27017",
19     "filterSet": false,
20     // 略去一部分暂时不讨论的内容
21 }

由于我们刚才删除了province和isp的单独索引,所以我们要来实验一下,如果使用单个字段查询,能否利用到联合索引。

先执行查询:

1 db.phonehomes.find({province: ‘福建‘}).explain()

返回结果:

 1 {
 2     "cursor": "BtreeCursor province_1_isp_1",
 3     "isMultiKey": false,
 4     "n": 10223,
 5     "nscannedObjects": 10223,
 6     "nscanned": 10223,
 7     "nscannedObjectsAllPlans": 10223,
 8     "nscannedAllPlans": 10223,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 79,
12     "nChunkSkips": 0,
13     "millis": 9,
14     "indexBounds": {
15         "province": [["福建", "福建"]],
16         "isp": [[{
17             "$minElement": 1
18         },
19         {
20             "$maxElement": 1
21         }]]
22     },
23     "server": "zhangjinyideMac-Pro.local:27017",
24     "filterSet": false,
25     // 略去一部分暂时不讨论的内容
26 }

可以发现这个查询使用了province_1_isp_1联合索引:

"cursor" : "BtreeCursor province_1_isp_1"

再执行:

1 db.phonehomes.find({isp: ‘中国电信‘}).explain()

返回结果:

 1 {
 2     "cursor": "BasicCursor",
 3     "isMultiKey": false,
 4     "n": 59548,
 5     "nscannedObjects": 327664,
 6     "nscanned": 327664,
 7     "nscannedObjectsAllPlans": 327664,
 8     "nscannedAllPlans": 327664,
 9     "scanAndOrder": false,
10     "indexOnly": false,
11     "nYields": 2559,
12     "nChunkSkips": 0,
13     "millis": 106,
14     "server": "zhangjinyideMac-Pro.local:27017",
15     "filterSet": false
16 }

然而,这个查询并没有使用任何索引,而是来了个全表扫描:

"cursor" : "BasicCursor"

这是怎么回事,难道用单独用isp做查询条件就不能使用province_1_isp_1联合索引吗?

对于联合索引来说,确实能为某些查询提供索引支持,但这要看是什么查询。全字段满足的查询(查询字段顺序无关)肯定是可以使用相应的联合索引的,这点毋庸置疑,刚才也看到了实例。那究竟怎么利用联合索引呢,在给出答案前我们再看一个例子。

这个例子需要建立一个province-city-isp的联合索引 :

1 db.phonehomes.ensureIndex({"province": 1, "city": 1, "isp": 1})

然后分别执行4个查询:

1. db.phonehomes.find({‘province‘: ‘福建‘, ‘isp‘: ‘中国电信‘}).explain()
2. db.phonehomes.find({‘isp‘: ‘中国电信‘, ‘province‘: ‘福建‘}).explain()
3. db.phonehomes.find({‘city‘: ‘厦门‘, ‘isp‘: ‘中国电信‘}).explain()
4. db.phonehomes.find({‘isp‘: ‘中国电信‘, ‘city‘: ‘厦门‘}).explain()
5. db.phonehomes.find({‘province‘: ‘福建‘, ‘city‘: ‘厦门‘}).explain()
6. db.phonehomes.find({‘city‘: ‘厦门‘, ‘province‘: ‘福建‘}).explain()

然后我们只考察"cursor"字段。

第一个查询的"cursor"为:

"cursor": "BtreeCursor province_1_city_1_isp_1"

第二个查询的"cursor"为:

"cursor": "BtreeCursor province_1_city_1_isp_1"

第三个查询的"cursor"为:

"cursor": "BasicCursor"

第四个查询的"cursor"为:

"cursor": "BasicCursor"

第五个查询的"cursor"为:

"cursor": "BtreeCursor province_1_city_1_isp_1"

第六个查询的"cursor"为:

"cursor": "BtreeCursor province_1_city_1_isp_1"

仔细观察可以发现几个规律:

1. 在字段相同的查询中,使用索引的情况和查询中字段摆放的顺序无关(参看1和2、3和4、5和6做对比)。
2. MongoDB中,一个给定的联合索引能否被某个查询使用,要看这个查询中字段是否满足"最左前缀匹配"。具体来说就是,当查询条件精确匹配索引的最左边连续或不连续的几个列时,该查询可以使用索引。

其中第一项很好理解,主要是第二项。

在上面第1和第2个查询中,查询条件为(查询字段顺序无关):

{‘province‘: ‘福建‘, ‘isp‘: ‘中国电信‘}

这满足了province_1_city_1_isp_1联合索引的"最左前缀匹配"原则(虽然并不是连续的,少了中间的city列)。

在上面第3和第4个查询中,查询条件为(查询字段顺序无关):

{‘city‘: ‘厦门‘, ‘isp‘: ‘中国电信‘}

这不满足province_1_city_1_isp_1联合索引的"最左前缀匹配"原则,因为并没有匹配到最左边的province列。

在上面第5和第6个查询中,查询条件为(查询字段顺序无关):

{‘province‘: ‘福建‘, ‘city‘: ‘厦门‘}

这满足了province_1_city_1_isp_1联合索引的"最左前缀匹配"原则(是连续的)

因此可以总结MongoDB中联合索引的使用方法:在MongoDB中,一个给定的联合索引能否被某个查询使用,要看这个查询中字段是否满足"最左前缀匹配"。具体来说就是,当查询条件精确匹配索引的最左边连续或不连续的几个列时,该查询可以使用索引。

时间: 2024-10-08 13:39:04

MongoDB索引(一) --- 入门篇:学习使用MongoDB数据库索引的相关文章

SQL Server调优系列进阶篇(如何维护数据库索引)

原文:SQL Server调优系列进阶篇(如何维护数据库索引) 前言 上一篇我们研究了如何利用索引在数据库里面调优,简要的介绍了索引的原理,更重要的分析了如何选择索引以及索引的利弊项,有兴趣的可以点击查看. 本篇延续上一篇的内容,继续分析索引这块,侧重索引项的日常维护以及一些注意事项等. 闲言少叙,进入本篇的主题. 技术准备 数据库版本为SQL Server2012,前几篇文章用的是SQL Server2008RT,内容区别不大,利用微软的以前的案例库(Northwind)进行分析,部分内容也会

MongoDB权威指南第二版学习笔记——MongoDB简介

MongoDB简介 MongoDB在功能和复杂性之间取得了很好的平衡,并且大大简化了原先十分复杂的任务,它具备支撑今天主流web应用的关键功能:索引.复制.分片.丰富的查询语法,特别灵活的数据模型.与此同时还不牺牲速度. MongoDB是一款强大.灵活,且易于扩展的通用型数据库.能扩展出非常多的功能,如二级索引.范围查询.排序.聚合,以及地理空间索引. 设计特点 易于使用 MongoDB是一个面向文档的数据库,比关系型数据库有更好的扩展性.用文档代替行.能够仅使用一条记录来表现发展的层次关系.

spring入门篇-学习笔记

1.spring IOC的作用就是用加载配置文件的方式代替在代码中new 对象的方式来实例化对象. 2.IOC 全称:Inversion  of Control,中文意思:控制反转 3.依赖注入有两种方式: 设值注入-> 通过添加set方法,并在配置文件的bean中添加property标签(property引用另一个bean)的方式注入 构造注入->通过构造方法,并在配置文件的bean中添加constructor-arg标签的方式注入 例子项目结构: 以下是各文件代码: InjectDao.j

数据库索引详解

[By GavinHacker] 转载请标明出处:http://www.cnblogs.com/gavinsp/p/5513536.html 关于数据库索引,相信大家用到最多的一定是数据库设计和数据库查询,本篇深度解析一下数据库索引的原理,涉及数据库本身的设计原理,对设计应用的数据库结构,和数据库查询也大有益处. (一)在了解数据库索引之前,首先了解一下数据库索引的数据结构基础,B+tree B+tree 是一个n叉树,每个节点有多个叶子节点,一颗B+树包含根节点,内部节点,叶子节点.根节点可能

B树在数据库索引中的应用剖析(转载)

引言 关于数据库索引,随便Google一个Oracle index,Mysql index总有大量的结果出来,其中不乏某某索引之n条经典建议.笔者认为,较之借鉴,在搞清楚了自己的需求的基础上,对备选方案的原理有个尽可能深入全面的了解会更有利于我们的选择和决策.因为某种方案或者技术呈现出某种优势(包括可能没有被介绍到但一定存在的限制),不是定义出来的,而是因为其实现机制决定的.就像LinkedList和ArrayList分别适用于什么应用不是Document里面定义的,是由其本身的结构决定的.数据

MySQL数据库索引的4大类型以及相关的索引创建

以下的文章主要介绍的是MySQL数据库索引类型,其中包括普通索引,唯一索引,主键索引与主键索引,以及对这些索引的实际应用或是创建有一个详细介绍,以下就是文章的主要内容描述. (1)普通索引 这是最基本的MySQL数据库索引,它没有任何限制.它有以下几种创建方式: 创建索引 CREATE INDEX indexName ON mytable(username(length)); 如果是CHAR,VARCHAR类型,length可以小于字段实际长度:如果是BLOB和TEXT类型,必须指定 lengt

B+树在mysql数据库索引中的使用

一:B-树是一种平衡的多路查找树,它在文件系统中很有用. 定义:一棵m 阶的B-树,或者为空树,或为满足下列特性的m 叉树: ⑴树中每个结点至多有m 棵子树. ⑵若根结点不是叶子结点,则至少有两棵子树. ⑶除根结点之外的所有非叶结点至少有[m/2] 棵子树: ⑷所有的非终端结点中包含以下信息数据:(n,A0,K1,A1,K2,-,Kn,An) 其中:n 为关键码的个数,Ki(i=1,2,-,n)为关键码且Ki<Ki+1,Ai 为指向子树根结点的指针(i=0,1,-,n),且指针Ai-1 所指子树

数据库----问题1:数据库索引底层是怎样实现的,哪些情况下索引会失效?

什么是索引: 一个索引是存储的表中一个特定列的值数据结构(最常见的是B-Tree).索引是在表的列上创建.所以,要记住的关键点是索引包含一个表中列的值,并且这些值存储在一个数据结构中.请记住记住这一点:索引是一种数据结构 . 哈希索引的缺点: 优点:在寻找值时哈希表效率极高,如果使用哈希索引,对于比较字符串是否相等的查询能够极快的检索出的值. 缺点:哈希表是无顺的数据结构,对于很多类型的查询语句哈希索引都无能为力.比如无法查询所有小于40岁的员工.因为哈希表只适合查询键值对-也就是说查询相等的查

第四部分 性能篇 第十章 MongoDB 索引

1.简介 MongoDB提供了多样性的索引支持,索引信息被保存在system.indexes中,且默认总是为_id创建索引,它的索引使用基本和MySQL的关系型数据库一样,其实可以这样说说,索引是凌驾于数据存储系统之上的另外一层系统,所以各种结构迥异的存储都有相同或者相似的索引实现及使用接口并不足为奇. 2.基础索引 在字段age上创建索引,1(升序),-1(降序) <span style="font-family:SimHei;font-size:14px;">db.us