## 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文档集中的每个文档才能返回查询结果。

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

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

文档数据库MongoDB支持的辅助索引包括单键索引、复合索引、多键索引、文本索引、地理空间索引等。
* 单键索引是指在单个文档属性上创建索引，如果索引字段上的值在文档集中是唯一的，则称为唯一索引；
* 复合索引是指在多个文档属性上创建索引；
* 多键索引是指在数组或者嵌套文档的属性上创建索引；
* 文本索引是指在文本属性上创建全文索引；
* 地理空间索引是指在包含地理坐标的属性上创建索引。

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

### 2.5.3 索引创建与使用

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

```bson
[例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"降序排列。

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

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


### 练习题

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

 <ol type="A">
 <li>db.myColl.findone({ category:"apple", price:20 })</li>
 <li>db.myColl.findone({ category:"apple" })</li>
 <li>db.myColl.findone({ price:{$gte:20, $lte:30} })</li>
 <li>db.myColl.findone({ category:"apple", price:{$gte:20, $lte:30} })</li>
 </ol>

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

 <ol type="A">
 <li>db.myColl.find({ category:"apple", price:20, score:5 })</li>
 <li>db.myColl.find({ score:{$gte:4} })</li>
 <li>db.myColl.find({ category:"apple", price:{$gte:20, $lte:30} })</li>
 <li>db.myColl.find({ category:"apple", score:{$gte:4} })</li>
 </ol>


[**上一页<<**](chapter2.4-R.md) | [**>>下一页**](chapter2.6-G.md)

