假设现在我们的用户表user中已经添加了索引index_un_age_sex(user_name,age,sex),表示给字段user_nameagesex添加了一个联合索引index_un_age_sex,但是根据上面覆盖索引的查找过程,我们发现上面这条SQL,由于查询条件中user_name为字符串两端模糊匹配,所以,无法通过索引index_un_age_sex查找用户,即无法命中索引index_un_age_sex,这在大规模用户的场景下,势必影响查询性能。那么,此时,我们有什么办法解决这个问题呢?

我们先用explain语句来分析上面这条SQL,我们看到explain结果中出现了Using index condition; Using where; Using filesort,像下面这样:

queryWrapper java 让某个ID排在最前面_学习

image-20210206192623928.png

Using index condition; Using where; Using filesort是什么意思?其实,这个关键字描述了这样一个过程:

  1. MySQL先使用索引index_age_sex过滤出部分用户记录,对应上面关键字中的Using index
  2. 使用条件中的user_name字段模糊匹配,从步骤1的结果中再过滤出部分用户记录,对应上面关键字中的Using where
  3. 对上一步得到的用户记录进行排序,对应上面关键字中的Using filesort

这下你大概知道Filesort是怎么一回事了。

Filesort

Filesort表示MySQL使用了临时文件对where条件过滤的中间结果进行排序。我们就以上面使用Filesort的SQL为例,看一下具体的排序过程:

1.命中索引index_age_sex(字段agesex的联合索引),在索引树index_age_sex中查找age >= 18 AND age <= 24 AND sex = 0的用户记录。

2.顺序遍历上一步中的查找结果,查找满足user_name LIKE "%am%”的记录。

3.顺序遍历上一步中的查找结果并排序,这里MySQL使用了快速排序,MySQL给每个线程分配一块内存用于排序,这块内存区域叫做sort_buffer。关于快速排序,很多同学在大学里已经学过了,我就不详细讲解这个过程了,只说一下两种情况:

(1) 当SELECT中的字段 + 排序字段的值大小小于等于参数max_length_for_sort_data,在sort_buffer中写入全字段,然后,对排序字段排序。

比如:本章《覆盖索引》中的SQL,SELECT子句为*,假如SELECT中的字段 + 排序字段的值大小小于等于参数max_length_for_sort_data,即表全部字段大小总和小于等于参数max_length_for_sort_data,MySQL将user表中满足查询条件的记录的所有字段写入sort_buffer,然后,依次对字段ageusername排序,最终得到排序后的完整结果。

(2) 当SELECT中的字段 + 排序字段的值大小大于参数max_length_for_sort_data,在sort_buffer中写入排序字段+主键ID,然后,对排序字段排序,最后,根据主键ID到聚簇索引取出对应记录。

比如:本章《覆盖索引》中的SQL,SELECT子句为*,假如SELECT中的字段 + 排序字段的值大小大于参数max_length_for_sort_data,即表全部字段大小总和大于参数max_length_for_sort_data,MySQL将user表中满足查询条件的记录ageusernameid写入sort_buffer,然后,依次对字段ageusername排序,排序后,根据主键id到聚簇索引获取对应记录。

对比上面两种排序的过程,我们发现采用下面的方案进行排序,会多一次回表(聚簇索引查找)的过程,如果聚簇索引在磁盘上,那么就会产生磁盘IO,影响性能。

所以,我们可以采用下面两个手段避免回表查询:

  1. SQL中的SELECT部分中的字段尽量不要用*,而是指定字段,确保SELECT中的字段 + 排序字段的值大小小于等于参数max_length_for_sort_data,这样MySQL就会采用上面的(1)方案排序
  2. 如果一定要使用*,那么,务必保证表中字段的总长度不超过max_length_for_sort_data,这样MySQL也会采用上面的(1)方案排序

那么,是不是只要保证SELECT中的字段 + 排序字段的值大小小于等于参数max_length_for_sort_data,排序性能一定是最好的呢?

假如本章《覆盖索引》中的排序字段username,我们设计它的长度为32字节和200字节,同时保证了SELECT中的字段 + 排序字段的值大小小于等于参数max_length_for_sort_data,那么,两种字段长度的设计对排序性能有什么不同的影响呢?

这时,我就不得不提一下这个排序比较的过程,通过这个过程的讲解,我来揭开上面这个问题的答案!

排序比较

MySQL对排序字段进行排序对比时使用了C函数库的memcmp,因为memcmap充分利用了64位CPU的特性,所以,性能非常高!为什么呢?

这里我就以64位CPU为例,看一下memcmp的比较过程,memcmp函数转化为汇编指令后,主要包含了下面这些过程:

  1. 通过MOV指令,从内存中读取用于比较的两个入参地址,并将地址分别写入两个rax寄存器
  2. 通过NOP指令,对上面的rax寄存器中的地址做内存对齐
  3. 通过CMP指令,对没有对齐的部分,单字节(byte)比较
  4. 通过CMP指令,针对对齐的部分,以32个字节为单位,通过冗余指令,做4次8字节(qword)比较,为什么以32字节单位,拆分4次比较两个入参地址?下面我会讲到。
  5. 通过CMP指令,针对步骤4中不足32个字节的部分,以8个字节为单位,做8字节(qword)比较
  6. 通过CMP指令,针对步骤5中最后剩余不足8个字节的部分,做单字节(byte)比较

我先以上面的第一步的MOV指令为例,看下指令在CPU中的处理过程。

下图是Intel的Nehalem处理器的内核架构,我们从上到下看一下MOV指令的处理过程:

queryWrapper java 让某个ID排在最前面_学习_02

1.指令提取单元(IFU)从ITLB读取指令MOV在CPU L1 Cache中的位置

2.指令提取单元(IFU)从分支预测单元(BPU)读取if…then中分支then中的起始指令,由于MOV指令操作不涉及if条件,所以,该读取结果为空

3.指令提取单元(IFU)根据步骤1得到的位置到CPU L1 Cache中的Instruction Cache中找到指令MOV,并提取MOV

4.指令提取单元(IFU)将MOV指令传递给解码器(ILD)

5.解码器(ILD)对指令MOV进行预解码,校验指令长度等,如果超过指定长度,会做其他处理,ps:这个将来我会在其他文章中详细讲解。

6.解码器(ILD)将MOV指令传递给指令队列(Instruction Queue)进行缓冲,ps:当指令非常多时,指令队列可以收集一批指令后,在一个时钟周期批量将这批指令下传,一批4条指令

7.指令队列将MOV指令传递给指令解码器(Instruction Decoders)进行解码,指令解码器包含4个解码器:1个复杂解码器(Complex Deocder)和3个简单解码器(Simple Decoder),前者用于解析复杂的一条指令,将其解析为1 ~ 4条微指令(不包含1),后者将一条简单指令解析为一条微指令,微指令一般缩写为uop。上面memcmp函数中的MOV指令含义是从内存中读取用于比较的两个入参地址,并将地址分别写入两个rax寄存器,属于复杂指令,所以,这条MOV指令被解析为两条微指令uops:从内存中读取入参地址uop1和将地址写入rax寄存器uop2

8.指令解析器将分解的两个uops传递给指令解码队列(IDQ),进行指令去重

  • 指令解码队列(IDQ)依次将两个uops传递给循环检测器(LSD),循环检测器检查uop是否存在类似while这样的循环语句,如果存在,对循环中重复的uop去重。由于uop1uop2均不存在循环,所以,循环检测器直接返回uop1uop2给指令解码队列(IDQ)
  • 微指令序列号生成器给uop1uop2生成两个序列号,将序列号传递给指令解码队列,分配给uop1uop2

9.指令解码队列将uop1uop2传递给MicroOps Fusion,做微指令聚合。对于有些指令相同的,在这里就聚合为一条微指令 + 指令个数。uop1uop2不相同,所以,不做聚合。

10.至此,CPU完成了指令MOV的执行前工作,这个过程一般称作Front End。接着,Fusion将uop1uop2传递给分配器Allocator,进行微指令执行单元分配

(1)Dispatcher分别将微指令uop1uop2写入重排序缓冲区(ROB),等待指令执行结果。

(2)Dispatcher同时将微指令写入中继器(RS),对于uop1uop2,这个写入是有区别的:

  • uop1是一个内存读取操作,所以,写入重排序缓冲区的uop1是完整微指令:uop1 ADDR1ADDR1为读取的内存地址。又因为uop1无依赖前置数据,所以,Dispatcher将uop1完整指令同时写入中继器(RS),待执行。
  • uop2是一个写寄存器操作,写入源数据当前不存在,所以,Dispatcher不会将uop2的完整指令写入中继器(RS)

11.中继器分析哪些微指令有依赖关系,哪些没有依赖关系,有依赖关系的串行执行,无依赖的并行执行。由于,当前中继器中只包含uop1,所以,只给uop1分配执行单元,即通过port2端口,将uop1完整指令传递给AGU Load执行单元,执行uop1,即该执行单元从内存排序缓冲区(MOB)中读取地址为ADDR1,如果不存在,再从CPU L1 Data Cache中读取,L1 Data Cache中没有,继续从L2 Cache读取,L2 Cache没有,就从L3 Data Cache中读取,CPU三级缓存中都没有,那只能通过地址总线从内存中读取ADDR1

12.当AGU Load执行单元成功读取到ADDR1后,发送完成状态COMPLETEADDR1到重排序缓冲区(ROB)

13.重排序缓冲区将uop1执行结果(状态)和ADDR1写入回退寄存器文件(RRF)

14.重排序缓冲区移除微指令uop1

15.此时,重排序缓冲区中有了ADDR1和步骤10写入的uop2,于是,重排序缓冲区将uop2ADDR1传递给Dispatcher,Dispatcher从寄存器别名表中命名一个rax寄存器,构造完整的uop2指令,即uop2 rax, ADDR1,表示将ADDR1写入rax寄存器,然后,将完整指令写入中继器(RS),准备执行uop2

16.中继器通过port 3端口分配AGU Store Address执行单元执行uop2,将ADDR1写入rax寄存器

17.执行单元执行成功后,将结果状态COMPLETE写入重排序缓冲区