深入分析mysql查询性能优化的几个思路

1、为什么查询速度会慢?

  • 如果把查询看作一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间,优化查询,实际上要优化其子任务,1、我们可以消除其中一些子任务,2、减少子任务的执行次数,3、让子任务运行得更快。
  • 通常来说。查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后在服务器上进行解析,生成执行计划、执行,并返回结果给客户端,其中"执行"可以认为是整个生命周期中最重要的阶段,执行包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等,完成这些任务时,查询需要在不同的地方花费时间,包括网络、CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的I/O操作上消耗时间,根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。

2、分析慢查询

  • 查询性能低最基本的原因:访问的数据太多
  • 分析低效的查询
    • 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列
    • 确认MySQL服务器层是否在分析大量超过需要的数据行
  • 是否向数据库请求了不需要的数据
    • 有些查询会请求超过实际需要的数据,然后这些多与的数据会被应用程序丢弃,这会给MySQL带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU和内存资源
    • 典型案例
      • 查询不需要的记录
      • 多表关联时返回全部列
      • 总是取出全部列
      • 重复查询相同的数据
  • MySQL是否在扫描额外的记录
    • 扫描的行数
      • 理想情况下扫描的行数和返回的行数应该是相同的,实际情况中可能并不都是这样,例如在做一个关联查询时,服务器必须要扫描多行才能生成结果集中的一行,比如GROUP BY
    • 访问类型
      • EXPLAIN语句中的type列反映了访问类型,如果查询没有办法找到合适的访问类型,那么最好的解决办法通常是增加一个合适的索引,好的索引可以让查询使用合适的访问类型,尽可能只扫描需要的数据行
    • 响应时间:执行时间+排队时间
      • 排队时间是指服务器因为等待某些资源而没有真正执行查询的时间------可能是等待I/O操作完成,也可能是等待行锁,等等
      • 服务时间是指数据库处理这个查询真正花了多少时间

3、重构查询的方式

  • 切分查询
    • 有时候对于一个大查询要"分而治之",将大查询切分为小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分的查询结果,定期清理大量数据时,如果用一个大的语句一次性完成的话,则可能需要一次性锁住很多数据,占满整个事务日志、耗尽系统资源、阻塞很多重要但小的查询
  • 分解关联查询
    • 很多高性能的应用都会对关联查询进行分解
    • 用分解关联查询的方式重构查询的优势:
      • 让缓存的效率更高:许多应用程序可以方便地缓存单表查询对应的结果对象,对MySQL的查询缓存来说,如果关联的某个表发生了变化,就无法使用查询缓存了。而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存了
    • 将查询分解后,执行单个查询可以减少锁的竞争
    • 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展
    • 查询本身的效率也可能会有所提升
    • 可以减少冗余记录的查询:在应用层做关联查询,这意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复的访问一部分数据。从这点看,这样的重构还可能会减少网络和内存消耗

4、查询执行的基础

  • MySQL执行一个查询的过程
    *
    1. 客户端发送一条查询给服务器
      1. 服务器先检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果。否则进入下一阶段
      1. 服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划
      1. MySQL根据优化器生成的执行计划,调用存储引擎API来执行查询
      1. 将结果返回给客户端
  • 排序优化 :无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。
    • 当不能使用索引生成排序结果的时候,MySQL需要自己进行排序
      • 如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,MySQL将这个过程统一称为文件排序(filesort),即使完全是内存排序不需要磁盘文件也是如此,如果需要排序的数据量少于"排序缓冲区",MySQL使用内存进行"快速排序"操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用"快速排序"进行排序,并将各个块的排序结果存放在磁盘上,然后将各个排好序的块进行合并(merge),最后返回排序结果
    • MySQL有两种排序算法
      • 两次传输排序(旧版本使用):读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行,需要进行两次数据传输,即需要从数据表中读取两次数据,第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,会产生大量的随机I/O,因此两次数据传输的成本非常高。优点是,排序时存储尽可能少的数据(排序列和行指针),这就让"排序缓冲区"中可能容纳尽可能多的行数进行排序。
      • 单次传输排序(新版本使用):先读取查询需要的所有列,然后再根据指定列进行排序,最后直接返回排序结果,因为不再需要从数据表读取两次数据,对于I/O密集型的应用,这样做的效率高了很多。相比两次传输排序,这个算法只需要做一次顺序I/O读取所有的数据,而无须做任何随机I/O,缺点是,如果需要返回的列非常多,非常大,会额外占用大量的空间,而这些列对排序来说是没有任何作用的。单条排序记录很大,可能会有更多的排序块需要合并。当查询所有需要的列的总长度不超过参数max_length_for_sort_data时,MySQL使用"单次传输排序"。 - 关联查询时如果需要排序,MySQL会分两种情况来处理
      • 如果ORDER BY 子句的所有列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就进行文件排序,EXPLAIN的Extra字段可以看到"Using filesort"
      • 否则,MySQL会将关联的结果存放到一个临时表中,然后在所有关联都结束后,再进行文件排序,EXPLAIN的Extra字段可以看到"Using temporary;Using filesort",查询中有LIMIT的话,也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量仍然会非常大,MySQL5.6之后,做了改进。当只需要返回部分排序结果的时候,例如使用LIMIT子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果再进行排序。
      • 查询执行引擎
        • 在解析和优化阶段,MySQL将生成查询对应的执行计划
        • MySQL的查询执行引擎则根据这个执行计划来完成整个查询,这里执行计划是一个数据结构,而不是和很多其它关系型数据库那样会生成对应的字节码,MySQL只是简单地根据执行计划给出的指令逐步执行,在这个过程中,大量的操作需要通过调用存储引擎实现的接口来完成,存储引擎接口有着丰富的功能,但是底层的接口却只有几十个,这种简单的接口模式,让MySQL存储引擎插件式架构成为可能。
      • 返回结果给客户端
        • 查询执行的最后一个阶段是将结果返回给客户端,即使查询不需要返回结果给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的记录的行数。
        • 如果查询可以被缓存,MySQL在这个阶段会将结果存放到查询缓存中。
        • MySQL将结果集返回给客户端是一个增量、逐步返回的过程,例如,关联操作,一旦服务器处理完最后一张关联表,开始生成第一条关联结果时,MySQL就可以开始向客户端逐步返回结果集了。这样的好处有两个:
          • 1、服务器端无须存储太多的结果,也就不会因为要返回太多结果而消耗太多内存。
          • 2、让MySQL客户端第一时间获得返回的结果
        • 结果集中的每一行都会以一个满足MySQL客户端/服务器通信协议的封包发送,在通过TCP协议进行传输,在TCP传输的过程中,可能会对MySQL的封包进行缓存再批量发送。

5、MySQL查询优化器的局限性

  • 关联子查询

    • MySQL的子查询实现的非常糟糕,最糟糕的一类查询是WHERE条件中包含IN()的子查询。例如以下sql:
    sql 复制代码
    SELECT * FROM sakila.film WHERE film_id IN ( SELECT film_id FROM sakila.film_actor WHERE actor_id = 1);
    • 看到这个sql我们会认为MySQL先执行IN()中的子查询,再执行IN()列表查询。很不幸,MySQL不是这样做的。

    • mysql真正的执行步骤:

      • 是将外层表压到子查询中,它认为这样可以更高效地查找到数据行,MySQL会将查询改写为:

        sql 复制代码
           SELECT * FROM sakila.film WHERE EXISTS ( SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id);
    • 这时子查询需要用到film_id来关联外部表film,因为需要film_id字段的值,所以MySQL认为无法先执行这个子查询。通过EXPLAIN可以看到子查询是一个相关子查询(DEPENDENT SUBQUERY)

    • MySQL会先选择对film表进行全表扫描,然后根据返回的film_id逐个执行子查询,如果外层的表是一个非常大的表,那么这个查询的性能会很糟糕

    • 重写查询:另一个优化的办法是使用函数GROUP_CONCAT()在IN()中构造一个由逗号分隔的列表,有时这比上面使用关联更快,因为使用IN()加子查询,性能经常非常糟糕,所以通常建议使用EXIST()等效地改写查询来获得更好的效率(正如上面MySQL改写的查询)

sql 复制代码
SELECT film.* FROM sakila.film INNER JOIN sakila.film_actor USING(film_id) WHERE actor_id = 1;
  • 最大值和最小值优化
    • 对于MIN()和MAX()的优化,MySQL的优化做的并不好。例如:
sql 复制代码
SELECT MIN(actor_id) FROM skila.actor WHERE first_name = 'PENELOPE';
  • 因为first_name字段上没有索引,因此MySQL将会进行一次全表扫描,可以在first_name创建一个前缀索引,最好加一个actor_id的索引

6、优化特定类型的查询

  • 优化关联查询
    • 确保ON或者USING子句上的列有索引:一般来说,两张表关联,只需要在关联顺序中第二个表的相应列上创建索引
    • 确保任何GROUP BY 和 ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程
  • 优化子查询
    • 尽可能使用并联
  • 优化GROUP BY 和 DISTINCT
    • 它们都可以使用索引来优化,也是最有效的优化办法
    • 当无法使用索引时,GROUP BY使用两种策略来完成
      • 使用临时表或者文件排序来做分组
      • 对于任何查询语句,这两种策略的性能都有可以提升的地方:通过使用SQL_BIG_RESULT和SQL_SMALL_RESULT来让优化器按照你希望的方式运行
      • 如果需要对关联查询做分组(GROUP BY),并且是按照查找表中的某个列做分组,那么通常采用查找表的标识列分组的效率会比其它列高
      • 如果没有通过ORDER BY子句显式的指定排序列,当查询使用GROUP BY子句的时候,结果集会自动按照分组的字段排序,如果不关心结果集的顺序,而这种默认排序又导致了需要文件排序,则可以使用ORDER BY NULL,让MySQL不再进行文件排序,也可以在GROUP BY子句中直接使用DESC 或者ASC关键字,使分组的结果按照需要的方向排序
  • 优化LIMIT分页
    • 分页操作,通常使用LIMIT加上偏移量实现,同时加上合适的ORDER BY子句。如果有对应的索引,通常效率不错,否则MySQL需要做大量的文件排序操作
    • 一个常见又令人头疼的问题是,偏移量非常大的时候,例如LIMIT 10000,20这样的查询,MySQL要查询10020条记录然后只返回最后20条,前面10000条记录都被抛弃,这样的代价非常高
      • 要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能
      • 一个最简单的办法就是尽可能使用覆盖索引扫描,而不是查询所有列。然后根据需要做一次关联操作再返回所需的列,对于偏移量很大的时候,这样做的效率提升非常大,它让MySQL扫描尽可能少的页面
      • 有时候也可以将LIMIT查询转化为已知位置的查询,让MySQL通过范围扫描得到对应的结果,例如,在一个位置列上有索引,并且预先计算出了边界值,就可以改写为BETWEEN...AND...查询
      • LIMIT和OFFSET的问题,其实是OFFSET的问题,它导致MySQL扫描大量不需要的行然后抛弃掉,如果可以使用书签记录上次读取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就避免了使用OFFSET,该技术的好处是,无论翻页到多么后面,其性能都很好(和查询第一页一样)
  • 优化UNION查询
    • MySQL总是通过创建并填充临时表的方式来执行UNION查询,因此很多优化策略在UNION查询中没法很好地使用
    • 除非确实需要消除重复的行,否则一定要用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致对整个临时表的数据做唯一性检查。这样做的代价很高。
相关推荐
堕落年代32 分钟前
Maven匹配机制和仓库库设置
java·maven
功德+n40 分钟前
Maven 使用指南:基础 + 进阶 + 高级用法
java·开发语言·maven
uhakadotcom1 小时前
Apache CXF 中的拒绝服务漏洞 CVE-2025-23184 详解
后端·面试·github
uhakadotcom1 小时前
CVE-2025-25012:Kibana 原型污染漏洞解析与防护
后端·面试·github
uhakadotcom1 小时前
揭秘ESP32芯片的隐藏命令:潜在安全风险
后端·面试·github
uhakadotcom1 小时前
Apache Camel 漏洞 CVE-2025-27636 详解与修复
后端·面试·github
uhakadotcom1 小时前
OpenSSH CVE-2025-26466 漏洞解析与防御
后端·面试·github
uhakadotcom1 小时前
PostgreSQL的CVE-2025-1094漏洞解析:SQL注入与元命令执行
后端·面试·github
香精煎鱼香翅捞饭1 小时前
java通用自研接口限流组件
java·开发语言
浪遏1 小时前
面试官😏 :文本太长,超出部分用省略号 ,怎么搞?我:🤡
前端·面试