深入理解MySQL Ⅳ -- SQL性能分析工具

文章目录

数据库服务器优化步骤

首先可以先尝试加缓存,如果加了缓存之后还是很慢,那么接下来要考虑进行慢查询。

通过慢查询得到耗时长的SQL语句之后,我们要使用show profiles工具判断该语句的两种情况:

  • 如果是SQL等待时间长,调优服务器参数(例如调整使用内存、调整并发连接数)
  • 如果是SQL执行时间长,那就有很多种解决情况,例如:索引优化、表连接优化、数据表设计优化等等【此过程借助EXPLAIN】

这些方法都尝试过之后,如果时间还是居高不下,那就说明数据库此时已经达到了瓶颈,我们可以考虑通过横向物理拓展突破单节点的物理瓶颈:

  • 读写分离(主从架构)
  • 分库分表(解决单表数据量大、单库资源瓶颈)

SQL性能分析工具

查看服务器性能参数、SQL执行频率

MySQL 客户端连接成功后,通过 show [session|global] status 命令可以提供服务器状态信息。

  • session 是查看当前会话
  • global 是查询全局数据

通过如下指令,可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次:

sql 复制代码
SHOW GLOBAL STATUS LIKE 'Com_______';
  • Com_delete: 删除次数
  • Com_insert: 插入次数
  • Com_select: 查询次数
  • Com_update: 更新次数

我们直接使用show global STATUS;可以看到一些其他参数,常用的有:

  • Connections:连接MySQL服务器的次数。
  • Uptime:MySQL服务器的上线时间。
  • Slow_queries:慢查询的次数。
  • Innodb_rows_read:Select查询返回的行数
  • Innodb_rows_inserted:执行INSERT操作插入的行数
  • Innodb_rows_updated:执行UPDATE操作更新的行数
  • Innodb_rows_deleted:执行DELETE操作删除的行数

通过上述指令,我们可以查看到当前数据库到底是以查询为主,还是以增删改为主,从而为数据库优化提供参考依据:

  • 如果是以增删改为主,我们可以考虑不对其进行索引的优化
  • 如果是以查询为主,那么就要考虑对数据库的索引进行优化了

那假如说是以查询为主,我们又该如何定位针对于那些查询语句进行优化呢? 次数我们可以借助于慢查询日志。

定位执行慢的SQL:慢查询日志

MySQL的慢查询日志,用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上(不含10秒)的语句,认为是超出了我们的最大忍耐时间值。

它的主要作用是,帮助我们发现那些执行时间特别长的SQL查询,并且有针对性地进行优化,从而提高系统的整体效率。当我们的数据库服务器发生阻塞、运行变慢的时候,检查一下慢查询日志,找到那些慢查询,对解决问题很有帮助。比如一条SQL执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的SQL,结合explain进行全面分析。

默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数**。如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响**。慢查询日志支持将日志记录写入文件。

慢查询日志参数查询:

sql 复制代码
show VARIABLES like 'slow_query_log';

开启慢查询日志:

sql 复制代码
set GLOBAL slow_query_log = 'ON';

修改long_query_time的阈值:

sql 复制代码
set global long_query_time = 1;
set long_query_time=1;

两个范围都要修改,这样才能当前会话以及以后的新建会话都生效。

如上的开启慢查询以及修改阈值的操作是暂时性的设置,也就是说在服务器重启后还是会恢复默认值,如果要想永久修改则要修改配置文件。

测试

执行如下SQL语句;

sql 复制代码
select * from tb_user; -- 这条SQL执行效率比较高, 执行耗时 0.00sec
select count(*) from tb_sku; -- 由于tb_sku表中, 预先存入了1000w的记录, count一次,耗时13.35sec

然后我们再看看慢日志:

我们也可以使用mysql官方的日志分析工具mysqldumpslow,常见的命令:

sql 复制代码
#按照查询时间排序,查看前五条 SQL 语句
mysqldumpslow -s t -t 5 /var/lib/mysql/xxx-slow.log

#得到返回记录集最多的10个SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/xxx-slow.log

#得到访问次数最多的10个SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/xxx-slow.log

#得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/xxx-slow.log

#另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/xxx-slow.log | more

我们发现,在慢查询日志中,只会记录执行时间超多我们预设时间(2s)的SQL,执行较快的SQL是不会记录的。

就这样我们可以通过慢查询日志,定位出执行效率比较低的SQL,从而有针对性的进行优化

查看 SQL 执行成本:SHOW PROFILE

show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。通过have_profiling参数,能够看到当前MySQL是否支持profile操作:

sql 复制代码
SELECT @@have_profiling ;

可以看到,当前MySQL是支持 profile操作的,但是开关是关闭的。可以通过set语句在session/global级别开启profiling:

sql 复制代码
SET profiling = 1;

开关已经打开了,接下来,我们所执行的SQL语句,都会被MySQL记录,并记录执行时间消耗到哪儿去了。 我们直接执行如下的SQL语句

sql 复制代码
select * from tb_user;
select * from tb_user where id = 1;
select * from tb_user where name = '白起';
select count(*) from tb_sku;

执行一系列的业务SQL的操作,然后通过如下指令查看指令的执行耗时:

sql 复制代码
-- 查看每一条SQL的耗时基本情况
show profiles;
-- 查看指定query_id的SQL语句各个阶段的耗时情况
show profile for query query_id;
-- 查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;

show profile的常用查询参数:

  • ALL:显示所有的开销信息。
  • BLOCK IO:显示块IO开销。
  • CONTEXT SWITCHES:上下文切换开销。
  • CPU:显示CPU开销信息。
  • IPC:显示发送和接收开销信息。
  • MEMORY:显示内存开销信息。
  • SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息。
  • SWAPS:显示交换次数开销信息。


EXPLAIN执行计划

前面我们介绍了三种用于SQL性能分析的方法:

  • 通过执行频率
  • 通过慢日志得到耗时多的sql语句
  • 通过profile得到sql语句的具体耗时

这些都是从时间的层面来判断一条sql语句的效率,比较粗糙,并不能真正的判定一条sql语句的性能,所以我们还要借助于第四个工具 --- explain

定位了查询慢的SQL之后,我们就可以使用EXPLAIN或DESCRIBE工具做针对性的分析查询语句。DESCRIBE语句的使用方法与EXPLAIN语句是一样的,并且分析结果也是一样的。

MySQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供它认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)。

这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。MySQL为我们提供了了EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划。

EXPLAIN能告诉我们:

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

语法:

sql 复制代码
-- 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;

输出的上述信息就是所谓的执行计划。在这个执行计划的辅助下,我们需要知道应该怎样改进自己的查询语句以使查询执行起来更高效。其实除了以SELECT开头的查询语句,其余的DELETE、INSERT、REPLACE以及UPDATE语句等都可以加EXPLAIN,用来查看这些语句的执行计划,只是平时我们对SELECT语句更感兴趣。

注意:执行EXPLAIN时并没有真正的执行该后面的语句,因此可以安全的查看执行计划。

Explain 执行计划中各个字段的含义:

  • table:不论我们的查询语句有多复杂,里边包含了多少个表 ,到最后也是需要对每个表进行单表访问的,所以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名(有时不是真实的表名字,可能是简称)。
  • id:select 查询的序列号,表示查询中执行 select 子句或者操作表的顺序(id相同,执行顺序从上到下;id不同,值越大越先执行)。id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好
  • select_type:表示 SELECT 的类型,常见取值有 SIMPLE(简单表,即不适用表连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION中的第二个或者后面的查询语句)、SUBQUERY(SELECT/WHERE之后包含了子查询)等
  • type:表示连接类型,性能由好到差的连接类型为 NULL、system、const、eq_ref、ref、range、index、all
  • possible_key:可能应用在这张表上的索引,一个或多个
  • Key:实际使用的索引,如果为 NULL,则没有使用索引
  • Key_len:表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下,长度越短越好
  • ref:当使用索引列等值查询时,与索引列进行等值匹配的对象信息
  • rows:MySQL认为必须要执行的行数,在InnoDB引擎的表中,是一个估计值,可能并不总是准确的,值越小越好
  • filtered:表示返回结果的行数占需读取行数的百分比,filtered的值越大越好

接下来详细的说说其中比较重要的几个列:

例子中用到的s1表信息:

首先是type

执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法,又称"访问类型",其中的type列就表明了这个访问方法是啥,是较为重要的一个指标。比如,看到type列的值是ref,表明MySQL即将使用ref访问方法来执行对s1表的查询。

一共有如下几种访问方法:

  • system:当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system。

  • const:当根据主键或者唯一索引与常数进行等值匹配时,对单表的访问方法就是const

  • eq_ref:在连接查询时,如果被驱动表是通过主键或者唯一索引进行等值匹配访问的,则该被驱动表的访问方式就是eq_ref

  • ref:当通过普通的二级索引与常量进行等值匹配的时候来查询某个表,那么该表的访问方式就是ref

  • ref_or_not:在ref的基础上加了一个索引列的值也可以是null

  • index_merge:索引合并。这意味着 MySQL 会对多个单列索引进行扫描,并将它们的结果合并(取交集、并集或差集),以高效地定位符合条件的行。通常是 MySQL 在无法使用复合索引(多列索引)时的替代方案。如果能创建合适的复合索引,可能比索引合并更高效。

  • range:使用索引获取范围区间的记录

  • index:全索引扫描也就是说MySQL会遍历整个索引来获取数据 ,而不是遍历表中的行(全表扫描,type=ALL)。这种扫描方式利用了索引,因此通常比全表扫描(ALL)效率更高,但它仍然需要扫描整个索引,所以当索引很大时,效率可能仍然较低,产生这种情况通常是因为查询需要返回的字段都包含在索引中(覆盖索引),但 WHERE 条件无法有效过滤数据,导致必须扫描整个索引,下图的例子中,查询以及需要返回的字段key_part2、key_part3是处在联合索引idx_key_part中的,所以不需要全表扫描,而是使用全索引扫描。

  • all:全表扫描。EXPLAIN SELECT * FROM s1;

总结:

然后再说说possible_keyskey

在EXPLAIN语句输出的执行计划中,possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。一般查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引l。比方说下边这个查询:

上述执行计划的possible_keys列的值是idx_key1,idx_key3,表示该查询可能使用到idx_key1,idx_key3两个索引l,然后key列的值是idx_key3,表示经过查询优化器计算使用不同索引l的成本后,最后决定使用idx_key3。

再就是key_len 字段,主要是在联合索引中发挥作用:

在 MySQL 的 EXPLAIN 分析中,key_len 字段用于表示查询实际使用的索引键的长度(以字节为单位),它的主要作用和意义如下:

  1. 判断索引使用的完整性
    key_len 可以显示查询实际用到的索引列长度,帮助判断是否使用了复合索引的全部列,还是只用到了部分前缀。例如,对于复合索引 (a, b, c)

    • key_len 只对应列 a 的长度,说明只用到了索引的第一列
    • key_len 对应 a + b 的长度,说明用到了索引的前两列
  2. 分析索引使用效率

    通常 key_len 越小,说明索引使用越高效。因为:

    • 较短的索引键占用更少的存储空间
    • 内存中可以缓存更多索引数据,减少磁盘 I/O
    • 比较操作更快,提升查询性能
  3. 验证索引选择是否合理

    通过 key_len 可以确认 MySQL 是否选择了预期的索引。例如:

    • 当预期使用某个长索引但 key_len 很小,可能意味着索引选择不合理
    • key_lenNULL 时,说明没有使用任何索引(全表扫描)
  4. 计算规则参考
    key_len 的计算与字段类型、字符集、是否为 NULL 有关:

    • 数值类型:TINYINT(1)、INT(4)、BIGINT(8)
    • 字符串类型:VARCHAR(n) 需额外加 2 字节(存储长度),CHAR(n) 固定长度
    • 字符集:utf8 每个字符占 3 字节,utf8mb4 占 4 字节
    • 允许为 NULL 时,需额外加 1 字节

例如,VARCHAR(10) NOT NULL 采用 utf8mb4 编码时,key_len = 10×4 + 2 = 42 字节。

通过观察 key_len,可以更深入地理解 MySQL 的索引使用策略,为索引优化提供重要依据。

再说说filtered

在 MySQL 中,EXPLAIN 语句的 filtered 列表示经过表条件过滤后剩余记录的百分比,用于估计在应用表级条件(如 WHERE 子句、连接条件等)后,还剩多少比例的记录会被保留并参与后续操作。

具体作用如下:

  1. 过滤效率评估filtered 的值范围是 0-100,值越高表示过滤后保留的记录比例越大。例如,filtered=10 表示经过过滤后,只有 10% 的记录会被保留。

  2. 优化器决策参考 :MySQL 优化器会结合 rows(估计扫描行数)和 filtered 计算实际参与下一步操作的记录数(约等于 rows × filtered/100),以此决定连接顺序、使用的索引等执行计划。

  3. 索引有效性判断 :如果 filtered 值很低(如小于 10),说明过滤条件能有效减少数据量,通常意味着索引使用合理;如果值很高(如接近 100),可能表示过滤条件效果差,需要优化索引或查询条件。

示例解读:

复制代码
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | t     | NULL       | ALL  | idx_name      | NULL | NULL    | NULL | 1000 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

上述结果中,rows=1000 表示估计扫描 1000 行,filtered=10.00 表示过滤后约 100 行(1000×10%)会参与后续处理。

通过 filtered 可以辅助判断查询中的过滤条件是否高效,进而优化索引设计或查询语句。

最后说说Extra,这个列是用来补充一些关于查询执行计划的额外重要信息,比较常见的有:

  1. Using index

    表示查询仅使用索引树中的信息就能获取所需数据,无需回表访问实际的表数据(覆盖索引),效率较高。

  2. Using where

    表示查询使用了 WHERE 子句过滤数据,但未使用索引来过滤,可能需要扫描全表或部分数据后再筛选。

  3. Using index condition

    表示使用了索引下推(Index Condition Pushdown)优化,存储引擎在索引扫描时就根据部分条件过滤数据,减少回表次数。

什么是索引下推?

索引下推(Index Condition Pushdown,简称 ICP)是 MySQL 5.6 及以上版本引入的一种查询优化技术,用于提升使用索引查询时的效率。

其核心原理是:将原本由 MySQL 服务器层(Server Layer)处理的部分过滤逻辑,下推到存储引擎层(Storage Engine Layer)执行,减少存储引擎回表读取数据的次数,从而提升查询性能。【注意:存储引擎(如 InnoDB)会处理与索引直接相关的条件,服务层(MySQL Server)主要处理无法通过索引直接过滤的条件】

  • 没有 ICP 时,存储引擎根据索引找到匹配的索引项后,会立即回表读取完整的行数据。服务器层再根据 WHERE 子句中的条件(包括索引列之外的过滤条件)过滤数据。
  • 启用 ICP 时,存储引擎在遍历索引时,会先检查索引中包含的字段是否满足 WHERE 子句中的部分条件(仅涉及索引列和可通过索引字段推导的条件)。只有满足条件的索引项,才会触发回表操作读取完整行数据;不满足的则直接过滤,减少回表次数。

示例:

假设有表 user,索引为 (name, age),执行查询:

sql 复制代码
SELECT * FROM user WHERE name LIKE '张%' AND age > 20;
  • 无 ICP :存储引擎会先通过索引找到所有 name LIKE '张%' 的索引项,然后全部回表取完整行,再由服务器层过滤 age > 20 的数据。
  • 有 ICP :存储引擎在遍历索引时,会同时检查 name LIKE '张%'age > 20(因为 age 是索引的一部分),只对同时满足两个条件的索引项回表,减少了回表次数。
  1. Using temporary

    表示 MySQL 需要创建临时表来存储中间结果(如用于 GROUP BYDISTINCT 或多表连接时),会增加性能开销,应尽量避免。

  2. Using filesort

    表示 MySQL 需要对结果进行排序,但无法利用索引完成排序,需在内存或磁盘中进行文件排序,大数据量时性能较差。

  3. Using join buffer

    表示多表连接时未使用索引,只能使用连接缓冲区(join buffer)来存储中间结果,效率较低。

  4. Impossible WHERE

    表示 WHERE 子句的条件永远为假(如 WHERE 1=0),查询不会返回任何结果。

  5. Range checked for each record (index map: N)

    表示 MySQL 无法确定使用哪个索引,需要为每行记录检查合适的索引范围,性能通常不佳。

  6. Using MRR

    表示使用了多范围读取(Multi-Range Read)优化,通过重新排序磁盘访问来提升查询效率,常见于范围查询。

  7. Using index for group-by

    表示 GROUP BY 操作可以通过索引完成,无需额外的排序或临时表。

这些 Extra 值能帮助开发者判断查询是否高效,例如出现 Using temporaryUsing filesort 可能意味着需要优化索引或查询结构。