文章目录
- 1、索引分类
- 2、索引管理
- 3、示例
- 查询a=1的记录
- 查询a=1 and b=5的记录
- 查询b=1的记录
- 按照c的值查询
- 按照b和c一起查
- 按照[a,c]两个字段查询
- 查询a=1 and b>=0 and c=1的记录
- 4.1、准备400万测试数据
- 4.2、无索引检索效果
- 4.3、主键检索
- 4.4、between and范围检索
- 4.5、in的检索
- 4.6、多个索引时查询如何走?
- 4.7、模糊查询
- 4.8、回表
- 4.9、索引覆盖
- 4.10、索引下推
- 4.11、数字使字符串类索引失效
- 4.12、函数使索引无效
- 4.13、运算符使索引无效
- 4.14、使用索引优化排序
第一篇 什么是索引?
1、来看一个问题
路人在搞计算机之前,是负责小区建设规划的,上级领导安排路人负责一个万人小区建设规划,并提了一个要求:可以快速通过户主姓名找到户主的房子;让路人出个好的解决方案。
方案1
刚开始路人没什么经验,实在想不到什么好办法。
路人告诉领导:你可以去敲每户的门,然后开门之后再去询问房主姓名,是否和需要找的人姓名一致。
领导一听郁闷了:我敲你的头,1万户,我一个个找,找到什么时候了?你明天不用来上班了。
这里面涉及到的时间有:走到每户的门口耗时、敲门等待开门耗时、询问户主获取户主姓名耗时、将户主姓名和需要查找的姓名对比是否一致耗时。加入要找的人刚好在最后一户,领导岂不是要疯掉了,需要重复1万次上面的操作。
上面是最原始,最耗时的做法,可能要找的人根本不在这个小区,白费力的找了1万次,岂不是要疯掉。
方案2
路人灵机一动,想到了一个方案:
- 给所有的户主制定一个编号,从1-10000,户主将户号贴在自家的门口
- 路人自己制作了一个户主和户号对应的表格,我们叫做: 户主目录表 ,共1万条记录,如下:
户主姓名 | 房屋编号 |
刘德华 | 00001 |
张学友 | 00002 |
路人 | 00888 |
路人甲java | 10000 |
此时领导要查找 路人甲Java 时,过程如下:
- 按照姓名在 户主目录表 查找 路人甲Java ,找到对应的编号: 10000
- 然后从第一户房子开始找,查看其门口户号是否是10000,直到找到为止
路人告诉领导,这个方案比方案1有以下好处:
- 如果要找的人不在这个小区,通过 户主目录表 就确定,不需要第二步了
- 步骤2中不需要再去敲每户的门以及询问户主的姓名了,只需对比一下门口的户号就可以了,比方
案1省了不少时间。
领导笑着说,不错不错,有进步,不过我找 路人甲Java 还是需要挨家挨户看门牌号1万次啊!。。。。。你再去想想吧,看看是否还有更好的办法来加快查找速度。
路人下去了苦思冥想,想出了方案3。
方案3
方案2中第2步最坏的情况还是需要找1万次。
路人去上海走了一圈,看了那边小区搞的不错,很多小区都是搞成一栋一栋的,每栋楼里面有100户,路人也决定这么搞。
路人告诉领导:
- 将1万户划分为100栋楼,每栋楼有25层,每层有4户人家,总共1万户
- 给每栋楼一个编号,范围是[001,100],将栋号贴在每栋楼最显眼的位置
- 给每栋楼中的每层一个编号,编号范围是[01,25],将层号贴在每层楼最显眼的位置
- 户号变为:栋号-楼层-层中编号,如 路人甲Java 户号是:100-20-04,贴在每户门口
户主目录表 还是有1万条记录,如下:
户主姓名 | 房屋编号 |
刘德华 | 001-08-04 |
张学友 | 022-18-01 |
路人 | 088-25-04 |
路人甲java | 100-25-04 |
此时领导要查找 路人甲Java 时,过程如下:
- 按照姓名在 户主目录表 查找 路人甲Java ,找到对应的编号是 100-25-04 ,将编号分解,得到:栋号(100)、楼层(25)、楼号(04)
- 从第一栋开始找,看其栋号是否是100,直到找到编号为100为止,这个过程需要找100次,然后到了第100栋楼下
- 从100栋的第一层开始向上走,走到每层看其编号是否为25,直到走到第25层,这个过程需要匹配25次
- 在第25层依次看看户号是否为 100-25-04 ,匹配了4次,找到了 路人甲Java
此方案分析:
- 查找 户主目录表 1万次,不过这个是在表格中,不用动身走路去找,只需要动动眼睛对比一下数字,速度还是比较快的
- 将方案2中的第2步优化为上面的 2/3/4 步骤,上面最坏需要匹配129次(栋100+层25+楼号4次),相对于方案2的1万次好多了
领导拍拍路人的肩膀:小伙子,去过上海的人确实不一样啊,这次方案不错,不过第一步还是需要很多次,能否有更好的方案呢?
路人下去了又想了好几天,突然想到了我们常用的字典,可以按照字典的方式对方案3中第一步做优化,然后提出了方案4。
方案4
姓首字母:A | |
姓名 | 户号 |
阿三 | 010-16-01 |
阿郎 | 017-11-04 |
啊啊 | 008-08-02 |
姓首字母:L | |
姓名 | 户号 |
刘德华 | 011-16-01 |
路人 | 057-11-04 |
路人甲Java | 048-08-02 |
现在查找户号步骤如下:
- 通过姓名获取姓对应的首字母
- 在26个表格中找到对应姓的表格,如 路人甲Java ,对应 L表
- 在L表中循环遍历,找到 路人甲Java 的户号
- 根据户号按照方案3中的(2/3/4)步骤找对应的户主
理想情况:
1万户主的姓氏分配比较均衡,那么每个姓氏下面分配385户(10000/26) ,那么找到某个户主,最多需要:26次+385次 = 410次,相对于1万次少了很多。
最坏的情况:
1万个户主的姓氏都是一样的,导致这1万个户主信息都位于同一个姓氏户主表,此时查询又变为了1万多次。不过出现姓氏一样的情况比较低。
如果担心姓氏不足以均衡划分户主信息,那么也可以通过户主姓名的笔画数来划分,或者其他方法,主要是将用户信息划分为不同的区,可以快速过滤一些不相关的户主。
上面几个方案为了快速检索到户主,用到了一些数据结构,通过这些数据结构对户主的信息进行组织,从而可以快速过滤掉一些不相关的户主,减少查找次数,快速定位到户主的房子。
2、索引是什么?
通过上面的示例,我们可以概况一下索引的定义:索引是依靠某些数据结构和算法来组织数据,最终引导用户快速检索出所需要的数据。
索引有2个特点:
- 通过数据结构和算法来对原始的数据进行一些有效的组织
- 通过这些有效的组织,可以引导使用者对原始数据进行快速检索
第二篇 MySQL索引原理详解
1、背景
使用mysql最多的就是查询,我们迫切的希望mysql能查询的更快一些,我们经常用到的查询有:
- 按照id查询唯一一条记录
- 按照某些个字段查询对应的记录
- 查找某个范围的所有记录(between and)
- 对查询出来的结果排序
mysql的索引的目的是使上面的各种查询能够更快。
2、预备知识
什么是索引?
上一篇中有详细的介绍,可以过去看一下:什么是索引?
索引的本质:通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺
序的事件,也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。
磁盘中数据的存取
以机械硬盘来说,先了解几个概念。
扇区:磁盘存储的最小单位,扇区一般大小为512Byte。
磁盘块:文件系统与磁盘交互的的最小单位(计算机系统读写磁盘的最小单位),一个磁盘块由连续几个(2^n)扇区组成,块一般大小一般为4KB。
磁盘读取数据:磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。
mysql中的页
mysql中和磁盘交互的最小单位称为页,页是mysql内部定义的一种数据结构,默认为16kb,相当于4个磁盘块,也就是说mysql每次从磁盘中读取一次数据是16KB,要么不读取,要读取就是16KB,此值可以修改的。
数据检索过程
我们对数据存储方式不做任何优化,直接将数据库中表的记录存储在磁盘中,假如某个表只有一个字段,为int类型,int占用4个byte,每个磁盘块可以存储1000条记录,100万的记录需要1000个磁盘块,如果我们需要从这100万记录中检索所需要的记录,需要读取1000个磁盘块的数据(需要1000次io),每次io需要9ms,那么1000次需要9000ms=9s,100条数据随便一个查询就是9秒,这种情况我们是无法接受的,显然是不行的。
一个磁盘块4kb,一条记录4byte,4kb / 4byte = 4 * 1024 byte / 4 byte = 1024 条 (1K)
3、我们迫切的需求是什么?(数据结构和算法)
我们迫切需要这样的数据结构和算法:
- 需要一种数据存储结构:当从磁盘中检索数据的时候能,够减少磁盘的io次数,最好能够降低到一个稳定的常量值
- 需要一种检索算法:当从磁盘中读取磁盘块的数据之后,这些块中可能包含多条记录,这些记录被加载到内存中,那么需要一种算法能够快速从内存多条记录中快速检索出目标数据
我们来找找,看是否能够找到这样的算法和数据结构。我们看一下常见的检索算法和数据结构。
3.1、循环遍历查找
从一组无序的数据中查找目标数据,常见的方法是遍历查询,n条数据,时间复杂度为O(n),最快需要1次,最坏的情况需要n次,查询效率不稳定。
3.2、二分法查找
二分法查找也称为折半查找,用于在一个有序数组中快速定义某一个需要查找的数据。
原理是:
先将一组无序的数据排序(升序或者降序)之后放在数组中,此处用升序来举例说明:用数组中间位置的数据A和需要查找的数据F对比,如果A=F,则结束查找;如果A<F,则将查找的范围缩小至数组中A数据右边的部分;如果A>F,则将查找范围缩小至数组中A数据左边的部分,继续按照上面的方法直到找到F为止。
示例:
从下列有序数字中查找数字9,过程如下
[1,2,3,4,5,6,7,8,9]
第1次查找:[1,2,3,4,5,6,7,8,9]中间位置值为5,9>5,将查找范围缩小至5右边的部分:[6、7、8、9]
第2次查找:[6、7、8、9]中间值为8,9>8 ,将范围缩小至8右边部分:[9]
第3次查找:在[9]中查找9,找到了。
可以看到查找速度是相当快的,每次查找都会使范围减半,如果我们采用顺序查找,上面数据最快需要1次,最多需要9次,而二分法查找最多只需要3次,耗时时间也比较稳定。
二分法查找时间复杂度是:O(logN)(N为数据量),100万数据查找最多只需要20次( =1048576 )
二分法查找数据的优点:定位数据非常快,前提是:目标数组是有序的。
3.3、有序数组
如果我们将mysql中表的数据以有序数组的方式存储在磁盘中,那么我们定位数据步骤是:
- 取出目标表的所有数据,存放在一个有序数组中
- 如果目标表的数据量非常大,从磁盘中加载到内存中需要的内存也非常大
步骤取出所有数据耗费的io次数太多,步骤2耗费的内存空间太大,还有新增数据的时候,为了保证数组有序,插入数据会涉及到数组内部数据的移动,也是比较耗时的,显然用这种方式存储数据是不可取的。
3.4、链表
链表相当于在每个节点上增加一些指针,可以和前面或者后面的节点连接起来,就像一列火车一样,每节车厢相当于一个节点,车厢内部可以存储数据,每个车厢和下一节车厢相连。
链表分为单链表和双向链表。
单链表
每个节点中有持有指向下一个节点的指针,只能按照一个方向遍历链表,结构如下:
双向链表
每个节点中两个指针,分别指向当前节点的上一个节点和下一个节点,结构如下:
链表的优点:
- 可以快速定位到上一个或者下一个节点
- 可以快速删除数据,只需改变指针的指向即可,这点比数组好
链表的缺点:
- 无法向数组那样,通过下标随机访问数据
- 查找数据需从第一个节点开始遍历,不利于数据的查找,查找时间和无需数据类似,需要全遍历,最差时间是O(N)
3.5、二叉查找树
二叉树是每个结点最多有两个子树的树结构,通常子树被称作“左子树”(left subtree)和“右子树” (right subtree)。二叉树常被用于实现二叉查找树和二叉堆。二叉树有如下特性:
- 每个结点都包含一个元素以及n个子树,这里0≤n≤2。 2、左子树和右子树是有顺序的,次序不能任意颠倒,左子树的值要小于父结点,右子树的值要大于父结点。
数组[20,10,5,15,30,25,35]使用二叉查找树存储如下:
每个节点上面有两个指针(left,rigth),可以通过这2个指针快速访问左右子节点,检索任何一个数据最多只需要访问3个节点,相当于访问了3次数据,时间为O(logN),和二分法查找效率一样,查询数据还是比较快的。
但是如果我们插入数据是有序的,如[5,10,15,20,30,25,35],那么结构就变成下面这样:
二叉树退化为了一个链表结构,查询数据最差就变为了O(N)。
二叉树的优缺点:
- 查询数据的效率不稳定,若树左右比较平衡的时,最差情况为O(logN),如果插入数据是有序的,退化为了链表,查询时间变成了O(N)
- 数据量大的情况下,会导致树的高度变高,如果每个节点对应磁盘的一个块来存储一条数据,需io次数大幅增加,显然用此结构来存储数据是不可取的
3.6、平衡二叉树(AVL树)
平衡二叉树是一种特殊的二叉树,所以他也满足前面说到的二叉查找树的两个特性,同时还有一个特性:
- 它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
平衡二叉树相对于二叉树来说,树的左右比较平衡,不会出现二叉树那样退化成链表的情况,不管怎么插入数据,最终通过一些调整,都能够保证树左右高度相差不大于1。
这样可以让查询速度比较稳定,查询中遍历节点控制在O(logN)范围内
如果数据都存储在内存中,采用AVL树来存储,还是可以的,查询效率非常高。不过我们的数据是存在磁盘中,用过采用这种结构,每个节点对应一个磁盘块,数据量大的时候,也会和二叉树一样,会导致树的高度变高,增加了io次数,显然用这种结构存储数据也是不可取的。
3.7、B-树
B杠树 ,千万不要读作B减树了,B-树在是平衡二叉树上进化来的,前面介绍的几种树,每个节点上面只有一个元素,而B-树节点中可以放多个元素,主要是为了降低树的高度。
一棵m阶的B-Tree有如下特性【特征描述的有点绕,看不懂的可以跳过,看后面的图】:
- 每个节点最多有m个孩子,m称为b树的阶
- 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子
- 若根节点不是叶子节点,则至少有2个孩子
- 所有叶子节点都在同一层,且不包含其它关键字信息
- 每个非终端节点包含n个关键字(健值)信息
- 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
- ki(i=1,…n)为关键字,且关键字升序排序
- Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)
B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同。
B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个3阶的B-Tree:
每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个键将数据划分成的三个范围域,对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
模拟查找关键字29的过程:
- 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
- 比较关键字29在区间(17,35),找到磁盘块1的指针P2
- 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
- 比较关键字29在区间(26,30),找到磁盘块3的指针P2
- 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
- 在磁盘块8中的关键字列表中找到关键字29
分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作,由于内存中的关键字是一个有序表结构,可以利用二分法快速定位到目标数据,而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-树相对于avl树,通过在节点中增加节点内部数据的个数来减少磁盘的io操作。
上面我们说过mysql是采用页方式来读写数据,每页是16KB,我们用B-树来存储mysql的记录,每个节点对应mysql中的一页(16KB),假如每行记录加上树节点中的1个指针占160Byte,那么每个节点可以存储1000(16KB/160byte)条数据,树的高度为3的节点大概可以存储(第一层1000+第二层10002+第三层10003)10亿条记录,是不是非常惊讶,一个高度为3个B-树大概可以存储10亿条记录,我们从10亿记录中查找数据只需要3次io操作可以定位到目标数据所在的页,而页内部的数据又是有序的,然后将其加载到内存中用二分法查找,是非常快的。
可以看出使用B-树定位某个值还是很快的(10亿数据中3次io操作+内存中二分法),但是也是有缺点的:
- B-不利于范围查找,比如上图中我们需要查找[15,36]区间的数据,需要访问7个磁盘块(1/2/7/3/8/4/9),io次数又上去了,范围查找也是我们经常用到的,所以]b-树也不太适合在磁盘中存储需要检索的数据。
3.8、B+树
先看个b+树结构图:
b+树的特征
- 每个结点至多有m个子女
- 除根结点外,每个结点至少有[m/2]个子女,根结点至少有两个子女
- 有k个子女的结点必有k个关键字
- 父节点中持有访问子节点的指针
- 父节点的关键字在子节点中都存在(如上面的1/20/35在每层都存在),要么是最小值,要么是最大值,如果节点中关键字是升序的方式,父节点的关键字是子节点的最小值
- 最底层的节点是叶子节点
- 除叶子节点之外,其他节点不保存数据,只保存关键字和指针
- 叶子节点包含了所有数据的关键字以及data,叶子节点之间用链表连接起来,可以非常方便的支
持范围查找
b+树与b-树的几点不同:
- b+树中一个节点如果有k个关键字,最多可以包含k个子节点(k个关键字对应k个指针);而b-树对应k+1个子节点(多了一个指向子节点的指针)
- b+树除叶子节点之外其他节点值存储关键字和指向子节点的指针,而b-树还存储了数据,这样同样大小情况下,b+树可以存储更多的关键字
- b+树叶子节点中存储了所有关键字及data,并且多个节点用链表连接,从上图中看子节点中数据从左向右是有序的,这样快速可以支撑范围查找(先定位范围的最大值和最小值,然后子节点中依靠链表遍历范围数据)
B-Tree和B+Tree该如何选择?
- B-Tree因为非叶子结点也保存具体数据,所以在查找某个关键字的时候找到即可返回。而B+Tree所有的数据都在叶子结点,每次查找都得到叶子结点。所以在同样高度的B-Tree和B+Tree中,BTree查找某个关键字的效率更高
- 由于B+Tree所有的数据都在叶子结点,并且结点之间有指针连接,在找大于某个关键字或者小于某个关键字的数据的时候,B+Tree只需要找到该关键字然后沿着链表遍历就可以了,而B-Tree还需要遍历该关键字结点的根结点去搜索。
- 由于B-Tree的每个结点(这里的结点可以理解为一个数据页)都存储主键+实际数据,而B+Tree非叶子结点只存储关键字信息,而每个页的大小有限是有限的,所以同一页能存储的B-Tree的数据会比B+Tree存储的更少。这样同样总量的数据,B-Tree的深度会更大,增大查询时的磁盘I/O次数,进而影响查询效率。
3.9、Mysql的存储引擎和索引
mysql内部索引是由不同的引擎实现的,主要说一下InnoDB和MyISAM这两种引擎中的索引,这两种引擎中的索引都是使用b+树的结构来存储的。
InnoDB中的索引:
Innodb中有2种索引:主键索引(聚集索引)、辅助索引(非聚集索引)。
- 主键索引:每个表只有一个主键索引,b+树结构,叶子节点同时保存了主键的值也数据记录,其他节点只存储主键的值。
- 辅助索引:每个表可以有多个,b+树结构,叶子节点保存了索引字段的值以及主键的值,其他节点只存储索引指端的值。
MyISAM引擎中的索引:
B+树结构,MyISM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
如下图:为了更形象说明这两种索引的区别,我们假想一个表存储了4行数据。其中Id作为主索引,Name作为辅助索引,图中清晰的显示了聚簇索引和非聚簇索引的差异。
我们看一下上图中数据检索过程。
InnoDB数据检索过程:
如果需要查询id=14的数据,只需要在左边的主键索引中检索就可以了。
如果需要搜索name='Ellison’的数据,需要2步:
- 先在辅助索引中检索到name='Ellison’的数据,获取id为14
- 再到主键索引中检索id为14的记录
辅助索引这个查询过程在mysql中叫做回表。
MyISAM数据检索过程:
- 在索引中找到对应的关键字,获取关键字对应的记录的地址
- 通过记录的地址查找到对应的数据记录
我们用的最多的是innodb存储引擎,所以此处主要说一下innodb索引的情况,innodb中最好是采用主键查询,这样只需要一次索引,如果使用辅助索引检索,涉及到回表操作,比主键查询要耗时一些。
innodb中辅助索引为什么不像myisam那样存储记录的地址?
表中的数据发生变更的时候,会影响其他记录地址的变化,如果辅助索引中记录数据的地址,此时会受影响,而主键的值一般是很少更新的,当页中的记录发生地址变更的时候,对辅助索引是没有影响的。我们来看一下mysql中页的结构,页是真正存储记录的地方,对应B+树中的一个节点,也是mysql中读写数据的最小单位,页的结构设计也是相当有水平的,能够加快数据的查询。
3.10、页结构
mysql中页是innodb中存储数据的基本单位,也是mysql中管理数据的最小单位,和磁盘交互的时候都是以页来进行的,默认是16kb,mysql中采用b+树存储数据,页相当于b+树中的一个节点。
页的结构如下图:
每个Page都有通用的头和尾,但是中部的内容根据Page的类型不同而发生变化。Page的头部里有我们关心的一些数据,下图把Page的头部详细信息显示出来:
我们重点关注和数据组织结构相关的字段:Page的头部保存了两个指针,分别指向前一个Page和后一个Page,根据这两个指针我们很容易想象出Page链接起来就是一个双向链表的结构,如下图:
再看看Page的主体内容,我们主要关注行数据和索引的存储,他们都位于Page的User Records部分,User Records占据Page的大部分空间,User Records由一条一条的Record组成。在一个Page内部,单链表的头尾由固定内容的两条记录来表示,字符串形式的"Infimum"代表开头,"Supremum"代表结尾,这两个用来代表开头结尾的Record存储在System Records的,Infinum、Supremum和User Records组成了一个单向链表结构。最初数据是按照插入的先后顺序排列的,但是随着新数据的插入和旧数据的删除,数据物理顺序会变得混乱,但他们依然通过链表的方式保持着逻辑上的先后顺序,如下图:
把User Record的组织形式和若干Page组合起来,就看到了稍微完整的形式。
innodb为了快速查找记录,在页中定义了一个称之为page directory的目录槽(slots),每个槽位占用两个字节(用于保存指向记录的地址),page directory中的多个slot组成了一个有序数组(可用于二分法快速定位记录,向下看),行记录被Page Directory逻辑的分成了多个块,块与块之间是有序的,能够加速记录的查找,如下图:
看上图,每个行记录的都有一个n_owned的区域(图中粉色区域),n_owned标识所属的slot这个这个块有多少条数据,伪记录Infimum的n_owned值总是1,记录Supremum的n_owned的取值范围为[1,8],其他用户记录n_owned的取值范围[4,8],并且只有每个块中最大的那条记录的n_owned才会有值,其他的用户记录的n_owned为0。
数据检索过程
在page中查询数据的时候,先通过b+树中查询方法定位到数据所在的页,然后将页内整体加载到内存中,通过二分法在page directory中检索数据,缩小范围,比如需要检索7,通过二分法查找到7位于slot2和slot3所指向的记录中间,然后从slot3指向的记录5开始向后向后一个个找,可以找到记录7,如果里面没有7,走到slot2向的记录8结束。
n_owned范围控制在[4,8]内,能保证每个slot管辖的范围内数据量控制在[4,8]个,能够加速目标数据的查找,当有数据插入的时候,page directory为了控制每个slot对应块中记录的个数([4,8]),此时page directory中会对slot的数量进行调整。
对page的结构总结一下
- b+树中叶子页之间用双向链表连接的,能够实现范围查找
- 页内部的记录之间是采用单向链表连接的,方便访问下一条记录
- 为了加快页内部记录的查询,对页内记录上加了个有序的稀疏索引,叫页目录(page directory)
整体上来说mysql中的索引用到了b+树,链表,二分法查找,做到了快速定位目标数据,快速范围查找。
第三篇 MySQL索引管理
1、索引分类
分为聚集索引和非聚集索引。
1.1、聚集索引
每个表有且一定会有一个聚集索引,整个表的数据存储在聚集索引中,mysql索引是采用B+树结构保存在文件中,叶子节点存储主键的值以及对应记录的数据,非叶子节点不存储记录的数据,只存储主键的值。当表中未指定主键时,mysql内部会自动给每条记录添加一个隐藏的rowid字段(默认4个字节)作为主键,用rowid构建聚集索引。
聚集索引在mysql中又叫主键索引。
1.2、非聚集索引(辅助索引)
也是b+树结构,不过有一点和聚集索引不同,非聚集索引叶子节点存储字段(索引字段)的值以及对应记录主键的值,其他节点只存储字段的值(索引字段)。
每个表可以有多个非聚集索引。
mysql中非聚集索引分为
单列索引:即一个索引只包含一个列。
多列索引(又称复合索引): 即一个索引包含多个列。
唯一索引: 索引列的值必须唯一,允许有一个空值。
1.3、数据检索的过程
看一张图:
上面的表中有2个索引:id作为主键索引,name作为辅助索引。
innodb我们用的最多,我们只看图中左边的innodb中数据检索过程:
如果需要查询id=14的数据,只需要在左边的主键索引中检索就可以了。
如果需要搜索name='Ellison’的数据,需要2步:
- 先在辅助索引中检索到name='Ellison’的数据,获取id为14
- 再到主键索引中检索id为14的记录
辅助索引相对于主键索引多了第二步。
2、索引管理
2.1、创建索引
方式1:
方式2:
方式3:创建联合索引(示例)
如果字段是char、varchar类型,length可以小于字段实际长度,如果是blog、text等长文本类型,必须指定length。
[unique]:中括号代表可以省略,如果加上了unique,表示创建唯一索引。
如果table后面只写一个字段,就是单列索引,如果写多个字段,就是复合索引,多个字段之间用逗号隔开。
2.2、删除索引
2.3、查看索引
查看某个表中所有的索引信息如下:
2.4、索引修改
可以先删除索引,再重建索引。
3、示例
准备200万数据
上图中使用存储过程循环插入了200万记录,表中有4个字段,除了sex列,其他列的值都是没有重复的,表中还未建索引。
插入的200万数据中,id,name,email的值都是没有重复的。
无索引我们体验一下查询速度
上面我们按id查询了一条记录耗时770毫秒,我们在id上面创建个索引感受一下速度。
创建索引
我们在id上面创建一个索引,感受一下:
上面的查询是不是非常快,耗时1毫秒都不到。
我们在name上也创建个索引,感受一下查询的神速,如下:
查询快如闪电,有没有,索引是如此的神奇。
创建索引并指定长度
通过email检索一下数据
耗时1秒多,回头去看一下插入数据的sql,我们可以看到所有的email记录,每条记录的前面15个字符是不一样的,结尾是一样的(都是@163.com),通过前面15个字符就可以定位一个email了,那么我们可以对email创建索引的时候指定一个长度为15,这样相对于整个email字段更短一些,查询效果是一样的,这样一个页中可以存储更多的索引记录,命令如下:
然后看一下查询效果:
耗时不到1毫秒,神速。
查看表中的索引
我们看一下test1表中的所有索引,如下
可以看到test1表中3个索引的详细信息(索引名称、类型,字段)。
删除索引
我们删除idx1,然后再列出test1表所有索引,如下:
本篇主要是mysql中索引管理相关一些操作,属于基础知识,必须掌握。
第四篇 如何正确使用索引
1、通常说的这个查询走索引了是什么意思?
当我们对某个字段的值进行某种检索的时候,如果这个检索过程中,我们能够快速定位到目标数据所在的页,有效的降低页的io操作,而不需要去扫描所有的数据页的时候,我们认为这种情况能够有效的利用索引,也称这个检索可以走索引,如果这个过程中不能够确定数据在那些页中,我们认为这种情况下索引对这个查询是无效的,此查询不走索引。
2、b+树中数据检索过程
2.1、唯一记录检索
如上图,所有的数据都是唯一的,查询105的记录,过程如下:
- 将P1页加载到内存
- 在内存中采用二分法查找,可以确定105位于[100,150)中间,所以我们需要去加载100关联P4页
- 将P4加载到内存中,采用二分法找到105的记录后退出
2.2、查询某个值的所有记录
如上图,查询105的所有记录,过程如下:
- 将P1页加载到内存
- 在内存中采用二分法查找,可以确定105位于[100,150)中间,100关联P4页
- 将P4加载到内存中,采用二分法找到最有一个小于105的记录,即100,然后通过链表从100开始向后访问,找到所有的105记录,直到遇到第一个大于100的值为止
2.3、范围查找
数据如上图,查询[55,150]所有记录,由于页和页之间是双向链表升序结构,页内部的数据是单项升序链表结构,所以只用找到范围的起始值所在的位置,然后通过依靠链表访问两个位置之间所有的数据即可,过程如下:
- 将P1页加载到内存
- 内存中采用二分法找到55位于50关联的P3页中,150位于P5页中
- 将P3加载到内存中,采用二分法找到第一个55的记录,然后通过链表结构继续向后访问P3中的60、67,当P3访问完毕之后,通过P3的nextpage指针访问下一页P4中所有记录,继续遍历P4中的所有记录,直到访问到P5中的150为止。
2.4、模糊匹配
数据如上图。
查询以 f 开头的所有记录
过程如下:
- 将P1数据加载到内存中
- 在P1页的记录中采用二分法找到最后一个小于等于f的值,这个值是f,以及第一个大于f的,这个值是z,f指向叶节点P3,z指向叶节点P6,此时可以断定以f开头的记录可能存在于[P3,P6)这个范围的页内,即P3、P4、P5这三个页中
- 加载P3这个页,在内部以二分法找到第一条f开头的记录,然后以链表方式继续向后访问P4、P5中的记录,即可以找到所有已f开头的数据
查询包含f的记录
包含的查询在sql中的写法是%f%,通过索引我们还可以快速定位所在的页么?
可以看一下上面的数据,f在每个页中都存在,我们通过P1页中的记录是无法判断包含f的记录在那些页的,只能通过io的方式加载所有叶子节点,并且遍历所有记录进行过滤,才可以找到包含f的记录。
所以如果使用了%值%这种方式,索引对查询是无效的。
2.5、最左匹配原则
当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的, 比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
来一些示例我们体验一下。
下图中是3个字段(a,b,c)的联合索引,索引中数据的顺序是以 a asc,b asc,c asc 这种排序方式存储在节点中的,索引先以a字段升序,如果a相同的时候,以b字段升序,b相同的时候,以c字段升序,节点中每个数据认真看一下。
查询a=1的记录
由于页中的记录是以a asc,b asc,c asc这种排序方式存储的,所以a字段是有序的,可以通过二分法快速检索到,过程如下:
- 将P1加载到内存中
- 在内存中对P1中的记录采用二分法找,可以确定a=1的记录位于{1,1,1}和{1,5,1}关联的范围内,这两个值子节点分别是P2、P4
- 加载叶子节点P2,在P2中采用二分法快速找到第一条a=1的记录,然后通过链表向下一条及下一页开始检索,直到在P4中找到第一个不满足a=1的记录为止
查询a=1 and b=5的记录
方法和上面的一样,可以确定a=1 and b=5的记录位于{1,1,1}和{1,5,1}关联的范围内,查找过程和a=1查找步骤类似。
查询b=1的记录
这种情况通过P1页中的记录,是无法判断b=1的记录在那些页中的,只能加锁索引树所有叶子节点,对所有记录进行遍历,然后进行过滤,此时索引是无效的。
按照c的值查询
这种情况和查询b=1也一样,也只能扫描所有叶子节点,此时索引也无效了。
按照b和c一起查
这种也是无法利用索引的,也只能对所有数据进行扫描,一条条判断了,此时索引无效。
按照[a,c]两个字段查询
这种只能利用到索引中的a字段了,通过a确定索引范围,然后加载a关联的所有记录,再对c的值进行过滤。
查询a=1 and b>=0 and c=1的记录
这种情况只能先确定a=1 and b>=0所在页的范围,然后对这个范围的所有页进行遍历,c字段在这个查询的过程中,是无法确定c的数据在哪些页的,此时我们称c是不走索引的,只有a、b能够有效的确定索引页的范围。
类似这种的还有>、<、between and,多字段索引的情况下,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配。
上面说的各种情况,大家都多看一下图中数据,认真分析一下查询的过程,基本上都可以理解了。
上面这种查询叫做最左匹配原则。
3、索引区分度
我们看2个有序数组
[1,2,3,4,5,6,7,8,8,9,10]
[1,1,1,1,1,8,8,8,8,8]
上面2个数组是有序的,都是10条记录,如果我需要检索值为8的所有记录,那个更快一些?
咱们使用二分法查找包含8的所有记录过程如下:先使用二分法找到最后一个小于8的记录,然后沿着这条记录向后获取下一个记录,和8对比,知道遇到第一个大于8的数字结束,或者到达数组末尾结束。
采用上面这种方法找到8的记录,第一个数组中更快的一些。因为第二个数组中含有8的比例更多的,需要访问以及匹配的次数更多一些。
这里就涉及到数据的区分度问题:
索引区分度 = count(distint 记录) / count(记录)。
当索引区分度高的时候,检索数据更快一些,索引区分度太低,说明重复的数据比较多,检索的时候需要访问更多的记录才能够找到所有目标数据。
当索引区分度非常小的时候,基本上接近于全索引数据的扫描了,此时查询速度是比较慢的。
第一个数组索引区分度为1,第二个区分度为0.2,所以第一个检索更快的一些。
所以我们创建索引的时候,尽量选择区分度高的列作为索引。
4、正确使用索引
4.1、准备400万测试数据
上面插入的400万数据,除了sex列,其他列的值都是没有重复的。
4.2、无索引检索效果
400万数据,我们随便查询几个记录看一下效果。
按照id查询记录
id=1的数据,表中只有一行,耗时近2秒,由于id列无索引,只能对400万数据进行全表扫描。
4.3、主键检索
test1表中没有明确的指定主键,我们将id设置为主键:
id被置为主键之后,会在id上建立聚集索引,随便检索一条我们看一下效果:
这个速度很快,这个走的是上面介绍的唯一记录检索。
4.4、between and范围检索
速度也很快,id上有主键索引,这个采用的上面介绍的范围查找可以快速定位目标数据。
但是如果范围太大,跨度的page也太多,速度也会比较慢,如下:
上面id的值跨度太大,1所在的页和200万所在页中间有很多页需要读取,所以比较慢。
所以使用between and的时候,区间跨度不要太大。
4.5、in的检索
in方式检索数据,我们还是经常用的。
平时我们做项目的时候,建议少用表连接,比如电商中需要查询订单的信息和订单中商品的名称,可以先查询查询订单表,然后订单表中取出商品的id列表,采用in的方式到商品表检索商品信息,由于商品id是商品表的主键,所以检索速度还是比较快的。
通过id在400万数据中检索100条数据,看看效果:
耗时不到1毫秒,还是相当快的。
这个相当于多个分解为多个唯一记录检索,然后将记录合并。
4.6、多个索引时查询如何走?
我们在name、sex两个字段上分别建个索引
看一下查询:
上面查询速度很快,name和sex上各有一个索引,觉得上面走哪个索引?
有人说name位于where第一个,所以走的是name字段所在的索引,过程可以解释为这样:
- 走name所在的索引找到javacode3500000对应的所有记录
- 遍历记录过滤出sex=2的值
我们看一下name='javacode3500000’检索速度,确实很快,如下:
走name索引,然后再过滤,确实可以,速度也很快,果真和where后字段顺序有关么?我们把name和sex的顺序对调一下,如下:
速度还是很快,这次是不是先走sex索引检索出数据,然后再过滤name呢?我们先来看一下sex=2查询速度:
看上面,查询耗时360毫秒,200万数据,如果走sex肯定是不行的。
我们使用explain来看一下:
possible_keys:列出了这个查询可能会走两个索引(idx1、idx2)
实际上走的却是idx1(key列:实际走的索引)。
当多个条件中有索引的时候,并且关系是and的时候,会走索引区分度高的,显然name字段重复度很低,走name查询会更快一些。
4.7、模糊查询
看两个查询
上面第一个查询可以利用到name字段上面的索引,下面的查询是无法确定需要查找的值所在的范围的,只能全表扫描,无法利用索引,所以速度比较慢,这个过程上面有说过。
4.8、回表
当需要查询的数据在索引树中不存在的时候,需要再次到聚集索引中去获取,这个过程叫做回表,如查询:
上面查询是*,由于name列所在的索引中只有name、id两个列的值,不包含sex、email,所以上面过程如下:
- 走name索引检索javacode3500000对应的记录,取出id为3500000
- 在主键索引中检索出id=3500000的记录,获取所有字段的值
4.9、索引覆盖
查询中采用的索引树中包含了查询所需要的所有字段的值,不需要再去聚集索引检索数据,这种叫索引覆盖。
我们来看一个查询:
name对应idx1索引,id为主键,所以idx1索引树叶子节点中包含了name、id的值,这个查询只用走idx1这一个索引就可以了,如果select后面使用*,还需要一次回表获取sex、email的值。
所以写sql的时候,尽量避免使用*,*可能会多一次回表操作,需要看一下是否可以使用索引覆盖来实现,效率更高一些。
【可参考这篇博客(后续整理)
4.10、索引下推
简称ICP,Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式,ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
举个例子来说一下:
我们需要查询name以javacode35开头的,性别为1的记录数,sql如下:
过程:
- 走name索引检索出以javacode35的第一条记录,得到记录的id
- 利用id去主键索引中查询出这条记录R1
- 判断R1中的sex是否为1,然后重复上面的操作,直到找到所有记录为止。
上面的过程中需要走name索引以及需要回表操作。
如果采用ICP的方式,我们可以这么做,创建一个(name,sex)的组合索引,查询过程如下:
- 走(name,sex)索引检索出以javacode35的第一条记录,可以得到(name,sex,id),记做R1
- 判断R1.sex是否为1,然后重复上面的操作,知道找到所有记录为止
这个过程中不需要回表操作了,通过索引的数据就可以完成整个条件的过滤,速度比上面的更快一些。
更详细的索引下推内容可看:
4.11、数字使字符串类索引失效
上面3条sql,我们插入了一条记录。
第二条查询很快,第三条用name和1比较,name上有索引,name是字符串类型,字符串和数字比较的时候,会将字符串强制转换为数字,然后进行比较,所以第二个查询变成了全表扫描,只能取出每条数据,将name转换为数字和1进行比较。
数字字段和字符串比较什么效果呢?如下:
id上面有主键索引,id是int类型的,可以看到,上面两个查询都非常快,都可以正常利用索引快速检索,所以如果字段是数组类型的,查询的值是字符串还是数组都会走索引。
4.12、函数使索引无效
name上有索引,上面查询,第一个走索引,第二个不走索引,第二个使用了函数之后,name所在的索引树是无法快速定位需要查找的数据所在的页的,只能将所有页的记录加载到内存中,然后对每条数据使用函数进行计算之后再进行条件判断,此时索引无效了,变成了全表数据扫描。
结论:索引字段使用函数查询使索引无效。
4.13、运算符使索引无效
id上有主键索引,上面查询,第一个走索引,第二个不走索引,第二个使用运算符,id所在的索引树是无法快速定位需要查找的数据所在的页的,只能将所有页的记录加载到内存中,然后对每条数据的id进行计算之后再判断是否等于1,此时索引无效了,变成了全表数据扫描。
结论:索引字段使用了函数将使索引无效。
4.14、使用索引优化排序
我们有个订单表t_order(id,user_id,addtime,price),经常会查询某个用户的订单,并且按照addtime升序排序,应该怎么创建索引呢?我们来分析一下。
在user_id上创建索引,我们分析一下这种情况,数据检索的过程:
- 走user_id索引,找到记录的的id
- 通过id在主键索引中回表检索出整条数据
- 重复上面的操作,获取所有目标记录
- 在内存中对目标记录按照addtime进行排序
我们要知道当数据量非常大的时候,排序还是比较慢的,可能会用到磁盘中的文件,有没有一种方式,查询出来的数据刚好是排好序的。
我们再回顾一下mysql中b+树数据的结构,记录是按照索引的值排序组成的链表,如果将user_id和addtime放在一起组成联合索引(user_id,addtime),这样通过user_id检索出来的数据自然就是按照addtime排好序的,这样直接少了一步排序操作,效率更好,如果需addtime降序,只需要将结果翻转一下就可以了。
5、总结一下使用索引的一些建议
- 在区分度高的字段上面建立索引可以有效的使用索引,区分度太低,无法有效的利用索引,可能需要扫描所有数据页,此时和不使用索引差不多
- 联合索引注意最左匹配原则:必须按照从左到右的顺序匹配,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整
- 查询记录的时候,少使用*,尽量去利用索引覆盖,可以减少回表操作,提升效率
- 有些查询可以采用联合索引,进而使用到索引下推(IPC),也可以减少回表操作,提升效率
- 禁止对索引字段使用函数、运算符操作,会使索引失效
- 字符串字段和数字比较的时候会使索引无效
- 模糊查询’%值%'会使索引无效,变为全表扫描,但是’值%'这种可以有效利用索引
- 排序中尽量使用到索引字段,这样可以减少排序,提升查询效率