学习笔记MySQL数据库高级版 - 索引优化慢查询锁机制等

Posted 棉花糖灬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习笔记MySQL数据库高级版 - 索引优化慢查询锁机制等相关的知识,希望对你有一定的参考价值。

本文是尚硅谷周阳(阳哥)老师的MySQL高级篇视频的学习笔记。由于视频比较老,所以在高版本的mysql中索引的地方做了优化,和视频的内容不完全一样,不过大体一致。从第四节锁机制开始的部分还没有整理。

一、MySQL介绍

常用存储引擎的对比:

MyISAM存储引擎:不支持主外键,不支持事务,表锁,只缓存索引,不缓存真实数据,表空间小,关注点是性能。

InnoDB存储引擎:支持主外键和事务,行锁,适合高并发的操作,不仅缓存索引还缓存真实数据,对内存要求较高,内存大小对性能有决定性影响,表空间大,关注点是事务。

二、索引优化分析

SQL性能下降(执行时间长,等待时间长)原因:

  • 查询语句写的烂
  • 索引失效
  • 关联查询有太多join
  • 服务器调优及各个参数设置(缓冲、线程数等)

1. 常用的join查询

(1) sql执行顺序

1) 手写

SELECT DISTINCT 
 <select_list>
FROM 
 <left_table> <join_type> 
JOIN 
 <right_table> 
ON 
 <join_condition>
WHERE 
 <where_condition>
GROUP BY 
 <group_by_list>
HAVING 
 <having_condition>
ORDER BY 
 <order_by_condition>
LIMIT 
 <limit_numver>

2) 机读

FROM 
 <left_table>
ON 
 <join_condition> <join_type> 
JOIN 
 <right_table>
WHERE 
 <where_condition>
GROUP BY 
 <group_by_list>
HAVING 
 <having_condition>
SELECT
DISTINCT 
 <select_list>
ORDER BY 
 <order_by_condition>
LIMIT 
 <limit_numver>

(2) 7种join的写法

1) 建表

create table tbl_dept
(
  id       int(11) not null auto_increment comment '部门id',
    deptName varchar(30) default null comment '部门名称',
  locAdd   varchar(40) default null comment '部门地址',
    primary key (id)
) engine = innodb
  auto_increment = 1
default charset = utf8
    comment = '部门表';
create table tbl_emp
(
      id     int(11) not null auto_increment comment '员工id',
    name   varchar(20) default null comment '员工姓名',
      deptId int(11)     default null comment '所属部门',
    primary key (id),
      key fk_dept_id(deptId)
    # constraint fk_dept_id foreign key (deptId) references tbl_dept (id)
  ) engine = InnoDB
  auto_increment = 1
    default charset = utf8
    comment = '职工表';

建表时有个坑点,原视频中写的是

key `fk_dept_id`(`deptId`)

其中用到的不是单引号,而是键盘上Esc键下方的符号。

2) 插入数据

insert into tbl_dept(deptName, locAdd) VALUES ('RD', 11);
insert into tbl_dept(deptName, locAdd) VALUES ('HR', 12);
insert into tbl_dept(deptName, locAdd) VALUES ('MK', 13);
insert into tbl_dept(deptName, locAdd) VALUES ('MIS', 14);
insert into tbl_dept(deptName, locAdd) VALUES ('FD', 15);
insert into tbl_emp(name, deptId) VALUES ('z3', 1);
insert into tbl_emp(name, deptId) VALUES ('z4', 1);
insert into tbl_emp(name, deptId) VALUES ('z5', 1);

insert into tbl_emp(name, deptId) VALUES ('w5', 2);
insert into tbl_emp(name, deptId) VALUES ('w6', 2);

insert into tbl_emp(name, deptId) VALUES ('s7', 3);
insert into tbl_emp(name, deptId) VALUES ('s8', 4);
insert into tbl_emp(name, deptId) VALUES ('s9', 51);

tbl_dept表中的内容:

tbl_emp表中的内容:

3) 内连接

select * from tbl_emp a inner join tbl_dept b on a.deptid = b.id;

查询结果:

4) 左连接

select * from tbl_emp a left join tbl_dept b on a.deptid = b.id;

查询结果:

5) 右连接

select * from tbl_emp a right join tbl_dept b on a.deptid = b.id;

查询结果:

6) 左连接特殊形式

select * from tbl_emp a left join tbl_dept b on a.deptid = b.id where b.id is null;

查询结果:

7) 右连接特殊形式

select * from tbl_emp a right join tbl_dept b on a.deptid = b.id where a.deptid is null;

查询结果:

8) 全连接(mysql不支持,oracal支持)

select * from tbl_emp a full join tbl_dept b on a.deptid = b.id;

可以用以下语句做等效查询:

select * from tbl_emp a inner join tbl_dept b on a.deptid = b.id
union
select * from tbl_emp a right join tbl_dept b on a.deptid = b.id;

查询结果:

9) 全连接特殊情况(mysql不支持)

select * from tbl_emp a full outer join tbl_dept b on a.deptid = b.id where a.deptid is null or b.id is null;

可以用以下语句做等效查询:

select * from tbl_emp a left join tbl_dept b on a.deptid = b.id where b.id is null;
union
select * from tbl_emp a right join tbl_dept b on a.deptid = b.id where a.deptid is null;

查询结果:

2. 索引

(1) 索引知识点

  • 索引时帮助MySQL高效获取数据的数据结构。其目的是提高查找效率,可用类比于字典。索引是排好序的快速查找数据结构

  • 索引会影响到where后面的查找条件,以及order by排序

  • 在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这种数据结构,就是索引

  • 索引本身也很大,所以不能全部存储在内存中,因此往往以索引文件的形式存储在磁盘上

  • 一般索引使用的是B树,聚集索引、次要索引、覆盖索引、复合索引、前缀索引、唯一索引使用的是B+树。除此之外还有哈希索引

  • 索引的优点:提高了数据检索效率、降低数据库的IO成本。降低数据排序的成本,降低了CPU的消耗

  • 索引的缺点:索引增加空间消耗,索引降低了更新表的速度,包括insert/update/delete等操作,因为更新表时,mysql还要保存索引文件

  • 单值索引:一个索引只包含单个字段

  • 复合索引:一个索引包含多个字段

(2) 索引SQL语句

create [unique] index indexName on talbeName(columnName)
alter tableName add [unique] index indexName on (columnName)
Drop index indexName on talbeName
show index from tableName

(3) mysql索引结构

  • B树索引:
  • Hash索引:
  • 全文full-text索引:
  • R树索引:

(4) 哪些情况需要建立索引

  • 主键自动创建唯一索引
  • 频繁作为查询条件的字段
  • 查询中与其他表关联的字段,外键关系建立索引
  • 频繁更新的字段不适合创建索引
  • where条件里用不到的字段不创建索引
  • 单间/组合做引的选择问题?在高并发下倾向创建组合索引
  • 查询中排序的字段创建索引
  • 查询中统计或分组字段(分组必排序)

(5) 哪些情况不需要建立索引

  • 表记录太少,300w以下
  • 经常增删改的表
  • 数据重复且分布平均的表字段,如性别

3. 性能分析

MySQL Query Optimizer:查询优化器

mysql常见瓶颈:CPU瓶颈、IO瓶颈、服务器硬件瓶颈

(1) 执行计划explain

1) 是什么

explain:执行计划。使用explain关键字可用模拟优化器执行sql查询语句,从而知道Mysql时如何处理你的sql语句的,以便于分析查询语句或表结构的性能瓶颈。使用:explain+SQL语句

2) 能干嘛

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可用使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

3) 执行计划包含的信息

  • id:select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。即表的读取顺序

    • id相同,执行顺序从上到下,看table列
    • 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
  • table:数据是关于哪张表的

  • type:访问类型

    • system:表只有一行记录,这是const类型的特例,平时不会出现,可以忽略不计

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

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

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

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

    • index:Full Index Scan,index和ALL区别为index类型只遍历所引述。这通常比ALL快,因为索引文件通常比数据文件小。

    • ALL:Full Table Scan,全表扫描,将遍历全表以找到匹配的行

    • 由好到差:system > const > eq_ref > ref > range > index > ALL,不是最全的,但是最常用的几个。一般来说,得保证达到range或ref级别。

  • possible_keys:显示可能应用在这张表中的索引,一个或多个。查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被实际查询使用

  • key:实际使用的索引,若为NULL,则没有使用索引。查询中若使用了覆盖索引(select要查询的字段和所建索引的字段完全吻合),则该索引仅出现在key列表中

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

  • ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值。表之间的引用。

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

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

    • using filesort:mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,mysql无法利用索引完成的排序操作称为“文件排序”。
    • using temporary:使用了临时表保存中间结果,mysql在对查询结果排序时使用了临时表。常见于排序order by和分组group by。分组和排序尽量和复合索引一致。
    • using index:表示相应的select操作使用了覆盖索引,避免了访问白哦的数据行,效率不错。如果同时出现using where,表明索引被用来执行索引值和查找,反之,表明索引用来读取数据而非执行查找动作。
    • using where:使用了where过滤
    • using join buffer:使员工了连接缓存
    • impossible where:where子句的值总是false,不能用来获取任何元组
    • select tables optimized away:在没有group by子句的情况下,基于索引优化min/max操作或者对于MyISAM存储引擎count(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的结果即完成优化。
    • distinct:优化distinct操作,在找到第一个匹配的就停止查找

(2) 覆盖索引

覆盖索引(covering index):又名索引覆盖,就是select的数据列只从索引中就能够得到,不必根据索引再次读取数据文件,换句话说查询列被所建的索引覆盖。

4. 索引优化

(0) 自己的理解

这里综述下我自己的理解:在建立符合索引时,有点类似于按多个字段进行排序,先按第一个字段升序排序,若第一个字段相同则按第二个字段升序排序,依此类推。

所以当查询的where子句的字段顺序与多列索引的最左前缀顺序一致时,就可以先按第一个字段在一棵树上查找,然后在按第二个字段在树上查找,查找速度比顺序查找快很多。但是如果where子句中出现了范围查找时,则在树上查找时就没那么方便了。

而如果所要查询的字段正好是索引字段的话,由于索引上就带有这些信息,所以不必再去访问磁盘查找具体的记录,反之就需要访问磁盘,增加了磁盘IO的时间。

根据索引的原理,所以当进行排序操作时,如果所排序的字段顺序满足索引字段的最左前缀,则速度会比较快。同时由于索引建立时是按升序排序的,所以如果已满足最左前缀原则,但有的升序有的降序,则与索引相矛盾了。

分组时,由于要先排序再分组,所以与排序的情况类似。

(1) 单表索引

1) 建表

create table if not exists article
(
    id          int(10) unsigned not null primary key auto_increment comment '文章id',
    author_id   int(10) unsigned not null comment '作者id',
    category_id int(10) unsigned not null comment '分类id',
    views       int(10) unsigned not null comment '被查看次数',
    comments    int(10) unsigned not null comment '评论',
    title       varbinary(255)   not null comment '标题',
    content     text             not null comment '文章内容'
) comment '文章表';

2) 插入数据

insert into article(author_id, category_id, views, comments, title, content)
values (1, 1, 1, 1, '1', '1'),
       (2, 2, 2, 2, '2', '2'),
       (1, 1, 3, 3, '3', '3');

3) 创建索引并测试1

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

explain select id, author_id from article where category_id=1 and comments>1 order by views desc limit 1;

分析:使用到了索引,但由于comments>1是范围查询,所以在此之后索引失效,用到了filesort

4) 创建索引并测试2

drop index index_article_ccv on article;

create index index_article_cv on article(category_id, views);

explain select id, author_id from article where category_id=1 and comments>1 order by views desc limit 1;

分析:使用到了索引,且没用用到filesort

(2) 双表索引

1) 创建class表

create table if not exists class
(
    id   int(10) unsigned not null auto_increment comment '类别编号',
    card int(10) unsigned not null comment '分类卡',
    primary key (id)
) comment '类别表';

create table if not exists book
(
    bookid int(10) unsigned not null auto_increment comment '图书编号',
    card   int(10) unsigned not null comment '分类卡',
    primary key (bookid)
) comment '图书表';

2) 插入数据

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 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)));

3) 对比增加索引前后的区别

explain select * from class c left join book b on c.card = b.card;

索引建在左表还是右表?

索引建立在右表

alter table book add index y (card);

explain select * from class c left join book b on c.card = b.card;

结果:查看rows字段的情况,rows的总行数少

drop index y on book;

索引建立在左表

alter table class add index y (card);

explain select * from class c left join book b on c.card = b.card;

结果:查看rows字段的情况,rows的总行数多

4) 结论

左连接条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,要在右表建立索引。而右连接则相反,需要在左表建立索引。

(3) 三表索引

1) 再建立一个phone表

create table if not exists phone
(
    phoneId int(10) unsigned not null auto_increment comment '手机编号',
    card    int(10) unsigned not null comment '手机类型',
    primary key (phoneId)
) comment '手机表';

2) 插入数据

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));

4)

先把class和book表上的索引清空

explain select * from class left join book on class.card = book.card left join phone on book.card = phone.card;

查看type字段,全为all,且使用了join buffer

alter table phone add index z(card);

alter table book add index y(card);

explain select * from class left join book on class.card = book.card left join phone on book.card = phone.card;

查看type字段,一个all,两个ref

5) 结论

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

(4) 索引优化,避免索引失效

1) 建立staffs表

create table staffs
(
    id       int primary key auto_increment comment '编号',
    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 '员工记录表';

2) 插入数据

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());

3) 索引优化规则

alter table staffs add index idx_staffs_nameAgePos (name, age, pos);

在以下操作中需要查看key和key_len字段,分别说明了是否用到了索引,以及在查询精度更精确时,是否key_len的长度更长,即用到索引的字段数。

  • 全值匹配我最爱:
    • explain select * from staffs where name = 'July' and age = 25 and pos = 'manager';:用到索引
  • 最佳左前缀法则:如果索引了多列,要遵守最佳左前缀法则,指的是查询从索引的最左前列开始,并且不跳过索引中间的列
    • explain select * from staffs where name = 'July';:用到索引
    • explain select * from staffs where name = 'July' and age = 25;:用到索引
    • explain select * from staffs where age = 25 and pos = 'manager';:没有用到索引,带头大哥不能死
    • explain select * from staffs where pos = 'manager';:没有用到索引
    • explain select * from staffs where name = 'July' and pos = 'manager';:用到了索引,但key_len和只查询name='July'的时候相同,所以pos字段没有用到索引。违背了中间兄弟不能断
  • 不在索引列上做任何操作(计算、函数、(自动or手动)类型转化),会导致索引失效而转向全表扫描
    • explain select * from staffs where left(name,4) = 'July';:没有用到索引,全表扫描
  • 存储引擎不能使用索引中范围条件右边的列(若where子句中出现了范围查询条件,则其右边的条件即使出现在了索引中,也会失效)
    • explain select * from staffs where name = 'July' and age = 25 and pos = 'manager';:用到了索引,key_len=140
    • explain select * from staffs where name = 'July';:用到了索引,key_len=74
  • explain select * from staffs where name = 'July' and age > 25 and pos = 'manager';:用到了索引,key_len=78,所以name和age用到了索引,pos没有用到索引
  • 尽量使用覆盖索引,减少select *
    • explain select * from staffs where name = 'July' and age = 25 and pos = 'manager';
    • explain select name,age,pos from staffs where name = 'July' and age = 25 and pos = 'manager';:extra字段比上个语句多了using index,即使用了索引获取字段的值
  • mysql在使用不等于(!=或<>)的时候无法使用索引会导致全表扫描
    • explain select * from staffs where name != 'July';
  • is null, is not null也无法使用索引,所以要尽量避免空值,可用默认值替代
    • explain select * from staffs where name is null;
  • like以通配符开头(’%abc…’)mysql索引失效会变成全表扫描的操作
    • explain select * from staffs where name like '%July%';:索引失效
    • explain select * from staffs where name like '%July';:索引失效
    • explain select * from staffs where name like 'July%';:用到了索引
    • 写在右边时最优,如果非要两边都写%,使用覆盖索引避免全表扫描
  • 字符串不加单引号索引失效。mysql会做隐式的类型转化
    • select * from staffs where name = '2000';:索引有效
    • select * from staffs where name = 2000;:索引失效
  • 少用or,用它连接时会索引失效

4) 一个特殊的例子

where a=3 and b like 'kk%' and c=4,索引为index(a,b,c)。则a能用,b能用,c也能用。where a=3 and b like '%kk' and c=4的时候只有a能用到索引。

5) 口诀

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

6) 索引优化建议

  • 对于单键索引,尽量选择针对当前query过滤性更好的索引

  • 在选择组合索引的时候,当前query中过滤性最后的字段在索引字段熟悉怒中,位置越靠左越好

  • 在选择组合索引的时候,劲力郎选择能够包含当前query中的where子句更多字段的索引

  • 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的

三、查询截取分析

1. 查询优化

  • 慢查询的开启并捕获

  • explain + 慢SQL分析

  • show profile查询SQL在MySQQL服务器里面的执行细节和生命周期情况

  • SQL数据库服务器的参数调优

(1) in和exists

永远小表驱动大表,即小的数据集驱动大的数据集

1) in优于exists的情况

select * from A where id in (select id from B)

等价于:

for select id from B
for select * from A where A.id = B.id

当B表的数据集必须小于A表的数据集时,用in由于exists

2) exists优于in的情况

select * from A where exists (select 1 from B where B.id = A.id)

等价于

for select * from A
for select * from B where B.id = A.id

当A表的数据集小于B表的数据集时,用exists优于in

注意:A表和B表的id字段应建立索引

4) exists解析

select * from table where exists (subquery)语法可以理解为:将主查询的数据,放到子查询中做条件验证,根据验证结果(ture或false)来觉得主查询的数据结果是否得以保留。exists (subquery)只返回true或false

(2) 排序优化

1) 准备工作

create talbe tblA(
	id in t primary key not null auto_increment,
	age int,
	birth timestamp not null
);

insert into tblA(age,birth) values(22,now());
insert into tblA(age,birth) values(23,now());
insert into tblA(age,birth) values(24,now());

create index idx_A_ageBirth on tblA(age,birth);

select * from tblA;

2) 各种排序及结果

explain select * from tblA where age > 20 order by age; # index
explain select * from tblA where age > 20 order by age,birth; # index
explain select * from tblA where age > 20 order by birth; # index 和 filesort
explain select * from tblA where age > 20 order by birth, age; # index 和 filesort

explain select * from tblA order by birth; # index和filesort
explain select * from tblA where birth > '2016-01-28 00:00:00' order by birth; # index和filesort
explain select * from tblA where birth > '2016-01-28 00:00:00' order by age; # index
explain select * from tblA order by age asc, birth desc; # index和filesort

3) 结论

order by满足两种情况会使用index方式排序:

  • order by语句使用索引最左前缀
  • 使用where子句与order by子句条件列组合满足索引最左前缀

4) 排序优化

  • order by子句尽量使用index方式排序,避免使用filesort方式排序
  • 尽可能在索引列上完成排序操作,遵照索引的最左前缀
  • 如果不在索引列上,filesort有两种算法:双路排序和单路排序

5) 优化策略

  • order by时尽量避免写select *,应只查询需要的字段

    • 当查询的字段大小总和小于max_length_for_sort_data而且排序字段你不是text|blob类型时,会用改进后的单路排序算法,否则用老的多路排序算法
    • 两种算法的数据都有可能超出sort_buffer的容量,超出后会创建临时文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort_buffer_size
  • 增大sort_buffer_size参数的设置

  • 增大max_length_for_sort_data

6) 例子

mysql两种排序方式:文件排序和扫描有序索引排序

索引:key a_b_c (a,b,c)

order by by a desc, b desc, c desc # 为index方式
where a = const order by b, c # 为index方式
where a = const and b = const order by c # 为index方式
where a = const and b > const order by b, c # 为index方式
where a in (...) order by b, c # 为filesort方式,对于排序来说,多个相等条件也是范围查询

7) group by关键字优化

  • group by实质是先排序后分组,遵照索引建的最佳左前缀
  • 当无法使用索引列,增大max_length_for_sort_data参数的设置+增大sort_buffer_size参数的设置
  • where高于having,能写where限定的条件就不要去写having限定了

2. 慢查询日志

(1) 知识点

是什么:mysql提供的一种日志记录,永安里记录mysql中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的SQL语句会被记录到慢查询日志中。long_query_time默认值为10,即10秒。默认没有开启慢查询日志,如果不是调优需要,一般不建议启用该功能。

查看默认情况:show variables like '%slow_query_log%';

开启:set global slow_query_log = 1;只对当前数据库有效,数据库重启后失效,要想永久生效需要修改配置文件

查看long_query_time:show variables like '%long_query_time%';

设置时间:set global long_query_time = 3;

设置完后需要用show global variables like '%long_query_time%'查看,或者重开一个连接查看

select sleep(4);让查询睡4秒进行测试

查询慢查询次数:show global status like '%Slow_queries%'

(2) 日志分析工具:mysqldumpslow

1) 参数

  • s:按照哪种方式排序
  • c:访问次数
  • I:锁定时间
  • r:返回记录
  • t:查询时间
  • al:平均锁定时间
  • ar:平均返回记录数
  • at:平均查询时间
  • t:返回前面多少条的记录
  • g:后面搭配一个正则表达式,大小写不敏感

2) mysqldumpslow语句

得到返回记录最多的10个SQL:mysqldumpslow -s r -t 10 慢查询日志路径

得到访问次数最多的10个SQL:mysqldumpslow -s c -t 10 慢查询日志路径

得到按时间排序的前10条里面含有左连接的查询语句:mysqldumpslow -s t -t 10 -g "left join" 慢查询日志路径

建议使用这些命令时与|more结合使用,否则会爆屏:mysqldumpslow -s r -t 10 慢查询日志路径 | more

3. 批量插入数据库脚本

(1) 创建表

create table dept(
	id in t 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 talbe emp(
    id in t 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 dediumint 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;

若报错,则用show variables like 'log_bin_trust_function_creators'查看,用set global log_bin_trust_function_creators=1设置开启,也是临时有效的

(2) 随机产生字符串

delimiter $$
create function rand_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 ;改回原来的分号作为结束符。

(3) 随机产生部门编号

delimiter $$
create function rand_num() returns int(5)
begin
	declare i int default 0;
	set i = floor(100 + rand()*10);
	return i;
end $$

(4) 创建往emp表中插入数据的存储过程

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),rand_string(6),'SALESMAN',0001,CURDATE(),2000,400,rand_num());
        until i = max_num
	end repeat;
	commit;
end $$

(5) 创建往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 $$

(6) 调用存储过程

call insert_emp(100001,50);

4. Show Profile

show profile是mysql提供的可以用来分析当前会话中语句执行的资源消耗情况,可以用于SQL的调优的测量。默认情况下处于关闭状态,并保存最近15次的运行结果

  • 是否支持show profile:show variables like ''profiling;

  • 开启功能:set profiling=on;

  • 运行sql:

    select * from emp group by id%10 limit 150000;
    select * from emp group by id%20 order by 5;
    
    
  • 查看结果:show profiles;

  • 诊断sql:show profile cpu, block io for query 上一步的Query_ID;常见类型:

    • all:所有开销信息
    • block io : 块IO相关开销
    • context switches : 上下文切换相关开销
    • cpu : cpu相关开销
    • ipc : 发送和接受相关开销
    • memory : 内存相关开销
    • page faults:页面错误相关开销
    • source:和Source_function,Source_file,Source_line相关的开销
    • swaps:交换次数相关开销
  • 开发日常需要注意的结论

    • converting HEAP to MyISAM:查询结果太大,内存不够用,往磁盘上搬了
    • Creating tmp talbe:创建临时表,先拷贝数据到临时表,使用完再删除
    • Copying to tmp table on disk:把内存中临时表复制到磁盘,危险!!!
    • locked

5. 全局查询日志

  • 开启:set global general_log=1;和set global log_output=“TABLE”;,此后编写的SQL语句将会记录到MySQL库里的general_log表中,可以用select * from mysql.general_log;查看
  • 永远不要在生产环境开启该功能

四、MySQL锁机制

锁是计算机协调多个进程或线程并发访问某一资源的机制。

从对数据操作的类型来分,分为读锁和写锁。从对数据操作的粒度来分,分为表锁和行锁。

读锁(共享锁):对同一数据,多个读操作可以同时进行而不会互相影响

写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁

表锁

表锁(偏读):偏向MyISAM存储引擎,开销小,加锁块,无死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低

读锁

# 建表
create table mylock(
	id int not null primary key auto_increament,
    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');
insert into mylock(name) values('f');

select * from mylock;

# 加锁
lock table mylock read, book write;
show open tables;
# 解锁
unlock tables;

# 在第一个终端中
# 加读锁
lock table mylock read;
select * from mylock;
# 不能修改和插入
update mylock set name="a2" where id=1;
# 不能读其他没有锁定的表
select * from book;

# 在第二个终端中
# 可以读
select * from mylock;
# 修改和插入被阻塞了,只有第一个终端解锁(释放锁)后才能更新
update mylock set name="a3" where id=1;
# 可以查询或更新未锁定的表
update staffs set name="z2" where id=1;
select * from staffs;

写锁

# 在第一个终端中
# 加写锁
lock table mylock write;
# 可以读
select * from mylock;
# 可以更新或插入
update mylock set name="a4" where id=1;
# 不能读其他未锁定的表
select * from book;

# 在第二个终端中
# 会阻塞,只有第一个终端释放锁后才能执行
select * from mylock;

MyISAM在执行查询语句前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁,。

对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其他进程的写操作。

对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其他进程的缩写操作。

简言之,就是读锁会阻塞写,但不会阻塞读。而写锁则会把读和写都阻塞。

表锁分析

show open tables:查看哪些表被加锁了,0的为未加锁的。

show status like "table%":有两个状态变量记录MySQL内部表锁的情况

  • Table_locks_immediate:产生表级锁定的次数,表示可以立即获取所的查询次数,每立即获取锁值加1
  • Table_locks_waited:出现表级锁定征用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则表名存在着较严重的表级锁争用情况

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

行锁

行锁(偏写):偏向InnoDB存储引擎,开销大,加锁慢,有死锁,锁定粒度小,发生锁冲突的概率最低,并发度最高

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_b_ind on test_innodb_lock(b);

# 第一个终端
# 设置手动提交
set autocommit=0;
select * from test_innodb_lock;
update test_innodb_lock set b="4001" where a=4;
select * from test_innodb_lock;
commit;
update test_innodb_lock set b="4002" where a=4;
commit;
update test_innodb_lock set b="4004" where a=4;

# 第二个终端
# 设置手动提交
set autocommit=0;
# 在第一个终端更新a=4的行后读,发现没变化
select * from test_innodb_lock;
# 在第一个终端提交后再查询,未变化
select * from test_innodb_lock;
commit;
# 已变化
select * from test_innodb_lock;
# 第一个终端更新后,第二个终端更新会阻塞
update test_innodb_lock set b="4003" where a=4;
commit;
# 两边都提交后才会发生变化
select * from test_innodb_lock;
# 终端1修改a=4,终端2修改a=9,可以进行
update test_innodb_lock set b="9001" where a=9;

无索引,行锁升级为表锁

# 先把表还原为原始状态

# 终端1
update test_innodb_lock set a=41 where b="4000";
select * from test_innodb_lock;
# b是varchar型,应该加引号,会做自动类型转换,但索引会失效
update test_innodb_lock set a=42 where b=4000;
commit;

# 终端2
# 终端1更新后终端2再更新,没问题
update test_innodb_lock set b="9001" where a=9;
# 由于无索引,导致行锁变表锁,从而更新语句阻塞。终端1提交后获得锁
update test_innodb_lock set b="9002" where a=9;

间隙锁

# 终端1
update test_innodb_lock set b="0629" where a>1 and a<6;
commit;

# 终端2
# 在终端1更新但没提交时执行插入,会阻塞,等终端1提交后,插入执行
insert into test_innodb_lokck values(2,"2000");

当我们用范围条件而不是相等条件检索数据,并请求共享或排它锁时,InnoDB会给符合条件的已有数据的索引项加锁,对于键值在条件范围内但不存在的记录,叫做间隙(GAP)。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。

间隙锁有个比较致命的弱点,就是当锁定一个范围的键值后,即使某些不存在的键值也会被无辜锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。

锁一行记录

# 终端1
begin;
# 锁定a="8"的记录,对该行的修改会阻塞
select * from test_innodb_lock where a="8" for update;

# 终端2
update test_innodb_lock set b="xxx" where a="8";

show status like "innodb_row_lock%";

其中,Innodb_row_lock_time_avg为等待平均时长,Innodb_row_lock_waits为等待总次数,Innodb_row_lock_time为等待总时长。

五、主从复制

复制的基本原理:slave会从master读取binlog来进行数据同步

MySQL复制过程分为三步:

  • master将改变记录到二进制日志(binary log),这些记录过程叫做二进制日志事件(binary log events);
  • slave将master的binary log events拷贝到它的中继日志(relay log);
  • slave重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步的且串行化的。

复制的基本原则

  • 每个slave只有一个master
  • 每个slave只能有一个唯一的服务器id
  • 每个master可以有多个slave

一注意从常见配置

MySQL版本一致且后台以服务运行

主从都配置在[mysqld]节点下,都是小写

以上是关于学习笔记MySQL数据库高级版 - 索引优化慢查询锁机制等的主要内容,如果未能解决你的问题,请参考以下文章

mysql学习第10篇:数据库之索引与慢查询优化

MySQL高级篇 - 性能优化

MySQL数据库学习第九篇索引原理与慢查询优化

MySQL高级学习笔记

mysql 查询的时候加了索引 查询还是很慢怎么办

MYSQL优化 学习笔记