LevelDB二级索引实现 姚凯文(kevinyao0901) 姜嘉祺
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

240 行
9.5 KiB

1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
1 个月前
  1. # 实验报告:在 LevelDB 中构建二级索引的设计与实现
  2. ## 实验目的
  3. 在 LevelDB 的基础上设计和实现一个支持二级索引的功能,优化特定字段的查询效率。通过此功能,用户能够根据字段值高效地检索对应的数据记录,而不需要遍历整个数据库。
  4. ---
  5. ## 实现思路
  6. ### 1. **二级索引的概念**
  7. 二级索引是一种额外的数据结构,用于加速某些特定字段的查询。在 LevelDB 中,键值对的存储是以 `key:value` 的形式。通过创建二级索引,我们将目标字段的值与原始 `key` 建立映射关系,存储在独立的索引数据库中,从而支持基于字段值的快速查询。
  8. 例如,原始数据如下:
  9. ```
  10. k_1 : name:Customer#000000001|address:IVhzIApeRb|phone:25-989-741-2988
  11. k_2 : name:Customer#000000002|address:XSTf4,NCwDVaW|phone:23-768-687-3665
  12. k_3 : name:Customer#000000001|address:MG9kdTD2WBHm|phone:11-719-748-3364
  13. ```
  14. 为字段 `name` 创建索引后,索引数据库中的条目如下:
  15. ```
  16. name:Customer#000000001-k_1 : k_1
  17. name:Customer#000000001-k_3 : k_3
  18. name:Customer#000000002-k_2 : k_2
  19. ```
  20. ### 2. **设计目标**
  21. - **创建索引**:扫描数据库中的所有记录,基于指定字段提取值,并将字段值和原始 `key` 编码后写入二级索引数据库 `indexDb_`
  22. - **查询索引**:在二级索引数据库中快速定位字段值对应的原始 `key`
  23. - **删除索引**:移除二级索引数据库中所有与目标字段相关的条目。
  24. ---
  25. ## 具体实现
  26. ### 1. **DBImpl 类的设计**
  27. 在 LevelDB 的核心类 `DBImpl` 中,增加了对二级索引的支持,包括:
  28. - **索引字段管理**:使用成员变量 `fieldWithIndex_` 保存所有已经创建索引的字段名。
  29. - **索引数据库**:使用成员变量 `indexDb_` 管理二级索引数据库。
  30. ```cpp
  31. class DBImpl : public DB {
  32. private:
  33. std::vector<std::string> fieldWithIndex_; // 已创建索引的字段列表
  34. leveldb::DB* indexDb_; // 存储二级索引的数据库
  35. };
  36. ```
  37. ### 2. **二级索引的创建**
  38. `DBImpl` 中实现 `CreateIndexOnField` 方法,用于对指定字段创建二级索引:
  39. - 遍历主数据库中的所有数据记录。
  40. - 解析目标字段的值。
  41. - 在索引数据库中写入二级索引条目,键为 `"fieldName:field_value-key"`,值为原始数据的键。
  42. 示例:
  43. ![error](Report/png/indexDb.png)
  44. #### 核心代码:
  45. ```cpp
  46. Status DBImpl::CreateIndexOnField(const std::string& fieldName) {
  47. // 检查字段是否已创建索引
  48. for (const auto& field : fieldWithIndex_) {
  49. if (field == fieldName) {
  50. return Status::InvalidArgument("Index already exists for this field");
  51. }
  52. }
  53. // 添加到已创建索引的字段列表
  54. fieldWithIndex_.push_back(fieldName);
  55. // 遍历主数据库,解析字段值并写入索引数据库
  56. leveldb::ReadOptions read_options;
  57. leveldb::Iterator* it = this->NewIterator(read_options);
  58. for (it->SeekToFirst(); it->Valid(); it->Next()) {
  59. std::string key = it->key().ToString();
  60. std::string value = it->value().ToString();
  61. // 提取字段值
  62. size_t field_pos = value.find(fieldName + ":");
  63. if (field_pos != std::string::npos) {
  64. size_t value_start = field_pos + fieldName.size() + 1;
  65. size_t value_end = value.find("|", value_start);
  66. if (value_end == std::string::npos) value_end = value.size();
  67. std::string field_value = value.substr(value_start, value_end - value_start);
  68. std::string index_key = fieldName + ":" + field_value;
  69. // 在索引数据库中创建条目
  70. leveldb::Status s = indexDb_->Put(WriteOptions(), Slice(index_key), Slice(key));
  71. if (!s.ok()) {
  72. delete it;
  73. return s;
  74. }
  75. }
  76. }
  77. delete it;
  78. return Status::OK();
  79. }
  80. ```
  81. ---
  82. ### 3. **二级索引的查询**
  83. `DBImpl` 中实现 `QueryByIndex` 方法,通过目标字段值查找对应的原始键:
  84. - 在索引数据库中遍历 `fieldName:field_value` 开头的条目。
  85. - 收集结果并返回。
  86. #### 核心代码:
  87. ```cpp
  88. std::vector<std::string> DBImpl::QueryByIndex(const std::string& fieldName) {
  89. std::vector<std::string> results;
  90. leveldb::ReadOptions read_options;
  91. leveldb::Iterator* it = indexDb_->NewIterator(read_options);
  92. for (it->Seek(fieldName); it->Valid(); it->Next()) {
  93. std::string value = it->value().ToString();
  94. if (!value.empty()) {
  95. results.push_back(value);
  96. }
  97. }
  98. delete it;
  99. return results;
  100. }
  101. ```
  102. ---
  103. ### 4. **二级索引的删除**
  104. `DBImpl` 中实现 `DeleteIndex` 方法,通过目标字段名移除对应的所有索引条目:
  105. -`fieldWithIndex_` 中移除字段。
  106. - 遍历索引数据库,删除所有以 `fieldName:` 开头的条目。
  107. #### 核心代码:
  108. ```cpp
  109. Status DBImpl::DeleteIndex(const std::string& fieldName) {
  110. auto it = std::find(fieldWithIndex_.begin(), fieldWithIndex_.end(), fieldName);
  111. if (it == fieldWithIndex_.end()) {
  112. return Status::NotFound("Index not found for this field");
  113. }
  114. // 从已创建索引列表中移除字段
  115. fieldWithIndex_.erase(it);
  116. // 遍历索引数据库,删除相关条目
  117. leveldb::ReadOptions read_options;
  118. leveldb::Iterator* it_index = indexDb_->NewIterator(read_options);
  119. for (it_index->SeekToFirst(); it_index->Valid(); it_index->Next()) {
  120. std::string index_key = it_index->key().ToString();
  121. if (index_key.find(fieldName + ":") == 0) {
  122. Status s = indexDb_->Delete(WriteOptions(), Slice(index_key));
  123. if (!s.ok()) {
  124. delete it_index;
  125. return s;
  126. }
  127. }
  128. }
  129. delete it_index;
  130. return Status::OK();
  131. }
  132. ```
  133. ---
  134. ### 示例流程
  135. 1. 插入原始数据:
  136. ```
  137. k_1 : name:Customer#000000001|address:IVhzIApeRb|phone:25-989-741-2988
  138. k_2 : name:Customer#000000002|address:XSTf4,NCwDVaW|phone:23-768-687-3665
  139. ```
  140. 2. 创建索引:
  141. - 调用 `CreateIndexOnField("name")`,索引数据库生成条目:
  142. ```
  143. name:Customer#000000001-k_1 : k_1
  144. name:Customer#000000002-k_2 : k_2
  145. ```
  146. 3. 查询索引:
  147. - 调用 `QueryByIndex("name:Customer#000000001")`,返回 `["k_1"]`
  148. 4. 删除索引:
  149. - 调用 `DeleteIndex("name")`,移除所有 `name:` 开头的索引条目。
  150. 测试结果:
  151. ![error](Report/png/test_result.png)
  152. **Benchmark测试运行结果及分析:**
  153. ![error](./Report/png/benchmark.png)
  154. 1. **插入时间 (Insertion time for 100001 entries: 516356 microseconds)**
  155. 这个时间(516356 微秒,约 516 毫秒)看起来是合理的,特别是对于 100001 条记录的插入操作。如果你的数据插入过程没有特别复杂的计算或操作,这个时间应该是正常的,除非硬件性能或其他因素导致延迟。
  156. 2. **没有索引的查询时间 (Time without index: 106719 microseconds)**
  157. 这个时间是查询在没有索引的情况下执行的时间。106719 微秒(大约 107 毫秒)对于没有索引的查询来说是可以接受的,尤其是在数据量较大时。如果数据库没有索引,查找所有相关条目会比较耗时。
  158. 3. **创建索引的时间 (Time to create index: 596677 microseconds)**
  159. 这个时间(596677 微秒,约 597 毫秒)对于创建索引来说是正常的,尤其是在插入了大量数据后。如果数据量非常大,索引创建时间可能会显得稍长。通常情况下,创建索引的时间会随着数据量的增加而增大。
  160. 4. **有索引的查询时间 (Time with index: 68 microseconds)**
  161. 这个时间(68 微秒)非常短,几乎可以认为是一个非常好的优化结果。通常,索引查询比没有索引时要快得多,因为它避免了全表扫描。因此,这个时间是非常正常且预期的,说明索引大大加速了查询。
  162. 5. **查询结果 (Found 1 keys with index)**
  163. 这里显示索引查询找到了 1 个键。是正常的, `name=Customer#10000` 应该返回 1 条记录。
  164. 6. **数据库统计信息 (Database stats)**
  165. ```
  166. Compactions
  167. Level Files Size(MB) Time(sec) Read(MB) Write(MB)
  168. --------------------------------------------------
  169. 0 2 6 0 0 7
  170. 1 5 8 0 16 7
  171. ```
  172. 这些信息表明数据库的压缩(Compaction)过程。`Level 0` 和 `Level 1` 显示了数据库的文件数和大小。此部分数据正常,意味着数据库在处理数据时有一些 I/O 操作和文件整理。
  173. 7. **删除索引的时间 (Time to delete index on field 'name': 605850 microseconds)**
  174. 删除索引的时间(605850 微秒,约 606 毫秒)比创建索引的时间稍长。这个时间是合理的,删除索引通常会涉及到重新整理数据结构和清理索引文件,因此可能比创建索引稍慢。
  175. **benchmark运行结果总结:**
  176. 整体来看,输出结果是正常的:
  177. - **插入和索引创建时间**:插入数据和创建索引所需的时间相对较长,但考虑到数据量和索引的生成,时间是合理的。
  178. - **有索引的查询时间**:索引加速了查询,这部分的时间(68 微秒)非常短,表现出色。
  179. - **删除索引的时间**:删除索引需要稍长时间,这也是常见的现象。
  180. ---
  181. ## 总结
  182. 本实验通过在 `DBImpl` 中集成索引管理功能,实现了对二级索引的创建、查询和删除。二级索引数据存储在独立的 `indexDb_` 中,通过高效的键值映射提升了字段值查询的效率。