假设现在我们的用户表user中已经添加了索引index_un_age_sex(user_name,age,sex)
,表示给字段user_name
、age
和sex
添加了一个联合索引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
,像下面这样:
image-20210206192623928.png
Using index condition; Using where; Using filesort
是什么意思?其实,这个关键字描述了这样一个过程:
- MySQL先使用索引
index_age_sex
过滤出部分用户记录,对应上面关键字中的Using index
- 使用条件中的
user_name
字段模糊匹配,从步骤1的结果中再过滤出部分用户记录,对应上面关键字中的Using where
- 对上一步得到的用户记录进行排序,对应上面关键字中的
Using filesort
这下你大概知道Filesort是怎么一回事了。
Filesort
Filesort表示MySQL使用了临时文件对where条件过滤的中间结果进行排序。我们就以上面使用Filesort的SQL为例,看一下具体的排序过程:
1.命中索引index_age_sex
(字段age
和sex
的联合索引),在索引树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
,然后,依次对字段age
和username
排序,最终得到排序后的完整结果。
(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表中满足查询条件的记录age
、username
和id
写入sort_buffer
,然后,依次对字段age
和username
排序,排序后,根据主键id到聚簇索引获取对应记录。
对比上面两种排序的过程,我们发现采用下面的方案进行排序,会多一次回表(聚簇索引查找)的过程,如果聚簇索引在磁盘上,那么就会产生磁盘IO,影响性能。
所以,我们可以采用下面两个手段避免回表查询:
- SQL中的SELECT部分中的字段尽量不要用
*
,而是指定字段,确保SELECT中的字段 + 排序字段的值大小小于等于参数max_length_for_sort_data
,这样MySQL就会采用上面的(1)方案排序 - 如果一定要使用
*
,那么,务必保证表中字段的总长度不超过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
函数转化为汇编指令后,主要包含了下面这些过程:
- 通过MOV指令,从内存中读取用于比较的两个入参地址,并将地址分别写入两个rax寄存器
- 通过NOP指令,对上面的rax寄存器中的地址做内存对齐
- 通过CMP指令,对没有对齐的部分,单字节(byte)比较
- 通过CMP指令,针对对齐的部分,以32个字节为单位,通过冗余指令,做4次8字节(qword)比较,为什么以32字节单位,拆分4次比较两个入参地址?下面我会讲到。
- 通过CMP指令,针对步骤4中不足32个字节的部分,以8个字节为单位,做8字节(qword)比较
- 通过CMP指令,针对步骤5中最后剩余不足8个字节的部分,做单字节(byte)比较
我先以上面的第一步的MOV指令为例,看下指令在CPU中的处理过程。
下图是Intel的Nehalem处理器的内核架构,我们从上到下看一下MOV指令的处理过程:
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去重。由于
uop1
和uop2
均不存在循环,所以,循环检测器直接返回uop1
和uop2
给指令解码队列(IDQ) - 微指令序列号生成器给
uop1
和uop2
生成两个序列号,将序列号传递给指令解码队列,分配给uop1
和uop2
9.指令解码队列将uop1
和uop2
传递给MicroOps Fusion,做微指令聚合。对于有些指令相同的,在这里就聚合为一条微指令 + 指令个数。uop1
和uop2
不相同,所以,不做聚合。
10.至此,CPU完成了指令MOV的执行前工作,这个过程一般称作Front End。接着,Fusion将uop1
和uop2
传递给分配器Allocator,进行微指令执行单元分配
(1)Dispatcher分别将微指令uop1
和uop2
写入重排序缓冲区(ROB),等待指令执行结果。
(2)Dispatcher同时将微指令写入中继器(RS),对于uop1
和uop2
,这个写入是有区别的:
uop1
是一个内存读取操作,所以,写入重排序缓冲区的uop1
是完整微指令:uop1 ADDR1
,ADDR1
为读取的内存地址。又因为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
后,发送完成状态COMPLETE
和ADDR1
到重排序缓冲区(ROB)
13.重排序缓冲区将uop1
执行结果(状态)和ADDR1
写入回退寄存器文件(RRF)
14.重排序缓冲区移除微指令uop1
15.此时,重排序缓冲区中有了ADDR1
和步骤10写入的uop2
,于是,重排序缓冲区将uop2
和ADDR1
传递给Dispatcher,Dispatcher从寄存器别名表中命名一个rax寄存器,构造完整的uop2
指令,即uop2 rax, ADDR1
,表示将ADDR1
写入rax寄存器,然后,将完整指令写入中继器(RS),准备执行uop2
。
16.中继器通过port 3端口分配AGU Store Address执行单元执行uop2
,将ADDR1
写入rax寄存器
17.执行单元执行成功后,将结果状态COMPLETE
写入重排序缓冲区