数据库索引的应用与底层实现

       关于数据库索引,它的重要性体现在了方方面面,因此也不约而同的成为了面试中的一大要点。在我近期的腾讯、阿里巴巴面试中数据库索引也确实占据了一些分量,下午去腾讯现场面,还没来得及去看自己不懂的问题,晚上阿里的电话就接踵而至,所以说我们必须掌握熟悉一些常见的以及面试中的热点问题,那么今天我就和大家一起来探索数据库索引的奥秘哈~

       本文以MySQL数据库为例,讨论与数据库索引相关的各种问题。MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。值得注意的是,MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。虽然MySQL支持多种索引类型,但MySQL常用的索引数据结构为B+Tree,因此我们主要来探讨一下B+Tree的底层实现~

       使用数据库索引很简单,如果你能写出创建表的语句,创建索引自然也轻而易举,会使用索引是一回事,而能够恰当好处且合理的使用索引则是另一回事,并且这两者之间完全不可同日而语,很大一部分程序员对索引的了解仅仅是创建使用索引可以使得查询速度变快,而这样的理解是远远不能达到程序员要求的,更不用说的得到面试官的青睐了,正因如此,我们就没有什么理由去逃避或者搪塞掉数据库索引相关的问题了。首先,我们来看一下数据库索引的作用吧!

       创建索引可以大大提高系统的性能。第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性;第二,可以大大加快数据的检索速度,这也是创建索引的最主要的原因;第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义;第四,在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间;第五,通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

       数据在磁盘上是以块的形式存储的,磁盘存储的原理我会在后面详细介绍,为确保对磁盘操作的原子性,访问数据的时候会一并访问所有数据块。磁盘上的这些数据块与链表类似,即它们都包含一个数据段和一个指针,指针指向下一个节点(数据块)的内存地址,而且它们都不需要连续存储(即逻辑上相邻的数据块在物理上可以相隔很远)。鉴于很多记录只能做到按一个字段排序,所以要查询某个未经排序的字段,就需要使用线性查找,即要访问N/2个数据块,其中N指的是一个表所涵盖的所有数据块。如果该字段是非键字段(也就是说不包含唯一值),那么就要搜索整个表空间,即要访问全部N个数据块。然而,对于经过排序的字段,可以使用二分查找,因此只要访问log2 N个数据块。同样,对于已经排过序的非键字段,只要找到更大的值,也就不用再搜索表中的其他数据块了。这样一来,性能就会有实质性的提升。首先,来看一个示例数据库表的模式:

字段名              数据类型         在磁盘上的大小
id (Primary key)   Unsigned INT     4 字节
firstName          Char(50)         50 字节
lastName           Char(50)         50 字节
emailAddress       Char(100)        100 字节

       注意:这里用char而不用varchar是为了精确地描述数据占用磁盘的大小。这个示例数据库中包含500万行记录,而且没有建立索引。接下来我们就分析针对该表的两个查询:一个查询使用id(经过排序的键字段),另一个查询使用firstName(未经排序的非键字段)。

       对于这个拥有r = 5 000 000条记录的示例数据库,在磁盘上要为每条记录分配 R = 204字节的固定存储空间。这个表保存在MyISAM数据库中,而这个数据库默认的数据库块大小为 B = 1024字节。于是,我们可计算出这个表的分块因数为 bfr = (B/R) = 1024/204 = 5,即磁盘上每个数据块保存5条记录。那么,保存整个表所需的数据块数就是 N = (r/bfr) = 5000000/5 = 1 000 000。

       使用线性查找搜索id字段——这个字段是键字段(每个字段的值唯一),需要访问 N/2 = 500 000个数据块才能找到目标值。不过,因为这个字段是经过排序的,所以可以使用二分查找法,而这样平均只需要访问log2 1000000 = 19.93 = 20 个块。显然,这会给性能带来极大的提升。再来看看firstName字段,这个字段是未经排序的,因此不可能使用二分查找,况且这个字段的值也不是唯一的,所以要从表的开头查找末尾,即要访问 N = 1 000 000个数据块。这种情况通过建立索引就能得到改善。

       如果一条索引记录只包含索引字段和一个指向原始记录的指针,那么这条记录肯定要比它所指向的包含更多字段的记录更小。也就是说,索引本身占用的磁盘空间比原来的表更少,因此需要遍历的数据块数也比搜索原来的表更少。以下是firstName字段索引的模式:

字段名         数据类型        在磁盘上的大小
firstName     Char(50)        50 字节
(记录指针)    Special         4 字节

       对于这个拥有r = 5 000 000条记录的示例数据库,每条索引记录要占用 R = 54字节磁盘空间,而且同样使用默认的数据块大小 B = 1024字节。那么索引的分块因数就是 bfr = (B/R) = 1024/54 = 18。最终这个表的索引需要占用 N = (r/bfr) = 5000000/18 = 277 778个数据块。

       现在,再搜索firstName字段就可以使用索引来提高性能了。对索引使用二分查找,需要访问 log2 277778 = 18.09 = 19个数据块。再加上为找到实际记录的地址还要访问一个数据块,总共要访问 19 + 1 = 20个数据块,这与搜索未索引的表需要访问277 778个数据块相比,简直是天壤之别。

       那么数据库索引为什么会这么强大呢,这就要回到本文的主题,我们都知道,一切事物的作用及功能都离不开它的内部构造,数据库索引亦是这样,这些好处皆取决于它的底层实现。一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。下面首先介绍一下内存和磁盘存取原理,然后再结合这些原理分析B-/+Tree作为索引的效率。

       主存的存取过程:当系统需要读取主存时,将地址信号放到地址总线上传给主存,主存读到地址信号后,解析信号并定位到指定存储单元,然后将此存储单元数据放到数据总线上,供其它部件读取。写主存的过程类似,系统将要写入单元地址和数据分别放在地址总线和数据总线上,主存读取两个总线的内容,做相应的写操作。

       如上所述,索引一般以文件形式存储在磁盘上,索引检索需要磁盘I/O操作。与主存不同,磁盘I/O存在机械运动耗费,因此磁盘I/O的时间消耗是巨大的。磁盘读取数据靠的是机械运动,当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,磁头需要移动对准相应磁道,这个过程叫做寻道,所耗费时间叫寻道时间,然后磁盘旋转将目标扇区旋转到磁头下,这个过程耗费的时间叫做旋转时间,最后便是对读取数据的传输。 所以每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分。其中:

  • 寻道时间是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下。
  • 旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms。
  • 传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。
       那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然会产生一个用户难以接受的一个时间开销。

       了解了内存与磁盘存取原理,然后我们来看为什么数据库索引采用B-/+Tree作为索引呢,首先我们来分析B-Tree,根据B-Tree的定义,可知检索一次最多最多需要访问h-1个节点(根节点常驻内存)。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,实际实现B-Tree还需要使用如下技巧:每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。

       综上所述,如果我们采用B-Tree存储结构,搜索时I/O次数一般不会超过3次,所以用B-Tree作为索引结构效率是非常高的。

       接下来我们来分析B+Tree的实现,从上面介绍我们知道,B树的搜索复杂度为O(h)=O(logdN),所以树的出度d越大,深度h就越小,I/O的次数就越少。B+Tree恰恰可以增加出度d的宽度,因为每个节点大小为一个页大小,所以出度的上限取决于节点内key和data的大小:

dmax=floor(pagesize/(keysize+datasize+pointsize))//floor表示向下取整

       由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,从而拥有更好的性能。下面通过一个实例来看一下B+Tree作为数据库索引是怎样进行查找的~

      

       B-树和B+树查找过程基本一致。如上图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。 

       通过本文的介绍,大家都体会到了数据库索引有多么的强大,那么在任何情况创建使用索引都会更优么,当然不是,任何事物都有自身的两面性,我们要学会扬长避短、恰当合理的使用数据库索引。一个新的问题诞生了,我们应该在什么时候创建索引呢~
       首先,索引是建立在数据库表中的某些列的上面,因此,在创建索引的时候,应该仔细考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列上创建索引,例如:经常需要搜索的列上使用索引,可以加快搜索的速度;在作为主键的列上,强制该列的唯一性;在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。

       同样,对于有些列不应该创建索引。一般来说,不应该创建索引的的这些列具有下列特点:第一,在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。第二,对于那些只有很小数据范围的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。第三,对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量比较多以至于存在特别大的空间需求。第四,当修改性能远远大于检索性能时不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。

       为了更全面的介绍数据库索引的相关知识,在文章的最后,我再跟大家分享一下索引具体创建方式以及MySQL的索引分类~
       1)主键索引:它是一种特殊的唯一索引,不允许有空值。一般是在建表的时候同时创建主键索引:   

    CREATE TABLE user(
        id int unsigned not null auto_increment,
        name varchar(50) not null,
        email varchar(40) not null,
        primary key (id)
    ); 
       2)普通索引:这是最基本的索引,它没有任何限制:
    create index idx_name on user(name(20)); 
       3)唯一索引:它与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值:
    CREATE UNIQUE INDEX idx_email ON user(email); 
       4)组合索引
    CREATE TABLE test (
        id INT NOT NULL,
        last_name CHAR(30) NOT NULL,
        first_name CHAR(30) NOT NULL,
        PRIMARY KEY (id),
        INDEX name (last_name,first_name)
   );
       name索引是一个对last_name和first_name的索引。索引可以用于为last_name,或者为last_name和first_name在已知范围内指定值的查询。因此,name索引用于下面的查询:  
    SELECT * FROM test WHERE last_name='Widenius';
    SELECT * FROM test WHERE last_name='Widenius' AND first_name='Michael';
       但是不能用于SELECT * FROM test WHERE first_name='Michael';这是因为MySQL组合索引为“最左前缀”的结果,简单的理解就是只从最左面的开始组合。
       至此我们从各方面探讨了数据库索引的相关问题,也涵盖了数据库索引的多个知识要点,数据库索引的重要性我就不再介绍了,在博文的开头我也有提到,希望本文对大家了解及掌握数据库索引的相关知识提供很大的帮助,从而在面试或日常学习中遇到数据库索引相关问题可以应对自如哈~
  • 21
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值