2.05 文档数据库的存储与索引¶
接下来,本节以MongoDB为例,介绍文档数据的组织方式以及索引的构建与使用。
2.05.1 文档和文档集存储结构¶
文档数据库系统采用前文中介绍的分页存储的方式组织文档,也就是说硬盘存储空间被划分成多个固定大小的页,文档被打包存放在一个个数据页中。数据页是文档数据库系统对文档进行读取的基本单元。一个数据页中存储着某一文档集的多个文档,而某一文档集的文档放在在多个数据页中。
为了实现高效的文档访问,文档数据库系统采用聚簇B+树索引结构替代Inode结构来组织文档和文档集。默认情况下,文档数据库系统会为每个文档集在文档的"_id"属性上创建聚簇B+树索引,也称主索引。在基于主索引的文档组织方式中,文档集中的文档按照"_id"属性值进行排序,排序后的文档依次被存放在一个个数据页中,数据页中的文档顺序与B+树索引的叶子节点的键值顺序一致。
另外,文档数据库系统会再维护一个目录项,将文档集的名称与主索引编号的映射关系存储在目录项中。当文档数据库系统启动时,会首先将磁盘中的目录项读入内存,通过目录项定位任一文档集的主索引,然后通过主索引的B+树结构快速定位数据页并访问文档。通过每个文档集在"_id"属性的聚簇B+树索引可以高效地实现文档的创建、查找、更新和删除功能。
2.05.2 文档数据库索引¶
文档数据库的主索引实现的是从文档的"_id"属性值到文档所在数据页地址的映射,它只能支持基于"_id"属性值的查询需求,不能满足其他的查询需求。例如,在例1.55中,从student文档集中查找gender属性为"female"并且department属性为"数学"的文档,文档数据库系统必须扫描student文档集中的每个文档才能返回查询结果。
为了尽可能地满足更多需求的高效查询,文档数据库系统允许在其他文档属性值上构建基于B+树结构的辅助索引,实现通过其他文档属性值快速定位文档所在的数据页,避免文档集的全部扫描。
文档数据库MongoDB支持的辅助索引包括单键索引、复合索引、多键索引、文本索引、地理空间索引等。 * 单键索引是指在单个文档属性上创建索引,如果索引字段上的值在文档集中是唯一的,则称为唯一索引; * 复合索引是指在多个文档属性上创建索引; * 多键索引是指在数组或者嵌套文档的属性上创建索引; * 文本索引是指在文本属性上创建全文索引; * 地理空间索引是指在包含地理坐标的属性上创建索引。
接下来,我们将主要介绍单键索引和复合索引的创建与使用,其他索引类型这里不做更多详细地讲解。
2.05.3 索引创建与使用¶
文档数据库系统对外提供了指令允许用户创建辅助索引。文档数据库系统MongoDB的索引创建指令为createIndex,该指令包含两个参数,第一个参数指定要创建的属性字段和索引键值的排序类型,第二个参数为可选参数。
[例2.06] 索引创建
db.student.createIndex({"sno":1}) // 单键索引
db.student.createIndex({"sname":1, "age":-1}) //复合索引
createIndex可以在一个属性或者多个属性上创建索引。前者称为单键索引,后者称为复合索引。在例2.06中,第一行表示在student文档集中的文档"sno"属性上创建索引,"sno"属性值(索引键值)按升序排列;第二行表示在文档的"sname"和"age"两个属性上创建索引,"sname"和"age"共同作为索引键值,索引键值首先按"name"升序排列,然后在每个"name"内按"age"降序排列。
用户显示执行createIndex指令之后,文档数据库系统以索引属性为键值构建B+树索引结构。当用户使用索引属性进行文档查询时,MongoDB可以通过对应索引的B+树快速地定位查询文档所在的数据块地址并读取文档内容。例2.07中,两个查询分别使用例2.06中的单键索引和复合索引查询文档。值得额外注意的是,如果想要使用复合索引实现文档的快速查询,那么查询指令中的查询条件需要满足索引的最左前缀原则,即查询条件中必须包含索引的第一个属性。也就是说,想要使用基于"sname"和"age"构建的复合索引,查询条件中必须包含"sname"属性。
练习题¶
1. 如果我们在属性price上创建一个索引(比如使用指令db.myColl.createIndex( { price: 1 }) ),那么以下哪个查询可以无法从这个索引获益?
- db.myColl.findone({ category:"apple", price:20 })
- db.myColl.findone({ category:"apple" })
- db.myColl.findone({ price:{$gte:20, $lte:30} })
- db.myColl.findone({ category:"apple", price:{$gte:20, $lte:30} })
2. 如果我在多个属性上创建一个复合索引,例如db.myColl.createIndex({ score: 1, price: 1, category: 1 }),那么以下哪个查询无法从索引获益?
- db.myColl.find({ category:"apple", price:20, score:5 })
- db.myColl.find({ score:{$gte:4} })
- db.myColl.find({ category:"apple", price:{$gte:20, $lte:30} })
- db.myColl.find({ category:"apple", score:{$gte:4} })