【MySQL】SQL调优-如何分析SQL性能

目录

压力测试工具

SQL语句性能分析

select_type

[* type列](#* type列)

[* Extra 列](#* Extra 列)

[* Using temporary](#* Using temporary)

[Using filesort](#Using filesort)

[Using where](#Using where)

[Using index](#Using index)

索引覆盖

回表查询


MySQL的优化涉及多个级别的配置、调优和性能评估。根据职位(开发人员或DBA),可以在单个 SQL语句、整个应用程序、单个数据库服务器或多个联网数据库服务器的级别进行优化;

别影响性能的重要因素:表结构、查询语句和数据库配置。

软件级别的因素会导致硬件级别的CPU和I/O操作。在优化数据库性能时,首先要学习软件级别的规则,但在真实的企业中,通常数据库遇到瓶颈 首先考虑换⼀个高性能的存储设置,比如把机械硬盘换成SSD,再考虑软件层面,最后考虑操作系 统层面的优化;

这里只讨论索引级别的优化;本文将介绍一些性能分析的方法;

压力测试工具

使用MySQL自带的压测工具(mysqlslap)模拟多个客户端同时查询,观察测试结果:

bash 复制代码
# 使用主键查询 查询100w次

mysqlslap -uroot -proot123 \
  --concurrency=100 \
  --iterations=100 \
  --create-schema="test" \
  --engine="innodb" \
  --number-of-queries=10000 \
  --query="SELECT id, sn, name, mail, age, gender, class_id FROM test.index_demo WHERE id = 1020000"
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
	Running for engine innodb
	Average number of seconds to run all queries: 0.257 seconds
	Minimum number of seconds to run all queries: 0.169 seconds
	Maximum number of seconds to run all queries: 0.345 seconds
	Number of clients running queries: 100
	Average number of queries per client: 100

 # 使⽤⾮索引列查询 查询300次

mysqlslap -uroot -proot123   --concurrency=30   --iterations=3   --create-schema="test"   --engine="innodb"   --number-of-queries=100   --query="SELECT id, sn, name, mail, age, gender, class_id FROM test.index_demo WHERE sn = '1020000';"
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
	Running for engine innodb
	Average number of seconds to run all queries: 3.982 seconds
	Minimum number of seconds to run all queries: 3.956 seconds
	Maximum number of seconds to run all queries: 3.997 seconds
	Number of clients running queries: 30
	Average number of queries per client: 3

命令参数解析:

基本连接选项

  • -uroot:使用 root 用户连接 MySQL

  • -proot123:使用密码 "root123"

性能测试核心参数

  • --concurrency=100:模拟 ​​100个并发客户端​​ 同时连接数据库。这个值越高,测试压力越大。

  • --iterations=100:整个测试将 ​​重复执行100次​​,用于获取更稳定的平均性能数据。

  • --create-schema="test":测试将在名为 "test" 的数据库中执行(如果数据库不存在会自动创建)

  • --engine="innodb":指定使用 ​​InnoDB 存储引擎​​ 进行测试(如果表已存在,会临时改为使用InnoDB)

  • --number-of-queries=10000:整个测试期间将 ​​总共执行10000次查询​​(所有客户端总查询数,比如:示例中100个客户端一轮总共需要执行1w次,执行100轮)

测试查询定义

  • --query="SELECT id, sn, name, mail, age, gender, class_id FROM test.index_demo WHERE id = 1020000"

    要测试的SQL查询语句:从test数据库的index_demo表中查询id=1020000的记录;

使用 show processlist 命令查看正在运行的线程:

SQL语句性能分析

在执行 SELECT , DELETE , INSERT , REPLACE ,和 UPDATE 之前都可以用执行计划分析SQL语句的执行情况,以便优化SQL语句。

注意:并不会真正的执行SQL,只是对SQL进行分析,最终返回一个分析结果

bash 复制代码
# 索引列
mysql> EXPLAIN select id, sn, name, mail, age, gender, class_id from index_demo where id = 1020000\G
*************************** 1. row ***************************
           id: 1                     # SELECT标识符,表示查询序列号,如果使用子查询或联表查询就会查到多个执行计划id也会递增;
  select_type: SIMPLE                # 查询类型,简单查询(无子查询/UNION)
        table: index_demo            # 查询的表名
   partitions: NULL                  # 查询涉及的分区(未分区则为NULL)
         type: const                 # 连接类型,const表示通过主键/唯一索引查找
possible_keys: PRIMARY               # 可能使用的索引(此处显示主键索引可用)
          key: PRIMARY               # 实际使用的索引(最终选择主键索引)
      key_len: 8                     # 使用的索引长度(字节)
          ref: const                 # 与索引比较的列/常量
         rows: 1                     # 预估需要检查的行数
     filtered: 100.00                # 按条件过滤后剩余行的百分比
        Extra: NULL                  # 附加执行信息
1 row in set, 1 warning (0.00 sec)

# 非索引列
mysql> EXPLAIN select id, sn, name, mail, age, gender, class_id from index_demo where sn = '1020000'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 993231
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

在性能分析时主要关注以下字段:

bash 复制代码
# 使用非索引列
type: ALL             # 全表扫描
key: NULL             # 未用索引
rows: 100000          # 扫描10万行
Extra: Using where    # 存储引擎层未过滤


# 使用索引列
type: const           # 主键精准定位
key: PRIMARY          # 使用主键
rows: 1               # 扫描1行
Extra: NULL           # 无额外操作

分析上边两个查询语句,第一个查询:实际用到的索引是主键索引,索引长度为8字节,索引比较的列是常量,估算要检查的行数为1行,按条件筛选率是100%;

第二个查询:没有用到索引,估算要检查的行数为993231行,按条件筛选率仅为10%;

select_type

|---------------|------------------------|
| select_type 值 | 说明 |
| SIMPLE | 简单SELECT(不使用UNION或子查询) |
| PRIMARY | 外层查询 |
| UNION | UNION中的第⼆个及之后的SELECT语句 |
| UNION RESULT | UNION的结果。 |
| SUBQUERY | 子查询中 |
| INSERT | INSERT 语句 |
| UPDATE | UPDATE 语句 |
| DELETE | DELETE 语句 |

测试语句:

bash 复制代码
explain select * from student where id = (select id from student1 where name = '宋江')\G

explain select * from student union select * from student1\G

* type列

性能排序与说明​

EXPLAIN输出的type列描述了表是如何连接的,性能从最高往低依次降低;​

类型 名称 触发场景 性能 优化建议
​system​ 系统表 查询系统表或只有1行的表 ★★★★★ 无需优化
​const​ 常量 用主键/唯一索引精确匹配 (WHERE id = 1 ★★★★★ 理想状态
​eq_ref​ 等值引用 JOIN时用主键/唯一索引关联 (A.id = B.id ★★★★☆ 检查JOIN字段索引
​ref​ 普通索引 用非唯一索引查找 (WHERE age = 20 ★★★☆☆ 确保索引选择性高
​fulltext​ 全文索引 使用全文索引搜索 ★★★☆☆ 优化MATCH条件
​ref_or_null​ 含NULL的索引 索引查询包含NULL值 (WHERE age = 20 OR age IS NULL ★★☆☆☆ 避免字段允许NULL
​index_merge​ 索引合并 合并多个索引的结果 (WHERE a=1 OR b=2 ★★☆☆☆ 改用复合索引
​unique_subquery​ 唯一子查询 子查询使用主键 (WHERE id IN (SELECT...) ★★☆☆☆ 改写成JOIN
​index_subquery​ 索引子查询 子查询使用普通索引 ★★☆☆☆ 改写成JOIN
​range​ 范围扫描 索引范围查询 (WHERE id > 100 ★☆☆☆☆ 控制范围数据量
​index​ 全索引扫描 遍历整个索引树 (SELECT indexed_col FROM table ☆☆☆☆☆ 避免SELECT无覆盖索引
​ALL​ 全表扫描 无索引可用 ⚠️最差 必须加索引
  • system:查询系统表或只有1行的表;

  • const:用主键/唯一索引精确匹配 (WHERE id = 1),结果最多有⼀个匹配的行,类型显示 为 const ,这种类型查询性能极高,且只会返回一行数据;

  • eq_ref:应⽤于多表连接的场景,表关联条件是主键索引或唯一非空索引时使⽤等号 ( = ) 进行索引列的比较,每行只匹配⼀条记录

sql 复制代码
select * from student s, account a where s.id = a.id;
  • ref:使用了普通索引,返回的结果可能是多行组成的结果集(WHERE age = 20
  • fulltext:使用全文索引搜索
  • ref_or_null:索引查询包含NULL值(WHERE age = 20 OR age IS NULL
  • index_merge:在查询中使用了多个索引,OR 两边必须是单独索引,最终通过不同索引检索数据,然后对结果集进行合并,Key_len显示最长的索引长度。
sql 复制代码
select * from index_demo where name = 'user_1020021' or id = 1030300\G
-- name 和 id 都是单独的索引列
  • unique_subquery:子查询使用主键(WHERE id IN (SELECT...)
sql 复制代码
-- ⼦查询中返回的是外层表的主键索引或唯⼀索引
value IN (SELECT primary_key FROM single_table WHERE some_expr)
  • index_subquery:类似于unique_subquery,只不过子查询中返回的是普通索引列;
  • range:使用索引列进行范围查询,当使用<>、>、>=、<、<=、is NULL、<=>、BETWEEN、LIKE或IN()操作符,索引列与常量进行较时为range;
  • index:扫描整个索引树而不扫描整个表,比如只使用索引排序而不使用条件查询时:
sql 复制代码
select * from index_demo order by sn limit 10\G
  • ALL:最差的情况,表示MySQL必须对全表进行逐行扫描才可以以找到匹配行;

针对这些type类型,在全文索引(fulltext)性能之下的其实都可以根据业务进行调整优化,尽可能的使用索引;特别需要避免的就是全表扫描;

* Extra 列

Extra 列中如果出现 Using filesort 和 Using temporary ,将会对查询效率有比较严重的影响;

Using filesort:使用文件排序,该场景必须要进行优化;

Using temporary:使用临时表排序,临时表所占用的是内存中的一片区域,当内存占满之后,就需要申请临时文件此时发生磁盘IO;

* Using temporary

当使用非索引列进行分组时,会用临时表进行排序,优化时可以考虑为分组的列加索引;

sql 复制代码
mysql> explain select avg(age) from index_demo group by gender\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 993231
     filtered: 100.00
        Extra: Using temporary
1 row in set, 1 warning (0.01 sec)

-- 使用索引列,减少内存的使用
mysql> explain select avg(age) from index_demo group by class_id\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: index
possible_keys: class_id
          key: class_id
      key_len: 8
          ref: NULL
         rows: 993231
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

Using filesort

当使用非索引列进行排序时会用到文件内排序,优化时可以考虑为排序的列加索引;

sql 复制代码
mysql> explain select * from index_demo where id < 1020000 order by age limit 10\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: NULL
         rows: 496615
     filtered: 100.00
        Extra: Using where; Using filesort -- 使用了文件内排序,进行了磁盘IO
1 row in set, 1 warning (0.00 sec)

mysql> explain select * from index_demo where id < 1020000 order by class_id limit 10\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: index
possible_keys: PRIMARY
          key: class_id
      key_len: 8
          ref: NULL
         rows: 20
     filtered: 50.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

Using where

使用了非索引列进行检索数据,且进行了全表扫描;

sql 复制代码
-- gender 非索引列
explain select * from index_demo where gender = 1\G

当使用索引列进行检索数据时,行范围查找,此时扫描的是索引树,也显示的是 Using where;

sql 复制代码
mysql> explain select * from index_demo where id < 102000\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: NULL
         rows: 3674
     filtered: 100.00
        Extra: Using where -- 扫描索引树,使用了索引
1 row in set, 1 warning (0.00 sec)

Using index

发生索引覆盖时显示using index,表示这是⼀个高效查询;

主键索引:主键索引的B+树叶子节点中,保存的是完整的数据行;

普通索引:普通索引生成的索引树的叶子节点中,保存的是索引列的值和主键值;

索引覆盖

当查询可以​​完全通过索引获取所需数据​ ​时,性能会有显著提升;查询的​​所有字段都包含在某个索引中​​,引擎无需回表查数据文件。

比如:

sql 复制代码
-- 创建一个包含 mail, age, class_id 的复合索引
create index idx_mail_age_classId on index_demo(mail, age, class_id);

-- 查询 mail,age,class_id 这三个列,判定条件也是索引中最左前缀列
mysql> explain select mail,age,class_id from index_demo where mail = '1020000@qq.com'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: ref
possible_keys: idx_mail_age_classId
          key: idx_mail_age_classId
      key_len: 83
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

如果新增一个列:

sql 复制代码
select mail,age,class_id,sn from index_demo where mail = '1020000@qq.com'\G

由于sn 不是索引中的列,只能通过索引记录中的主键ID,再到主表中查询所有的数据,最终返回结果集,这个现象叫回表查询

回表查询

当使用索引检索数据时,查询的列不只包含索引列,这时需要通过索引中记录的主键值到主表中进行查询,这个现象叫做回表查询;

sql 复制代码
mysql> explain select * from index_demo where mail = '1020000@qq.com'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: index_demo
   partitions: NULL
         type: ref
possible_keys: idx_mail_age_classId
          key: idx_mail_age_classId
      key_len: 83
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

即使使用了复合索引,但也发生了回表查询;回表查询需要额外的磁盘I/O(如果数据页不在缓冲池中),对于高频查询,建议优化为覆盖索引查询;

相关推荐
Elastic 中国社区官方博客3 小时前
在 Elasticsearch 中使用 Mistral Chat completions 进行上下文工程
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
编程爱好者熊浪5 小时前
两次连接池泄露的BUG
java·数据库
南宫乘风6 小时前
基于 Flask + APScheduler + MySQL 的自动报表系统设计
python·mysql·flask
TDengine (老段)6 小时前
TDengine 字符串函数 CHAR 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
qq7422349847 小时前
Python操作数据库之pyodbc
开发语言·数据库·python
姚远Oracle ACE7 小时前
Oracle 如何计算 AWR 报告中的 Sessions 数量
数据库·oracle
Dxy12393102167 小时前
MySQL的SUBSTRING函数详解与应用
数据库·mysql
码力引擎8 小时前
【零基础学MySQL】第十二章:DCL详解
数据库·mysql·1024程序员节
杨云龙UP8 小时前
【MySQL迁移】MySQL数据库迁移实战(利用mysqldump从Windows 5.7迁至Linux 8.0)
linux·运维·数据库·mysql·mssql
l1t8 小时前
利用DeepSeek辅助修改luadbi-duckdb读取DuckDB decimal数据类型
c语言·数据库·单元测试·lua·duckdb