提到数据库索引,我想你并不陌生,在平常工做中会常常接触到。好比某一个SQL查询比较慢,分析完缘由以后,你可能就会说“给某个字段加个索引吧”之类的解决方案。但到底什么是索引,索引又是如何工做的呢?今天就让咱们一块儿来聊聊这个话题吧。mysql
数据库索引的内容比较多,我分红了上下两篇文章。索引是数据库系统里面最重要的概念之一,因此我但愿你可以耐心看完。在后面的实战文章中,我也会常常引用这两篇文章中提到的知识点,加深你对数据库索引的理解。算法
一句话简单来讲,索引的出现其实就是为了提升数据查询的效率,就像书的目录同样。一本500页的书,若是你想快速找到其中的某一个知识点,在不借助目录的状况下,那我估计你可得找一下子。一样,对于数据库的表而言,索引其实就是它的“目录”。sql
索引的出现是为了提升查询效率,可是实现索引的方式却有不少种,因此这里也就引入了索引模型的概念。能够用于提升读写效率的数据结构不少,这里我先给你介绍三种常见、也比较简单的数据结构,它们分别是哈希表、有序数组和搜索树。数据库
下面我主要从使用的角度,为你简单分析一下这三种模型的区别。数组
哈希表是一种以键-值(key-value)存储数据的结构,咱们只要输入待查找的值即key,就能够找到其对应的值即Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把key换算成一个肯定的位置,而后把value放在数组的这个位置。数据结构
不可避免地,多个key值通过哈希函数的换算,会出现同一个值的状况。处理这种状况的一种方法是,拉出一个链表。框架
假设,你如今维护着一个身份证信息和姓名的表,须要根据身份证号查找对应的名字,这时对应的哈希索引的示意图以下所示:函数
图中,User2和User4根据身份证号算出来的值都是N,但不要紧,后面还跟了一个链表。假设,这时候你要查ID_card_n2对应的名字是什么,处理步骤就是:首先,将ID_card_n2经过哈希函数算出N;而后,按顺序遍历,找到User2。工具
须要注意的是,图中四个ID_card_n的值并非递增的,这样作的好处是增长新的User时速度会很快,只须要日后追加。但缺点是,由于不是有序的,因此哈希索引作区间查询的速度是很慢的。性能
你能够设想下,若是你如今要找身份证号在[ID_card_X, ID_card_Y]这个区间的全部用户,就必须所有扫描一遍了。
因此,哈希表这种结构适用于只有等值查询的场景,好比Memcached及其余一些NoSQL引擎。
而有序数组在等值查询和范围查询场景中的性能就都很是优秀。仍是上面这个根据身份证号查名字的例子,若是咱们使用有序数组来实现的话,示意图以下所示:
这里咱们假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的。这时候若是你要查ID_card_n2对应的名字,用二分法就能够快速获得,这个时间复杂度是O(log(N))。
同时很显然,这个索引结构支持范围查询。你要查身份证号在[ID_card_X, ID_card_Y]区间的User,能够先用二分法找到ID_card_X(若是不存在ID_card_X,就找到大于ID_card_X的第一个User),而后向右遍历,直到查到第一个大于ID_card_Y的身份证号,退出循环。
若是仅仅看查询效率,有序数组就是最好的数据结构了。可是,在须要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面全部的记录,成本过高。
因此,有序数组索引只适用于静态存储引擎,好比你要保存的是2017年某个城市的全部人口信息,这类不会再修改的数据。
二叉搜索树也是课本里的经典数据结构了。仍是上面根据身份证号查名字的例子,若是咱们用二叉搜索树来实现的话,示意图以下所示:
二叉搜索树的特色是:每一个节点的左儿子小于父节点,父节点又小于右儿子。这样若是你要查ID_card_n2的话,按照图中的搜索顺序就是按照UserA -> UserC -> UserF -> User2这个路径获得。这个时间复杂度是O(log(N))。
固然为了维持O(log(N))的查询复杂度,你就须要保持这棵树是平衡二叉树。为了作这个保证,更新的时间复杂度也是O(log(N))。
树能够有二叉,也能够有多叉。多叉树就是每一个节点有多个儿子,儿子之间的大小保证从左到右递增。二叉树是搜索效率最高的,可是实际上大多数的数据库存储却并不使用二叉树。其缘由是,索引不止存在内存中,还要写到磁盘上。
你能够想象一下一棵100万节点的平衡二叉树,树高20。一次查询可能须要访问20个数据块。在机械硬盘时代,从磁盘随机读一个数据块须要10 ms左右的寻址时间。也就是说,对于一个100万行的表,若是使用二叉树来存储,单独访问一个行可能须要20个10 ms的时间,这个查询可真够慢的。
为了让一个查询尽可能少地读磁盘,就必须让查询过程访问尽可能少的数据块。那么,咱们就不该该使用二叉树,而是要使用“N叉”树。这里,“N叉”树中的“N”取决于数据块的大小。
以InnoDB的一个整数字段索引为例,这个N差很少是1200。这棵树高是4的时候,就能够存1200的3次方个值,这已经17亿了。考虑到树根的数据块老是在内存中的,一个10亿行的表上一个整数字段的索引,查找一个值最多只须要访问3次磁盘。其实,树的第二层也有很大几率在内存中,那么访问磁盘的平均次数就更少了。
N叉树因为在读写上的性能优势,以及适配磁盘的访问模式,已经被普遍应用在数据库引擎中了。
无论是哈希仍是有序数组,或者N叉树,它们都是不断迭代、不断优化的产物或者解决方案。数据库技术发展到今天,跳表、LSM树等数据结构也被用于引擎设计中,这里我就再也不一一展开了。
你内心要有个概念,数据库底层存储的核心就是基于这些数据模型的。每碰到一个新数据库,咱们须要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景。
截止到这里,我用了半篇文章的篇幅和你介绍了不一样的数据结构,以及它们的适用场景,你可能会以为有些枯燥。可是,我建议你仍是要多花一些时间来理解这部份内容,毕竟这是数据库处理数据的核心概念之一,在分析问题的时候会常常用到。当你理解了索引的模型后,就会发如今分析问题的时候会有一个更清晰的视角,体会到引擎设计的精妙之处。
如今,咱们一块儿进入相对偏实战的内容吧。
在MySQL中,索引是在存储引擎层实现的,因此并无统一的索引标准,即不一样存储引擎的索引的工做方式并不同。而即便多个存储引擎支持同一种类型的索引,其底层的实现也可能不一样。因为InnoDB存储引擎在MySQL数据库中使用最为普遍,因此下面我就以InnoDB为例,和你分析一下其中的索引模型。
在InnoDB中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。又由于前面咱们提到的,InnoDB使用了B+树索引模型,因此数据都是存储在B+树中的。
每个索引在InnoDB里面对应一棵B+树。
假设,咱们有一个主键列为ID的表,表中有字段k,而且在k上有索引。
这个表的建表语句是:
mysql> create table T( id int primary key, k int not null, name varchar(16), index (k))engine=InnoDB;
表中R1~R5的(ID,k)值分别为(100,1)、(200,2)、(300,3)、(500,5)和(600,6),两棵树的示例示意图以下。
从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引的叶子节点存的是整行数据。在InnoDB里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在InnoDB里,非主键索引也被称为二级索引(secondary index)。
根据上面的索引结构说明,咱们来讨论一个问题:基于主键索引和普通索引的查询有什么区别?
也就是说,基于非主键索引的查询须要多扫描一棵索引树。所以,咱们在应用中应该尽可能使用主键查询。
B+树为了维护索引有序性,在插入新值的时候须要作必要的维护。以上面这个图为例,若是插入新的行ID值为700,则只须要在R5的记录后面插入一个新记录。若是新插入的ID值为400,就相对麻烦了,须要逻辑上挪动后面的数据,空出位置。
而更糟的状况是,若是R5所在的数据页已经满了,根据B+树的算法,这时候须要申请一个新的数据页,而后挪动部分数据过去。这个过程称为页分裂。在这种状况下,性能天然会受影响。
除了性能外,页分裂操做还影响数据页的利用率。本来放在一个页的数据,如今分到两个页中,总体空间利用率下降大约50%。
固然有分裂就有合并。当相邻两个页因为删除了数据,利用率很低以后,会将数据页作合并。合并的过程,能够认为是分裂过程的逆过程。
基于上面的索引维护过程说明,咱们来讨论一个案例:
你可能在一些建表规范里面见到过相似的描述,要求建表语句里必定要有自增主键。固然事无绝对,咱们来分析一下哪些场景下应该使用自增主键,而哪些场景下不该该。
自增主键是指自增列上定义的主键,在建表语句中通常是这么定义的: NOT NULL PRIMARY KEY AUTO_INCREMENT。
插入新记录的时候能够不指定ID的值,系统会获取当前ID最大值加1做为下一条记录的ID值。
也就是说,自增主键的插入数据模式,正符合了咱们前面提到的递增插入的场景。每次插入一条新记录,都是追加操做,都不涉及到挪动其余记录,也不会触发叶子节点的分裂。
而有业务逻辑的字段作主键,则每每不容易保证有序插入,这样写数据成本相对较高。
除了考虑性能外,咱们还能够从存储空间的角度来看。假设你的表中确实有一个惟一字段,好比字符串类型的身份证号,那应该用身份证号作主键,仍是用自增字段作主键呢?
因为每一个非主键索引的叶子节点上都是主键的值。若是用身份证号作主键,那么每一个二级索引的叶子节点占用约20个字节,而若是用整型作主键,则只要4个字节,若是是长整型(bigint)则是8个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
因此,从性能和存储空间方面考量,自增主键每每是更合理的选择。
有没有什么场景适合用业务字段直接作主键的呢?仍是有的。好比,有些业务的场景需求是这样的:
只有一个索引;
该索引必须是惟一索引。
你必定看出来了,这就是典型的KV场景。
因为没有其余索引,因此也就不用考虑其余索引的叶子节点大小的问题。
这时候咱们就要优先考虑上一段提到的“尽可能使用主键查询”原则,直接将这个索引设置为主键,能够避免每次查询须要搜索两棵树。
今天,我跟你分析了数据库引擎可用的数据结构,介绍了InnoDB采用的B+树结构,以及为何InnoDB要这么选择。B+树可以很好地配合磁盘的读写特性,减小单次查询的磁盘访问次数。
因为InnoDB是索引组织表,通常状况下我会建议你建立一个自增主键,这样非主键索引占用的空间最小。但事无绝对,我也跟你讨论了使用业务逻辑字段作主键的应用场景。
最后,我给你留下一个问题吧。对于上面例子中的InnoDB表T,若是你要重建索引 k,你的两个SQL语句能够这么写:
alter table T drop index k; alter table T add index(k);
若是你要重建主键索引,也能够这么写:
alter table T drop primary key; alter table T add primary key(id);
个人问题是,对于上面这两个重建索引的做法,说出你的理解。若是有不合适的,为何,更好的方法是什么?
你能够把你的思考和观点写在留言区里,我会在下一篇文章的末尾给出个人参考答案。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。
我在上一篇文章末尾给你留下的问题是:如何避免长事务对业务的影响?
这个问题,咱们能够从应用开发端和数据库端来看。
首先,从应用开发端来看:
确认是否使用了set autocommit=0。这个确认工做能够在测试环境中开展,把MySQL的general_log开起来,而后随便跑一个业务逻辑,经过general_log的日志来确认。通常框架若是会设置这个值,也就会提供参数来控制行为,你的目标就是把它改为1。
确认是否有没必要要的只读事务。有些框架会习惯无论什么语句先用begin/commit框起来。我见过有些是业务并无这个须要,可是也把好几个select语句放到了事务中。这种只读事务能够去掉。
业务链接数据库的时候,根据业务自己的预估,经过SET MAX_EXECUTION_TIME命令,来控制每一个语句执行的最长时间,避免单个语句意外执行太长时间。(为何会意外?在后续的文章中会提到这类案例)
其次,从数据库端来看:
监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill;
Percona的pt-kill这个工具不错,推荐使用;
在业务功能测试阶段要求输出全部的general_log,分析日志行为提早发现问题;
若是使用的是MySQL 5.6或者更新版本,把innodb_undo_tablespaces设置成2(或更大的值)。若是真的出现大事务致使回滚段过大,这样设置后清理起来更方便。