关于SQL查询语句的优化,有一些一般的优化步骤,本节就介绍一下通用的优化步骤。

一条查询语句是如何执行的

首先,我们如果要明白一条查询语句所运行的过程,这样我们才能针对过程去进行优化。

参考我之前画的一张MySQL基础架构图:



MySQL中一条查询语句的生命周期大概可以分为

  1. 客户端通过连接器连接服务器
  2. 在解析器中解析SQL语句
  3. 优化器进行优化
  4. 执行器执行生成的执行计划
  5. 返回结果给客户端

一般而言,执行器执行阶段是最为重要的阶段,也就是说,最耗费性能的阶段。所以我们的优化,其实就是在优化执行器的执行计划。

如何发现与解决慢查询

查询性能低下的原因是因为访问的数据量太大,使得一个查询很慢,那么我们如何发现并且解决慢查询呢。

慢查询日志

MySQL中有一个日志叫做慢查询日志SLOW_QUERY_LOG,它会记录该MySQL中响应时间超过阈值(long_query_time)的语句,慢查询日志默认不开启,并且默认阈值为10,如果是在线上环境的话不建议开启慢查询日志,有调优需要的时候在开启比较合适。

mysql> show variables like '%slow_query_log%';
+---------------------+-------------------------------------------------+
| Variable_name | Value |
+---------------------+-------------------------------------------------+
| slow_query_log | OFF |
| slow_query_log_file | /var/lib/mysql/iZwz9iiwojnmkotgajxjlqZ-slow.log |
+---------------------+-------------------------------------------------+
2 rows in set (0.01 sec) mysql> set global slow_query_log=1;
Query OK, 0 rows affected (0.00 sec)

使用set global slow_query_log=1开启慢查询在重启MySQL之后会失效,如果要一直生效的话,那么就需要修改配置文件my.cnf。

slow_query_log_file就可以看到你所存储的慢查询日志的位置,我们首先执行语句:

mysql> select sleep(12);
+-----------+
| sleep(12) |
+-----------+
| 0 |
+-----------+
1 row in set (12.00 sec)

然后进入/var/lib/mysql/iZwz9iiwojnmkotgajxjlqZ-slow.log

/usr/sbin/mysqld, Version: 8.0.27-0ubuntu0.20.04.1 ((Ubuntu)). started with:
Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock
Time Id Command Argument
# Time: 2022-02-04T08:27:51.234016Z
# User@Host: root[root] @ localhost [] Id: 1653
# Query_time: 12.001395 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 1
SET timestamp=1643963259;
select sleep(12);

我们就可以看到,一条慢查询已经记录到慢查询日志中,然后我们可以根据后面会提到的正常的优化步骤进行逐步优化。

注:因为我本身专注后端,MySQL的慢查询日志这里只是简单使用,其实慢查询日志还有很多其他的参数,同时还有多种MySQL日志分析工具可以协助,等到以后如果有需要的时候,我会专门出一章来讲述MySQL日志分析工具的使用。

通用的优化思路

SQL语句的优化有一些通用的思路,这里一一介绍一下。

尽量不获取不需要的数据

有些时候,查询会请求超过实际需要的数据,然后在应用程序中丢弃多余数据,这样会给MySQL服务器带来额外的负担,并且增加网络开销,损耗应用服务器的CPU和内存资源。

典型的场景:

  • 查询不需要的数据
  • 多表关联时返回全部列
  • 总是取出全部列
  • 重复查询相同的数据

总结而言,我认为其实就是分为两点:

  1. 慎用SELECT * FROM
  2. 只取出需要的字段

减少MySQL扫描的额外记录行数

对于MySQL的查询语句而言,其返回的数据行和扫描的数据行是不同的,我们可以用一个很简单的例子来说明:

这是一张利用存储过程生成了100w条数据的数据表



其中索引有这些:



我们执行一条查询SQL语句:

mysql> EXPLAIN SELECT * from user where account = 12345;
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
| 1 | SIMPLE | user | NULL | ref | test | test | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

EXPLAIN待会再解释,大家只要知道该关键字可以看到大致扫描行数即可。

可以看到扫描行数为1行,返回的行数也是一行。

我们删除掉‘test’索引,再来执行同样的语句:

mysql> EXPLAIN SELECT * from user where account = 12345;
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | user | NULL | ALL | NULL | NULL | NULL | NULL | 991536 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

可以看到,我们的扫描行数非常大,是一个全表扫描,但是我们返回的行数其实是一行,也就是说我们做了大量的无用扫描。

一般MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:

  • 在索引中使用WHERE条件过滤不匹配的记录。这是在存储引擎完成的。
  • 使用覆盖索引扫描返回记录,直接从索引中过滤不需要的记录并返回命中的结果。MySQL的服务层完成的,不需要回表。
  • 从数据表中返回数据,然后过滤不满足条件的记录。在MySQL服务器层完成。

重构查询的方式

常见的重构查询的方式有以下几种:

  1. 将一个复杂查询拆分为多个简单查询
  2. 切分查询,对大查询分而治之,一次查询一小部分结果
  3. 分解关联查询

一个复杂查询还是多个简单查询

在传统的实现中,强调在数据库层完成尽可能多的工作,在过去是认为网络通信、查询解析和优化是一件代价很高的事情。

但是MySQL的设计让连接和断开连接都十分轻量级,返回一个小的查询结果方面很高效。MySQL内部每秒能够扫描内存中上百万行数据,相较而言,MySQL响应数据给客户端速度很慢。

所以有的时候可以权衡一下是否需要将一个大查询分解为多个小查询。

切分查询

有时候对于一个大查询分而治之,将一个大查询分解为小查询。每个查询功能完全一样,每次只返回一小部分。

最典型的场景便是删除旧数据,比如要删除100w旧数据,如果一次SQL执行全部删除,那么该SQL语句会执行相当长的过程,同时可能导致锁住大量数据、占满整个事务日志、耗尽系统资源、阻塞其他查询。那么我们可以一次删除1w的旧数据,这样子就可以将删除全部旧数据的性能损耗平摊到一个很长的时间,大大降低对于服务器的影响。

分解关联查询

关于JOIN关键字,很多DBA会直接禁用JOIN语句,原因是JOIN语句可能会扫描大量的行数,在后期的博客中,我会专门总结。简单而言,如果被驱动表上面不能使用索引,那么会直接每次查询被驱动表的时候都进行全表扫描,A JOIN B的扫描行数会直接是A的行数 * B的行数

所以我们可以将一个JOIN语句拆分成多个简单的查询,然后在应用程序中进行组装。

分解关联查询可以有以下优点:

  1. 执行单个查询可以减少锁的竞争
  2. 应用层做关联,更容易对数据库进行拆分,更容易做到高性能和可扩展
  3. 可以减少冗余记录的查询
  4. 这种形式相当于在应用层做了哈希关联,很多情况下哈希关联效率高很多

MySQL优化器

MySQL的优化器是一个基于成本的优化器,有些原因会导致MySQL优化器选择错误的执行计划:

  1. 统计信息不准确。
  2. 执行计划的成本估计并不等于实际成本。
  3. MySQL的最优并不只是考虑响应时间。
  4. MySQL不考虑并发执行的情况。
  5. MySQL有时候会基于一些固定规则优化。

MySQL能够处理的优化类型有以下:

  1. 重新定义关联表的顺序
  2. 将外连接转为内连接
  3. 使用等价变化规则:比如合并和减少一些比较、移除一些恒等或者不恒等的判断,例如(5=5 AND a>5)会被改写为a>5。
  4. 优化COUNT()、MIN()、MAX()
  5. 预估并转化为常数表达式
  6. 覆盖索引扫描
  7. 子查询优化
  8. 提前终止查询:LIMIT、发现一个不成立的条件。
  9. 等值传播:使用USING的时候,不用重复声明两个表中相同的字段。

    10.列表IN()的比较:MySQL中的IN()是将列表中的数据先排序,然后二分查询。

EXPLAIN关键字的应用

EXPLAIN关键字对于经常进行SQL语句优化的同学肯定很熟悉,我在这里简单介绍一下,EXPLAIN是显示该SQL语句的预计执行计划,是否使用索引、中间表、排序等,以及预计扫描行数等数据。

尾言

以上便是MySQL的SQL查询语句性能优化的概述,后期将会针对以下问题出针对性的博客进行具体场景的优化:

  1. 优化COUNT()查询
  2. 优化LIMIT分页
  3. 关联语句的优化
  4. 排序的优化

以及EXPLAIN的详解、常见的优化失效的情况,以及如何解决。

深入MySQL(四):MySQL的SQL查询语句性能优化概述的更多相关文章

  1. Mysql常用30种SQL查询语句优化方法

    出处:http://www.antscode.com/article/12deee70111da0c4.html 1.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使 ...

  2. MySQL 常用30种SQL查询语句优化方法

    1.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描. 2.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉 ...

  3. 一次SQL查询语句的优化

    1.项目中之前的"我关注的拍品列表"需要添加筛选功能,因为目前显示的关注的拍品太多没有进行分类,用户体验差. 2.添加筛选条件之后,可以筛选出“未开始”“进行中”“已结束”三种情况 ...

  4. 转: 从Mysql某一表中随机读取n条数据的SQL查询语句

    若要在i ≤ R ≤ j 这个范围得到一个随机整数R ,需要用到表达式 FLOOR(i + RAND() * (j – i + 1)).例如, 若要在7 到 12 的范围(包括7和12)内得到一个随机 ...

  5. MySQL 笔记整理(2) --日志系统,一条SQL查询语句如何执行

    笔记记录自林晓斌(丁奇)老师的<MySQL实战45讲> 2) --日志系统,一条SQL查询语句如何执行 MySQL可以恢复到半个月内任意一秒的状态,它的实现和日志系统有关.上一篇中记录了一 ...

  6. MySQL 笔记整理(1) --基础架构,一条SQL查询语句如何执行

    最近在学习林晓斌(丁奇)老师的<MySQL实战45讲>,受益匪浅,做一些笔记整理一下,帮助学习.如果有小伙伴感兴趣的话推荐原版课程,很不错. 1) --基础架构,一条SQL查询语句如何执行 ...

  7. 从Mysql某一表中随机读取n条数据的SQL查询语句

    若要在i ≤ R ≤ j 这个范围得到一个随机整数R ,需要用到表达式 FLOOR(i + RAND() * (j – i + 1)).例如, 若要在7 到 12 的范围(包括7和12)内得到一个随机 ...

  8. MySQL数据库详解(一)执行SQL查询语句时,其底层到底经历了什么?

    一条SQL查询语句是如何执行的? 前言 ​ 大家好,我是WZY,今天我们学习下MySQL的基础框架,看一件事千万不要直接陷入细节里,你应该先鸟瞰其全貌,这样能够帮助你从高维度理解问题.同样,对于MyS ...

  9. mysql数据库系统学习(一)---一条SQL查询语句是如何执行的?

    本文基于----MySQL实战45讲(极客时间----林晓斌 )整理----->https://time.geekbang.org/column/article/68319 一.第一节:一条sq ...

随机推荐

  1. 基于Spring实现策略模式

    背景: 看多很多策略模式,总结下来实现原理大体都差不多,在这里主要是讲解下自己基于Spring更优雅的实现方案:这个方案主要是看了一些开源rpc和Spring相关源码后的一些思路,所以在此进行总结 首 ...

  2. RabbitMQ除开RPC的五种消模型----原生API

    2.五种消息模型 RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习.那么也就剩下5种. 但是其实3.4.5这三种都属于订阅模型,只不过进行路由的方式不同. 通过一个 ...

  3. Metasploit生成木马入侵安卓手机

    开始 首先你需要一个Metasploit(废话) Linux: sudo apt install metasploit-framework Termux: 看这里 指令 sudo su //生成木马文 ...

  4. JavaScript DOM 基础操作

    JavaScript DOM 基础操作 一.获取元素的六方式 document.getElementById('id名称') //根据id名称获取 document.getElementsByclas ...

  5. 【采坑小计】prometheus的remote write协议遇到的问题

    没有读懂源码以前,无脑试错总是效率很低的! 1.thanos receiver报store locally for endpoint : conflict 接口返回的日志: store locally ...

  6. 学习鸟哥linux私房菜--安装中文输入法fcitx

    首先需要卸载前面安装的scim,查阅命令,参考网址如下 http://www.cnblogs.com/propheteia/archive/2012/06/26/2563383.html 链接中博主采 ...

  7. Spring boot + Vue axios 文件下载

    后端代码: @GetMapping("/{sn}") @ApiOperation(value = "获取文件",notes = "获取文件" ...

  8. linux如何复制文件

    目录 一:复制文件 二:文件详细属性 一:复制文件 copy的缩写cp. 格式: cp [参数] [被复制文件的路径] [复制到的新路径] 参数: -r : 递归复制 -a : 保证某些属性不变 案列 ...

  9. python类和函数

    #/usr/bin/python #coding=utf-8 class Car(): def __init__(self,name,year): self.name = name self.year ...

  10. Mysql-5.7主从部署-yum方式

    一.环境准备 # rpm -qa |grep mariadb |xargs yum remove -y # setenforce 0(临时关闭),(selinux配置文件:SELINUX=disabl ...