索引就「相当于我们字典中的目录」,可以极大的提高我们在数据库的查询效率。
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
哈希表是一种以键-值(key-value)存储数据的结构,我们只要输入待查找的值即key,就可以找到其对应的值即Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把key换算成一个确定的位置,然后把value放在数组的这个位置。比方说在维护着一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,那么身份证号代表key,对应的名字就是value。
不可避免地,多个key值经过哈希函数的换算,会出现同一个值的情况。也就是说对于两个不同的身份证号,计算出同样的值(这个值作为姓名的存放地址),处理这种情况的一种方法是,拉出一个链表。
需要注意的是key不是依序递增,这样做的好处是增加新的key时速度会很快,只需要往后追加。但缺点是,因为不是有序的,所以哈希索引做区间查询的速度是很慢的。比如要找身份证号在[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))。
二叉搜索树的查找,最坏情况就是目标结点位于最深的叶子结点,就是树的高度[log2n+1] (以2为底),去掉常数项,去掉取整运算,再根据换底公式去掉对数的底数,就得到了O(logn)。
多叉树就是每个节点有多个儿子,儿子之间的大小保证从左到右递增。二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上。
数据库索引是存储在磁盘上的,当数据量大的时候,索引文件本身就十分庞大,所以不能把整个索引全部加载到内存当中,只能逐一加载磁盘页,磁盘页对应着索引树的节点。最坏情况下,磁盘IO数由索引树高决定。
想象一下一棵100万节点的平衡二叉树,树高20。一次查询可能需要访问20个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要10 ms左右的寻址时间。也就是说,对于一个100万行的表,如果使用二叉树来存储,单独访问一个行可能需要20个10 ms的时间,这个查询可真够慢的。
为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用“N叉”树。这里,“N叉”树中的“N”取决于数据块的大小。以InnoDB的一个整数字段索引为例,这个N差不多是1200。这棵树高是4的时候,就可以存1200的3次方个值,这已经17亿了。考虑到树根的数据块总是在内存中的,一个10亿行的表上一个整数字段的索引,查找一个值最多只需要访问3次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。
N叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。
数据库底层存储的核心就是基于这些数据模型的。每碰到一个新数据库,我们需要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景。
首先要知道B-树就是B树,中间的符号不是减号。B树是一种多路平衡查找树,每一个节点最多包括K个孩子,K被称为B树的阶,K的大小取决于磁盘页的大小。
一个m阶的B树具有如下几个特征:
以3阶B树为例:
以上图中的B-树为例,假如要查询的数值是5,第一次磁盘IO在内存中和9比较;第二次磁盘IO在内存中和(2,6)比较;第三次磁盘IO在内存中和(3,5)比较,然后找到查询的数值5.
通过流程可以看出,B-树在查询过程中的比较次数不比二叉树少,尤其单一节点元素多时比较次数反较二叉树多,但是相比磁盘IO的速度,在内存中的耗时几乎可以忽略,所以只要树足够低,IO次数足够少,就可以提高查找性能。节点中的元素多些也没有关系,仅仅是多几次内存交互,只要不超过磁盘页大小即可。
B-树的插入和删除都比较麻烦,在上图B-树的情况下,要插入4的值,自顶向下查找4的节点位置,发现4应当插入到节点元素3,5之间。
节点3,5已经是两元素节点,无法再增加。父亲节点 2, 6 也是两元素节点,也无法再增加。根节点9是单元素节点,可以升级为两元素节点。于是拆分节点3,5与节点2,6,让根节点9升级为两元素节点4,9。节点6独立为根节点的第二个孩子。
插入一个元素让B-树发生了连锁改变,但是也正是这种机制,维护了B-树的多路平衡。
当要删除一个节点时,比如例子中的元素11.自顶向下查找元素11的节点位置。删除11后,节点12只有一个孩子,不符合B树规范。因此找出12,13,15三个节点的中位数13,取代节点12,而节点12自身下移成为第一个孩子。(这个过程称为左旋)
主要应用于文件系统和部分数据库索引,比如著名的非关系型数据库MongoDB
B+树是B-树的一种变体,查询性能比B-树更高。B+树较B-树有共同点也有新的特征
一个m阶的B+树具有如下几个特征:
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
首先,每一个父节点中的元素都出现在子节点中,是子节点中的最大(或最小)元素。同时需要注意根节点的最大元素等同于整棵树的最大元素,以后无论插入或删除多少元素,始终要保持最大元素在根节点当中。且由于父节点的元素都出现在子节点,所以叶子节点包含全部信息,且每一叶子节点都有指向下一节点的指针,形成有序链表。
B+树还有一个重要特征,就是卫星数据的位置。卫星数据指索引元素指向的数据记录,比如数据库中的某一行。
B-树中无论中间节点还是叶子节点都带有卫星数据,但是B+树只有叶子节点带有卫星数据,其余中间节点仅仅是索引,没有任何数据联系。
在数据库的聚集索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针。
B+树查询性能高效,以单元素查询和范围查询为例,
单元素查询时B+树自顶向下逐层查找节点,最终找到匹配的叶子节点。相比B-树的单元素查询有两点不同:
B-树做范围查询,必须使用繁琐的中序遍历;但是B+树做范围查询,只要在链表上遍历即可,比B-树的中序遍历简单的多。
单一节点存储更多的元素,使得查询的IO次数更少。
所有查询都要查找到叶子节点,查询性能稳定。
所有叶子节点形成有序链表,便于范围查询。
Inodb存储引擎 默认是 B+Tree索引
MyISAM 存储引擎 默认是Fulltext索引;
Memory 存储引擎 默认 Hash索引,Memory表也可以使用B+Tree索引
由于在数据库中页的大小是固定的,InnoDB存储引擎中页的大小默认为16KB,如果非叶子节点不存数据,那么这些非叶子节点就可以存储更多的索引键值。在同等数据量下,B+树会比B树更矮、更胖。B+树在B树的基础上进一步压缩了树的高度,减少了磁盘IO次数,提高了索引查找效率。
(即B+树的优势)
用一个例子说明,假设有一个表user,有几个字段id、c1、c2、c3、c4,其中id是主键,c1、c2、c3字段建立联合索引。
select * from user where c1= 12 and c2= 14 and c3 = 3 // q1 索引全匹配
select * from user where c1= 12 and c2= 14 // q2 索引部分匹配
select * from user where c1= 12 and c3 = 3 // q3 索引部分匹配
select * from user where c2= 12 and c3 = 3 // q4 索引无法匹配
对于联合索引,全部命中索引字段可以执行索引;部分命中并符合最左匹配原则,也可能会执行索引;不满足最左匹配原则,则无法命中索引,具体过程如下:
上图就是一个联合索引的B+树示意图,InnoDB会使用聚簇索引在B+树维护索引和数据文件。第一行(1,1,5,12,13……)是c1行,其余c2\c3和粉色id行。c1行是单调递增的;如果第一列相等则再根据第二列排序,依次类推就构成了上图的索引树。
对于上面的q1 索引全匹配,存储引擎首先从根节点(一般常驻内存)开始查找:第一个索引的第一个索引列为1,12大于1;第二个索引的第一个索引列为56,12小于56;于是
联合索引会有最左匹配原则,我们创建的c1、c2、c3索引,相当于创建了(c1)、(c1、c2)(c1、c2、c3)三个索引。联合索引是首先使用多列索引的第一列构建的索引树,用上面的例子就是优先使用c1列构建,当c1列值相等时再以c2列排序,若c2列的值也相等则以c3列排序。我们可以取出索引树的叶子节点看一下。
索引的第一列也就是c1列可以说是从左到右单调递增的,但我们看c2列和c3列并没有这个特性,它们只能在c1列值相等的情况下这个小范围内递增,如第一叶子节点的第1、2个元素和第二个叶子节点的后三个元素。
由于联合索引是上述那样的索引构建方式及存储结构,所以联合索引只能从多列索引的第一列开始查找。所以如果你的查找条件不包含c1列如(c2, c3)、(c2)、(c3) 是无法应用缓存的,以及跨列也是无法完全用到索引如(c1, c3),只会用到c1列索引。
最左前缀匹配原则:在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。可以根据5.中的例子来理解,即只有c1列可以说是从左到右单调递增的,c2列在c1列值相等的情况下这个小范围内递增。
回表就是先通过数据库索引扫描出该索引树中数据所在的行,取到主键 id,再通过主键 id 取出主键索引数中的数据,即基于非主键索引的查询需要多扫描一棵索引树.
索引下推(Index Condition Pushdown,简称ICP),是MySQL5.6版本的新特性,它能减少回表查询次数,提高查询效率。如果存在某些被索引的列的判断条件时,MySQL 将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合 MySQL 服务器传递的条件,「只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器」 。
索引下推的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。
举例说明,使用一张用户表tuser,表里创建联合索引(name, age)
select * from tuser where name like '张%' and age=10;
根据索引最左匹配原则,这个语句在搜索索引树的时候,只能用 张
,找到的第一个满足条件的记录id为1。
覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取,可以减少回表的次数。比如:
select id from t where age = 1;
id 为主键索引,age 为普通索引,age 这个索引树存储的就是逐渐信息,可以直接返回
以下随便列举几个,不同版本的 mysql 场景不一
1.最左前缀法则(带头索引不能死,中间索引不能断
2.不要在索引上做任何操作(计算、函数、自动/手动类型转换),不然会导致索引失效而转向全表扫描
3.不能继续使用索引中范围条件(bettween、<、>、in等)右边的列,如:
select a from user where c > 5 and b = 4;
4.索引字段上使用(!= 或者 < >)判断时,会导致索引失效而转向全表扫描
5.索引字段上使用 is null / is not null 判断时,会导致索引失效而转向全表扫描。
6.索引字段使用like以通配符开头(‘%字符串’)时,会导致索引失效而转向全表扫描,也是最左前缀原则。
7.索引字段是字符串,但查询时不加单引号,会导致索引失效而转向全表扫描
8.索引字段使用 or 时,会导致索引失效而转向全表扫描
MySQL可以分为Server层和存储引擎层两部分。
Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。
连接器负责跟客户端建立连接、获取权限、维持和管理连接。
首先输入用户密码,如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接
则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。建立连接的过程通常是比较复杂的,所以我建议你在使用中要尽量减少建立连接的动作,也就是尽量使用长连接。
但是全部使用长连接后,你可能会发现,有些时候MySQL占用内存涨得特别快,这是因为MySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是MySQL异常重启了。
怎么解决这个问题呢?你可以考虑以下两种方案。
MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过
的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。key是查询的语句,value是
查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客
户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存
中。你可以看到,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接返回结
果,这个效率会很高。
大多数情况下不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此
很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库
来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。
比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。
将参数query_cache_type设置成DEMAND,这样对于默认的SQL语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用SQL_CACHE显式指定。MySQL 8.0版本直接将查询缓存的整块功能删掉了,也就是说8.0开始彻底没有这个功能了。
首先,MySQL需要知道你要做什么,因此需要对SQL语句做解析。分析器先会做**“词法分析”。你输入的是由多个字符串和空格组成的一条SQL语句,MySQL需要识别出里面的字符串分别是什么,代表什么**。比如"select"关键字代表这是一个查询语句。
然后做**“语法分析”**。根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。比如select少打了开头的字母“s”。
比如’Unknown column ‘k’ in ‘where clause’的报错,就是MySQL没有识别出 ‘k’ 代表什么。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
选择索引是优化器的工作。而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的CPU资源越少。当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。
MySQL在真正开始执行语句之前,并不能精确地知道满足这个条件的记录有多少条,而只能根
据统计信息来估算记录数。这个统计信息就是索引的“区分度”。显然,一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。
MySQL是怎样得到索引的基数的呢? 把整张表取出来一行行统计,虽然可以得到精确的结果,但是代价太高了,所以只能选择“采样统计”。
采样统计的时候,InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。而数据表是会持续更新的,索引统计信息也不会固定不变。所以,当变更的数据行数超过1/M的时候,会自动触发重新做一次索引统计。
在MySQL中,有两种存储索引统计的方式,可以通过设置参数innodb_stats_persistent
的值来选择:
设置为on的时候,表示统计信息会持久化存储。这时,默认的N是20,M是10。
设置为off的时候,表示统计信息只存储在内存中。这时,默认的N是8,M是16。
如果是并没有涉及到临时表和排序的简单的查询语句,MySQL选错索引肯定是在判断扫描行数的时候出问题了。不过需要注意对比主键索引,使用普通索引还需要把回表的代价算进去。
MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
开始执行的时候,要先判断一下你对这个表T有没有执行相应操作的的权限,比如查询。如果没有,就会返回没有权限的错误。如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。
【1】公众号【moon聊技术 】:MySQL 常见面试题总结!
【2】Mysql45讲—极客时间 林晓斌
【3】事务的特性——原子性(实现原理)
【4】数据库事务原子性、一致性是怎样实现的?
【5】MySQL 中的WAL机制
【6】漫画:什么是B-树? 漫画:什么是B+树?
【7】MySQL B+树如何实现联合索引
【8】五分钟搞懂MySQL索引下推