当前位置: 首页 > 科技观察

连explain都不会用,你还好意思说自己精通MySQL查询优化?

时间:2023-03-23 10:06:45 科技观察

Explain简介Explain关键字是Mysql中sql优化常用的“关键字”。通常使用Explain来“不执行sql查看sql的执行计划”,从而快速找出sql的问题所在。在讲解Explain之前,先创建需要的“用户表user,角色表role,用户角色关系表role_user”作为测试表://usertableDROPTABLEIFEXISTS`user`;CREATETABLE`user`(`id`int(11)NOTNULL,`name`varchar(25)DEFAULTNULL,`age`int(11)NOTNULLDEFAULT0,`update_time`datetimeDEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`user`(`id`,`name`,`age`,`update_time`)VALUES(1,'张三',23,'2020-12-2215:27:18'),(2,'李四',24,'2020-06-2115:27:18'),(3,'王舞',25,'2020-07-2015:27:18');DROPTABLEIFEXISTS`role`;CREATETABLE`role`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(10)DEFAULTNULL,PRIMARYKEY(`id`),KEY`index_name`(`name`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`role`(`id`,`name`)VALUES(1,'产品经理'),(2,'技术经理'),(3,'项目总监');DROPTABLEIFEXISTS`role_user`;CREATETABLE`role_user`(`id`int(11)NOTNULL,`role_id`int(11)NOTNULL,`user_id`int(11)NOTNULL,PRIMARYKEY(`id`),KEY`index_role_user_id`(`role_id`,`user_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`role_user`(`id`,`role_id`,`user_id`)VALUES(1,2,1),(2,1,2),(3,3,3);我们先执行一条sql:explainselect*fromuserwhereid=2;,执行后可以看到执行结果如下:可以看到有12个字段,都有对应的值。这是explain的执行计划。如果你能看懂这个执行计划,你离精通sql优化也就不远了。下面详细介绍一下这12个字段分别是什么意思。id字段id表示执行select查询语句的序号,是sql执行顺序的标识,SQL按照id从大到小执行,相同id为一组,从上到下执行。什么意思呢?比如执行这条sql:explainselect*fromuserwhereidin(selectuser_idfromrole_user);+----+------------+-----------+------------+--------+------------+-------------------+--------+------+------+----------+----------------------------------------------------------------------------+|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|+----+------------+------------+-----------+--------+----------------+--------------------+-------+-----+------+--------+------------------------------------------------------------------------------+|1|SIMPLE|user|NULL|ALL|PRIMARY|NULL|NULL|NULL|3|100.00|NULL||1|SIMPLE|role_user|NULL|index|NULL|index_role_user_id|8|NULL|3|33.33|Usingwhere;Usingindex;FirstMatch(user);Usingjoinbuffer(BlockNestedLoop)|+----+------------+----------+------------+--------+------------+-------------------+--------+-----+-----+---------+------------------------------------------------------------------------------+都显示了idsare一样,也就是说sql的执行是从上到下执行的。第一条记录对应user表,然后第二条记录对应role_user表。这是同一个id。如果id不同,比如执行下面的sql:explainselect(select1fromuserlimit1)fromrole;:+----+------------+-------+-------------+--------+----------------+-----------+---------+------+------+--------+------------+|id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra|+----+------------+--------+------------+--------+----------------+------------+--------+--------+------+--------+------------+|1|PRIMARY|role|NULL|index|NULL|index_name|33|NULL|3|100.00|Usingindex||2|SUBQUERY|user|NULL|index|NULL|PRIMARY|4|NULL|3|100.00|Usingindex|+----+------------+--------+------------+--------+--------------+------------+--------+------+------+---------+------------+你会看到有两条记录,两条记录的id会不一样,id越大,前面执行,可以看到执行了id=2的user表,也就是子查询部分,最后执行的是最外层。”结论:“这就是id标识的sql的执行顺序。一般复杂查询会有多条记录,简单查询只有一条记录。复杂查询中,相同id为一组,执行顺序为从上到下。id大的先执行;在Mysql8中,子查询将被优化,所以有时即使是复杂的查询也只有一条记录。select_type字段select_type表示查询的类型,即对应的是简单查询还是复杂查询。如果是复杂查询,还包括:“简单子查询,from子句的子查询,联合查询”。我们分别来看一下select_type中的所有查询类型。simplesimple表示没有任何复杂查询的简单查询。在PRIMARY复杂查询中,“最外层select语句的查询类型为PRIMARY”,例如执行如下sql:explainselect*fromrolewhereid=(selectidfromrole_userwhererole_id=(selectidfromuserwhereid=2);最外层select,即select*fromrolewhereid=?将被标记为PRIMARY类型。SUBQUERY在“selectorwhere”子查询中会被表示为一个SUBQUERY类型。比如上一句执行的SQL中有两个子查询为SUBQUERY。DERIVED"DERIVED表示派生表或派生表,在from包含的子查询中将表示为DERIVED类型。"Mysql会递归执行这些子查询,并将结果放在一个临时表中。执行sql:explainselect*from(selectnamefromuserunionselectnamefromrole)awherea.name='张三';在Mysql5.7以上版本进行了优化,增加了derived_merge(派生合并),可以加快查询效率。当UNION出现“在UNION查询语句中,第二个select查询语句会表示为UNION”:UNIONRESULT“UNION查询语句的结果被标记为UNIONRESULT”,如上面执行的sql:explainselect*from(selectnamefromuserunionselectnamefromrole)awherea.name='张三';从第四行记录中的表字段可以看出,第四行记录来自于第二行和第三行,所以一个UNION查询语句的结果会被标记为UNIONRESULT其他七上面的select_type是比较常见的,也有一些不常见的,仅供理解:DEPENDENTUNION:也就是UNION查询语句中的第二条或后面的语句,但是依赖于外层查询。DEPENDENTSUBQUERY:子查询中的第一个select语句也依赖于外部查询。UNCACHEABLESUBQUERY:子查询的结果无法缓存,必须重新计算外连接的第一行。表字段很容易看出“表字段表示查询的是哪个表”。一个是现有表。比如上面的user和role就是我们自己创建的表,也可以代表派生表。例如:UNIONRESULT的表字段表示为,即查询第二行和第三行的结果记录。类型字段“类型字段或访问类型所代表的sql关联的类型”。从这个字段中,我们可以判断出这条sql查找数据库表时查找记录的大概范围,可以直接体现sql的效率。类型字段也有很多种,主要有以下几种:system、const、eq_ref、ref、range、index、ALL。它的性能体现从高到低,即system>const>eq_ref>ref>range>index>ALL。下面详细说说这个属性。systemsystem是const的特例,“表示表中只有一行记录”,这个几乎不会出现,也是为了理解。constconst表示通过索引一次就可以找到数据。一般const出现在“唯一索引或主键索引中的等值查询”中。因为表中只有一条数据匹配,所以查找速度非常快。示例:解释select*fromuserwhereid=2;eq_refeq_ref表示使用唯一索引或主键索引扫描作为表链接匹配条件。对于每个索引键,表中只有一条记录与其匹配。例如:explainselect*fromuserleftjoinrole_useronuser.id=role_user.user_idleftjoinroleonrole_user.role_id=role.id;refref性能比eq_ref差,同时也表示表的链接匹配条件,即使用哪些表字段作为查询索引列上的值,ref和eq_ref的区别在于eq_ref使用的是唯一索引或一个主键索引。ref扫描的结果可能会找到满足条件的多行数据,本质上是一种返回匹配行的索引访问。例如:explainselect*fromuserwherename='张三';range"range使用索引来检索给定范围的行数据,一般在where之后使用between,<>,in等查询语句时会出现范围":explainselect*fromuserwhereid>2;index表示会遍历索引树,index以更快的速度避开ALL,但是index的出现说明你需要检查你的索引是否使用正确:解释selectidfromuser;ALL"ALL和index的区别即ALL从硬盘读取,index从索引文件读取。”ALL全表扫描是指Mysql会从表头扫描到表尾。这时候通常需要增加索引进行优化,或者说查询不使用索引作为条件查询:explainselect*fromuser;possible_keyspossible_keys字段表示该列查询语句可能使用的索引。这只是可能的,列出的索引可能不会被实际使用。当没有使用索引且为NULL时,表示需要添加索引来优化查询。如果表中的数据比较少,数据库认为全表扫描比较快,有可能是NULL。key域key域和possible_keys的区别在于它代表了实际使用的索引,即possible_keys包含了key的值。如果想让Mysql使用或忽略possible_keys中的索引,可以使用FORCEINDEX、USEINDEX或IGNOREINDEX。key_len字段表示SQL查询语句中索引使用的字节数。这个字节数不是实际长度,而是通过计算查询中使用的索引的长度得到的。它显示了最大可能的索引字段长度。一般来说,在不损失精度的前提下,key_len越小越好。比如上面测试表的id是int类型,int类型由4个字节组成:explainselect*fromuserwhereid=2;key_len是针对不同的类型有自己的计算规则,具体计算规则如下:数据类型占用的字节数Stringchar(n):n字节长度varchar(n):2字节存储字符串长度,如果是utf-8,长度3n+2valuetypetinyint:1bytesmallint:2bytesint:4bytesbigint:8bytestimetypedate:3bytestimestamp:4bytesdatetime:8bytesifindex时是astring类型,实际存储的字符串很长,已经超过string类型的最大存储长度(768字节)。Mysql会使用类似的左前缀索引进行处理。ref字段ref表示列与索引的比较,表连接的匹配条件,表示使用哪些列或常量来查询索引列上的值。行字段行指示要扫描的估计行数。一般Mysql会根据统计的表信息和索引的选择来估计查找记录需要扫描的行数。请注意,这不是实际结果集中的行数。partitions和filteredfieldspartitions表示匹配的分区;filtered表示查询表行在表中的百分比。Extra字段该字段显示SQL查询的附加信息,主要有以下几种情况:Usingindex表示查询列被索引覆盖,是查询性能比较高的表现,即查询的信息可以通过搜索索引得到,不用回表,索引使用正确:explainselectidfromuserwhereid=2;如果同时出现usingwhere,则表示使用该索引进行索引键值的查找;如果没有usingwhere,则表示该索引用于读取数据的动作,而不是执行查询。使用where该属性与使用索引相反。查询列未被索引覆盖。非索引的前导列用于where条件之后。它只使用了where条件:explainselectuser.*fromuser,role,role_userwhereuser.id=role_user.user_idandrole.id=role_user.role_id;Usingtemporary"Usingtemporary就是使用临时表来存储中间结果,并且在对结果进行排序时一般会使用临时表”,例如:sortorderby和Groupquerygroupby。例子:explainselect*from(selectnamefromuserunionselectnamefromrole)awherea.name='张三';UsingfilesortUsingfilesort表示文件排序,表示Mysql使用外部索引对数据进行排序,不使用表中的索引进行排序:explainselect*fromuserorderbyname;UsingjoinbufferUsingjoinbuffer的意思是使用连接缓存:说明selectuser.*fromuser,role,role_userwhereuser.id=role_user.user_idandrole.id=role_user.role_id;强调在获取连接条件时,不使用索引,而是使用连接缓冲区来存储中间结果。如果出现这个值,一般表示需要增加索引进行优化。Impossiblewhere当where后面的条件一直为false时会出现Impossiblewhere,可以忽略,比较少出现:explainselect*fromuserwherename='hah'andname='sfsd';selecttablesoptimizedaway是指select语句返回数据时不遍历表或索引,例如:explainselectmin(id)fromuser;Extra字段中还有其他的属性,但是几乎没有见过,也没有出现,所以我就具体说明一下,有兴趣的可以自己去了解一下,这里只列出这些常见的。理论说了这么多,总要实践的。下面以用户测试表为例进行测试练习。实践(1)通过查询explainselect*fromuserwherename='张三';名称字段不创建索引。我们可以通过创建联合索引index_name_age_time来解决:altertableuseraddindexindex_name_age_time(name,age,update_time);再次查询时,会使用索引:(2)联合索引的使用必须遵循“最左前缀规则”,关于最左前缀规则的使用原理,我之前写过一篇详细的文章,大家可以参考[]。(3)使用索引进行查询时,不要进行任何函数操作,否则索引会失败:例:EXPLAINSELECT*FROMuserWHEREname='张三';但是在使用的时候,使用left()函数,比如:EXPLAINSELECT*FROMemployeesWHEREleft(name,2)='ZhangSan';,会导致索引失败。(4)在数据库的查询中不要使用(!=或<>)等判断条件和isnull、isnotnull、like以%开头的关键字进行判断,否则索引会失效: