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

第一篇:入门篇,学习使用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中,一个给定的联合索引能否被某个查询使用,要看这个查询中字段是否满足"最左前缀匹配"。具体来说就是,当查询条件精确匹配索引的最左边连续或不连续的几个列时,该查询可以使用索引。

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

  1. Mysql数据库学习笔记之数据库索引(index)

    什么是索引: SQL索引有两种,聚集索引和非聚集索引,索引主要目的是提高了SQL Server系统的性能,加快数据的查询速度与减少系统的响应时间. 聚集索引:该索引中键值的逻辑顺序决定了表中相应行的物 ...

  2. Spring Data MongoDB 一:入门篇(环境搭建、简单的CRUD操作)

    一.简介 Spring Data  MongoDB 项目提供与MongoDB文档数据库的集成.Spring Data MongoDB POJO的关键功能区域为中心的模型与MongoDB的DBColle ...

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

    前言 上一篇我们研究了如何利用索引在数据库里面调优,简要的介绍了索引的原理,更重要的分析了如何选择索引以及索引的利弊项,有兴趣的可以点击查看. 本篇延续上一篇的内容,继续分析索引这块,侧重索引项的日常 ...

  4. python自动化测试入门篇-jemter连接mysql数据库

    jmeter对数据库的操作主要包括以下几个步骤:1.导入mysqlde jdbc的jar包:2.创建数据库连接配置:3.线程组添加jdbc request;4.启动按钮,添加查看结果树 一.准备好驱动 ...

  5. B树在数据库索引中的应用剖析

    引言 关于数据库索引,google一个oracle index,mysql index总 有大量的结果,其中很多的使用方法推荐,**索引之n条经典建议云云.笔者认为,较之借鉴,在搞清楚了自己的需求的基 ...

  6. (转)B-树和B+树的应用:数据搜索和数据库索引

    B-树 1 .B-树定义 B-树是一种平衡的多路查找树,它在文件系统中很有用. 定义:一棵m 阶的B-树,或者为空树,或为满足下列特性的m 叉树: ⑴树中每个结点至多有m 棵子树: ⑵若根结点不是叶子 ...

  7. 好书推荐之Mysql三剑客 :《高性能Mysql》、《Mysql技术内幕》、《数据库索引设计与优化》

    Mysql三剑客系列书籍: 大佬推荐 首先推荐<高性能 MySQL>,这本书是 MySQL 领域的经典之作,拥有广泛的影响力.不但适合数据库管理员(DBA)阅读,也适合开发人员参考学习.不 ...

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

    以下的文章主要介绍的是MySQL数据库索引类型,其中包括普通索引,唯一索引,主键索引与主键索引,以及对这些索引的实际应用或是创建有一个详细介绍,以下就是文章的主要内容描述. (1)普通索引 这是最基本 ...

  9. 知识点:Mysql 数据库索引优化实战(4)

    知识点:Mysql 索引原理完全手册(1) 知识点:Mysql 索引原理完全手册(2) 知识点:Mysql 索引优化实战(3) 知识点:Mysql 数据库索引优化实战(4) 一:插入订单 业务逻辑:插 ...

随机推荐

  1. Android Project和app中两个build.gradle配置的区别

    Android 开发也挺长时间了,从开始就使用的AndroidStudio开发,但是说下来其实自己对AS(AndroidStudio简称)还真的是不了解不深入.好吧,其实我只知道AS是一个相当强大的工 ...

  2. 详解Android Activity---启动模式

    相关的基本概念: 1.任务栈(Task)   若干个Activity的集合的栈表示一个Task.   栈不仅仅只包含自身程序的Activity,它也可以跨应用包含其他应用的Activity,这样有利于 ...

  3. 谈谈HashMap与HashTable

    谈谈HashMap与HashTable HashMap 我们一直知道HashMap是非线程安全的,HashTable是线程安全的,可这是为什么呢?先聊聊HashMap吧,想要了解它为什么是非线程安全的 ...

  4. tp框架表单验证 及ajax

    之前的表单验证都是用js写的,这里也可以使用tp框架的验证.但是两者比较而言还是js验证比较好,因为tp框架验证会运行后台代码,这样运行速度和效率就会下降. 自动验证是ThinkPHP模型层提供的一种 ...

  5. Linux基础(七)

    一.nfs服务 nfs(Network File System)即网络文件系统,它允许网络中的计算机之间通过TCP/IP网络共享资源.常用于Linux系统之间的文件共享. nfs在文件传送过程中依赖r ...

  6. kevin的黎明十分

    今天在搜索struts2的相关知识的时候,博客园让我提起了兴趣.其间看到了hongton同学的分享,感觉受益颇深!所以今天我加入了博客园,希望自己以后能在这个大家园中分享知识,收获人生!  即兴之下, ...

  7. org.w3c.dom.Element 缺少 setTextContent 步骤

    org.w3c.dom.Element 缺少 setTextContent 方法 今天将项目环境由jdk5改为jdk6,eclipse重新编译工程后,却突然出现org.w3c.dom.Element没 ...

  8. HDU 6040---Hints of sd0061(STL)

    题目链接 Problem Description sd0061, the legend of Beihang University ACM-ICPC Team, retired last year l ...

  9. HTML随笔1

    1.编号列表: <ol type="A" start="1">    //type中有"A","1",&qu ...

  10. Hibernate 中Criteria Query查询详解【转】

    当查询数据时,人们往往需要设置查询条件.在SQL或HQL语句中,查询条件常常放在where子句中.此外,Hibernate还支持Criteria查询(Criteria Query),这种查询方式把查询 ...