SQL的执行与优化

文章目录

  • MySQL查询原理与优化
  • 一、select语句的执行顺序
  • 二、join 的执行与优化
    • 1、驱动表 & 被驱动表
    • 2、Simple Nested Loop Join
    • 3、Index Nested Loop Join
    • 4、Block Nested Loop Join
    • 5、Hash Join
    • 6、join 优化小结
  • 三、on 与 where 对比
  • 四、group by 的执行与优化
    • 1、group by 执行
    • 2、Explain - Using temporary
    • 3、group by 优化小结
  • 五、order by 的执行与优化
    • 1、FireSort 排序算法(两种)
      • 1)单路排序(快)
      • 2)双路排序(慢)
      • 3)单路排序 vs 双路排序
      • 4)选择算法的依据
    • 2、FireSort 相关参数
      • 1)sort_buffer
      • 2)max_length_for_sort_data
    • 3、使用索引优化排序
      • 1)联合索引
      • 2)覆盖索引(减少回表)
    • 4、关联查询 与 排序
    • 5、IndexSort 失效情况
      • 1)最左匹配原则
      • 2)排序字段顺序与索引不一致
      • 3)排序规则不一致
      • 4)回表数据太多
      • 5)小结
    • 6、Explain - Using filesort
    • 7、FileSort排序效率一定差?
    • 8、order by 优化小结
    • 9、order by 注意事项
  • 六、limit 的执行与优化
    • 1、子查询关联
    • 2、自增id - maxId
  • 七、子查询 的执行与优化
    • 1、in子查询分析
    • 2、semi join
    • 3、materialization
    • 4、子查询的优化器策略
    • 5、in 和 exists
    • 6、子查询 优化小结
  • 八、其他优化
    • 1、几种count性能对比
    • 2、别用 select *
    • 3、使用 limit1
    • 4、及时commit

MySQL查询原理与优化

一、select语句的执行顺序

select distinct 字段/聚合函数    		
from1 别名       	 
inner join2 别名     
on 连接条件          	 
where 筛选(聚合之前)           
group by 分组列表     		-- 这里开始可以使用 select 中的别名
having 筛选(聚合之后)           
order by 排序列表    	
limit 起始条目索引,条目数; 
1. fromfrom子句中的前两个表计算笛卡尔积,产生虚拟表VT1。(选择相对小的表做基础表)2. on对虚拟表VT1进行on条件过滤,筛选出满足 <on-condition> 的行,产生虚拟表VT2。3. join如果指定了外连接,表中未匹配的行就会作为外部行添加到虚拟表VT2中,产生虚拟表VT3。left join  :将左表在第二步中过滤掉的行添加进来。right join :将右表在第二步中过滤掉的行添加进来。	4. where对虚拟表VT3进行where条件过滤。符合 <where-condition> 的记录插入到虚拟表VT4中。5. group by根据 group by 子句中的列,对虚拟表VT4中的记录进行分组操作,产生虚拟表VT5。6. having对虚拟表VT5进行having过滤,符合 <having-condition> 的记录插入到虚拟表VT6中。7. select筛选出虚拟表VT6中select指定的列,插入到虚拟表VT7中。8. distinct移除虚拟表VT7中相同的行,产生虚拟表VT8。9. order by将虚拟表VT8中的记录按照 <order by list> 进行排序操作,返回游标。10. limit取出指定行的记录,产生虚拟表VT9, 并作为结果集返回。
  1. 聚合函数的执行顺序:
    • 聚合函数 是在 group by之后having之前 执行的。
  2. 别名的使用:
    • 在oracle中,别名的使用都是严格遵循sql执行顺序的,group by后面不能用别名。
    • 在mysql中,group by 开始就可以使用别名。
  3. 其他注意点:
    • having 筛选器 是 第一个 也是 唯一一个 应用到 已分组数据 的筛选器。
    • order by 返回的不是虚拟表,而是游标(包含特定的物理顺序的逻辑组织的对象)

二、join 的执行与优化

1、驱动表 & 被驱动表

对于外连接,以左外连接 a left join b 为例

  • 以a表作为驱动表,b表作为被驱动表(也可能会被优化为内连接,那就不一定是a为驱动表了)
  • 如果只能添加一个字段的索引,一定要给被驱动表添加索引
  • 被驱动表如果没有索引,会有一个 Using join buffer 的优化
    • 8.0.18版本之前默认:Using join buffer (Block Nested Loop)
    • 8.0.18版本开始默认:Using join buffer (hash join)

对于内连接,例如 a inner join b

  • 查询优化器是可以决定谁作为驱动表,谁作为被驱动表的
    • 如果两张表都有索引 / 两张表都没有索引,则小表作为驱动表大表作为被驱动表小表驱动大表
    • 如果一张表有索引,一张表没有索引,则没有索引的表作为驱动表有索引的表作为被驱动表有索引的被驱动

Tips:Explain的结果中,上边一行为驱动表,下边一行为被驱动表

2、Simple Nested Loop Join

从表A取出一条数据,遍历表B,将匹配到的数据放到result(驱动表A中的每一条记录都与被驱动表B的所有记录进行匹配)

在这里插入图片描述

这种方式的效率是非常低的,假设A表数据100,B表数据1000条,则 join比较次数 为 A*B=10w

在这里插入图片描述

3、Index Nested Loop Join

lndex Nested-Loop Join 优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。

从表A取出一条数据,通过索引匹配表B中的数据(避免和内层表的每条记录进行比较,极大的减少了和内层表的匹配次数)

请添加图片描述

驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本比较固定,故mysql优化器都倾向使用记录数少的作为驱动表

请添加图片描述

如果被驱动表加的索引不是主键索引,还得进行一次回表查询。因此,如果被驱动表的索引是主键索引,效率会更高。

4、Block Nested Loop Join

如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,则每次都要从驱动表中加载一条记录,然后把被驱动表的记录加载到内存与其匹配,匹配结束后清除内存,继续匹配。这样周而复始,大大增加了IO的次数。

Block Nested Loop Join 优化的思路是减少IO的次数,引入 join buffer缓冲区,不再逐条获取驱动表的数据,而是一块一块的获取

将驱动表join相关的部分数据(大小受join buffer的限制)缓存到 join buffer中,然后全表扫描被驱动表,一次性和 join buffer中的所有驱动表记录进行匹配(内存中操作),将 Simple Nested Loop Join 中的多次比较合并成一次,降低了被驱动表的访问频率

注意:这里缓存的不只是关联表的列,select后面的列也会放到 join buffer,因此查询的时候尽量减少不必要的字段。

在这里插入图片描述

请添加图片描述

Block Nested Loop Join 相关信息的查看:

  • block_nested_loop

    通过 show variables like %optimizer_switch% 查看 block_nested_loop 状态。(默认开启)

  • join_buffer_size

    驱动表批量获取的数量取决于 join buffer 的大小,可以通过 show variables like %join_buffer% 查看(默认256k)

5、Hash Join

从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join,默认都会使用hash join

  • Nested Loop:

    对于被连接的数据子集较小的情况下,Nested Loop是个较好的选择。

  • Hash Join

    Hash Join是做大数据集连接时的常用方式。优化器使用两个表中相对较小的表,利用Join Key在内存中建立散列值,然后扫描相对
    较大的表,并探测散列值,找出与Hash表匹配的行。

Hash Join 的特点:

  • 这种方式适用于较小的表完全可以放入内存中的情况,这样总成本就是访问两个表的成本之和。
  • 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。
  • Hash Join 能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。
  • Hash Join 只能应用于等值连接,这是由Hash的特点决定的。

请添加图片描述

6、join 优化小结

  1. 整体效率比较:
    • Index NLJ > Block NLJ > Simple NLJ
  2. 小表驱动大表(本质就是减少外层循环的数据数量)(小表的衡量标准是:表行数*每行大小)
    • 表行数:不是看总行数,还要考虑where条件过滤后的结果
    • 行大小:select的列的数据总大小
  3. 一般来说只需要为被驱动表的关联字段创建索引(减少内层表的循环匹配次数)
  4. 确保任何的 GROUP BY 和 ORDER BY 中的表达式只涉及到一个表中的列(这样MySQL才有可能使用索引来优化这个过程)
  5. 增大 join buffer size 的大小(一次缓存的数据越多,被驱动表的IO次数就越少)
  6. 减少 驱动表 不必要的字段查询(字段越少,join buffer 缓存的数据越多)

三、on 与 where 对比

两张表在 join 时,首先会计算笛卡尔积

  • on后面的条件会对这个笛卡尔积做一个过滤,形成一张临时表
  • 如果没有where,就直接返回结果;如果有where,就对上一步的临时表再进行过滤。
-- 使用inner join时(结果没有区别)
select * from A inner join B on condition1 and condition2
select * from A inner join B on condition1 where condition2-- 使用left join时(condition1只对B生效,condition2对A和B都生效)
select * from A left join B on condition1 where condition2-- 使用right join时(condition1只对A生效,condition2对A和B都生效)
select * from A right join B on condition1 where condition2

on的condition过滤的行还可以在第三步outer join时再次添加回来,而where的过滤就是最终的。

  • 对于内连接来说,on和where没有区别
  • 对于左连接来说,on后面的条件只对右表有效;
  • 对于右连接来说,on后面的条件只对左表有效;

四、group by 的执行与优化

1、group by 执行

group by1、被分组字段有几类,最终结果集中就有几条(每一类只会显示首条结果)-- 如sex有男,女两类,结果集就只有2条2、分组查询中,select之后跟【与分组字段一对一关系的字段】和【聚合函数】-- 因为只会取首条结果,如果不是一对一的关系,没有意义3、一般和聚合函数一起使用:-- 无分组,聚合函数将表中所有符合条件的数据当成一组。-- 有分组,聚合在分组之后执行,对每一组数据进行聚合4、分组之后就不需要再去重了:-- 分组的时候是将列中唯一的值分成一组,这意味着所有的记录都是不同的。

2、Explain - Using temporary

使用Explain分析时,如果Extra出现Using temporary

  • 说明mysql会创建一个内部的临时表来执行操作

这时候就要注意是否需要对group by进行优化了

3、group by 优化小结

  • group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接使用索引。

  • group by 先排序再分组,遵照索引的最左前缀法则

  • 当无法使用索引列,增大 max_length_for_sort_datasort_buffer_size 参数的设置

  • where效率高于having,能写在where限定的条件就不要写在having中了

  • 减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。

  • 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。

五、order by 的执行与优化

无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序

select city, name, age from t where city='杭州' order by name limit 1000;

请添加图片描述

1、FireSort 排序算法(两种)

当不能使用索引生成排序结果的时候,MySQL需要自己进行排序:

  • 数据量小:排序在内存中进行
  • 数据量大:需要使用磁盘辅助排序。

MySQL将这个过程统一称为文件排序(Using Filesort),即使完全是内存排序不需要任何磁盘文件时也是如此。

1)单路排序(快)

先读取查询所需要的所有列,然后再根据指定列进行排序,最后直接返回排序结果。(MySQL 4.1才引入)

单路排序的流程

  1. 初始化sort_buffer,确定放入的字段(要排序的字段 & 要查询的字段
  2. city索引树 找到所有符合 where条件主键id
  3. 主键索引树 获取 name、city、age三个字段的值,存入sort_buffer中;(回表查询)
  4. sort_buffer中的数据,按照 排序字段name快速排序
  5. 按照排序结果,取结果集的 前1000行 返回给客户端。

请添加图片描述

2)双路排序(慢)

读取行指针需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。

双路排序的流程

  1. 初始化sort_buffer,确定放入的字段(要排序的字段 & 主键id
  2. city索引树 找到所有符合 where条件主键id
  3. 主键索引树 获取 name、id这两个字段的值,存入sort_buffer中;(回表查询1)
  4. sort_buffer中的数据,按照 排序字段name快速排序
  5. 按照排序结果,取结果集的 前1000行
  6. 根据主键id的值,到原表中取出city、name和age三个 要查询的字段 返回给客户端。(回表查询2)

请添加图片描述

3)单路排序 vs 双路排序

【单路排序】

  1. 从磁盘读取 要排序的字段要返回的字段 ,放到sort_buffer中。(第1次回表)
    • 读取字段多,占用的 sort_buffer 更多,发生外部排序的概率更大。
  2. 排序后直接从内存中返回 要查询的字段 。(不需要再回表查询)

【双路排序】

  1. 从磁盘读取 要排序的字段主键id ,放到sort_buffer中。(第1次回表)
    • 只需要读取少量字段,发生外部排序的概率更小。
  2. 排序后根据 主键id回表查询要返回的字段 。(第2次回表)

4)选择算法的依据

MySQL选择算法的依据是内存是否足够,即 单行的长度(要查询的所有列的总长度)是否超过 max_length_for_sort_data

  • 内存足够:选择单路排序,这样排序后可以从内存中直接返回查询结果,不用再回到原表去取数据。
  • 内存不够:改用双路排序,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。

这也就体现了MySQL的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。

2、FireSort 相关参数

1)sort_buffer

MySQL会给每个线程分配一块内存用于排序,称为 sort_buffer

请添加图片描述

FireSort 可能在内存中完成,也可能需要使用外部排序,这取决于参数 sort_buffer_size排序所需的内存

  • 排序的数据量 小于 sort_buffer_size:排序就在内存中完成。(快速排序
  • 排序的数据量 大于 sort_buffer_size:内存放不下,则不得不利用磁盘临时文件辅助排序。(归并排序
    • MySQL会先将数据分块,对每个独立的块使用 快速排序 进行排序,并将各个块的排序结果存放在磁盘上。
    • 然后将各个排好序的块进行合并(merge),最后返回排序结果。

提高 sort_buffer_size 会降低外部排序的概率,减少磁盘临时文件的生成,减少磁盘I/O。

set sort_buffer_size = 524288;

注意事项:

MySQL在排序时,对每一个排序记录都会分配一个足够大的定长空间来存放(根据数据库定义的字段结构大小)

  • 如果是 VARCHAR 列,则需要分配其完整长度,以容纳其中最长的字符串。
  • 如果使用UTF-8字符集,那么MySQL将会为每个字符预留三个字节。

因此,排序消耗的临时空间可能要比磁盘上的原表要大很多。

2)max_length_for_sort_data

如果 要查询的所有列的总长度 超过了 max_length_for_sort_data的大小,MySQL就会使用双路排序

请添加图片描述

提高 max_length_for_sort_data 会降低改用双路排序的概率。

set max_length_for_sort_data = 8192;

3、使用索引优化排序

MySQL之所以需要生成临时表,并且在临时表上做排序操作,是因为原来的数据都是无序的。如果能够保证从索引树取出的数据,天然就是有序的话,就可以不用再排序了。

InnoDB的索引树 默认就是按照索引字段递增排序的,因此只要对排序字段加上索引,就可以不用再排序了。

1)联合索引

# 创建一个city和name的联合索引
alter table t add index city_user(city, name);

请添加图片描述

这样就保证了,取“下一条记录”的遍历过程中,只要city的值是杭州,name的值就一定是有序的。

select city, name, age from t where city='杭州' order by name limit 1000;

请添加图片描述

索引查询的过程(有序)

  1. 从 索引 (city,name) 找到第一个满足 city='杭州' 条件的主键id;
  2. 主键索引树 获取整行,取name、city、age三个字段的值,作为结果集的一部分直接返回
  3. 从索引 (city,name) 取下一个记录主键id;
  4. 重复步骤2、3,直到查到第1000条记录,或者是不满足 city='杭州' 条件时循环结束。

2)覆盖索引(减少回表)

# 添加覆盖索引进一步简化流程(无需回表查询)
alter table t add index city_user_age(city, name, age);

请添加图片描述

覆盖索引查询的过程(有序且无需回表)

  1. 从索引 (city,name,age) 找到第一个满足 city='杭州' 条件的记录,

    取出其中的name、city、age三个字段的值,作为结果集的一部分直接返回;

  2. 从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;

  3. 重复执行步骤2,直到查到第1000条记录,或者是不满足 city='杭州' 条件时循环结束。

4、关联查询 与 排序

在关联查询的时候,如果需要排序,MySQL会分两种情况来处理这样的文件排序。

  • 如果所有排序字段都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就进行文件排序。
    • 在MySQL的 EXPLAIN 结果中可以看到 Extra 字段会有 Using filesort
  • 除此之外的所有情况,MySQL都会先将关联的结果存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。
    • 在MySQL的 EXPLAIN 结果中可以看到 Extra 字段会有 Using temporary; Using filesort

如果查询中有 LIMIT 的话,LIMIT 也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大。

MySQL 5.6 在这里做了很多重要的改进。当只需要返回部分排序结果时,例如使用了LIMIT 子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果,然后再进行排序。

5、IndexSort 失效情况

满足以下条件,MySQL才能在排序的时候使用上索引(Index排序)。

  1. ORDER BY 子句 需要满足索引的最左匹配原则
  2. 索引的列顺序 和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时。
  3. 回表数据不能太多,否则优化器认为回表代价太大,会直接选择FileSort
  4. 如果查询需要关联多张表,则只有当 ORDER BY 子句引用的字段全部为第一个表时,才能使用索引做排序。

MySQL可以使用同一个索引既满足排序,又用于查找行。设计索引时应该尽可能地同时满足这两种任务,这样是最好的。

这里测试使用的仍然是:MySQL索引 第五节的 class 和 student 表

CREATE TABLE `class` (`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,`className` VARCHAR ( 30 ) DEFAULT NULL,`address` VARCHAR ( 40 ) DEFAULT NULL,`monitor` INT NULL,PRIMARY KEY ( `id` ) 
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;CREATE TABLE `student` (`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,`stuno` INT NOT NULL,`name` VARCHAR ( 20 ) DEFAULT NULL,`age` INT ( 3 ) DEFAULT NULL,`classId` INT ( 11 ) DEFAULT NULL,PRIMARY KEY ( `id` ) #CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
# 添加索引
CREATE INDEX idx_age_name_classid ON student(age, `name`, classid)

1)最左匹配原则

组合索引左边的列必须存在

# Using filesort(联合索引最左边的列age不存在)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY classid limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY `name`, classid limit 10;# Using filesort(classid左边的列`name`不存在)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, classid limit 10;# Using filesort(stuno不属于联合索引)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid, stuno limit 10;
# index排序(符合最左匹配原则)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid limit 10;

如果 WHERE 子句 或 JOIN 子句中 对 左边列 指定了 常量 的时候,ORDER BY可以缺少左边列

# index排序(where子句中用到了联合索引的最左字段)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age=45 ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age=45 ORDER BY `name`, classid limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age=45 and `name`='zs' ORDER BY classid limit 10;# index排序(join子句中用到了联合索引的最左字段)
EXPLAIN SELECT SQL_NO_CACHE classid FROM student s INNER JOIN class c ON s.classid = c.id 
WHERE age=45 ORDER BY `name`, classid;EXPLAIN SELECT SQL_NO_CACHE classid FROM student s INNER JOIN class c 
ON s.classid = c.id AND age=45 ORDER BY `name`, classid;

除了常量,使用范围查询或模糊查询都不行

# Using filesort(范围查询)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age>45 ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age>45 ORDER BY `name`, classid limit 10;# Using filesort(in)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age IN (18,28) ORDER BY `name` limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE age IN (18,28) ORDER BY `name`, classid limit 10;# Using filesort(模糊查询)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student 
WHERE age=45 and `name` LIKE 'z%' ORDER BY classid limit 10;EXPLAIN SELECT SQL_NO_CACHE stuno FROM student 
WHERE age=45 and `name` LIKE '%z%' ORDER BY classid limit 10;

2)排序字段顺序与索引不一致

和 WHERE的组合索引规则 不同的是,ORDER BY的字段顺序 必须与 组合索引的字段顺序 完全一致,否则无法使用index排序

# where使用上了索引(和联合索引顺序不一致)
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classid = 100 and name = 'Abel' and age = 10;# Using filesort(和联合索引顺序不一致)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY classid, `name`, age limit 10;# order by使用上了索引(和联合索引顺序完全一致)
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid limit 10;

3)排序规则不一致

ORDER BY 子句排序规则不一致(同时存在 升序asc 和 降序desc)

# Using filesort(ORDER BY子句 同时存在 升序、降序)
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` DESC, classid ASC;
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` ASC, classid DESC;
# index排序
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` ASC, classid ASC;
EXPLAIN SELECT SQL_NO_CACHE classid FROM student WHERE age = 45 ORDER BY `name` DESC, classid DESC;

4)回表数据太多

查询的数据量很大,并且查询的字段需要回表,优化器会认为回表的代价太大,直接选择FileSort,而不使用index排序

# Using filesort(查询的字段都需要回表查询,而且数据量很大,优化器认为回表代价太大,直接选择FileSort)
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, `name`;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid;
# index排序(查询字段使用了主键索引,不用回表)
EXPLAIN SELECT SQL_NO_CACHE id FROM student ORDER BY age, `name`, classid;# index排序(查询字段使用了覆盖索引,不用回表)
EXPLAIN SELECT SQL_NO_CACHE age, `name`, classid FROM student ORDER BY age, `name`, classid;

如果 使用了limit后数据量很少,优化器认为回表代价比较小,会使用index排序

# index排序(limit后数据量少,回表代价小,优化器选择使用索引)
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, `name`, classid limit 10;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid  limit 10;# Using filesort(limit后数据量仍然很大)
EXPLAIN SELECT SQL_NO_CACHE * FROM student ORDER BY age, `name`, classid limit 100000;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student ORDER BY age, `name`, classid limit 100000;

如果 使用了where筛选后数据量很少,优化器也认为回表代价比较小,会使用index排序

# index排序(where条件筛选过后数据量少)
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=45 ORDER BY `name`, classid limit 100000;
EXPLAIN SELECT SQL_NO_CACHE stuno FROM student WHERE stuno=1 ORDER BY age, `name`, classid limit 100000;

5)小结

# 建立组合索引(a,b,c),判断 order by 语句中索引的使用情况
- ORDER BY a
- ORDER BY a, b
- ORDER BY a, b, c
- ORDER BY a DESC, b DESC, c DESC # 如果WHERE使用索引的最左前缀定义为常量,则 order by 能使用索引
- WHERE a = const ORDER BY b, c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b, c
- WHERE a = const AND b > const ORDER BY b, c # 不能使用索引进行排序
- ORDER BY a ASC, b DESC, c DESC 		/* 排序不一致 */
- WHERE g = const ORDER BY b, c 		/* 丢失a索引 */
- WHERE a = const ORDER BY c 			/* 丢失b索引 */
- WHERE a = const ORDER BY a, d 		/* d不是索引的一部分 */
- WHERE a in (...) ORDER BY b, c 		/* 对于排序来说,多个相等条件也是范围查询 */

6、Explain - Using filesort

使用Explain分析时,如果Extra出现Using filesort

  • 说明排序没有使用到索引,只能在内存中(记录较少的时候)或磁盘中(记录较多的时候)进行文件排序

这时候就要注意是否需要对order by进行优化了

7、FileSort排序效率一定差?

Using filesort 的效率不一定就低,要具体问题具体分析

# 只有age使用了索引,没有Using filesort
CREATE INDEX idx_age_name ON student(age,name);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 45 AND stuno < 10000 ORDER BY name; /*0.121s*/# age和stuno使用了索引,Using filesort排序
CREATE INDEX idx_age_stuno_name ON student(age,stuno,name);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 45 AND stuno < 10000 ORDER BY name; /*0.002s*/

原因:

  • 所有的排序都是在条件过滤之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。
  • 相对的 stuno < 101000 这个条件,如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比更高,是最优选择。

结论:

  1. 两个索引同时存在,mysql自动选择最优的方案(对于这个例子,mysql 选择 idx_age_stuno_name)。但是, 随着数据量的变化,选择的索引也会随之变化的 。
  2. 当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在【范围条件】的字段上。反之,亦然。

8、order by 优化小结

  1. 尽量使用索引优化排序,避免FileSort排序(具体问题具体分析,参考第7点)

    • 索引可以保证数据的有序性,不需要再到内存中进行排序,效率更高。
    • FileSort排序一般在内存中进行,占用CPU较多,还有可能产生磁盘临时文件导致多次I/O,效率较低。
  2. 实在无法使用索引时,优化FileSort排序

    • 提高 sort_buffer_size

      排序的数据量 大于 sort_buffer_size时,会利用磁盘临时文件辅助排序,导致多次I/O,降低效率。

    • 提高 max_length_for_sort_data

      如果 要查询的所有列的总长度 超过了 max_length_for_sort_data时,就会使用双路排序

    • *order by 时不要使用 select ,只 select 需要的字段

      减少每次写入 sort_buffer 的数据大小,降低 MySQL 改用双路排序的概率。

9、order by 注意事项

  • 返回的不是虚拟表,而是游标(包含特定的物理顺序的逻辑组织的对象)
  • sql是基于集合的理论的,集合不会预先对他的行排序,它只是成员的逻辑集合,成员的顺序是无关紧要的。
  • 对表进行排序的查询可以返回一个对象,这个对象包含特定的物理顺序的逻辑组织。这个对象就叫游标。
  • 正因为返回值是游标,所以使用 order by 子句查询不能应用于表达式。
  • 排序是很需要成本的,除非你必须要排序,否则最好不要指定order by。

六、limit 的执行与优化

limit查询的offset越大,查询越慢

-- 时间: 0.001ms
select * from student limit 0,20
-- 时间: 0.006ms
select * from student limit 10000,20
-- 时间: 0.037ms
select * from student limit 100000,20
-- 时间: 0.108ms
select * from student limit 1000000,20

1、子查询关联

# 在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容
EXPLAIN SELECT * FROM student t, (SELECT id FROM student ORDER BY id LIMIT 300000,20) a WHERE t.id = a.id;

2、自增id - maxId

# 如果是自增ID,也可以用如下方式
SELECT max(id) as maxId FROM student LIMIT 300000;
EXPLAIN SELECT * FROM student WHERE id > maxId LIMIT 20

七、子查询 的执行与优化

参考文章:https://blog.51cto.com/wujianwei/2534400?source=drt

1、in子查询分析

有以下子查询示例:

SELECT * FROM t1 WHERE t1.a IN (SELECT t2.b FROM t2 WHERE id < 10);

你肯定认为这个 SQL 会这样执行:

select t2.b from t2 where id < 10; 	-- 结果:1,2,3,4,5,6,7,8,9 
select * from t1 where t1.a in(1,2,3,4,5,6,7,8,9);

而 MySQL5.6 以前,MySQL 会将相关的外层表压到子查询中,优化器认为这样效率更高。也就是说,是像下面这样执行的

select * from t1 where exists(select b from t2 where id < 10 and t1.a=t2.b);

执行计划为:

+----+--------------------+-------+-------+---------+------+----------+-------------+
| id | select_type        | table | type  | key     | rows | filtered | Extra       |
+----+--------------------+-------+-------+---------+------+----------+-------------+
|  1 | PRIMARY            | t1    | ALL   | NULL    |  100 |   100.00 | Using where |
|  2 | DEPENDENT SUBQUERY | t2    | range | PRIMARY |    9 |    10.00 | Using where |
+----+--------------------+-------+-------+---------+------+----------+-------------+

不相关子查询 变成了 相关子查询(select_type:DEPENDENT SUBQUERY),子查询需要根据 b 来关联外表 t1,因为需要外表的 t1 字段,所以子查询是没法先执行的。执行流程为:

  1. 扫描 t1,从 t1 取出一行数据 R;
  2. 从数据行 R 中,取出字段 a 执行子查询,如果得到结果为 TRUE,则把这行数据 R 放到结果集;
  3. 重复 1、2 直到结束。

总的扫描行数为 100+100*9=1000(这是理论值,实际值为 964,看规律是子查询结果集每多一行,总扫描行数就会少几行)。

2、semi join

这样会有个问题,如果外层表是一个非常大的表,对于外层查询的每一行,子查询都得执行一次,这个查询的性能会非常差。我们很容易想到将其改写成 join 来提升效率:

select t1.* from t1 inner join t2 on t1.a=t2.b and t2.id<10;

这样优化可以让 t2 表做驱动表,t1表关联字段有索引,查找效率非常高。
但这里会有个问题,join 是有可能得到重复结果的,而 in (select …) 子查询语义则不会得到重复值。

在这种情况下,可以使用 semi join 来优化子查询。semi join 是 MySQL5.6 加入的新特性,是解决重复值问题的一种特殊联接。
MySQL5.6 以前,优化器只有 exists 一种策略来优化子查询。

经过 semijoin 优化后的SQL和执行计划分为:

# 注意这是优化器改写的SQL,客户端上是不能用 semi join 语法的 
select `t1`.`id`,`t1`.`a`,`t1`.`b` 
from `t1` semi join `t2` 
where( (`t1`.`a` = `<subquery2>`.`b`) and (`t2`.`id` < 10) ); # 执行计划
+----+--------------+-------------+-------+---------+---------------+------+-------------+
| id | select_type  | table       | type  | key     | ref           | rows | Extra       |
+----+--------------+-------------+-------+---------+---------------+------+-------------+
|  1 | SIMPLE       | <subquery2> | ALL   | NULL    | NULL          | NULL | Using where |
|  1 | SIMPLE       | t1          | ref   | a       | <subquery2>.b |    1 | NULL        |
|  2 | MATERIALIZED | t2          | range | PRIMARY | NULL          |    9 | Using where |
+----+--------------+-------------+-------+---------+---------------+------+-------------+

semijoin 优化实现比较复杂,其中又分FirstMatch、Materialize 等策略,上面的执行计划中 select_type=MATERIALIZED 就是代表使用了 Materialize 策略来实现的 semijoin。这里 semijoin 优化后的执行流程为:

  1. 先执行子查询,把结果保存到一个临时表中,这个临时表有个主键用来去重;
  2. 从临时表中取出一行数据 R;
  3. 从数据行 R 中,取出字段 b 到被驱动表 t1 中去查找,满足条件则放到结果集;
  4. 重复执行 2、3,直到结束。

这样一来,子查询结果有9行,即临时表也有9行(这里没有重复值),总的扫描行数为 9+9+9*1=27 行,比原来的 1000 行少了很多。

3、materialization

MySQL5.6版本中加入的另一种优化特性 materialization,就是把子查询结果物化成临时表,然后代入到外查询中进行查找。内存临时表包含主键(hash 索引),消除重复行,使表更小。如果子查询结果太大,超过 tmp_table_size 大小,会退化成磁盘临时表。

这跟前面提到的“我们误以为的”过程相似,这样子查询只需要执行一次,而不是对于外层查询的每一行都得执行一遍。不过要注意的是,这样外查询依旧无法通过索引快速查找到符合条件的数据,只能通过全表扫描或者全索引扫描,

经过 materialization 优化后的执行计划为:

+----+-------------+-------+-------+---------+------+------+-------------+
| id | select_type | table | type  | key     | ref  | rows | Extra       |
+----+-------------+-------+-------+---------+------+------+-------------+
|  1 | PRIMARY     | t1    | ALL   | NULL    | NULL |  100 | Using where |
|  2 | SUBQUERY    | t2    | range | PRIMARY | NULL |    9 | Using where |
+----+-------------+-------+-------+---------+------+------+-------------+

总扫描行数为 100+9=109.

4、子查询的优化器策略

对于不同类型的子查询,优化器会选择不同的策略。MySQL官方文档

  1. 对于 INANY 子查询,优化器有如下策略选择:
    • semijoin
    • Materialization
    • exists
  2. 对于 NOT IN<>ALL 子查询,优化器有如下策略选择:
    • Materialization
    • exists
  3. 对于 derived 派生表,优化器有如下策略选择:
    • derived_merge(将派生表合并到外部查询中,MySQL 5.7引入)
    • 将派生表物化为内部临时表,再用于外部查询。

优化器策略的选择:

  1. 对于子查询,先检查是否满足各种优化策略的条件
    • update 和 delete 语句中子查询不能使用 semijoin、materialization 优化策略
    • 子查询中有 union 则无法使用 semijoin 优化策略
  2. 然后优化器会按成本进行选择
  3. 实在没得选就会用 exists 策略来优化子查询

优化器策略的开关:

semijoin 和 materialization 的开关是通过 optimizer_switch 参数中的 semijoin={on|off}、materialization={on|off} 标志来控制的。上文中不同的执行计划就是对 semijoin 和 materialization 进行开/关产生的。

exists 策略是没有参数来开启或者关闭的

# 查看MySQL采用的优化器策略
show variables like 'optimizer_switch'; # 修改优化器策略
set global optimizer_switch='materialization=on,semijoin=on';
set global optimizer_switch='materialization=off,semijoin=off';

5、in 和 exists

-- in子查询,子查询的数据先被查询出来,然后根据子查询的结果集进行主查询
explain select * from employee where dep_id in (select id from department)-- 子查询获取department表中5条数据,作为外层循环5次
for(select id from department d)-- 每次循环执行employee表中的查询select * from employee e where e.dep_id=d.id
-- exits子查询,主查询执行一次(查询e表),子查询依赖主查询获取的数据执行一次(查询d表)
explain select * from employee e 
where exists (select 1 from department d where d.id = e.dep_id)-- 主查询获取employee表中8条数据,作为外层循环8次
for(select * from employee e)-- 每次循环执行department表中的查询select 1 from department d where d.id = e.dep_id

结论:小表驱动大表

  • 主查询 < 子查询,用 existsexists是外表驱动)
  • 主查询 > 子查询,用inin是内表驱动)

另外,exists子查询只返回truefalse,因此子查询中的 select * 可以是 select 1 或 其它

6、子查询 优化小结

子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。但是子查询的执行效率不高

  1. 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。

    查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。

  2. 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表,都不会存在索引,所以查询性能会受到一定的影响。

  3. 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

子查询优化建议:

  1. 使用 连接查询 替代 子查询。
    • 连接查询不需要建立临时表,通常比子查询效率更高,尤其是在大型数据集上。
  2. 使用 EXISTS 替代 IN
    • EXISTS只检查是否存在满足条件的行,而IN会在子查询中返回所有匹配项,然后检查是否与主查询中的值匹配。
  3. 尽量减少子查询的嵌套层级
    • 每个嵌套层级都会增加查询的复杂度和执行时间
  4. 使用合适的索引

结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

八、其他优化

1、几种count性能对比

count() 是一个特殊的函数,有两种非常不同的作用:

  • 统计某个列值的数量(要求列值是非空的,即不统计NULL )
  • 统计行数(确认括号内的表达式值不可能为空时)

count() 对于返回的结果集,一行行地判断,如果参数不是NULL,累计值就加1,否则不加。最后返回累计值。

# count(主键id)InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,直接按行累加。# count(1)InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,直接按行累加。# count(字段)not null一行行地从记录里面读出这个字段,判断不能为null,直接按行累加;null判断到有可能是null,还要把值取出来再判断一下,不是null才累加。# count(*)它会忽略所有的列而直接统计所有的行数。MySQL优化器优化了count(*)的语义为“取行数”,count(*)肯定不是null不取值,且直接按行累加

结论:

  • 按照效率排序的话,count(*) ≈ count(1) > count(主键id) > count(字段)

注意事项:

  • 如果采用 count(字段) ,尽量采用二级索引,因为主键采用的是聚簇索引,包含的信息多,明显大于二级索引
  • 对于 count(*)count(1)来说,系统会自动采用占用空间更小的二级索引来统计。
    • 如果有多个二级索引,会使用 ken_len 小的二级索引。
    • 如果没有二级索引,才会使用 主键索引 进行统计。
  • count(1) 执行得要比 count(主键id) 快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。

2、别用 select *

在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表>查询。原因:

  1. MySQL 在解析的过程中,会通过 查询数据字典 将 * 按序转换成所有列名,这会大大的耗费资源和时间。
  2. 无法使用 覆盖索引
  3. 可能会使排序产生磁盘临时文件导致多次I/O。

3、使用 limit1

如果针对的是会全表扫描的 SQL 语句,并且可以确定结果集只有一条,,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。

如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT1 了。

4、及时commit

只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。

COMMIT 所释放的资源:

  • 回滚段上用于恢复数据的信息
  • 被程序语句获得的锁
  • redo / undo log buffer 中的空间
  • 管理上述 3 种资源中的内部花费

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/544702.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

订阅OnlyFans攻略

在数字化的今天&#xff0c;社交媒体平台已经成为我们生活中的一部分。其中&#xff0c;OnlyFans以其独特的内容和用户群体&#xff0c;吸引了大量创作者的加入。而Wildcard&#xff0c;作为一种便捷的支付工具&#xff0c;与OnlyFans的结合更是为创作者和粉丝带来了前所未有的…

监视和内存观察

监视和内存观察 5.监视和内存观察5.1 监视5.2 内存 5.监视和内存观察 在调试的过程中我们&#xff0c;如果要观察代码执行过程中&#xff0c;上下文环境中的变量的值&#xff0c;有哪些方法呢&#xff1f; 这些观察的前提条件一定是开始调试后观察&#xff0c;比如&#xff1…

NVENC 视频编码器 API 编程指南 ( 中文转译 )

基于 NVIDIA Kepler™ 和更高版本 GPU 架构的 NVIDIA GPU 包含基于硬件的 H.264/HEVC/AV1 视频编码器&#xff08;以下简称 NVENC&#xff09;。NVENC 硬件采用 YUV/RGB 作为输入&#xff0c;并生成符合H.264/HEVC/AV1 标准的视频比特流。可以使用 NVIDIA 视频编解码器 SDK 中提…

【Qt问题】vs里直接打开qt项目的ui会崩溃

问题描述&#xff1a; 当我用VS2019开发Qt的时候&#xff0c;在VS编译器里要用到Qt的ui设计&#xff0c;但是双击打开发现直接就崩溃了。 解决办法&#xff1a; 崩溃的原因很简单&#xff0c;是因为VS默认用的qt designer打开&#xff0c;所以会崩溃&#xff0c;我们右键单击…

智慧城市与数字孪生:共创未来城市的智慧生活

目录 一、智慧城市与数字孪生的概念与特点 二、智慧城市与数字孪生共创智慧生活的路径 1、城市规划与建设的智能化 2、城市管理与服务的智慧化 3、城市安全与应急管理的智能化 三、智慧城市与数字孪生面临的挑战与对策 四、智慧城市与数字孪生的发展趋势与展望 1、技术…

macOS系统中通过brew安装MongoDB

Macos 修改目录权限&#xff1a; sudo chmod -R 777 你的文件夹 本文使用homebrew进行安装简单&#xff0c;因为从官网下载安装包并手动安装需要移动安装包到合适的目录下并配置环境变量等一大堆操作后才能使用数据库&#xff08;若没有安装过brew请自行百度进行安装brew&am…

vscode jupyter 如何关闭声音

网上之前搜的zen模式失败 仅仅降低sound失败 #以下是成功方式&#xff1a; 首先确保user和remote的声音都是0&#xff1a; 然后把user和remote的以下设置都设置为off就行了&#xff01; 具体操作参考 https://stackoverflow.com/questions/54173462/how-to-turn-off-or-on-so…

jvm 内存泄露、内存溢出、栈溢出区别

JVM&#xff08;Java虚拟机&#xff09;是负责执行Java程序的运行环境。以下是对内存泄露、内存溢出和栈溢出这几个概念的解释&#xff1a; 内存泄露&#xff08;Memory Leak&#xff09;&#xff1a; 内存泄露指的是程序中分配的内存空间在不再被使用时没有被释放的情况。这可…

Visual Studio配置libtorch(cuda安装一步到位)

Visual Studio配置libtorch visual Studio安装cuDNN安装CUDAToolkit安装libtorch下载Visual Studio配置libtorch(cuda版本配置) visual Studio安装 visual Studio点击安装 具体的安装和配置过程这里就不进行细讲了&#xff0c;可以参考我这篇博客Visual Studio配置OpenCV(保姆…

很好的一本书,推荐给你们《Hello 算法》

算法犹如美妙的交响乐&#xff0c;每一行代码都像韵律般流淌。 愿这本书在你的脑海中轻轻响起&#xff0c;留下独特而深刻的旋律。 本项目旨在打造一本开源免费、新手友好的数据结构与算法入门教程。 全书采用动画图解&#xff0c;内容清晰易懂、学习曲线平滑&#xff0c;引导…

Echo框架:高性能的Golang Web框架

Echo框架&#xff1a;高性能的Golang Web框架 在Golang的Web开发领域&#xff0c;选择一个适合的框架是构建高性能和可扩展应用程序的关键。Echo是一个备受推崇的Golang Web框架&#xff0c;以其简洁高效和强大功能而广受欢迎。本文将介绍Echo框架的基本特点、使用方式及其优势…

如何在webapp中于动发布一个应用

目录 第一步&#xff1a;在webapp文件夹内自定义文件夹第二步&#xff1a;生成一个文本&#xff0c;并把后缀改为 .html第三步&#xff1a;进入bin文件夹打开服务第四步&#xff1a;打开方式选择java第六步&#xff1a;输入你想输出的东西第七步&#xff1a;双击运行即可 第一步…