MySQL高级(索引、查询优化、主从复制)

1.索引

什么是索引?

索引是一种数据结构。索引是为了加速对表中数据行的检索而创建的一种分散的存储结构。

作用:

(1)快速取数据;

(2)保证数据记录的唯一性;

(3)实现表与表之间的参照完整性;

(4)在使用ORDER by、group by子句进行数据检索时,利用索引可以减少排序和分组的时间。

索引分类

单值索引

唯一索引(列数据唯一)

复合索引(多列索引)

哪些情况下需要建立索引?

1.主键自动建立唯一索引

2.频繁作为查询条件的字段应该创建索引

3.查询中与其它表关联的字段,外键关系建立索引

4.不频繁更新的字段适合创建索引,因为每次更新不单单是更新了记录还会更新索引

5.Where条件里用到的字段创建索引

6.单键/组合索引的选择问题,who?(在高并发下倾向创建组合索引)

7.查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度

哪些情况不需要建索引?

1.表数据太少

2.经常增删的表

​ 提高了查询速度,但是降低表更新的速度,因为增删的时候还需要另外保存索引文件

3.列中有太多重复的字段,不用建。 一般100万条记录,distinct=2就没必要建索引了

性能优化ELPLAIN

使用EXPLAIN可以模拟优化器执行SQL查询语句,从而知道MYSQL如何处理sql

image-20210531104604851

id*

  • id类似优先级
  • id相同时:执行顺序由上到下
  • id不同时:如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行。
  • id有相同也有不同的:先按照优先级执行id值大的查询,再对id相同的顺序执行

select type

  • SIMPLE

    简单的select查询,不包含子查询和union

  • PRIMARY

    查询中若包含任何复杂的子部分,最外层查询被标记为PRIMARY

  • SUBQUERY

    在select或者where列表中包含了子查询

  • DERIVED

    FROM列表中包含的子查询为DERIVED(衍生),MYSQL会递归执行这些子查询,把结果放在临时表里。

  • UNION

    若第二个SELECT出现在UNION之后,则被标记为UNION; 若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED

  • UNION RESULT

    从UNION表中获取结果的select

type*

显示查询使用了何种类型

从好到坏:system>const>eq_ref>ref>range>index>ALL ALL:全表扫描

一般得保证查询至少达到range级别,最好能达到ref

  • system

    表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计

  • const

    表示通过索引一次(指定id)就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量

  • eq_ref

    唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描

  • ref

    非唯一性索引扫描,返回匹配某个单独值的所有行.本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体.

    eq_ref区别在于,eq_ref只存在单独行数据,最终返回结果也只有一行。但是ref不一定,如建立联合索引时查询单独某一列索引值,返回多行数据。eq_ref是一对一,ref是一对多

  • range

    只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引一般就是在你的where语句中出现了between、<、>、in等的查询,这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。

  • index

    虽然all和Index都是读全表,但index是从索引中读取的,而all是从硬盘中读的,通常比ALL快。

  • 总结:从上到下,type本质上指定了查询的范围,越往下查询范围越大,最大的是All,全表扫描,最小的是System,只查询一行,其他的都是查询范围大小的不同

possible_keys

显示可能应用到这张表上的索引。查询涉及到的字段如果有索引,则索引会被列出,但不一定会被实际查询使用。

key*

  • 实际使用的索引,如果为null,则没有使用到索引。
  • 查询中若使用了覆盖索引,则该索引仅出现在key列表中

keylen

  • 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好。
  • 显示的值为索引字段最大可能长度,并非实际使用长度,key_len是根据表定义计算而得的,不是通过表内检索出来的。

ref

显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列被用来查找索引列上的值。

rows*

根据表统计信息及索引选用情况,大致估算出找到所需记录所需要读取的行数。即每张表有多少行被优化器查询

extra*

包含不适合在其他列中显示但十分重要的额外信息

  • Using FileSort

MySQL会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序被称为文件排序。最好不要出现Using FileSort

image-20210531120306016

上面截图中创建了col1、col2、col3的联合索引,但是order by的时候第一个直接by col3,组合索引不能中断,不能跳过,所以就不会走索引了。

  • Using temporary

使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于order bygroup by

image-20210531121021978

Using FIleSort 和 Using temporary都会严重拖慢Sql语句的查询效率,应该尽量避免

  • Using index

表示相应select操作使用了覆盖索引,避免访问了表的数据行,效率比较高。

如果同时出现Using where,表明索引被用来执行索引键值的查找。

如果没有出现Using where,表明索引用来读取数据而非执行查找动作。

覆盖索引:或称为索引覆盖。就是select的数据列只从索引中就能获得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,即查询列要被所建的索引覆盖。 辅助键树找到叶子节点直接从内存读取数据,不用再走一遍主键树获取到叶子节点的整行数据,不需要回表查询

  • 如果使用覆盖索引,注意select中只取出需要的列,不可select *。因为如果将所有字段一起索引会导致索引文件过大,查询性能下降。
  • 字段中的顺序和覆盖索引的顺序一致,不能跳过某一列
  • Using where
  • Using join buffer:使用了连接缓存
  • impossible where : where的值总是false,不能用来获取任何元组。
  • select tables optimized away: 在没有Group by子句的情况下,基于索引优化MIN/ MAX操作或者对于MyISAM优化了count(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。
    • distinct:优化distinct操作,在扎到第一匹配的元组后即停止查找动作。

配置文件

log-bin:二进制文件,用于主从复制

log-err:记录严重错误信息,开启会影响性能

log:查询日志,开启会影响性能

数据文件

  • .frm文件:存放表结构
image-20210531093057869

mysql8.0表结构文件都以.ibd结尾

  • .myd文件:存放表数据
  • .myi文件:存放表索引

MySQL逻辑架构

连接层、服务层、引擎层、存储层

img

show engines;:查看所有存储引擎

image-20210531093732565

show variables like '%storage_engine%':查看存储引擎

image-20210531093912935

MyISAM于Innodb区别

image-20210531094204731

具体细节可查看博客https://blog.csdn.net/qq_35642036/article/details/82820178

性能下降的原因

  • 查询语句不够好
  • 索引失效
  • 关联查询太多join(设计缺陷或不得已的需求)
  • 服务器调优及各个参数

Sql执行顺序

我们写的sql:

image-20210531095509129

机器读到的sql:

image-20210531095539024
image-20210531095653125

Join理论

image-20210531100410819
image-20210531100446080
image-20210531100645140

MySQL不支持全外连接,使用union联合left joinright join,union自带去重

索引优化

单表优化案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE IF NOT EXISTS `article`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`author_id` INT (10) UNSIGNED NOT NULL,
`category_id` INT(10) UNSIGNED NOT NULL ,
`views` INT(10) UNSIGNED NOT NULL ,
`comments` INT(10) UNSIGNED NOT NULL,
`title` VARBINARY(255) NOT NULL,
`content` TEXT NOT NULL
);


INSERT INTO `article`(`author_id`,`category_id` ,`views` ,`comments` ,`title` ,`content` )VALUES
(1,1,1,1,'1','1'),
(2,2,2,2,'2','2'),
(3,3,3,3,'3','3');

SELECT * FROM ARTICLE;

查询语句:select id,author_id from article where category_id = 2 AND comments > 1 ORDER BY views DESC limit 1;

结果看似没有问题,而且很快

image-20210531132746595

但是我们用Explain查看一下执行过程

image-20210531132821269

发现type是全表查询,而且最后使用了文件内排序,两者都会非常影响性能,所以这不是一条好的查询语句。

优化过程

通过show index from arcticle只有主键索引,所以我们尝试添加索引。

  • 由于where条件后面查询有字段,首先我们尝试对where后的字段建立索引

create index idx_article_ccv on article(category_id,comments,views);

此时已经建立了三个索引。

image-20210531133403578

再次使用explain 进行查看

image-20210531133431226

发现type已经变为range,说明检索的范围已经缩小,达到了优化的效果。但是Using filesort仍然存在,我们看看还能不能继续优化呢?

如果将 comment > 1改为comment = 1,那么不会出现filesort,组合索引进行范围查找会导致索引部分失效。

对于(a,b,c)组合索引,是一个以a字段排序而b与c相对有序的B+树,引擎可以通过二分查找定位到a=1的数据,b在a=1确定得情况下是有序的(所以b的有序是相对的),依然可以通过二分查找取出所有b大于2的数据,但这些数据的b字段可能有很多个不同的值,所以c字段是无序的,无法用二分查找来查询c=3的数据,故c用不到索引。

所以 a = 1用到了索引,b>2用到了索引,c=3用不到索引,因为b相对于a确定值,c相对于b确定值,但b的值不唯一。

所以我们删掉之前的索引drop index idx_article_ccv on article;

既然comment不能确定,那我不给他建,只给另外两个建索引不就不会失效了吗?

create index idx_article_cv on article(category_id,views);

然后再执行查询语句

image-20210531134820965

发现type变成了refExtra里的Using filesort也不见了!说明这个方法很成功!

那我能不能单独对comments字段加个索引呢?

create index idx_article_cmt on article(comments);

image-20210531135207441

最后发现虽然可用的索引增加了,但是最终用到的索引仍然只有一个,所以我们对comments字段新加的索引没起到作用。

backward index scan:倒叙索引范围查找,使用DESC时会出现,MySQL8.0后特有的倒叙索引。

实现原理:

img

上图是MySQL Innodb的B+树结构,我们只关注叶子节点,发现不同于传统的B+树,叶子节点间变成了双向链表,因此范围查询时不仅能从前往后,也能从后往前。所以倒叙索引就是基于此实现的从后往前遍历。

降序索引的引入,MySQL 8.0再也不会对group by操作进行隐式排序

两表优化案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
CREATE TABLE IF NOT EXISTS `class`(
`id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);


CREATE TABLE IF NOT EXISTS `book`(
`bookid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
);


INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO class(card)VALUES(FLOOR(1+(RAND()*20)));

INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO book(card)VALUES(FLOOR(1+(RAND()*20)));

查询语句:explain select * from class left join book on class.card = book.card;

分析结果:

image-20210531140622529

查询type是全表查询,需要优化

优化过程

对于此左连接查询,首先我们尝试给右表book连接的字段加索引create index idx_book_card on book(card)

再次查询

image-20210531153242923

发现右表查询范围缩小了,有优化效果。

那么我们给主表的连接字段加上索引呢?

drop index idx_book_card on book;

create index idx_class_card on class(card);

再次查询

image-20210531153734786

虽然typeall变成了index,但是需要扫描的行rows仍然是20行。优化程度不如在右表的字段上加索引。

总结:当使用左右连接时,如果是左连接,则在右表加索引,如果是右连接,则在左表加索引。因为没有建立索引的表是主表,不管是否建立索引都要全表扫描。

三表优化案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE IF NOT EXISTS `phone`(
`phoneid` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`card` INT (10) UNSIGNED NOT NULL
)ENGINE = INNODB;



INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));
INSERT INTO phone(card)VALUES(FLOOR(1+(RAND()*20)));

查询语句:select * from class LEFT JOIN book ON class.card = book.card LEFT JOIN phone ON book.card = phone.card;

查询分析:

image-20210531155054223

发现全部都是全表扫描,性能非常不好。

优化过程

首先根据上一个案例,我们对右表,即bookphone的连接字段各自建立索引。

1
2
create index idx_book_card on book(card);
create index idx_phone_card on phone(card);
image-20210531155341186

发现总查询次数减少了很多次,优化效果很好。

总结

对于Join语句的优化

  • 尽可能减少Join语句中NestedLoop循环总次数;永远用小结果集驱动大结果集
  • 优先优化NestedLoop中的内层循环
  • 保证Join语句中被驱动表上的Join条件字段已经被索引
  • 当无法保证被驱动表的Join字段被索引且内存资源充足的前提下,可以多使用JoinBuffer的设置

索引失效

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE staffs(
id INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(24)NOT NULL DEFAULT'' COMMENT'姓名',
`age` INT NOT NULL DEFAULT 0 COMMENT'年龄',
`pos` VARCHAR(20) NOT NULL DEFAULT'' COMMENT'职位',
`add_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT'入职时间'
)CHARSET utf8 COMMENT'员工记录表';
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('z3',22,'manager',NOW());
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('July',23,'dev',NOW());
INSERT INTO staffs(`name`,`age`,`pos`,`add_time`) VALUES('2000',23,'dev',NOW());

ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(`name`,`age`,`pos`)

非全值匹配,违背最左前缀法则

对于上面的表,我们建立了ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(name,age,pos)三列的复合索引。

但是当我们查询的时候,依次按照列名进行查询没有问题,只是key_len的长度稍微增多了一些。

image-20210601091056841

但是如果不按照顺序查询

image-20210601091209195

只查询age = 22的数据,发现索引就失效了。这就违背了最左前缀匹配法则

最左前缀匹配法则:在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配

原理:

image-20210601091455788

如图创建一个(a,b)的联合索引,在节点中是以键值对存储的,对于a来说,B+树中的排列从左到右是有序的,但是单纯看b的排列却是无序的。但是我们发现如果a的值确定后,所对应的b的值才是有序的。就解释了如果a的值没有匹配到,那么b列索引就不能确定,只能进行全表扫描,导致索引失效。

但是注意:如果我们只查询name和pos字段,不查询age字段,索引看起来不会失效.

image-20210601092020042

但是实际用到的索引只有name一列,后面的索引都失效了,和上面select * from staffs where name = 'z3'效果一样。

索引列上有计算

image-20210601200229981

如果对索引列进行了计算,会导致索引失效。

存储引擎不能使用索引范围条件右边的列

即如果where条件有<、>等范围查询,那么会导致右边的索引全部失效。

image-20210601200616007

尽量使用覆盖索引,减少select *

即查询时尽量只查询想要的字段,减少回表查询次数。

使用!= 或 <>时无法使用索引导致全表扫描

image-20210601201242730

is null 或 is not null 也无法使用索引

image-20210601201513274

like以通配符%开头会导致索引失效

like '%z3'会导致索引失效

image-20210601201634912

like 'z%3' 和 like 'z3%'不会导致索引失效

image-20210601201720305

如何解决'like %abc%'索引失效的问题?

避免使用select * ,对必要的字段建立覆盖索引,如select name,age,pos from staffs where name like 'z%3',对name.age,pos建立联合索引,注意如果查询的字段没有索引也会导致索引失效。

image-20210601202516892

字符串不加单引号会导致索引失效

image-20210602092300399

如果字符串不加单引号,会进行强制类型转换导致索引失效。 相当于在索引列上做了隐式类型转换,导致索引失效。

or 有时会导致索引失效

image-20210602092809411

OR导致索引是在特定情况下的,并不是所有的OR都是使索引失效,如果OR连接的是同一个字段,那么索引不会失效,反之索引失效

image-20210602093511984

小口诀

全值匹配我最爱,最左前缀要遵守; 带头大哥不能死,中间兄弟不能断; 索引列上少计算,范围之后全失效; Like百分写最右,覆盖索引不写星; 不等空值还有or,索引失效要少用; VAR引号不可丢,SQL高级也不难!

查询截取分析

查询优化

发现慢Sql查询的时候一般流程:

  1. 观察查询的执行情况,保留观察至少一天,查看产生的慢SQL情况。
  2. 开启慢查询日志,设置阈值,查询时间超过阈值的SQL抓取出来进行留存。
  3. explain + 慢SQL分析
  4. show profile
  5. 运维经理  DBA进行数据库服务器的参数调优

小表驱动大表

即小的数据集驱动大的数据集

对于数据量小的表与数据量大的表关联查询,用exits性能优于in

select ... from TABLE where exists (子查询)

可以理解为将主查询的数据放到子查询中进行验证,根据验证结果来决定主查询的数据结果是否保留。

Order BY优化

Order By 在下面情况中不会Using filesort:

  • Order by语句使用索引的最前列
  • Where子句和Order By子句条件组合满足索引最前列

如果不在索引列上,就会使用filesort,MySQL会启动单路排序和双路排序。

  • 双路排序:MySQL4.1之前是双路排序,即扫描两次磁盘进行排序。两次IO非常耗时
  • 单路排序:从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
    • 但是单路排序也存在问题,由于每次只去固定大小的数据进入buffer,如果数据量太大反而会导致频繁IO。
    • 优化策略:增大sort_buffer_sizemax_length_for_sort_data

Group By优化

group by实质是先排序后再进行分组,遵循最左前缀匹配。

当无法使用索引列时,增大sort_buffer_sizemax_length_for_sort_data

where高于having,能写在where条件下的就不要写having了。

慢查询日志

慢查询日志用来记录执行时间超过指定阈值的SQL。

指标为long_query_time,默认为10s。

默认没有开启慢查询日志,如果非调优需要,一般不建议开启该参数,因为开启会进行文件IO读写,会带来一定的性能影响。

查询是否开启:show variables like '%slow_query_log%%';

image-20210603090715944

显示已经开启。

开启关闭命令:set global slow_query_log = 1/0;只对当前数据库生效,重启后会失效。

如果需要永久生效,需要修改配置文件

添加slow_query_log=1slow_query_log_file = ???;

查看记录sql的阈值:show variables like 'long_query_time%';

image-20210603091244093

如果执行时间正好等于阈值,不会被记录,大于阈值时才会被记录

设置阈值:set global long_quey_time = 10;

但是设置完后再查看依然没有改变,需要重新开启一个会话,或者使用show global variables like 'long_query_time%';

模拟检查是否生效

我们使用select sleep(4);来模拟sql执行了4s时间,然后查看慢查询日志是否有该条记录。

image-20210603092008929
image-20210603092038245

结果可以看到该条sql的执行信息,包括执行用户、执行时间、是否加锁、使用的数据库,执行时间戳以及执行的sql语句。

查询当前有多少条慢查询记录:show global status like 'Slow_queries%'

image-20210603092315266

查到目前有一条慢查询sql记录。可以用作系统健康度进行检查的指标。

日志分析工具mysqldumpslow

退出mysql,在控制台界面执行mysqldumpslow --help可以查看帮助命令。

工具mysql内置安装,如果没有此命令,可以用find /MySQL安装路径 -name mysqldumpslow查询路径,然后建立软连接ln -s 路径 /usr/bin;

image-20210603093913899
1
2
3
4
5
c: 相同查询以查询条数和从大到小排序。
t: 以查询总时间的方式从大到小排序。
l: 以查询锁的总时间的方式从大到小排序。
at: 以查询平均时间的方式从大到小排序。
al: 以查询锁平均时间的方式从大到小排序。
使用示例

上面图片结果为查询10条执行时间最长的sql

批量插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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=GBK;

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=GBK;
//函数
delimiter $$
create function ran_string(n int) returns varchar(255)
begin
declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
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));
set i=i+1;
end while;
return return_str;
end $$
//函数
delimiter $$
create function rand_num() returns int(5)
begin
declare i int default 0;
set i=floor(100+rand()*10);
return i;
end $$
//存储过程
delimiter $$
create procedure insert_emp(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 emp(empno,ename,job,mgr,hiredate,sal,comm,deptno) values((start+i),ran_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
until i=max_num
end repeat;
commit;
end $$
//存储过程
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),ran_string(10),ran_string(8));
until i=max_num
end repeat;
commit;
end $$

但是在这之前我们需要设置一个参数show variables like 'log_bin_trust_function_creators';

image-20210603101158350

开启创建函数的权限。

set global log_bin_trust_function_creators = 1;

然后测试调用

image-20210603101408010

image-20210603101423649

可以看到插入50万条数据花费了一分多钟

插入500万数据

然后我们查询全部。花费了0.58秒?

查询全部

Show Profile

是MySQL提供可以用来分析当前会话中语句执行的资源消耗情况,可以用于SQL的调优测量。

默认关闭状态,并保存最近15次的结果。

可以用show variables like '%profiling'查看

查询状态

set profiling = on;开启

模拟慢查询

select * from emp group by id%10 limit 150000;

花费1.43s

然后我们通过show profiles查看

show profiles

可以看到我们能执行的每一条sql和对应的执行时间

根据每条的执行时间,我们可以通过诊断命令show profile cpu,block io for query [num]查看

正常sql

我们可以看到这条sql的完整的生命周期,和每步查询的时间。

除了以上的参数,还有如下:

1
2
3
4
5
6
7
8
9
10
type:
ALL --显示所有的开销信息
| BLOCK IO --显示块IO相关开销
| CONTEXT SWITCHES --上下文切换相关开销
| CPU --显示CPU相关开销信息
| IPC --显示发送和接收相关开销信息
| MEMORY --显示内存相关开销信息
| PAGE FAULTS --显示页面错误相关开销信息
| SOURCE --显示和Source_function,Source_file,Source_line相关的开销信息
| SWAPS --显示交换次数相关开销的信息

注意status如果出现以下的状态,说明情况急需要优化:

1
2
3
4
converting HEAP to MYISAM : '查询结果太大、开始往磁盘上转移'
creating tmp table : '创建临时表,将结果拷贝进临时表,然后再删除临时表'
Coping to tmp table on disk : '把内存中的临时表复制进磁盘(危险)'
locked : '已经上锁'

我们对4号语句进行分析,发现存在创建临时表

创建临时表

全局查询日志

不要在生产环境开启该功能!

  • 配置启动

    ​ 配置文件添加

    1
    2
    3
    general_log=1	
    general_log_file = 路径
    log_output=FILE
  • 编码启动

    1
    2
    3
    4
    set global general_log = 1;
    set global out_put = 'TABLE';
    #查看日志
    select * from mysql.general_log;

数据库锁理论

锁的分类

按对数据的操作类型分

  • 读锁(共享锁):多个读操作可以同时进行而不被影响
  • 写锁(排他锁):当前写操作没有完成前,会阻断其他写操作和读操作。

按对数据操作的粒度分

  • 表锁(偏读)
  • 行锁(偏写)
  • 页锁

读锁

  • 表锁:偏向MyISAM引擎,开销小,加锁快;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低

表锁读锁案例

1
2
3
4
5
6
7
8
9
10
create table mylock (
id int not null primary key auto_increment,
name varchar(20) default ''
) 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');

手动增加表锁:lock table mylock read,book write;

查看是否加锁:show open tables;

查看表锁

删除锁:unlock tables;

读锁是共享锁,看下面案例

读锁案例

我们使用 lock table mylock readmylock表加一个读锁。最后发现自己和其他会话可以正常读到数据,但是不能对该表进行其他操作(增加、删除、修改),其他会话如果需要操作该表,会进入到阻塞状态,等待锁被释放后再执行。

被阻塞

且对mylock表加锁后,也不能去查询其他表的数据。

加共享锁(读锁)的线程只能读自己加锁的表

小结

总之,即读锁会阻塞写,但是不会阻塞读,写锁会把读和写都阻塞

分析表锁定:show status like 'table%'

结果

Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每次获取值+1

Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁,每等待一次锁值+1),越高说明表级锁竞争越严重。

MyISAM读写锁调度是锁优先,因此不适合于做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。

行锁

偏向Innodb引擎,开销大,加锁慢,会出现死锁。锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

与MyISAM最大的不同有两点:

  • 支持事务
  • 采用了行级锁

MySQL默认支持行锁,但是如果索引失效,会导致行锁膨胀为表锁,造成性能下降,甚至出现阻塞情况!

间隙锁

间隙锁(Next-Key锁):用范围条件(如>、<等)进行检索数据时,Innodb会给符合条件的已有数据记录的索引项加锁,对于范围内但是不存在的记录,叫做“间隙”。

update [table] set [column] = 'aaa' where id > 2 and id < 6;如果数据表中不存在2-6中的某一条数据,也不能对其进行添加,即2-6的间隙已经被锁住了。

Innodb解决幻读即采用了间隙锁和MVCC

危害:因为查询过程中进行范围查找会锁定范围内的所有键值,即使不存在。如果需要进行插入某个在范围内不存在的数据项,会导致插入失败,造成性能下降。

锁定一行:select * from table where id = 8 for update

for update即添加行锁,可以手动锁定某些行,如果我们需要洗数据,我们可以手动先锁定某些行,结束后再去除行锁,不然不加锁洗数据可能结果还是乱的。

主从复制

MySQL的主从复制是一个异步的复制过程(虽然一般情况下感觉是实时的),数据将从一个MySQL数据库(Master)复制到另一个MySQL数据库(Slave),在Master和Slave之间实现整个主从复制的过程是由三个线程参与完成的。其中两个线程(SQL线程和IO线程)在Slave端,另一个线程(I/O线程)在Master端。   要实现MySQL的主从复制,首先必须打开Master端的binlog记录功能,否则就无法实现。binlog: binary log,是主库中保存所有更新事件日志的二进制文件。因为整个复制过程实际上就是Slave从Master端获取binlog日志,然后在Slave上以相同顺序执行获取的binlog日志中的记录的各种SQL操作。

image-20210607101956846

复制的基本原则

  • 每个Slave只有一个Master
  • 每个Slave只能有一个唯一的服务ID
  • 每个Master可以有多个Slave

使用docker配置

使用docker搭建mysql主从集群,顺便复习docker。

docker images查看是否有mysql镜像。

然后我们创建容器

docker run -d -p 3307:3306 --name master -e MYSQL_ROOT_PASSWORD=root mysql

主MySQL端口为3307

docker run -d -p 3308:3306 --name slave1 -e MYSQL_ROOT_PASSWORD=root mysql

从1端口为3308

docker exec -it master /bin/sh进入master容器中

  • 修改my.cnf

把Docker中的/etc/mysql/my.cnf文件拷贝到Docker的/var/lib/mysql目录,及主机的/usr/local/mysql/目录中

cp /etc/mysql/my.cnf /var/lib/mysql

1
2
3
4
主:修改配置my.conf
# 主从复制打开的二进制文件
server_id = 1
log-bin = mysql-master-log

具体可查看博客https://blog.csdn.net/why15732625998/article/details/80463041


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!