Skip to content

Latest commit

 

History

History
104 lines (70 loc) · 8.71 KB

File metadata and controls

104 lines (70 loc) · 8.71 KB

2.5 文档数据库的存储与索引

接下来,本节以MongoDB为例,介绍文档数据的组织方式以及索引的构建与使用。

2.5.1 文档和文档集存储结构

文档数据库系统采用前文中介绍的分页存储的方式组织文档,也就是说硬盘存储空间被划分成多个固定大小的页,文档被打包存放在一个个数据页中。数据页是文档数据库系统对文档进行读取的基本单元。一个数据页中存储着某一文档集的多个文档,而某一文档集的文档放在在多个数据页中。

为了实现高效的文档访问,文档数据库系统采用聚簇B+树索引结构替代Inode结构来组织文档和文档集。默认情况下,文档数据库系统会为每个文档集在文档的"_id"属性上创建聚簇B+树索引,也称主索引。在基于主索引的文档组织方式中,文档集中的文档按照"_id"属性值进行排序,排序后的文档依次被存放在一个个数据页中,数据页中的文档顺序与B+树索引的叶子节点的键值顺序一致。

另外,文档数据库系统会再维护一个目录项,将文档集的名称与主索引编号的映射关系存储在目录项中。当文档数据库系统启动时,会首先将磁盘中的目录项读入内存,通过目录项定位任一文档集的主索引,然后通过主索引的B+树结构快速定位数据页并访问文档。通过每个文档集在"_id"属性的聚簇B+树索引可以高效地实现文档的创建、查找、更新和删除功能。

2.5.2 文档数据库索引

文档数据库的主索引实现的是从文档的"_id"属性值到文档所在数据页地址的映射,它只能支持基于"_id"属性值的查询需求,不能满足其他的查询需求。例如,在例1.55中,从student文档集中查找gender属性为"female"并且department属性为"数学"的文档,文档数据库系统必须扫描student文档集中的每个文档才能返回查询结果。

[例2.5] 文档查询
db.student.find( {
  "gender": "female",
  "department": "数学"
} )

为了尽可能地满足更多需求的高效查询,文档数据库系统允许在其他文档属性值上构建基于B+树结构的辅助索引,实现通过其他文档属性值快速定位文档所在的数据页,避免文档集的全部扫描。

文档数据库MongoDB支持的辅助索引包括单键索引、复合索引、多键索引、文本索引、地理空间索引等。

  • 单键索引是指在单个文档属性上创建索引,如果索引字段上的值在文档集中是唯一的,则称为唯一索引;
  • 复合索引是指在多个文档属性上创建索引;
  • 多键索引是指在数组或者嵌套文档的属性上创建索引;
  • 文本索引是指在文本属性上创建全文索引;
  • 地理空间索引是指在包含地理坐标的属性上创建索引。

接下来,我们将主要介绍单键索引和复合索引的创建与使用,其他索引类型这里不做更多详细地讲解。

2.5.3 索引创建与使用

文档数据库系统对外提供了指令允许用户创建辅助索引。文档数据库系统MongoDB的索引创建指令为createIndex,该指令包含两个参数,第一个参数指定要创建的属性字段和索引键值的排序类型,第二个参数为可选参数。

[例2.6] 索引创建
db.student.createIndex({"sno":1})  // 单键索引
db.student.createIndex({"sname":1, "age":-1})  //复合索引

createIndex可以在一个属性或者多个属性上创建索引。前者称为单键索引,后者称为复合索引。在例2.6中,第一行表示在student文档集中的文档"sno"属性上创建索引,"sno"属性值(索引键值)按升序排列;第二行表示在文档的"sname"和"age"两个属性上创建索引,"sname"和"age"共同作为索引键值,索引键值首先按"name"升序排列,然后在每个"name"内按"age"降序排列。

[例2.7] 索引使用
db.student.find({"sno":"2022001"})
db.student.find({"sname":"沐辰", "age":"21"})

用户显示执行createIndex指令之后,文档数据库系统以索引属性为键值构建B+树索引结构。当用户使用索引属性进行文档查询时,MongoDB可以通过对应索引的B+树快速地定位查询文档所在的数据块地址并读取文档内容。例2.7中,两个查询分别使用例2.6中的单键索引和复合索引查询文档。值得额外注意的是,如果想要使用复合索引实现文档的快速查询,那么查询指令中的查询条件需要满足索引的最左前缀原则,即查询条件中必须包含索引的第一个属性。也就是说,想要使用基于"sname"和"age"构建的复合索引,查询条件中必须包含"sname"属性。

2.5.4 索引使用规则

索引可以提高数据查询的效率,那么,索引是不是越多越好呢? 是否需要在每一个属性上都创建索引呢?答案当然是否定的。其原因有以下两点:

  • 耗费存储空间。每创建一个索引需要占用存储空间来存储B+树结构,B+树的每一个节点对应存储空间中的一个数据块。有时一个索引所用的存储空间与一个文档集中所有文档所用的存储空间几乎相同。索引越多,耗费的存储空间也就越大。
  • 增加数据更新操作的代价。当对文档数据进行增删改操作时,索引结构也要做相应地改变。比如,删除某个文档时,需要同时将该文档在索引结构中的索引键值进行删除。否则,之后通过索引进行查询时就会出错。如果文档集拥有很多索引,那么就会增加对文档集的更新代价,从而导致系统性能降低。

因此,在开发应用软件过程中是否需要创建索引由开发人员进行抉择,在查询效率、存储空间和更新代价之间进行取舍。

那么,我们到底应该在哪些属性上创建索引呢?总的来说,适合创建索引的属性需要具备以下特点:

  • 常用,即经常被用作查询条件的属性。比如,我们经常用人的姓名来查询文档,那么就有必要在名字上创建索引,这样就能够通过索引提高查询效率。
  • 稳定,即不会经常被修改的属性。一旦属性被修改,那么基于该属性创建的索引结构也会被修改。B+树节点的修改通常需要先删除节点,然后再插入新节点,因此一次索引更新的代价是巨大的。我们知道个人的银行账户号是不变的,因此可以在账户号上创建索引,而账户余额是经常变化的属性,因此不适合用于创建索引。
  • 区分度高,即文档集中拥有相同属性值的文档数量很少。比如,文档"_id"属性,它的区分度就很高,通过某一“_id”值能够唯一地确定某一文档;再比如人的姓名,其区分度相对较高。在学校、班级、公司等范围内,重名的人相对比较少。因此,"_id"属性和姓名都适合用于创建索引。在它们的索引结构中,叶子节点的键值要么指向一个数据块要么只指向几个数据块。但是,人的性别只有男和女两种取值,其区分度很低,不适合用于创建索引。

以上简单地介绍了MongoDB文档数据库的数据组织方式、索引的创建以及索引的使用原则。其他文档数据库系统的索引创建与使用,读者需要阅读对应系统的相关文档。

练习题

1. 如果我们在属性price上创建一个索引(比如使用指令db.myColl.createIndex( { price: 1 }) ),那么以下哪个查询可以无法从这个索引获益?

  1. db.myColl.findone({ category:"apple", price:20 })
  2. db.myColl.findone({ category:"apple" })
  3. db.myColl.findone({ price:{$gte:20, $lte:30} })
  4. db.myColl.findone({ category:"apple", price:{$gte:20, $lte:30} })

2. 如果我在多个属性上创建一个复合索引,例如db.myColl.createIndex({ score: 1, price: 1, category: 1 }),那么以下哪个查询无法从索引获益?

  1. db.myColl.find({ category:"apple", price:20, score:5 })
  2. db.myColl.find({ score:{$gte:4} })
  3. db.myColl.find({ category:"apple", price:{$gte:20, $lte:30} })
  4. db.myColl.find({ category:"apple", score:{$gte:4} })

3. 请问以下哪种情况最适合使用索引?

  1. 属性a常用作查询条件,属性b频繁被修改。在a上创建索引。
  2. 属性a常用作查询条件,属性b频繁被修改。在b上创建索引。
  3. 属性a常用作查询条件,文档频繁被插入和删除。在a上创建索引。
  4. 属性a常用作查询条件,属性a频繁被修改。在a上创建索引。

上一页<< | >>下一页