SQL优化
1.为什么要对SQL语句进行优化
在应用开发过程中,由于初期数据量小,开发人员在编写SQL时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据量的急剧增长,很多SQL语句开始逐渐显露出性能问题,对生产的影响也越来越大,此时这些有问题的SQL语句就成为整个系统的瓶颈,因此我们必修要对它们进行优化
2.SQL优化的步骤
2.1.查看SQL执行频率
MySQL客户端连接成功后,通过
show[session|global] status
命令可以提供服务器状态信息。show[session|global] status
可以根据需要加上参数session
或者global
来显示session
级(当前连接)的计结果和global
级(自数据库上传启动至今)的统计结果。如果不写,则默认使用参数是session
。如下命令显示了当前
session
(连接)中所有参数的值(也就是不同的SQL语句执行的频次):show status like 'Com_______';
参数 含义 Com_select 执行 select 操作的次数,一次查询只累加 1。 Com_insert 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次。 Com_update 执行 UPDATE 操作的次数。 Com_delete 执行 DELETE 操作的次数。 Innodb_rows_read select 查询返回的行数。 Innodb_rows_inserted 执行 INSERT 操作插入的行数。 Innodb_rows_updated 执行 UPDATE 操作更新的行数。 Innodb_rows_deleted 执行 DELETE 操作删除的行数。 Connections 试图连接 MySQL 服务器的次数。 Uptime 服务器工作时间。 Slow_queries 慢查询的次数。 如下命令显示整个数据库从启动至今的不同SQL语句的执行频次统计结果:
show global status like 'Com_______';
还可以通过如下命令,针对于Innodb存储引擎SQL语句执行频次统计查询:
show status like 'innodb_rows_%';
根据如上指令我们可以看出数据库是以什么操作为主。
2.2.定位低效率执行SQL
我们可以通过以下两种方式来定位执行效率低的SQL语句。
慢查询日志:通过慢查询日志定位那些执行效率低的SQL语句,用–log–slow-queries[=file_name]选项启动时,mysqld写一个包含所有执行时间超过long_query_time秒的SQL语句的日志文件。
通过命令
show processlist
用来查看当前MySQL在进行的线程,包括线程的状态,是否锁表等,可以实时的查看SQL的执行情况,同时对一些锁表操作进行优化。慢查询日志是在查询结束后才记录,所以在应用反映执行效率出现问题的时候查询慢查询日志并不能定位到问题。执行
show processlist;
命令来查看一下列说明:
id
:用户登录时,系统分配中的connection_id
,可以使用函数connect_id()
查看
user
:显示当前用户,如果不是root,这个命令只显示用户权限范围内的sql语句
host
:显示这个语句时从那个ip的那个端口上发的,可以用来追踪出现问题语句的用户
db
:显示这个进程目前连接的是那个数据库
command
:显示当前连接执行的命令,一般取值为休眠sleep
,查询query
,连接connect
等
time
:显示这个状态持续的时间,单位是秒(时间的开始时间是依据show processlist;
命令执行后)
state
:显示使用当前连接的sql语句的状态,很重要的列,state描述的是语句执行中的某一个状态,一个sql语句,以查询为例,可能需要经过copying to tmp table result
,sending data
等状态才可以完成
info
:显示sql语句,是判断问题语句的重要依据演示案例:
创建一张t_user表,往表中插入300万条数据,对其中的一条数据进行精确查询。在查询的过程中,我们通过执行
show processlist;
命令,看是否能看到执行的查询语句CREATE TABLE `t_user`( id int PRIMARY KEY AUTO_INCREMENT, username varchar(20), password varchar(20), birthday date, sex varchar(10), age int, address varchar(255) );
详细如下所示:
有上面的建表语句可以看出来,表中的
password
列并没有设置索引。所以在查询的时候回比较慢。在执行查询语句后,再通过show processlist;
名令来查看当前在进行的线程。如果查询速度慢的话,我们就可以在返回结果中看到这个线程以及线程相关的详细信息。select * from t_user where password = '11111';
2.3.explain分析执行计划
使用方式:
我们可以通过
explain
或者desc
命令获取MySQL如何执行select
语句的信息,包括select
语句执行过程中如何连接和连接的顺序查询SQL语句的执行计划:
explain select * from t_user where id = 11111;
explain select * from t_user where password = '11111';
列的含义:
字段 含义 id select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。 select_type 表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接或者子查询)、 PRIMARY(主查询,即外层的查询)、UNION(UNION 中的第二个或者后面的查询语 句)、SUBQUERY(子查询中的第一个 SELECT)等 table 输出结果集的表 type 表示表的连接类型,性能由好到差的连接类型为( system —> const -----> eq_ref ------> ref -------> ref_or_null----> index_merge —> index_subquery -----> range -----> index ------> all ) possible_keys 表示查询时,可能使用的索引 key 表示实际使用的索引 key_len 索引字段的长度 rows 扫描行的数量 extra 执行情况的说明和描述 ref 引用
explain属性说明:
准备工作,设计三张表。详细如下:
CREATE TABLE `t_role` ( `id` varchar(32) NOT NULL, `role_name` varchar(255) DEFAULT NULL, `role_code` varchar(255) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `unique_role_name` (`role_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `t_user` ( `id` varchar(32) NOT NULL, `username` varchar(45) NOT NULL, `password` varchar(96) NOT NULL, `name` varchar(45) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `unique_user_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(32) DEFAULT NULL, `role_id` varchar(32) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_ur_user_id` (`user_id`), KEY `fk_ur_role_id` (`role_id`), CONSTRAINT `fk_ur_role_id` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_ur_user_id` FOREIGN KEY (`user_id`) REFERENCES `t_user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; INSERT INTO t_role (id, role_name, role_code, description) VALUES('10', '超 级管理员', 'super', '超级管理员'); INSERT INTO t_role (id, role_name, role_code, description) VALUES('5', '学 生', 'student', '学生'); INSERT INTO t_role (id, role_name, role_code, description) VALUES('7', '老 师', 'teacher', '老师'); INSERT INTO t_role (id, role_name, role_code, description) VALUES('8', '教 学管理员', 'teachmanager', '教学管理员'); INSERT INTO t_role (id, role_name, role_code, description) VALUES('9', '管 理员', 'admin', '管理员'); INSERT INTO t_user (id, username, password, name) VALUES('1', 'super', '$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe', ' 超级管理员'); INSERT INTO t_user (id, username, password, name) VALUES('2', 'admin', '$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe', ' 系统管理员'); INSERT INTO t_user (id, username, password, name) VALUES('3', 'itcast', '$2a$10$8qmaHgUFUAmPR5pOuWhYWOr291WJYjHelUlYn07k5ELF8ZCrW0Cui', 'test02'); INSERT INTO t_user (id, username, password, name) VALUES('4', 'stu1', '$2a$10$pLtt2KDAFpwTWLjNsmTEi.oU1yOZyIn9XkziK/y/spH5rftCpUMZa', '学 生1'); INSERT INTO t_user (id, username, password, name) VALUES('5', 'stu2', '$2a$10$nxPKkYSez7uz2YQYUnwhR.z57km3yqKn3Hr/p1FR6ZKgc18u.Tvqm', '学 生2'); INSERT INTO t_user (id, username, password, name) VALUES('6', 't1', '$2a$10$TJ4TmCdK.X4wv/tCqHW14.w70U3CC33CeVncD3SLmyMXMknstqKRe', '老师 1'); INSERT INTO user_role (id, user_id, role_id) VALUES(1, '1', '5'); INSERT INTO user_role (id, user_id, role_id) VALUES(2, '1', '7'); INSERT INTO user_role (id, user_id, role_id) VALUES(3, '2', '8'); INSERT INTO user_role (id, user_id, role_id) VALUES(4, '3', '9'); INSERT INTO user_role (id, user_id, role_id) VALUES(5, '4', '8'); INSERT INTO user_role (id, user_id, role_id) VALUES(6, '5', '10');
如下图是三张表的关系图:
t_user:用户表t_role:角色表
user_role:用户角色表
用户表与角色表之间的关系为多对多的关系,user_role即为它们的中间表
explain-id:
id字段是select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。id情况有三种:
id相同表示加载表的顺序是从上到下的
例如:
explain select * from t_role r,t_user u,user_role ur where r.id = ur.role_id and u.id = ur.user_id;
id不同id值越大,优先级别越来越高,越先被执行
例如:
EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1'));
id有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下执行。在所有的组中,id的值越大,优先级越高,越先执行
例如:
EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ;
explain-select_type:
表示SELECT的类型,常见的取值,如下所示:
执行效率,从上倒下递减
select_type | 含义 |
---|---|
SIMPLE | 简单的select查询,查询中不包含子查询或者UNION(联合查询) |
PRIMARY | 查询中若包含了子查询,最外层查询标记为该标识 |
SUBQUERY | 在select 或 where 列表中包含了子查询 |
DERIVED | 在from列表中包含的子查询,被标记为DERIVED(衍生)MYSQL会递归执行这些子查询,把结果放到临时表中 |
UNION | 若第二个 select 出现在 UNION 之后,则标记为UNION;若UNION包含在FROM子句的子查询中,外层select将被标记为:DERIVED |
UNION RESULT | 从UNION表获取结果的select |
SIMPLE
PRIMARY、SUBQUERY
DERIVED
UNION、UNION RESULT
explain-select_table:
展示该行属于那张表
explain-type:
type:显示的是访问类型,是较为重要的一个指标,可取值为:
type | 含义 |
---|---|
NULL | MySQL不访问任何表,索引。直接返回结果 |
system | 表中只有一行数据(等于系统表),这个是const类型的特例,一般不会出现 |
const | 表示通过索引一次就找到了,const用于比较primary key 或者 unique索引。应为只匹配一行数据所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量。const用于将主键或唯一索引的所有其他部分常量值进行比较 |
eq_ref | 类似ref,区别在于使用的是唯一性索引,使用主键的关联查询,关联查询出的记录只有一条。常见于主键或唯一索引扫描 |
ref | 非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,返回所有匹配某个单独值的所有行(多个) |
range | 只检索给定返回的行,使用一个索引来选择行。where 之后出现between,<,>,in等操作 |
index | index与ALL的区别为:index类型只是遍历了索引树,通常比ALL快,ALL是遍历数据文件 |
ALL | 将遍历全表以找到匹配的行 |
NULL
system
- const
- eq_ref
- ref
- index
- ALL
结果值从最好到最坏依次是:
NULL > system > const > eq_ref > ref > range > index > all
完整的:NULL > system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般来说,我们需要保证查询至少达到range级别,最好达到ref。
explain-key:
possible_keys:显示可能应用在这张表的索引,一个或者多个
key:实际使用的索引,如果为null,则没有使用索引。
key_len:表示索引中使用的字节数,该值为索引字段最大可能长度,并非实际使用长度,在不损失精确性的前提下越短越好。
explain-rows:
扫描行的数量
explain-extra:
其他额外的执行计划信息,在该列显示
extra | 含义 |
---|---|
using file sort | 说明mysql会对数据使用一个外部的索引排序,而不是按照表的索引顺序进行读取。称为文件排序,效率低 |
using temporary | 使用临时表保存中间结果,MySQL在对查询结果排序时使用临时表,常见于order by和 group by 效率低 |
using index | 表示相应的select操作使用了覆盖索引,避免访问表的数据行,效率高 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W4xZNmoT-1614603889433)(/Users/jiangnan/Library/Application Support/typora-user-images/image-20210301202031788.png)]
2.3.show profile分析SQL
Mysql从5.0.37版本开始增加了对 show profiles 和 show profile 语句的支持。show profiles 能够在做SQL优化时帮助我们了解时间都耗费在什么地方。
通过
select @@have_profiling;
命令查看当前MySQL是否支持profile:YES:支持
NO:不支持通过
select @@profiling;
命令查看profiling是否开启,默认是关闭的,可以通过set profiling = 1;
命令在Session级别开启profiling:profiling开启后,执行一些SQL语句。再通过
show profiles;
命令来查看这些SQL语句再执行过程中耗时记录:通过
show profile for query query_id;
语句可以查看到该SQL执行过程中每个线程的状态和消耗的时间:Sending data 状态表示MySQL线程开始访问数据行并把结果返回给客户端,而不仅仅是返回个客户端。由于 在Sending data状态下,MySQL线程往往需要做大量的磁盘读取操作,所以经常是整各查询中耗时最长的状态。
2.4.trace分析优化器执行计划
MySQL5.6提供对SQL的追踪trace,通过trace文件能够进行近一步了解优化器执行的计划
首先我们要打开trace,设置格式为JSON,并设置trace文件最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能完整的展示
set optimizer_trace = 'enabled=on',end_markers_in_json=on; set optimizer_trace_max_mem_size = 1000000;
执行一条SQL语句
select * from t_user;
检查information_schema.optimizer_trace就可以知道MySQL是如何执行SQL的
select * from information_schema.optimizer_trace\G;