首页 > 编程知识 正文

数据库索引实现原理有哪些,索引实现原理,数据结构

时间:2023-05-03 22:43:23 阅读:215735 作者:1319

MySQLwlddy实现

在MySQL中,wlddy属于存储引擎级别的概念,不同存储引擎对wlddy的实现方式是不同的,本文主要讨论MyISAM和InnoDB两个存储引擎的wlddy实现方式。

MyISAMwlddy实现

MyISAM引擎使用B+Tree作为wlddy结构,叶节点的data域存放的是数据记录的地址。下图是MyISAMwlddy的原理图:

图8

 

这里设表一共有三列,假设我们以Col1为主键,则图8是一个MyISAM表的主wlddy(Primary key)示意。可以看出MyISAM的wlddy文件仅仅保存数据记录的地址。在MyISAM中,主wlddy和辅助wlddy(Secondary key)在结构上没有任何区别,只是主wlddy要求key是唯一的,而辅助wlddy的key可以重复。如果我们在Col2上建立一个辅助wlddy,则此wlddy的结构如下图所示:

图9

 

同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中wlddy检索的算法为首先按照B+Tree搜索算法搜索wlddy,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

MyISAM的wlddy方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集wlddy区分。

InnoDBwlddy实现

虽然InnoDB也使用B+Tree作为wlddy结构,但具体实现方式却与MyISAM截然不同。

第一个重大区别是InnoDB的数据文件本身就是wlddy文件。从上文知道,MyISAMwlddy文件和数据文件是分离的,wlddy文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个wlddy结构,这棵树的叶节点data域保存了完整的数据记录。这个wlddy的key是数据表的主键,因此InnoDB表数据文件本身就是主wlddy。

图10

 

图10是InnoDB主wlddy(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种wlddy叫做聚集wlddy。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

第二个与MyISAMwlddy的不同是InnoDB的辅助wlddydata域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助wlddy都引用主键作为data域。例如,图11为定义在Col3上的一个辅助wlddy:

图11

 

这里以英文字符的ASCII码作为比较准则。聚集wlddy这种实现方式使得按主键的搜索十分高效,但是辅助wlddy搜索需要检索两遍wlddy:首先检索辅助wlddy获得主键,然后用主键到主wlddy中检索获得记录。

了解不同存储引擎的wlddy实现方式对于正确使用和优化wlddy都非常有帮助,例如知道了InnoDB的wlddy实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助wlddy都引用主wlddy,过长的主wlddy会令辅助wlddy变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

下一章将具体讨论这些与wlddy有关的优化策略。

wlddy使用策略及优化

MySQL的优化主要分为结构优化(Scheme optimization)和查询优化(Query optimization)。本章讨论的高性能wlddy策略主要属于结构优化范畴。本章的内容完全基于上文的理论基础,实际上一旦理解了wlddy背后的机制,那么选择高性能的策略就变成了纯粹的推理,并且可以理解这些策略背后的逻辑。

示例数据库

为了讨论wlddy策略,需要一个数据量不算小的数据库作为示例。本文选用MySQL官方文档中提供的示例数据库之一:employees。这个数据库关系复杂度适中,且数据量较大。下图是这个数据库的E-R关系图(引用自MySQL官方手册):

图12

 

最左前缀原理与相关优化

高效使用wlddy的首要条件是知道什么样的查询会使用到wlddy,这个问题和B+Tree中的“最左前缀原理”有关,下面通过例子说明最左前缀原理。

这里先说一下联合wlddy的概念。在上文中,我们都是假设wlddy只引用了单个的列,实际上,MySQL中的wlddy可以以一定顺序引用多个列,这种wlddy叫做联合wlddy,一般的,一个联合wlddy是一个有序元组<a1, a2, …, an>,其中各个元素均为数据表的一列,实际上要严格定义wlddy需要用到关系代数,但是这里我不想讨论太多关系代数的话题,因为那样会显得很枯燥,所以这里就不再做严格定义。另外,单列wlddy可以看成联合wlddy元素数为1的特例。

以employees.titles表为例,下面先查看其上都有哪些wlddy:

 

从结果中可以到titles表的主wlddy为<emp_no, title, from_date>,还有一个辅助wlddy<emp_no>。为了避免多个wlddy使事情变复杂(MySQL的SQL优化器在多wlddy时行为比较复杂),这里我们将辅助wlddydrop掉:

这样就可以专心分析wlddyPRIMARY的行为了。

情况一:全列匹配。

很明显,当按照wlddy中所有列进行精确匹配(这里精确匹配指“=”或“IN”匹配)时,wlddy可以被用到。这里有一点需要注意,理论上wlddy对顺序是敏感的,但是由于MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的wlddy,例如我们将where中的条件顺序颠倒:

效果是一样的。

情况二:最左前缀匹配。

当查询条件精确匹配wlddy的左边连续一个或几个列时,如<emp_no>或<emp_no, title>,所以可以被用到,但是只能用到一部分,即条件所组成的最左前缀。上面的查询从分析结果看用到了PRIMARYwlddy,但是key_len为4,说明只用到了wlddy的第一列前缀。

情况三:查询条件用到了wlddy中列的精确匹配,但是中间某个条件未提供。

此时wlddy使用情况和情况二相同,因为title未提供,所以查询只用到了wlddy的第一列,而后面的from_date虽然也在wlddy中,但是由于title不存在而无法和左前缀连接,因此需要对结果进行扫描过滤from_date(这里由于emp_no唯一,所以不存在扫描)。如果想让from_date也使用wlddy而不是where过滤,可以增加一个辅助wlddy<emp_no, from_date>,此时上面的查询会使用这个wlddy。除此之外,还可以使用一种称之为“隔离列”的优化方法,将emp_no与from_date之间的“坑”填上。

首先我们看下title一共有几种不同的值:

只有7种。在这种成为“坑”的列值比较少的情况下,可以考虑用“IN”来填补这个“坑”从而形成最左前缀:

 这次key_len为59,说明wlddy被用全了,但是从type和rows看出IN实际上执行了一个range查询,这里检查了7个key。看下两种查询的性能比较:

“填坑”后性能提升了一点。如果经过emp_no筛选后余下很多数据,则后者性能优势会更加明显。当然,如果title的值很多,用填坑就不合适了,必须建立辅助wlddy。

情况四:查询条件没有指定wlddy第一列。

由于不是最左前缀,wlddy这样的查询显然用不到wlddy。

情况五:匹配某列的前缀字符串。

此时可以用到wlddy,但是如果通配符不是只出现在末尾,则无法使用wlddy。(原文表述有误,如果通配符%不出现在开头,则可以用到wlddy,但根据具体情况不同可能只会用其中一个前缀)

情况六:范围查询。

范围列可以用到wlddy(必须是最左前缀),但是范围列后面的列无法用到wlddy。同时,wlddy最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到wlddy。

可以看到wlddy对第二个范围wlddy无能为力。这里特别要说明MySQL一个有意思的地方,那就是仅用explain可能无法区分范围wlddy和多值匹配,因为在type中这两者都显示为range。同时,用了“between”并不意味着就是范围查询,例如下面的查询:

看起来是用了两个范围查询,但作用于emp_no上的“BETWEEN”实际上相当于“IN”,也就是说emp_no实际是多值精确匹配。可以看到这个查询用到了wlddy全部三个列。因此在MySQL中要谨慎地区分多值匹配和范围匹配,否则会对MySQL的行为产生困惑。

情况七:查询条件中含有函数或表达式。

很不幸,如果查询条件中含有函数或表达式,则MySQL不会为这列使用wlddy(虽然某些在数学意义上可以使用)。例如:

虽然这个查询和情况五中功能相同,但是由于使用了函数left,则无法为title列应用wlddy,而情况五中用LIKE则可以。再如:

显然这个查询等价于查询emp_no为10001的函数,但是由于查询条件是一个表达式,MySQL无法为其使用wlddy。看来MySQL还没有智能到自动优化常量表达式的程度,因此在写查询语句时尽量避免表达式出现在查询中,而是先手工私下代数运算,转换为无表达式的查询语句。

wlddy选择性与前缀wlddy

既然wlddy可以加快查询速度,那么是不是只要是查询语句需要,就建上wlddy?答案是否定的。因为wlddy虽然加快了查询速度,但wlddy也是有代价的:wlddy文件本身要消耗存储空间,同时wlddy会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护wlddy,因此wlddy并不是越多越好。一般两种情况下不建议建wlddy。

第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建wlddy,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建wlddy,超过2000条可以酌情考虑wlddy。

另一种不建议建wlddy的情况是wlddy的选择性较低。所谓wlddy的选择性(Selectivity),是指不重复的wlddy值(也叫基数,Cardinality)与表记录数(#T)的比值:

Index Selectivity = Cardinality / #T

显然选择性的取值范围为(0, 1],选择性越高的wlddy价值越大,这是由B+Tree的性质决定的。例如,上文用到的employees.titles表,如果title字段经常被单独查询,是否需要建wlddy,我们看一下它的选择性:

title的选择性不足0.0001(精确值为0.00001579),所以实在没有什么必要为其单独建wlddy。

有一种与wlddy选择性有关的wlddy优化策略叫做前缀wlddy,就是用列的前缀代替整个列作为wlddykey,当前缀长度合适时,可以做到既使得前缀wlddy的选择性接近全列wlddy,同时因为wlddykey变短而减少了wlddy文件的大小和维护开销。下面以employees.employees表为例介绍前缀wlddy的选择和使用。

从图12可以看到employees表只有一个wlddy<emp_no>,那么如果我们想按名字搜索一个人,就只能全表扫描了:

如果频繁按名字搜索员工,这样显然效率很低,因此我们可以考虑建wlddy。有两种选择,建<first_name>或<first_name, last_name>,看下两个wlddy的选择性:

<first_name>显然选择性太低,<first_name, last_name>选择性很好,但是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法?可以考虑用first_name和last_name的前几个字符建立wlddy,例如<first_name, left(last_name, 3)>,看看其选择性:

选择性还不错,但离0.9313还是有点距离,那么把last_name前缀加到4:

这时选择性已经很理想了,而这个wlddy的长度只有18,比<first_name, last_name>短了接近一半,我们把这个前缀wlddy 建上:

此时再执行一遍按名字查询,比较分析一下与建wlddy前的结果:

性能的提升是显著的,查询速度提高了120多倍。

前缀wlddy兼顾wlddy大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当wlddy本身包含查询所需全部数据时,不再访问数据文件本身)。

InnoDB的主键选择与插入优化

在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。

经常看到有帖子或博客讨论主键选择问题,有人建议使用业务无关的自增主键,有人觉得没有必要,完全可以使用如学号或身份证号这种唯一字段作为主键。不论支持哪种论点,大多数论据都是业务层面的。如果从数据库wlddy优化角度看,使用InnoDB引擎而不使用自增主键绝对是一个糟糕的主意。

上文讨论过InnoDB的wlddy实现,InnoDB使用聚集wlddy,数据记录本身被存于主wlddy(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。

如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前wlddy节点的后续位置,当一页写满,就会自动开辟一个新的页。如下图所示:

图13

 

这样就会形成一个紧凑的wlddy结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护wlddy上。

如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有wlddy页得中间某个位置:

图14

 

此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的wlddy结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。

因此,只要可以,请尽量在InnoDB上采用自增字段做主键。

本文参考:http://blog.codinglabs.org/articles/theory-of-mysql-index.html

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。