diy数据库(十)--索引

来源:互联网 发布:google seo 书推荐 编辑:程序博客网 时间:2024/06/05 05:55

一、什么是索引

1、索引是一类用于快速定位目标数据的数据结构,索引的本质就是减少I/O次数(如果索引在磁盘上),或者减少读写次数(如果索引在内存上)。

2、常见的索引有B树索引、B+树索引、散列索引等

(1)B树索引

          B树索引的定义不必多言,我们需要知道的关键一点就是:B树是一个多叉平衡查找树。我们知道在内存中组织有序数据一般会用红黑树,红黑树是一种平衡二叉树,那为什么磁盘数据的索引不用平衡二叉树呢?首先,读取磁盘数据的耗时操作在于寻找磁盘块的位置,所以磁盘的I/O次数决定了开销的大小。读磁盘数据的读写会一次性读取整块的数据到内存中,所以一次读一块数据到内存和读一个块内更少的数据到内存中的开销差别并不大。基于上述两个原因,我们在做索引时(如果用树结构),要保证树的高度更小,每个节点大一点都不要紧。这正是B树和B+树完胜平衡二叉查找树的原因。当然如果在内存中,平衡二叉查找树肯定会毫不客气的告诉你:我的地盘我做主。

(2)B+树索引

          这里不展开说,B+树主要的主要优势在于1、稳定:因为所有data都处于最底层,所以每个数据查找时的磁盘I/O次数一样(当然,在没有数据缓存的条件下)。2、大部分情况更快:相同的条件下,B+树的高度比B树小,而一般情况下,大部分数据都位于树的最下面几层,所以我们可以认为,在大多数情况下,B+树中的查找会更快。3、范围查找效率高:因为B+树的底层节点构成一个链表。

(3)散列索引

          这种索引在精确查找时非常快,但是它只能用于对等比较,例如=和<=>操作符(但是快很多)。它们不能被用于像<这样的范围查询条件。假如系统只需要使用像“键值对”的这样的存储结构,尽量使用散列类型索引。一般很多K/V数据库会用散列索引,所以这导致这些数据库不适合范围查找。


二、diydb的索引实现方案

        说了这么多,diydb的索引是怎么实现的呢?答案是散列索引(所以,作为一个非内存型数据库,diydb表示有点脸红。这也是diydb不能商用的主要原因之一)。但是diydb有一点做的不错,那就是在同一个桶中冲突的数据是通过红黑树来组织的,这个样非常适合冲突量大的情况。而且,我不会告诉你,diydb的索引是完全放在内存中的,每次加载数据库文件时,都会同时生成一个散列索引。商用数据库(非内存型)肯定是不会这么做的,因为当数据量很大的情况下,内存是放不下所有索引的。好吧,尽管diydb的索引看起来非常简陋,但他给整个数据库提供了完整的索引模块接口,也体现了索引在整个数据库中的作用和执行时机。

值得注意的是,在所有插入和删除的同时维护索引。


三、从代码看diydb的索引

#ifndef IXM_HPP__#define IXM_HPP__#include "ossLatch.hpp"#include "bson.h"#include <map>#include "dmsRecord.hpp"using namespace bson ;#define IXM_KEY_FIELDNAME "_id"//每个索引项的key字段都必须包含_id#define IXM_HASH_MAP_SIZE 1000//hash索引中桶的数量struct ixmEleHash//索引中的元素(key:data    value:recordID),recordID表示_id为data的一条记录在数据库文件中的位置{   const char  *data ;   dmsRecordID recordID ;} ;class ixmBucketManager{private:   class ixmBucket//桶   {   private:      std::multimap<unsigned int, ixmEleHash> _bucketMap;//因为索引键值可能是重复的      ossSLatch _mutex ;//每个桶有一把锁   public:      int isIDExist ( unsigned int hashNum, ixmEleHash &eleHash ) ;      int createIndex ( unsigned int hashNum, ixmEleHash &eleHash ) ;      int findIndex ( unsigned int hashNum, ixmEleHash &eleHash ) ;      int removeIndex ( unsigned int hashNum, ixmEleHash &eleHash ) ;   } ;   // process data处理数据的函数   int _processData ( BSONObj &record, dmsRecordID &recordID, // record是一个输入的bson对象                      unsigned int &hashNum,                  // 输出散列值                      ixmEleHash &eleHash,                    // 索引项                      unsigned int &random ) ;      // 桶的编号private :   std::vector<ixmBucket *>_bucket ;//1000个散列桶public :   ixmBucketManager ()   {   }   ~ixmBucketManager ()   {      ixmBucket *pIxmBucket = NULL ;      for ( int i = 0; i < IXM_HASH_MAP_SIZE; ++i )      {         pIxmBucket = _bucket[i] ;         if ( pIxmBucket )            delete pIxmBucket ;      }   }   int initialize () ;//初始化散列索引中的每个桶   int isIDExist ( BSONObj &record ) ;//查找一个对象的id是否存在   int createIndex ( BSONObj &record, dmsRecordID &recordID ) ;//创造一个索引项(索引键是record的_id字段,索引值recordID是record这条记录在数据库文件中的位置)   int findIndex ( BSONObj &record, dmsRecordID &recordID ) ;//根据record中的_id查找记录id(也就是pageid和slotid),即record这条记录在数据库文件中的位置   int removeIndex ( BSONObj &record, dmsRecordID &recordID ) ;//根据record中的_id删除record这条记录对应的索引项} ;#endif
具体类的实现代码会在最终的带注释代码中给出。


四、总结

1、前面在《diy数据库(八)--客户端和服务器之间的通信协议》中讲到过,在客户端发过来的消息中,具体要操作的一条记录(即数据库中的一行数据)是通过bson对象来指定的,bson对象中最核心的字段就是_id,那么我们怎么通过这个_id来找到数据库文件中对应的那条记录呢?就是通过散列索引,先通过给定的散列函数求出散列值(一个unsigned int 变量),然后通过散列值找到对应索引桶,进而找到的对应的索引项,索引项的值中就包含了要操作的那条记录在数据库文件的位置,即记录id(包括所在页号和所在的槽号),数据管理模块(DMS)就会通过记录id去找到数据库文件中的那条数据(当然,由于数据库文件已经被映射到虚拟内存空间中,所以相当于找到了那条数据在进程空间中的地址),然后对那条数据进行操作。

2、我们看到上一篇文章《diy数据库(八)--diydb的数据持久化和存储格式》中讲到过,数据管理模块的主要类dmsFile 中包含一个指向ixmBucketManager类型对象的属性_ixmBucketMgr (即:指向索引的指针),_ixmBucketMgr 指向的索引正是dmsFile 对象对应的数据库(因为diydb是一个单文件的数据库)的索引。


0 0