目录
一、MySQL的锁机制
1、介绍
(1)定义
锁是计算机协调多个进程或线程并发访问某一资源(比如表)的机制(避免争抢),它可以保证数据并发访问的一致性与有效性
(2)锁的分类
从对数据操作的粒度分: 表锁:操作时,会锁定整个表
行锁:操作时,会锁定当前操作行
从对数据操作的类型分: 读锁(共享锁/S锁):针对同一份数据,多个读操作可以同时进行而
不会相互影响
写锁(排它锁/X锁):当前操作没有完成之前,它会阻断其他写锁
与读锁
MyISAM只支持表级锁,InnoDB支持表级锁与行级锁
锁冲突:两个事务在同一时刻申请同一资源
2、表锁
MyISAL存储引擎,在我们执行select语句前,系统会给涉及的所有表自动加读锁,在执行update、delete、insert前,系统会给涉及的表自动加写锁,锁机制一般都是系统自动执行,一般不需要手动通过lock table命令给表加锁
lock table 表名 read; : 加读锁
lock table 表名 write; : 加写锁
注意当事务A对表1加了锁之后只能对表1进行操作,无法对其它表进行操作
unlock tables : 解锁
3、行锁
InnoDB与MyISM最大的不同有两点:一是支持事务,二是采用了行级锁,当然InnoDB也支持表锁,与MyISM的表锁一样
InnoDB存储引擎,在我们执行update、delete、insert前,系统会给涉及的表的行自动加写锁,对于普通的select语句InnoDB不会加任何锁
select * from 表名 where ...... lock in share mode; : 加读锁
select * from 表名 where ...... for update; : 加写锁
commit; : 提交了事务会自动解锁
二、MySQL的日志
在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的方方面面,以帮助数据库管理员追踪数据库曾经发生过的各种事情。
在MySQL中日志分为错误日志、二进制日志、查询日志、慢查询日志
1、错误日志
错误日志记录着当MySQL启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息,当数据库出错时,可以首先查看此日志。错误日志是默认开启的,错误日志文件默认存放在MySQL的数据目录下叫做hostname.err的文件中(hostname是主机名)
show variables like 'log_error%'; : 查看日志位置
2、二进制日志(BINLOG)
二进制日志记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制,就是二进制日志实现的。MySQL8.0默认已经开启了二进制日志。
(1)配置
但对于其它低版本的MySQL需要手动配置开启二进制日志(windows系统配置my.ini文件,linux系统配置my.cnf文件),并配置日志格式,然后对MySQL服务器进行重启,使其生效。
在my.ini文件或my.cnf文件中添加:
log_bin = 二进制日志文件名(例如binlog) : 配置开启binlog日志,生成的binlog日志文件名
是二进制文件名.00000x
binlog_format = STATEMENT | ROW(MySQL8.0默认) | MIXED : 配置二进制日志格式
STATEMENT | ROW | MIXED |
以sql语句的形式记录,主从复制时,从库会将日志解析为原文本,并在从库中重新执行一次。 | 以数据变更的方式记录日志,旧数据--新数据。 | 混合了STATEMENT与ROW格式。 |
(2)操作
show variables like 'log_bin' ; :查看MySQL是否开启了binlog日志
show variables like 'binlog_format'; : 查看binlog日志的格式
show binlog events; : 查看所有已记录的binlog日志
show master status ; : 查看最新binlog日志保存文件
show binlog events in 'binlog.000007'; : 查看指定binlog日志
show binlog events in 'binlog.000007' from Pos;: 从指定的位置Pos开始,查看指定的binlog日志
show binlog events in 'binlog.000007' from Pos limit 条数;: 从指定的位置Pos开始,查看指定的binlog日志,限制查询条数
show binlog events in 'binlog.000007' from Pos limit 偏移,条数;: 从指定的位置Pos开始,查看指定的binlog日志,限制查询条数,带有偏移
reset master; : 清空binlog日志
3、查询日志
(1)配置
查询日志记录客户端的所有操作语句,而二进制日志不包含数据查询语句,默认情况下查询日志是未开启的,需要手动开启(my.ini或my.cnf),然后重启服务器:
general_log = 1 (0:关闭 1:开启)
general_log_file = 查询日志文件名(如果没有指定默认为host_name.log)
(2)操作
show variables like 'general_log'; : 查看MySQL是否开启了查询日志
show variables like 'general_log_file'; : 查看查询日志文件路径
set global general_log = 1; : 临时开启查询日志
4、慢查询日志
慢查询日志记录了所有执行时间超过参数long_query_time设置值(默认10s,最小为0,精度可以到微秒)的所有SQL语句。慢查询日志默认情况下是关闭的。慢查询日志可以排查哪些sql很费时,效率低,这时可以对这些sql语句进行优化。
(1)配置
slow_query_log = 1 : 控制慢查询日志的开启
slow_query_log_file = slow_query.log : 指定慢查询日志的文件名(没有指定默认为hostname-slow.log文件)
long_query_time = 10 : 设置时间限制(默认10)
(2)操作
show variables like 'slow_query_log'; : 查看慢查询日志是否开启
show variables like 'slow_query_log_file'; : 查看慢查询日志文件路径
set global slow_query_log = 1; : 临时开启慢查询日志
三、MySQL的优化
在应用的开发过程中,由于初期数据量小,开发人员写SQL语句时更重视功能上的实现,但是当应用系统正式上线后,随着生产数据的急剧增长,很多SQL语句开始逐渐显露出性能问题,此时这些有问题的SQL语句就成为整个系统性能的瓶颈,因此我们必须要对它们进行优化。
MySQL优化的方式有:从设计上优化;从查询上优化;从索引上优化;从存储上优化
1、查看SQL执行频率
在优化之前,先要查看一下服务器的状态信息show [session | global] status,即当前数据库的主要操作类型时什么(是增删改操作多呢,还是查询的操作多),通过这种查询可以方便我们制定更优的优化方案。
show session status like 'Com_______'; :查看当前会话SQL执行类型的统计信息(7个_)
show global status like 'Com_______'; :查看自从上一次MySQL服务器启动后SQL执行类
型的统计信息
show status like 'Innodb_rows_%'; :查看针对innodb引擎的统计信息
2、定位效率低的SQL语句
方式1:慢查询日志(可以找到那些已经执行完的效率低的SQL语句)
方式2:show processlist; :查看当前正在执行的SQL语句( 实时查看),它的状态信息,
消耗时间情况等。
比较重要的信息是Time State info
3、explain分析慢SQL语句的执行计划
假设已经定位到那些效率低的SQL语句了,然后可以使用explain命令来查询该SQL语句的执行计划,看一下该SQL语句为什么慢,在那个步骤执行的慢。
(1)数据准备:
(2)操作
explain select * from user where id = 1;
# 如果涉及多张表id表示系统操作表的顺序,是先对A表进行的操作还是先对B表
explain select * from user where name = '张飞';
字段解释
通过explain可以查看效率低的SQL语句是不是因为涉及的表太多、没有用到索引等等而导致的效率低。
(3)explain之id字段解释
id字段存在三种可能出现的情况:
情况1:id相同表示它们的优先级一样,加载表的顺序是从上到下(按书写顺序操作)
explain select * from user u, user_role ur, role r where u.id = ur.uid and ur.rid = r.rid;
情况2:id不同,值越大优先级越高(一般涉及子查询)
explain select * from role where rid = (select rid from user_role where uid = (select id from user where name = '张飞'));
情况3:id有相同,也有不同。相同的id优先级一样从上到下顺序执行,id大的优先级越大
explain select * from role where rid in (select rid from user_role where uid = (select id from user where name = '小巧'));
(4)explain之select_type字段解释
SIMPLE : 简单的select查询,不涉及子查询,union等复杂操作的select语句
PRIMARY : 子查询中最外层的select
SBUQUERY : 子查询中内层的select(在select 或 where 字段中的子select语句)
DERIVED : 子查询中内层的select(在from字段中的子select语句(临时表))
UNION : 若第二个select出现在UNION之后,则标记为UNION,UNION之前的select标记
为PRIMARY
UNION RESULT : UNION语句的临时结果
explain select * from (select * from user limit 2) t;
explain select * from user where id = 1 union select * from role where rid = 3;
(5)explain之type字段解释(重点)
type字段表示该select语句是通过什么样的连接/访问方式获得结果的
/*
NULL
Mysql不访问任何表,索引,直接返回结果
*/
explain select 1+1;
explain select sleep(1)
/*
system
select语句正在访问系统表(mysql自带的表),且该系统表的数据量少,查询系统表会直接从内存中读取
数据,不需要进行磁盘IO操作,MySQL5.7版本以上就不会显示system了,而是all,即使系统表只有一
条数据,也是all类型
*/
explain select * from mysql.tables_priv;
/*
const 命中主键或唯一索引,且被连接的部分是一个常量值
*/
explain select * from user where id = 1;
-- 注意 explain select * from user where name = '张飞'; type = ALL 因为name不是主键列,没有主键索引,也没有unique索引
-- 给name字段添加唯一索引
create unique index index_name on user(name); -- 创建唯一索引之后上诉select....张飞变为const类型
drop index index_name on user;
-- 给name字段添加普通索引
create index index_name on user(name); -- 创建普通索引之后上诉select....张飞变为ref类型
/*
eq_ref
针对两张表情况,前表通过主键索引或非空唯一索引直接定位,然后对后表进行全表扫描匹配,且后表中的
数据仅有一条与前表匹配,且后表中不允许出现前表没有的数据,总结:两张表是完全的一一对应关系且
两张表有一张表命中主键才可以是eq_ref类型
*/
-- 数据准备
create table user2(
id int,
name varchar(20)
);
insert into user2 values(1,'张三'),(2,'李四'),(3,'王五');
create table user2_ex(
id int,
age int
);
insert into user2_ex values(1,20),(2,21),(3,22);
explain select * from user2 a, user2_ex b where a.id = b.id; -- 没有任何索引时a,b type = ALL
-- 给user2添加主键索引
alter table user2 add primary key(id);
explain select * from user2 a, user2_ex b where a.id = b.id; -- a type = eq_ref b type = ALL
insert into user2_ex values(1,25);
explain select * from user2 a, user2_ex b where a.id = b.id; -- 又全变为ALL
alter table user2 drop primary key;
/*
ref
针对单表查询时,该表查询的列存在普通索引;针对两张表情况时,前表存在普通索引,前表允许有null值与
重复值,后表不允许有空值,与重复值 总结:在eq_ref限制的基础上,允许前表重复,有空值,两张表是
多对一的关系
*/
-- 数据准备
create table user3(
id int,
name varchar(20)
);
insert into user3 values(1,'张三'),(2,'李四'),(3,'王五');
create table user3_ex(
id int,
age int
);
insert into user3_ex values(1,20),(2,21),(3,22);
-- 创建普通索引
create index index_id on user3(id);
explain select * from user3 a, user3_ex b where a.id = b.id;
drop index index_id on user3;
-- range 根据给定的范围找到的结果,where之后出现<,>,between,in等操作
explain select * from user where id > 2;
-- index 需要扫描索引上的全部数据
alter table user2 add primary key(id);
explain select id from user2;
-- all 全表扫描
explain select * from user2;
效率从好到会依次是system > const > eq_ref > ref > range > index > all 所以我们优化要尽可能的往system方向(向左)优化。
(6)explain之key_len字段解释
create table user4(
id int,
name varchar(30),
address char(30)
);
insert into user4 values(1,'sss','ssssss'),(2,'ddd','dddddd')
-- 给user4的id列添加主键索引
alter table user4 add primary key(id);
explain select * from user4 where id =2; -- int占4字节,主键不允许有空值 key_len = 4
alter table user4 drop primary key;
-- 给user4的id列添加普通索引
create index index1 on user4(id);
explain select * from user4 where id =2; -- int占4字节,普通索引允许有空值 key_len = 4+1=5
drop index index1 on user4;
-- 给address字段添加普通索引
create index index2 on user4(address);
explain select * from user4 where address = 'ssssss'; -- char(30)定长字符串30个字符,一个中文字符占3字节在utf-8中,允许null key_len = 30*3+1 = 91
drop index index2 on user4;
-- 给address字段添加主键索引
alter table user4 add primary key(address);
explain select * from user4 where address = 'ssssss'; -- key_len = 30*3 = 90 不允许null
alter table user4 drop primary key;
-- 给name字段添加普通索引
create index index3 on user4(name);
explain select * from user4 where name = 'sss'; -- varchar(30)变长字符串相比于定长字符串要多加2bytes,允许nyll key_len = 30*3+2+1 = 93
drop index index3 on user4;
-- 给name字段添加主键索引
alter table user4 add primary key(name);
explain select * from user4 where name = 'sss'; -- key_len = 30*3+2 = 92
alter table user4 drop primary key;
(7)explain之extra字段解释
extra记录了额外的执行计划信息,主要有以下三种显示方式:
using filesort : 在排序过程中(order by)排序字段没有可用的索引,系统会自动使用一个外部的
索引进行排序,效率低(既表明order by字段没有建立索引)
using temporary :在分组group by的过程中需要建立临时表来暂存中间结果,效率低
using index : group by字段有索引,在分组的过程中直接可以在索引树上获取数据(是一种高效
的方式)
4、show profile 查看SQL语句的时间消耗
(1)show profiles命令显示若干SQL总时间消耗
select @@have_profiling; : 查看当前MySQL是否支持profile功能
set profiling = 1; : 开启profile功能(默认开启)
# 例如某些SQL语句
show databases;
use mydb5;
show tables;
select count(*) from user;
select * from user where id > 2;
show profiles;
(2)show profile for query id命令显示指定id的SQL在整个执行过程中不同状态的时间消耗
例如:show profile for query 22;
在找到最耗时的SQL语句后,MySQL支持进一步选择all, cpu, block io, context switch, page faults等选项查询该SQL语句到底是在使用什么资源时消耗时间最长。例如选择查看cpu的消耗时间:
show profile cpu for query 22;
5、trace跟踪器
在MySQL内部有很多组件(...优化器...),trace可以分析优化器的执行计划,这样我们就能知道为什么优化器选择A计划,而不选择B计划。
首先打开trace,设置格式为json(json格式是人所能看懂的格式,其他格式都是针对计算机的人看不懂,设置trace最大能够使用的内存大小防止在解析过程中因内存不足而导致结果显示不全)
SET optimizer_trace = "enabled=on", end_markers_in_json = on;
set optimizer_trace_max_mem_size = 1000000;
然后执行SQL语句,执行时MySQL自动对该语句进行优化,并将优化的结果保存在系统数据库information_schema的optimizer_trace表中,查看该表就能得到SQL语句具体的内部优化过程。
注意:select * from information_schema.optimizer_trace \G;只能在命令行下运行。
6、索引优化
(1)数据准备
其中sellerid为主键,(name, status, address)为组合普通索引
(2)避免索引失效--全值匹配
where字段出现的顺序与建立的索引顺序一致,且都是常数,效率高
explain select * from tb_seller where name='小米科技' and status = '1' and address = '北京市';
用and连接字段时,字段顺序不会影响效果,MySQL内部的优化器会自动将其优化为与索引顺序一致的情况。
explain select * from tb_seller where address = '北京市' and name='小米科技' and status = '1';
(3)避免索引失效--最左前缀法则
explain select * from tb_seller where name='小米科技';
explain select * from tb_seller where name='小米科技' and status = '1';
可以看到key_len分别市303,309,索引全部生效,效率高。
违背情况:
explain select * from tb_seller where status = '1';
索引未生效
explain select * from tb_seller where name = '小米科技' and address = '北京市';
key_len长度为303与where字段只有name时的情况一致,既目前的SQL语句在查询过程中只有name字段索引生效,address字段的索引未生效。
总结:索引顺序为name,status,address则where字段要么只有name要么是name,status要么是name,status,address,不能跳过最左的字段name,不能跳过中间的字段。
(4)避免索引失效--其他匹配原则
A:范围查询右边的列,不能使用索引
explain select * from tb_seller where name = '小米科技' and status > '1' and address = '北京市';
explain select * from tb_seller where name = '小米科技' and address = '北京市' and status > '1' ;
第一条sql的key_len为309,只有name,status字段用到索引;第二条sql的key_len也是309即只有name,status生效,这是因为sql各字段书写的顺序不是按照name,status,address书写的,mysql中的优化器会对该sql进行优化,自动优化为第一条sql的样子了。
B:不要在索引列上进行运算操作,索引将失效
explain select * from tb_seller where substring(name,5,2)='有限';
C:字符串不加单引号,索引失效
explain select * from tb_seller where name = '小米科技' and status = 1;
explain select * from tb_seller where name = '小米科技' and status = '1';
D:避免使用select *
explain select * from tb_seller where name = '小米科技' and address = '北京市';
explain select name from tb_seller where name = '小米科技' and address = '北京市';
explain select nickname from tb_seller where name = '小米科技' and address = '北京市';
注意Extra处使用select * 显示Using index condition表示在查询过程中使用了索引,此外还需要去原表和磁盘上读取数据,效率低。
Using where;Using index表示所查的内容在索引树上就能全部找到不需要去原表和磁盘上读取数据,效率高(覆盖索引)。
在查询过程中建议使用覆盖索引:select查询的内容是所建索引的子集。
Using index(单独出现):当前查询使用了覆盖索引,效率高
Using where(单独出现):当前查询未使用覆盖索引,效率低
Null:用到了索引一部分字段没有覆盖索引,需要回表查看
E:尽量少使用or关键字---------一般情况下使用or是不会使用索引的
计算机看到or关键字,回自动取最坏的情况例如:
explain select * from tb_seller where name = '小米科技' or address = '西安市';
or表示或者的意思既在第一次筛选时可以按name筛选也可以按address筛选,如果按name筛选,时符合最左原则,如果按address筛选则不符合最左原则,此时有不符合的情况则该语句的索引失效。
explain select * from tb_seller where name = '小米科技' or status = '1';
情况一样不会使用索引
explain select * from tb_seller where name = '小米科技' or name = '千度科技';
F、以%开头的like模糊查询,索引失效
explain select * from tb_seller where name like '小米%';
explain select * from tb_seller where name like '%有限%';
解决方案:使用覆盖索引
explain select name from tb_seller where name like '%有限%';
G:如果MySQL评估后发现使用索引还不如全表扫描快时,即使建立的索引,系统也不会用
create index index_address on tb_seller(address);
explain select * from tb_seller where address = '北京市';
原始tb_seller表中address字段大量都是北京市,即使给address字段建立的索引,但优于重复的内容太多导致从建立索引树,到使用索引树来查找所用的时间消耗比全表扫描都慢,因此即便address建立的索引,系统也不会使用。
explain select * from tb_seller where address = '西安市';
西安市只有一条数据,数据量少,此时使用索引比全表扫描快,因此系统会使用索引。
上述情况是由数据本身的特点决定的,查找数据量少的值索引生效
H:is null 、is not null
create index index_nickname on tb_seller(nickname);
explain select * from tb_seller where nickname is NULL;
explain select * from tb_seller where nickname is NOT NULL;
由数据本身的特点决定,数据量少的情况索引生效
I:in、not in
where字段为普通索引情况:
in:数据量少的情况下索引生效
not in:索引必然失效
explain select * from tb_seller where nickname in('阿里小店','百度小店'); # 生效
explain select * from tb_seller where nickname not in('阿里小店','百度小店'); # 失效
explain select * from tb_seller where nickname in('阿里小店','百度小店','华为小店','传智博客','黑马程序员','罗技小店','掌趣小店','千度小店','OPPO官方旗舰店','新浪官方旗舰店'); # 失效
explain select * from tb_seller where nickname not in('阿里小店','百度小店','华为小店','传智博客','黑马程序员','罗技小店','掌趣小店','千度小店','OPPO官方旗舰店','新浪官方旗舰店','小米官方旗舰店'); # 失效
where字段为主键索引情况:
in与not in无论什么情况索引都生效
J:尽量使用复合索引
(name, status, address)复合索引就等价于name单列索引+(name,status)索引+(name, status, address)索引
如果一张表有多个单列索引,即使在where中这些索引都使用了,但只有一个最优的索引生效(由优化器决定)
例如:先删除上述复合索引,然后分别建立name,status,address的单列索引
drop index idx_seller_name_sta_addr on tb_seller;
create index index_name on tb_seller(name);
create index index_status on tb_seller(status);
create index index_address on tb_seller(address);
explain select * from tb_seller where name = '小米科技' and status = '1' and address = '西安市';
key为index_address实际生效的索引只有一个。
explain select * from tb_seller where status = '1' and address = '西安市';
7、SQL优化
(1)大批量导入数据
当需要向表中大批量导入数据(在某个文件中保存,100万个数据)时,需要进行优化来加快导入速度。
A:主键顺序插入
将导入的数据文件按主键排序好后在导入可以加快速度。
show global variables like 'local_infile'; : 查看导入功能是否开启
set global local_infile = 1; : 开启
加载数据语法:load data local infile '文件绝对路径' into table 表名 fields terminated by '字段间分隔符' lines terminated by '行分隔符';
B:关闭唯一性校验
有唯一索引的表在数据插入时,会对每一条数据进行唯一性校验,如果本身我们要插入的表肯定是满足唯一索引的,此时就不需要系统在进行校验。
在导入数据前关闭唯一性校验,在导入结束后恢复唯一性校验,可以提高导入速度
SET UNIQUE_CHECKS=0;
SET UNIQUE_CHECKS=1;
(2)insert优化
A:插入多行数据时,尽量使用多个值的insert语句,这种方式将大大缩减客户端与数据库之间的连接,关闭等消耗。效率比分开执行单个insert快
原方式:
insert into tb_test values(1,'Tom');
insert into tb_test values(2,'Jam');
insert into tb_test values(3,'Jerry');
优化后:
insert into tb_test values(1,'Tom'),(2,'Jam'),(3,'Jerry');
B:在事务中进行插入可以提高效率
begin;
insert into tb_test values(1,'Tom');
insert into tb_test values(2,'Jam');
insert into tb_test values(3,'Jerry');
commit;
C:数据有序插入可以提高效率
(3)order by优化
A:尽量不要让SQL语句出现Using filesort
补充:explain select id, age from emp order by age, salary; --Using index
Using filesort说明使用了系统额外的排序,效率低(有两种情况要么order by字段没有索引,要么该sql语句需要回表过程),Using index说明整条语句的执行全程都是在使用我们自己建的索引,没有用到其他手段,效率高(条件:order by字段有索引且查找的内容在索引树中可以全部找到)。
B:filesort优化
在某些特定任务需求下我们不得不Using filesort,此时需要加快filesort的排序速度。MySQL提供两种排序算法:二次扫描算法和一次扫描算法。
二次扫描算法在MySQL低版本用的多,它首先在内存中进行排序,内存不够用会自动在临时表中排序,因此该操作涉及临时表的使用,可能会导致大量随机I/O操作,效率低。
一次扫描算法在MySQL高版本中常用,它一次性取出所有满足条件的字段,然后在内存中全部排序,不使用临时表。该方式内存开销大,但效率高。
MySQL根据sort_buffer_size和max_length_for_sort_data变量的值,来决定使用什么算法。可以通过提高它们的值,使MySQL选择一次扫描算法排序,提高排序效率。
show variables like 'max_length_for_sort_data';
show variables like 'sort_buffer_size';
(4)子查询优化
在某些情况下子查询可以被高效的连接查询(JOIN)代替,子查询一般都涉及数据的临时保存,会用到临时表,效率低。连接查询直接得结果,效率高。当然join有时候效率也不高,毕竟需要进行表与表的关联。
(5)limit查询优化
假如目前的表格数据量很庞大,大量的数据都没什么用,我只想查看900000-900010的数据,需要利用limit 900000,10,此时需要MySQL对前900010个数据进行排序,仅仅返回900000-900010的记录,其他记录丢弃,查询代价很大,需要进行优化。
A:在索引上完成排序分页操作,然后根据主键关联回原表查询所需要的其他列内容。
假设有一张tb_user表,该表有1000000条数据,主键为id列
select * from tb_user limit 0,10; # 0s
select * from tb_user limit 900000,10; # 0.643s
select * from tb_user a, (select id from tb_user order by id limit 900000,10) b where a.id = b.id; # 0.486s
B:可以把limit查询转换成某个位置的查询,该方案适用于主键自增的表
select * from tb_user where id > 900000 limit 10; # 0s
补充:
(1)select sleep(10); : 休眠10s