1、索引
1.1、概念
1.2、索引的优势和劣势
1.3、索引的分类
1.4、mysql索引结构
1.4.1、B树的检索原理
1、初始化介绍:
一颗B+树,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示)。
如:磁盘块1包含数据项17,35,包含指针P1,P2,P3,
P1表示小于17的磁盘块,P2表示在17-35的磁盘块,P3表示大于35的磁盘块
真实的数据存在于叶子节点即3,5,9,10,13,15,28,29,36,60,75,79,90,99
非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17,35并不真实存在于数据表中
2、查找过程
如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘IO时间)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO。29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO。同时内存中做二分查找找到29,结束查询,总计三次IO。
第三层的B+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高是巨大的,如果没有索引,每个数据项都要发生一次IO,那么共需要百万次的IO,显然成本是非常高的。
1.5、哪些情况需要创建索引
1.6、哪些情况不能创建索引
1.7、性能分析
1.7.1、Mysql Query Optimizer
1.7.2、Mysql常见瓶颈
1.7.3、Explain
1.7.3.1、是什么(查看执行计划)
1.7.3.2、能干什么
1.7.3.3、怎么玩
1.7.3.4、字段的含义
1.7.3.4.1、Id
Id相同:
Id不同:
就像一层层递归,想知道t2就必须先知道t1,想知道t1又必须先知道t3
Id相同不同,同时存在:
1.7.3.4.2、select-type
1、类别
查询的类型,主要是用于区别普通查询,联合查询,子查询等复杂查询
1.7.3.4.3、table
显示这一行的数据是关于哪张表的
1.7.3.4.4、type
访问类型排列:
1.7.3.4.4.1、System
表只有一行记录(等于系统表),这是constant(常量)类型的特例,平时不会出现,这个也可以忽略不计。在现实生活中,肯定会有大量的数据,不可能只有一行记录,这个只是理想中的最优。
1.7.3.4.4.2、Const
表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。如:where后面的条件是一个主键,mysql就能将该查询转换为一个常量。
(select * from t1 where id=1)这里面的id=1就相当于一个常量,这个id是主键,具有唯一性,所以t1表的查询就是const,d1表的查询只有一张表一行数据,所以就是system
1.7.3.4.4.3、eq-ref
唯一性索引扫描,对于每一个索引键,表中只有一条数据与之匹配。常见于主键或唯一索引扫描(例如,要找一个公司的ceo,一个公司只有一个ceo)
1.7.3.4.4.4、Ref
非唯一性索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体(例如,要找一个公司的程序员,一个公司可以有很多程序员)
1.7.3.4.4.5、range
只检索给定范围的行,使用一个索引来选择行。Key列显示使用了哪个索引,一般就是在你的where语句中出现了between、<、>、in等查询,这种范围扫描索引比扫描全表要好,因为它只需要开始于索引的某一点,而结束于另一点,不用扫描全部索引。
1.7.3.4.4.6、index
Full index scan,index与all区别为index类型只遍历索引树。这通常比all快,因为索引文件通常比数据文件小。(也就是说虽然all和index都是读全表,但是index是从索引中读取出的,而all是从硬盘中读的)。
1.7.3.4.4.7、all
全表扫描
1.7.3.4.5、possible-keys
1.7.3.4.6、key
上图中 应该是复合索引,而不是覆盖索引
t2表中的col1和col2字段被作为一个复合索引名为idx_col1_col2,我们查询的字段是col1,col2,这刚好是一个索引,所以可以直接在索引树里面进行完成,它既然是在索引树里面完成的,那使用到的索引一定就是该索引树。
1.7.3.4.7、key_len
查询条件越精确,越多,key_len就会越长
1.7.3.4.8、ref
1.7.3.4.9、rows
没建索引之前
建立索引之后
1.7.3.4.10、extra
1.7.3.4.10.1、Using filesort(最好不要出现)
这里创建了一个复合索引,把字段col1,2,3都整合为索引,前者的explain语句后面order by的条件只用到了col3,整个语句中只用到了col1和col3,然而复合索引是col1,2,3,所以在extra里面出现了using filesort,这是因为没有安全索引的要求来;而后者,explian语句完整的包括了col1,2,3,所以在extra里面并没有出现using filesort。在实际应用中,应该尽量避免using filesort的出现。
1.7.3.4.10.2、using temporary(最好不要出现)
总结:当使用到索引,也使用到group by的时候,group by后面一定要跟索引的个数和顺序一样,否则很容易出现临时表和文件排序,而这两个都是我们不希望出现的。
1.7.3.4.10.3、using index(最好要出现)
覆盖索引的意思就是,我用了col1,col2,col3作为创建复合索引的字段,那最后查询的时候,所查询的字段也正好是col1,col2,col3。
1.7.3.4.10.4、其他不重要的
1、using where
表明使用了过滤
2、using jion buffer
使用了连接缓存
3、impossible where
Where子句的值总是false,不能用来获取任何元组
一个人的名字不可能同时为两个,所以提示,这个where是不可能的where
4、select tables optimized away
5、Distinct
1.8、索引优化
1.8.1、索引分析
索引最好建立在要经常查询的字段中
如果是单表查询,直接建立一个索引即可
如果是两表连接的查询,那左连接就把索引建在右边的表中
右连接就把索引建在左边的表中。
1.8.2、索引失效(要避免)
索引失效的原因如下
1.8.2.1、全值匹配
只要建立复合索引时,例如name,age,pos这几个字段按顺序建立一个复合索引,在explain的时候,在where条件语句中把name写在第一位,并且按顺序写条件,就不会失效,可以参考下面的第二条。
1.8.2.2、最佳左前缀
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列
如果我们建立一个按顺序的复合索引name,age,pos,然后第一个例子我们的条件只有age和pos,可以看到索引已经失效了,而且进行了全表扫描,这是我们应该要避免的
同理看第二个例子,我们只查询pos字段,虽然这个字段也在索引里面,但是他不是作为索引的第一个,索引这样查询的时候,也是会进行全表扫描,并且索引失效
总结:当建立了复合索引的时候,在查询条件语句中(where语句),一定要把位于第一个的字段也放在里面,也就是这里的name。
如果说我们建立索引的时候,是按age,name,pos这样的顺序排列的,那在查询条件语句中,我们就必须把age写在第一位。也就是说,哪个字段放在建立复合索引的第一个位置,那么那个字段名就必须出现在where语句中。
注意:这就相当于是一辆火车,位于复合索引第一个位置的就是火车头,火车头不能没有,后面的字段相当于火车厢,火车厢可以有很多节,但要注意的是,火车厢一定是按顺序一节一节的,不能说中间断了一节或者几节,不然火车一样开不了,就像下面这样:
这里的where条件是name和pos,但是按照顺序的话应该是name,age,pos,可以看到这里虽然使用到了索引,但是这只是一个假象,再仔细看key_len,如果是name和pos都使用到了索引,那在key_len这里应该是大于74的,之前我们只查询name的时候,key_len就是74,当我们查询得越精确,这个长度会越大,而这里只有74,如果这个证据不够明显,再看ref,这里只有一个const,我们的where语句中是有两个“=”的,前后用and连接,说明这里有两个常量,正常来说,这里应该是两个const才对,由此可见,这里虽然使用到了索引,但是这个索引只是name使用的索引,后面的pos并没有使用到索引,这里还是属于索引失效。这也正说明了,我们的顺序是name,age,pos,当我们越过中间的age,也就是中间的火车厢,那后面的火车厢就断了,无法继续行驶。
正常情况下的查询两个字段
1.8.2.3、不在索引列上做任何操作
计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
第一次explain的时候是正常使用到了索引的name,所以查询也是很不错的
第二次在name这个索引外面加了个条件,left是个函数,所以查询出来是很垃圾的
如果不用explain就这样正常去查询,select * from staffs where name=‘july’和
Select * from staffs where left(name,4)=’july’都是可以查询出july的,但是当使用explain查询性能的时候,就可以很清楚的看到如果在索引列上加了操作,就会使索引失效
1.8.2.4、存储引擎不能使用索引中范围条件右边的列
范围 若有索引则能使用到索引,范围条件右边的索引会失效(范围条件右边与范围条件使用的同一个组合索引,右边的才会失效。若是不同索引则不会失效)
可以看到前面三个都能正常查询,但是最后一个,当索引列变成了一个范围,这个时候的type就变成了range,说明name这个索引列用到了,age这个索引列也用到了,也可以从key_len这个值来看,是78,说明也是用到了两个索引列,但是,很明显,age后面的那个pos就索引失效了。而且这里的name和age这两个索引列是不一样的,name是正儿八经的查找,而age是一个范围,就变成了排序。
1.8.2.5、尽量使用覆盖索引
只访问索引的查询(索引列和查询列一致)),减少select *
当索引列和查询列一致时,extra会出现使用索引的提示,这是很好的。
第一个:
当age是一个范围的时候,key_len就变成了74,说明age这个索引并没有被使用到,这里的type已经变成了ref,不再是range,而age>25是一个range的范围,ref的优先级比range高,所以这里age这个索引列并不能被使用到。
第二个:
这里的age是一个常量,所以可以看到,这里的key_len是78,ref也是两个const,说明这里的age索引列是被用到了
第三个:
同理第二个
1.8.2.6、mysql在使用(!= 或者<>)的时候无法使用索引
索引 idx_nameAgeJob
idx_name
使用 != 和 <> 的字段索引失效( != 针对数值类型。 <> 针对字符类型
前提 where and 后的字段在混合索引中的位置比当前字段靠后 where age != 10 and name=‘xxx’ ,这种情况下,mysql自动优化,将 name=‘xxx’ 放在 age !=10 之前,name 依然能使用索引。只是 age 的索引失效)
1.8.2.7、is not null 也无法使用索引,但是is null是可以使用索引的
1.8.2.8、like以通配符开头(’%abc…’)mysql索引失效
使用的like关键字的时候,如果%通配符出现在最前面,就会索引失效,引发全表扫描。
like ‘%abc%’ type 类型会变成 all
like ‘abc%’ type 类型为 range ,算是范围,可以使用索引
注意:在实际生产环境中,有可能必须使用到%字符串%这种形式
例如:我们用不会让索引失效的方法去查询,连结果都查询不到,这样是没有意义的。
使用%字符串%可以让索引不失效的办法:
CREATE TABLE `tbl_user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`NAME` VARCHAR(20) DEFAULT NULL,
`age` INT(11) DEFAULT NULL,
email VARCHAR(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
这张表里一共有四个字段,分别是id,name,age,email
CREATE INDEX idx_user_nameAge ON tbl_user(NAME,age);
创建一个复合索引,将字段name,age作为索引列
可以看到,这里即使使用了%字符串%,只要查询列表是索引列表或者索引列表的一部分,索引都不会失效。
因为id是自增的主键,也是一个索引,所以就算select后面有id字段,也是不会使索引失效的。
但是如果在查询列表中加入了不是索引列的字段,就会让索引失效。
1.8.2.9、字符串不加单引号索引失效
这里只是单纯地查找而已,name是一个varchar类型,在mysql中字符串加不加单引号都能查出来,建议要加。
但是如果加了explain就不一样了:
很明显,后面因为字符串name没有加单引号,所以索引失效了。这也就是1.8.2.3所说的,不要在索引列上做任何操作,自动或者手动进行数据转换也不可以。在mysql中,数值和字符可以自动转换。
1.8.2.10、少用or
可以正常查询,但是性能不高
1.8.3、总结
假设index(a,b,c)
Where语句 索引是否被使用
where a = 3 Y,使用到a
where a = 3 and b = 5 Y,使用到a,b
where a = 3 and b = 5 and c = 4 Y,使用到a,b,c
where b = 3 或者 where b = 3 and c = 4 或者 where c = 4 N
where a = 3 and c = 5 使用到a, 但是c不可以,b中间断了
where a = 3 and b > 4 and c = 5 使用到a和b, c不能用在范围之后,b后断了
where a = 3 and b like 'kk%' and c = 4 Y,使用到a,b,c
where a = 3 and b like '%kk' and c = 4 Y,只用到a
where a = 3 and b like '%kk%' and c = 4 Y,只用到a
where a = 3 and b like 'k%kk%' and c = 4 Y,使用到a,b,c
1.9、注意事项
【建表语句】
create table test03(
id int primary key not null auto_increment,
c1 char(10),
c2 char(10),
c3 char(10),
c4 char(10),
c5 char(10)
);
insert into test03(c1,c2,c3,c4,c5) values('a1','a2','a3','a4','a5');
insert into test03(c1,c2,c3,c4,c5) values('b1','b2','b3','b4','b5');
insert into test03(c1,c2,c3,c4,c5) values('c1','c2','c3','c4','c5');
insert into test03(c1,c2,c3,c4,c5) values('d1','d2','d3','d4','d5');
insert into test03(c1,c2,c3,c4,c5) values('e1','e2','e3','e4','e5');
select * from test03;
【建索引】
create index idx_test03_c1234 on test03(c1,c2,c3,c4);
show index from test03;
1.9.1、注意一
这里的顺序和创建索引列的顺序不一样,但是一样使用到了索引,而且是全部索引,这是因为,mysql会自动在内部进行调优。之前说的一定要按顺序来,其实指的是,中间的某一个索引列不能断,就是所有的索引列必须都写上,哪怕创建索引的时候处于第一个位置的索引列在where条件中不是处于第一个位置,只要把所有的索引列都写在where语句中,mysql就会自动进行调优,索引不会失效。
总之就是一句话:
如果索引列全部都写上去了,就不用关心其顺序,mysql会自动进行调优,但是如果索引列没有全部写上去,按顺序从c1,c2,c3,c4这样写,只要是按顺序的,中间没有少,(c1,c2或者c1,c2,c3)这样索引也不会失效,但是如果是(c1,c3,c4)中间少了个c2,这样就只有c1用到了索引,c3,c4的索引就失效了。
1.9.2、注意二
explain select * from test03 where c1='a1' and c2='a2' and c3>'a3' and c4='a4';
这里的c3是个范围,所以在c3这里就是范围之后全失效,c4也就用不到索引了。
1.9.3、注意三
explain select * from test03 where c1='a1' and c2='a2' and c4>'a4' and c3='a3';
这里跟注意二是相似的,但是有一点不同的是,这里把所有的索引列全部都写上了,所以mysql会自动进行调优,它会把c3=‘a3’放到c4>‘a4’的前面,而c4因为是个范围,所以范围之后全失效,但是c4后面已经没有东西了,所以无所谓。所以其实所有的索引列都用到了索引。
1.9.4、注意四
explain select * from test03 where c1='a1' and c2='a2' and c4='a4' order by c3;
这里虽然把所有的索引列都写上了,但是可以看到key_len是22,说明只有c1和c2用到了索引,c4因为中间断了个c3,所以没用到。
但是严格意义上来说,c3也是用到了索引,索引的作用有两个,一个是查找,一个是排序,这里的c3其实就是在排序。
1.9.5、注意五
explain select * from test03 where c1='a1' and c2='a2' order by c3;
跟注意四一样,其实这里有没有c4都一样,因为oder by c3跟and c3不一样,mysql不能进行自动调优,所以在c2的后面,就已经断了,所以这个结果会跟注意四的结果一样。
1.9.6、注意六
explain select * from test03 where c1='a1' and c2='a2' order by c4;
这里c1,c2用到了索引,但是没有c3,所以就只有两个用到了索引,但是这里还出现了filesort,这个是我们要尽量避免的,因为我们索引列是c1,c2,c3,c4,这里c3不见了,直接跳到c4,mysql只能自己内部进行排序,就出现了filesort,这是很耗性能的。
1.9.7、注意七
1.9.7.1、 无filesort
explain select * from test03 where c1='a1' and c5='a5' order by c2,c3;
只用c1一个字段索引,但是c2、c3用于排序,
1.9.7.2、有filesort
explain select * from test03 where c1='a1' and c5='a5' order by c3,c2;
我们建的索引是c1,c2,c3,c4,它没有按照顺序来,c3,c2 颠倒了。
对比一下:
explain select * from test03 where c1='a1' and c2='a2' order by c2,c3;
两者没有任何差别,因为在c2=‘a2’后面,跟的不是c3=‘a3’而是c5=‘a5’,中间断了个c3和c4,所以c5是索引失效的。
1.9.8、注意八
explain select * from test03 where c1='a1' and c2='a2' and c5='a5' order by c2,c3;
这里c1=‘a1’,c2=‘a2’都存在了,c1,c2是正常索引,所以后面的order by c2,c3也可以正常进行排序,并没有断掉中间的一个,所以没有filesort
对比一下:
explain select * from test03 where c1='a1' and c2='a2' and c5='a5' order by c3,c2;
这里和上面的1.9.7.2不一样,在1.9.7.2里面,where后面只有c1=‘a1’,后面就是c5=‘c5’,然后后面order by的时候,是c3在前,c2在后,这样的话,是顺序颠倒的,所以会出现filesort
但是这里where后面是c1=‘a1’,c2=‘a2’,这里已经用到了c1,c2两个索引了,所以后面order by的顺序即使是c3在前,c2在后,因为c2的索引已经使用到了,c2就变成了一个固定的常量,所以后面的order by 就变成了order by c3,常量(c2),所以不会出现filesort。
1.9.9、注意九
explain select * from test03 where c1='a1' and c4='a4' group by c2,c3;
对比一下:
前面的是c2,c3的顺序,所以没有异常,后面的顺序是c3,c2,不仅出现了filesort,还出现了temporary。
2、查询截取分析
2.1、查询优化
2.1.1、小表驱动大表
实验:
1、有索引 大表驱动小表
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
select sql_no_cache sum(sal) from emp where exists (select 1 from dept where emp.deptno=dept.deptno);
##用 exists 是否存在,存在返回一条记录,exists 是作为一个查询判断用,所以 select 后返回什么不重要。
select sql_no_cache sum(sal) from emp inner join dept on emp.deptno=dept.deptno;
2、有索引 小表驱动大表
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e where exists (select 1 from emp where e.deptno=emp.deptno);
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e inner join (select distinct deptno from emp) m on m.deptno=e.deptno;
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
有索引小表驱动大表 性能优于 大表驱动小表
3、无索引 小表驱动大表
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e where exists (select 1 from emp where e.deptno=emp.deptno);
select sql_no_cache sum(e.sal) from (select * from emp where id<10000) e inner join (select distinct deptno from emp) m on m.deptno=e.deptno;
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
4、无索引大表驱动小表
select sql_no_cache sum(sal) from emp where deptno in (select deptno from dept);
select sql_no_cache sum(sal) from emp where exists (select 1 from dept where emp.deptno=dept.deptno);
select sql_no_cache sum(sal) from emp inner join dept on emp.deptno=dept.deptno;
结论:
有索引的情况下
用 inner join 是最好的 其次是 in ,exists最糟糕
无索引的情况下小表驱动大表
因为join 方式需要distinct ,没有索引distinct消耗性能较大
所以 exists性能最佳 in其次 join性能最差?
无索引的情况下大表驱动小表
in 和 exists 的性能应该是接近的 都比较糟糕 exists稍微好一点 超不过5% 但是inner join 优于使用了 join buffer 所以快很多
如果left join 则最慢
2.1.2、order by关键字优化(关注filesort)
ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序
2.1.2.1、建表
CREATE TABLE tblA(
id int primary key not null auto_increment,
age INT,
birth TIMESTAMP NOT NULL,
name varchar(200)
);
INSERT INTO tblA(age,birth,name) VALUES(22,NOW(),'abc');
INSERT INTO tblA(age,birth,name) VALUES(23,NOW(),'bcd');
INSERT INTO tblA(age,birth,name) VALUES(24,NOW(),'def');
创建索引
CREATE INDEX idx_A_ageBirth ON tblA(age,birth,name);
查看表
SELECT * FROM tblA;
2.1.2.2、案例
1、explain SELECT * FROM tblA where age>20 order by age;
我们建立的索引是age,birth,name三者都有,这里age在第一个位置,火车头有了,而且age是在索引上,所以不会出现filesort。
2、explain SELECT * FROM tblA where age>20 order by age,birth;
同理,这里也不会出现filesort
3、explain SELECT * FROM tblA where age>20 order by birth;
这里出现了filesort,是因为where后面并不是一个常量,而且,如果想不出现filesort,那在order by 后面也应该按照创建索引列的顺序去写,如果顺序颠倒了,就会出现filesort。
4、explain SELECT * FROM tblA where age>20 order by birth,age;
与3同理
5、explain SELECT * FROM tblA where age=20 order by birth,age;
这里的age是一个常量,所以order by后面就相当于 order by birth,常量(age),所以不会出现filesort。
6、explain SELECT * FROM tblA order by birth;
创建索引的时候是age,birth,name,这里连火车头age都没有,直接就按照birth排序,mysql只能内部进行排序,就出现了filesort。
7、explain SELECT * FROM tblA where birth > '2020-09-02 00:00:00'order by birth;
与6同理
8、explain SELECT * FROM tblA where birth > '2020-09-02 00:00:00'order by age;
Filesort出现的原因是因为排序,如果有跳过中间的一个给后面的一个排序,例如(where c1=a1 order by c3)这样就会出现filesort,或者将要排序的索引列顺序颠倒,例如(where c1=a1 order by c3,c2),这样也会出现filesort,但是这里的order by 后面就是age,就是索引列的第一个,所以不会出现filesort。
9、explain SELECT * FROM tblA order by age asc,birth desc;
我们在创建索引的时候,是自动按照升序来排的,也就是说age,birth都是按照升序排列的,但是当我们把birth按照降序排了之后,原来的升序就用不到了,所以mysql只能内部进行排序就出现了filesort。
与下面进行对比:
explain SELECT * FROM tblA order by age asc,birth asc;
这里两个都是升序,就跟原来的默认升序一样,所以不会出现filesort。
2.1.2.3、结论
MySQL支持二种方式的排序,FileSort和Index,Index效率高,它指MySQL扫描索引本身完成排序。FileSort方式效率较低。
1、ORDER BY满足两情况,会使用Index方式排序:
ORDER BY 语句使用索引最左前列
使用Where子句与Order BY子句条件列组合满足索引最左前列
where子句中如果出现索引的范围查询(即explain中出现range)会导致order by 索引失效。
2、尽可能在索引列上完成排序操作,遵照索引建的最佳左前缀
第二种中,where a = const and b > const order by b , c 不会出现 using filesort b , c 两个衔接上了
但是:where a = const and b > const order by c 将会出现 using filesort 。因为 b 用了范围索引,断了。而上一个 order by 后的b 用到了索引,所以能衔接上 c
3、如果不在索引列上,filesort有两种算法:mysql就要启动双路排序和单路排序
双路排序:
MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据,读取行指针和orderby列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。
(多路排序需要借助 磁盘来进行排序。所以 取数据,排好了取数据。两次 io操作。比较慢
单路排序 ,将排好的数据存在内存中,省去了一次 io 操作,所以比较快,但是需要内存空间足够。)
在mysql4.1之后,出现了第二种改进的算法,就是单路排序:
从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
单路虽然比多路要好,但是单路也存在一些问题:
在sort_buffer中,方法B比方法A要多占用很多空间,因为方法B是把所有字段都取出, 所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取取sort_buffer容量大小,再排……从而多次I/O。
本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。
解决办法:
1、增大sort_buffer_size参数的设置(用于单路排序的内存大小)
2、增大max_length_for_sort_data参数的设置(单次排序字段大小。(单次排序请求))
3、去掉select 后面不需要的字段(select 后的多了,排序的时候也会带着一起,很占内存,所以去掉没有用的)
提高Order By的速度
-
Order by时select * 是一个大忌只Query需要的字段, 这点非常重要。在这里的影响是:
1.1 当Query的字段大小总和小于max_length_for_sort_data 而且排序字段不是 TEXT|BLOB 类型时,会用改进后的算法——单路排序, 否则用老算法——多路排序。
1.2 两种算法的数据都有可能超出sort_buffer的容量,超出之后,会创建tmp文件进行 合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高 sort_buffer_size。 -
尝试提高 sort_buffer_size
不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为 这个参数是针对每个进程的 -
尝试提高 max_length_for_sort_data
提高这个参数, 会增加用改进算法的概率。但是如果设的太高,数据总容量超出 sort_buffer_size的概率就增大,明显症状是高的磁盘I/O活动和低的处理器使用率.
2.1.2.4、分页查询的优化–limit
EXPLAIN SELECT SQL_NO_CACHE * FROM emp ORDER BY deptno LIMIT 10000,40
那我们就给deptno这个字段加上索引吧。
然并卵。
优化: 先利用覆盖索引把要取的数据行的主键取到,然后再用这个主键列与数据表做关联:(查询的数据量小了后)
EXPLAIN SELECT SQL_NO_CACHE * FROM emp INNER JOIN (SELECT id FROM emp e ORDER BY deptno LIMIT 10000,40) a ON a.id=emp.id
最后比较一下查询速度:
优化前:
优化后:
实践证明: ①、order by 后的字段(XXX)有索引 ②、sql 中有 limit 时,
当 select id 或 XXX字段索引包含字段时 ,显示 using index
当 select 后的字段含有 order by 字段索引不包含的字段时,将显示 using filesort
2.1.4.5、group by优化
2.2、慢查询日志
2.2.1、什么是慢查询日志
MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上的语句。
由他来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合之前explain进行全面分析。
2.2.2、如何使用
2.2.2.1、前期说明
默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。
当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件
2.2.2.2、查看是否开启以及如何开启
查看慢查询开启状态:SHOW VARIABLES LIKE ‘%slow_query_log%’;
开启慢查询:set global slow_query_log=1;
2.2.2.3、慢查询工作原理
这个是由参数long_query_time控制,默认情况下long_query_time的值为10秒,
命令:SHOW VARIABLES LIKE 'long_query_time%';
可以使用命令修改,如果使用命令就是暂时的,当mysql服务重启后,这个修改就不存了,会恢复到原来的默认值:set global long_query_time=5
也可以在my.cnf参数里面修改,这里的修改就是配置文件的修改,是永久性的:
【mysqld】下配置:
slow_query_log=1;
slow_query_log_file=/var/lib/mysql/atguigu-slow.log
long_query_time=3;
log_output=FILE
假如运行时间正好等于long_query_time的情况,并不会被记录下来。也就是说,
在mysql源码里是判断大于long_query_time,而非大于等于。
2.2.2.4、查询当前系统中共有多少慢查询记录
show global status like '%Slow_queries%';
2.3、批量插入数据脚本
插入1qw条数据
2.3.1、建库建表
新建库
create database bigData;
use bigData;
#1 建表dept
CREATE TABLE dept(
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
deptno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,
dname VARCHAR(20) NOT NULL DEFAULT "",
loc VARCHAR(13) NOT NULL DEFAULT ""
) ENGINE=INNODB DEFAULT CHARSET=UTF8 ;
#2 建表emp
CREATE TABLE emp
(
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
empno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0, /*编号*/
ename VARCHAR(20) NOT NULL DEFAULT "", /*名字*/
job VARCHAR(9) NOT NULL DEFAULT "",/*工作*/
mgr MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,/*上级编号*/
hiredate DATE NOT NULL,/*入职时间*/
sal DECIMAL(7,2) NOT NULL,/*薪水*/
comm DECIMAL(7,2) NOT NULL,/*红利*/
deptno MEDIUMINT UNSIGNED NOT NULL DEFAULT 0 /*部门编号*/
)ENGINE=INNODB DEFAULT CHARSET=UTF8;
2.3.2、设置参数
创建函数,假如报错:This function has none of DETERMINISTIC…
#由于开启过慢查询日志,因为我们开启了 bin-log, 我们就必须为我们的function指定一个参数。
show variables like 'log_bin_trust_function_creators';
set global log_bin_trust_function_creators=1;
这样添加了参数以后,如果mysqld重启,上述参数又会消失,永久方法:
windows下 my.ini[mysqld]加上log_bin_trust_function_creators=1
linux下 /etc/my.cnf下my.cnf[mysqld]加上log_bin_trust_function_creators=1
2.3.3、创建函数
保证每条数据都不同
1、随机产生字符串:
DELIMITER $$
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN ##方法开始
DECLARE chars_str VARCHAR(100 ) DEFAULT
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
##声明一个 字符窜长度为 100 的变量 chars_str ,默认值是所有大小写字母
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
##循环开始
WHILE i < n DO
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
##concat 连接函数 ,substring(a,index,length) 从index处开始截取
SET i = i + 1;
END WHILE;
RETURN return_str;
END $$
#假如要删除
#drop function rand_string;
2、随机产生部门编号:
#用于随机产生部门编号
DELIMITER $$
CREATE FUNCTION rand_num( )
RETURNS INT(5)
BEGIN
DECLARE i INT DEFAULT 0;
SET i = FLOOR(100+RAND()*10);
RETURN i;
END $$
#假如要删除
#drop function rand_num;
2.3.4、创建存储过程
1、创建往emp表中插入数据的存储过程
DELIMITER $$
CREATE PROCEDURE insert_emp10000(IN START INT(10),IN max_num INT(10))
BEGIN
DECLARE i INT DEFAULT 0;
#set autocommit =0 把autocommit设置成0 ;提高执行效率
SET autocommit = 0;
REPEAT ##重复
SET i = i + 1;
INSERT INTO emp10000 (empno, ename ,job ,mgr ,hiredate ,sal ,comm ,deptno ) VALUES ((START+i) ,rand_string(6),'SALESMAN',0001,CURDATE(),FLOOR(1+RAND()*20000),FLOOR(1+RAND()*1000),rand_num());
UNTIL i = max_num ##直到 上面也是一个循环
END REPEAT; ##满足条件后结束循环
COMMIT; ##执行完成后一起提交
END $$
#删除
DELIMITER ;
drop PROCEDURE insert_emp;
2、往dept表中插入数据的存储过程
#执行存储过程,往dept表添加随机数据
DELIMITER $$
CREATE PROCEDURE insert_dept(IN START INT(10),IN max_num INT(10))
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO dept (deptno ,dname,loc ) VALUES (START +i ,rand_string(10),rand_string(8));
UNTIL i = max_num
END REPEAT;
COMMIT;
END $$
#删除
DELIMITER ;
drop PROCEDURE insert_dept;
2.3.5、调用存储过程
调用dept:
DELIMITER ;
CALL insert_dept(100,10);
#执行存储过程,往emp表添加50万条数据
DELIMITER ; #将 结束标志换回 ;
CALL insert_emp(100001,500000);
CALL insert_emp10000(100001,10000);
2.4、show profil
2.4.1、show profile是什么
是mysql提供可以用来分析当前会话中语句执行的资源消耗情况。可以用于SQL的调优的测量
官网:http://dev.mysql.com/doc/refman/5.5/en/show-profile.htm
默认情况下,参数处于关闭状态,并保存最近15次的运行结果
2.4.2、分析步骤
1、查看当前的mysql是否支持show profile
Show variables like 'profiling';
默认是关闭,使用前需要开启
2、开启功能,默认是关闭,使用前需要开启
show variables like 'profiling';
set profiling=1;
3、运行sql语句
select * from emp group by id%10 limit 150000;
select * from emp group by id%20 order by 5
这两条语句只是为了让它查询的时间变长,方便我们在后台查看,我们已经开启了show profile,开启之后我们所有发过的指令都会被记录下来。
4、查看结果
第一列是我们在开启show profile之后,一共执行过多少次命令,第二列是执行的时间,第三列是我们执行的sql语句。
5、诊断sql
show profile cpu,block io for query n (n为上一步前面的问题SQL数字号码);
先来查询一下query3:show profile cpu,block io for query 3;
这是一句sql命令完整的生命周期。
诊断sql不止可以诊断cpu和io,还有其他的参数:
ALL --显示所有的开销信息
BLOCK IO --显示块IO相关开销
CONTEXT SWITCHES --上下文切换相关开销
CPU --显示CPU相关开销信息
IPC --显示发送和接收相关开销信息
MEMORY --显示内存相关开销信息
PAGE FAULTS --显示页面错误相关开销信息
SOURCE --显示和Source_function,Source_file,Source_line相关的开销信息
SWAPS --显示交换次数相关开销的信息
6、日常开发需要注意的结论
converting HEAP to MyISAM 查询结果太大,内存都不够用了往磁盘上搬了。
Creating tmp table 创建临时表
Copying to tmp table on disk 把内存中临时表复制到磁盘,危险!!!
locked
以上面的query8为例:
出现了tmp table临时表,现在如果有1千万多万条数据,我们查询出来的符合要求的有900万条,然后我们要把这900万条数据复制到临时表里面,用完之后再删除,这是很耗性能的,所以这里的这四个都是不能出现的。
2.5、全局查询日志
注意:这个只能在测试的时候用,绝对不能在生产环境中使用
2.5.1、配置文件开启
在mysql的my.cnf中,设置如下:
#开启
general_log=1
#记录日志文件的路径
general_log_file=/path/logfile
#输出格式
log_output=FILE
2.5.2、sql命令开启
set global general_log=1;
#全局日志可以存放到日志文件中,也可以存放到Mysql系统表中。存放到日志中性能更好一些,存储到表中
set global log_output='TABLE';
此后 ,你所编写的sql语句,将会记录到mysql库里的general_log表,可以用下面的命令查看
select * from mysql.general_log;
3、Mysql锁机制
3.1、概述
3.1.1、定义
锁是计算机协调多个进程或线程并发访问某一资源的机制。
在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
3.1.2、生活中的例子
打个比方,我们到淘宝上买一件商品,商品只有一件库存,这个时候如果还有另一个人买,
那么如何解决是你买到还是另一个人买到的问题?
这里肯定要用到事务,我们先从库存表中取出物品数量,然后插入订单,付款后插入付款表信息,然后更新商品数量。在这个过程中,使用锁可以对有限的资源进行保护,解决隔离和并发的矛盾。
3.2、锁的分类
3.2.1、对数据的操作类型(读/写)分
读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。
写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁。
3.2.2、对数据操作的粒度分
行锁
表锁
3.3、三锁
3.3.1、表锁(偏读)
3.3.1.1、特点
偏向MyISAM存储引擎,开销小,加锁快;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
3.3.1.2、案例分析(读锁)
3.3.1.2.1、建表与基本操作
【表级锁分析–建表SQL】
create table mylock(
id int not null primary key auto_increment,
name varchar(20)
)engine myisam;
insert into mylock(name) values('a');
insert into mylock(name) values('b');
insert into mylock(name) values('c');
insert into mylock(name) values('d');
insert into mylock(name) values('e');
select * from mylock;
【查看表上加过的锁】
show open tables;
当前状态下没有锁,看In_use参数即可,这里是0就是没有锁。
【手动增加表锁】
lock table 表名字1 read(write),表名字2 read(write),表名字n read(write);
lock table mylock read,test03 write;
将mylock这个表设置一个read读锁,test03这个表设置一个write写锁
【释放表锁】
unlock tables;
3.3.1.2.2、开启两个端口操作这个表
1、先将mylock这个表设置一个表锁,然后进行查看mylock表。开启另外一个端口,在另外一个端口查看这个表
因为读锁是共享锁,所以多个端口也是共同访问的。
2、查看能不能进行update的写操作
虽然设置的是读锁,但是写操作一样不能进行,锁还没有释放,当前框能做的只有查看自己的表。
3、查看能不能查看test03表
设了读锁的不能查看其它表,新的mysql对话可以访问别的表
4、新的mysql对话能否对设置读锁的表进行更改
输入更新的命令之后,一直在等待,没有任何反应,因为此时mylock表还在锁着,无法对其进行更新操作。专业名词叫阻塞。
将mylock解锁
再看黑底的mysql对话
这里的update已经操作成功,说明,只有锁被释放了,其他对话框才能对加锁的表进行更新的操作。
3.3.1.3、案例分析(写锁)
1、mylock表设置写锁
2、查看能否读自己加写锁的表
可以读自己加写锁的表
3、能否改自己锁过的表
可以
4、能否读其他表
不可以
5、新建会话能否读加写锁的表
阻塞,等待解锁
解锁之后,查看成功
注意:有些时候,新会话查询在没有解锁的情况下也能查询成功,那是因为当我们查询同一个数据次数多了,mysql会自动将数据加入到缓存里,而多次访问之后,就会直接从缓存里面取数据,所以最好每次换不同的条件,例如(每次都换个不同的id)
6、新建会话能否读其他没加锁的表
可以
3.3.1.4、结论
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。
MySQL的表级锁有两种模式:
表共享读锁(Table Read Lock)
表独占写锁(Table Write Lock)
锁类型 他人可读 他人可写
读锁 是 否
写锁 否 否
结论:
结合上表,所以对MyISAM表进行操作,会有以下情况:
1、对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
2、对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。
简而言之,就是读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞
3.3.1.5、表锁分析
查看哪些表被加锁了
Show open tables;
如何分析表锁定
可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定:
Show status like‘table%’;
table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取一次锁值+1
table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值+1),此值高说明存在着较严重的表级锁争用情况
此外,myisam的读写锁调度是写优先,这也是myisam不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永久阻塞。
3.3.2、行锁
3.3.2.1、特点
偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁
3.3.2.2、行锁支持事务,复习老知识
1、事务及其ACID属性:
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
l 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
l 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
l 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
l 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
2、并发事务处理带来的问题
更新丢失:
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。
例如,两个程序员修改同一java文件。每程序员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖前一个程序员所做的更改。
如果在一个程序员完成并提交事务之前,另一个程序员不能访问同一文件,则可避免此问题。
脏读:
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”。
一句话:事务A读取到了事务B已修改但尚未提交的的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取
的数据无效,不符合一致性要求。
不可重复读:
在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。一句话:一个事务范围内两个相同的查询却返回了不同数据。
幻读:
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
一句话:事务A 读取到了事务B提交的新增数据,不符合隔离性。
3.3.2.3、事务隔离级别
脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
常看当前数据库的事务隔离级别:show variables like ‘tx_isolation’;
3.3.2.4、案例分析
3.3.2.4.1、建表
create table test_innodb_lock (a int(11),b varchar(16))engine=innodb;
insert into test_innodb_lock values(1,'b2');
insert into test_innodb_lock values(3,'3');
insert into test_innodb_lock values(4,'4000');
insert into test_innodb_lock values(5,'5000');
insert into test_innodb_lock values(6,'6000');
insert into test_innodb_lock values(7,'7000');
insert into test_innodb_lock values(8,'8000');
insert into test_innodb_lock values(9,'9000');
insert into test_innodb_lock values(1,'b1');
建立索引
create index test_innodb_a_ind on test_innodb_lock(a);
create index test_innodb_lock_b_ind on test_innodb_lock(b);
查看test_innodb_lock表
select * from test_innodb_lock;
3.3.2.4.2、演示
1、Mysql5.0之后会自动进行提交,为了演示行锁,先把自动提交关了
Set autocommit = 0;
2、两个会话对同一个数据进行更新操作
会话二也进行更新操作,会话二阻塞
会话一提交
查看会话二,会话二更新成功。
3、两个会话对不同的数据进行更新操作
会话一对b=b2进行操作
因为我们把自动提交关闭了,所以这里要对会话一进行操作,就要先把会话二的数据提交,所以时间才会是16秒
会话二对b=3进行操作
直接执行成功,可见,如果不是对同一个数据进行操作是不会造成阻塞的。
注意:
会话一和会话二都是设置了不自动提交,新开的会话三是默认的自动提交。
会话一更新了b的数据,然后进行提交,然后在会话二里面进行查看,发现会话二里面显示的数据,并不是更新后的数据,而会话三显示的数据就是更新后的数据。
会话二进行提交之后,再次查询,数据已经更新完毕
3.3.2.5、无索引或索引失效-行锁升级为表锁
session-1 | session-2 |
---|---|
正常情况,各自锁定各自的行,互相不影响,一个2000另一个3000 | |
由于在column字段b上面建了索引,如果没有正常使用,会导致行锁变表锁 | – |
比如没加单引号导致索引失效,行锁变表锁(b是char类型) | 被阻塞,等待。只到Session_1提交后才阻塞解除,完成更新 |
这个实验当中,我们修改的是不同的数据,如果是行锁,是可以实现更新操作的,不需要等待的时间。但是这里却出现了阻塞,等到会话一commit之后,会话二才操作成功。
这种现象就叫做行锁变表锁,有索引的时候,特别要注意,不能让索引失效了,索引失效不仅会让性能下降,也会让行锁变成表锁。
3.3.2.6、select也可以加锁(锁定某一行)
1、共享锁(Share Lock)
共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获取共享锁的事务只能读数据,不能修改数据。
用法
SELECT … LOCK IN SHARE MODE;
在查询语句后面增加 LOCK IN SHARE MODE ,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表(行),而且这些线程读取的是同一个版本的数据。
2、排他锁(eXclusive Lock)
排他锁又称写锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。排他的意思就是,A给B加了锁,那A就独占B,其他的会话不能对其进行操作。
用法:
SELECT … FOR UPDATE;
给a=1加排他锁
Session2想更改a=1的值,只能阻塞
Session1进行commit提交
Session更新成功
在查询语句后面增加 FOR UPDATE ,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。
3.3.2.7、间隙锁的危害
间隙锁带来的插入问题
Session_1 | Session_2 |
---|---|
阻塞产生,暂时不能插入 | |
commit; | 阻塞解除,完成插入 |
Session1中我们做的是更新操作,将a字段从1到5全部更新一遍,但是在这个表里面,a的值是没有2的,同时在session2中插入a=2的数据,按道理来说,这是行锁,对不同数据的操作应该是没问题的,但是session2却阻塞了,直到session1 commit后,才成功。
Session1当中,a的值是2,3,4,实际上是没有2的,但是mysql为了保证数据的完整性,它会一起把2也给锁了,所以session2对a=2的数据进行操作,也是会造成阻塞的。
【什么是间隙锁】
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,
InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(GAP Lock)。
【危害】
因为Query执行过程中通过过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。
间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害
3.3.2.8、案例结论
Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,Innodb的整体性能和MyISAM相比就会有比较明显的优势了。
但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MyISAM高,甚至可能会更差。
3.3.2.9、行锁分析
【如何分析行锁定】
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
show status like ‘innodb_row_lock%’;
这里是因为我挂机吃饭去了,所以数据有点异常,数据太大了
对各个状态量的说明如下:
Innodb_row_lock_current_waits: 当前正在等待锁定的数量;
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度;
Innodb_row_lock_time_avg: 每次等待所花平均时间;
Innodb_row_lock_time_max: 从系统启动到现在等待最常的一次所花的时间;
Innodb_row_lock_waits: 系统启动后到现在总共等待的次数;
对于这5个状态变量,比较重要的主要是
Innodb_row_lock_time_avg(等待平均时长),
Innodb_row_lock_waits(等待总次数)
Innodb_row_lock_time(等待总时长)这三项。
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
最后可以通过
SELECT * FROM information_schema.INNODB_TRX\G;
来查询正在被锁阻塞的sql语句。
3.3.2.10、行锁优化建议
3.3.3、页锁
4、主从复制
4.1、复制的基本原理
Slave会从master读取binlog来进行数据同步
原理图:
三步骤:
1、master将改变记录到二进制日志(binary log)。这些记录过程叫做二进制日志事件,binary log events
2、slave将master的binary log events拷贝到它的中继日志(relay log)
3、slave重做中继日志中的事件,将改变应用到自己的数据库中。
Mysql的复制是异步的且串行化的。
4.2、复制的基本原则
1.每个slave只有一个master
2、每个slave只能有一个唯一的服务器ID
3、每个master可以有多个slave
4.3、复制的最大问题
网络延时
4.4、主从常见配置
4.4.1、主从机的配置文件配置
1、mysql版本一致,且后台以服务运行,不管主机与从机都是同一个系统,还是一个是windows,一个是linux,因为数据库之间要实现主从,那他们就必须能够通信,先用命令查询服务器的ip地址,然后互相去ping对方,能ping通才能继续下一步。
2、主从配置都在mysqld节点下,都是小写,配置文件是my.ini或者my.cnf
主机修改配置文件:
(1)、[必须]主服务器ID:server-id=1
(2)、[必须]启用二进制日志:log-bin=自己本地的路径/data/mysqlbin
例如:log-bin=D:/devSoft/MySQLServer5.5/data/mysqlbin
(3)、[可选]启用错误日志:log-err=自己本地的路径/data/mysqlerr
例如:log-err=D:/devSoft/MySQLServer5.5/data/mysqlerr
(4)、[可选]根目录:basedir="自己本地路径"
例如:basedir="D:/devSoft/MySQLServer5.5/"
(5)、[可选]临时目录:tmpdir="自己本地路径"
例如:tmpdir="D:/devSoft/MySQLServer5.5/"
(6)、[可选]数据目录:datadir="自己本地路径/Data/"
例如:datadir="D:/devSoft/MySQLServer5.5/Data/"
(7)、read-only=0:主机,读写都可以
(8)、[可选]设置不要复制的数据库:binlog-ignore-db=mysql
(9)、[可选]设置需要复制的数据库:binlog-do-db=需要复制的主数据库名字
从机修改配置文件:我的从机是linux的
[必须]从服务器唯一ID
[可选]启用二进制日志
因修改过配置文件,请主机+从机都重启后台mysql服务
4.4.2、主从机都关闭防火墙
windows手动关闭
关闭虚拟机linux防火墙 service iptables stop
Linux的如果不行,就去查看linux的笔记,查看防火墙部分
Linux端必须关闭防火墙,不然本机去ping虚拟机是ping不通的,本机和虚拟机要在同一个网段上,设置虚拟机的时候要注意分配id地址要跟本机的网段一样,具体可以参考博客。
Window也可以直接关闭防火墙,这样虚拟机就可以ping通本机了,但是本机也可以不关闭防火墙,下面是window不关闭防火墙,也能让虚拟机ping通的做法。
打开本地的防火墙,找到高级设置
在windows defender 防火墙属性那里,将入站连接设置为允许
点击入站规则,找到文件和打印机共享(IPV4-in),一定要是配置文件那一列是专用和公用那一列,然后将其设置为是。
即可ping通。
4.4.3、在windows主机上建立账户并授权给slave
1、GRANT REPLICATION SLAVE ON . TO ‘随意的名字’@‘从机器数据库IP’ IDENTIFIED BY ‘密码’;
例如:GRANT REPLICATION SLAVE ON *.* TO 'zhangsan'@'192.168.14.167' IDENTIFIED BY '123456';
赋予权限之后,还需要刷新一下权限,权限才能生效。
这里是创建一个账户,所以名字可以自己随便取,只要后面的ip是从机的ip即可,密码也是自己随意设置,,但是后面在从机里设置名字和密码的时候,要和这里一致。
2、查询master的状态:show master status;
记录下file和position的值,意思就是,从机要从mysqlbin.000005这个文件的第1267行开始复制。
如果有很多二进制文件,那master需要的二进制文件一定是最后一个。
3、注意:执行完此步骤后不要再操作主服务器MYSQL,防止主服务器状态值变化
4.4.4、在linux从机上配置需要复制的主机
1、
CHANGE MASTER TO MASTER_HOST='主机ip',
MASTER_USER='zhangsan',
MASTER_PASSWORD='123456',
MASTER_LOG_FILE='上面file的名字',MASTER_LOG_POS=上面position的值;
2、启动slave复制功能
3、查看slave的状态
这两个数据都是yes说明配置成功
因为很多原因,可能是环境的问题,会导致这两个不是同时为yes,可以看第一点里的图片,可能在之前就已经启动过从机或者其他原因,那就要先把从机关闭,stop slave,然后重新再进行一遍流程,但是要注意,这里重新开始的流程,position已经不是原来的position,因为我们已经在数据库里做了一些操作,每次重新启动slave之前,都要查询一下,show master status,查看position在哪个位置。
4.4.5、开始主从复制
全部配置成功之后,我们就可以在主机建库建表,然后对数据进行一些操作,这些操作都会在从机中记录下来,例如我们在主机建了一张表,建好之后,我们可以在从机中查到表里的数据,这就是主从复制。