自整理---Mysql高级笔记

第 1 章 MySQL 的架构介绍

1、MySQL 简介

什么是 MySQL?

  1. MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司。
  2. MySQL是一种关联数据库管理系统,将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
  3. Mysql是开源的,所以你不需要支付额外的费用。
  4. Mysl支持大型的数据库,可以处理拥有上千万条记录的大型数据库。
  5. MySQL使用标准的SQL数据语言形式。
  6. Mysql可以允许于多个系统上,并且支持多种语言,这些编程语言包括C、C++、Python、Java、Ped、PHP、Eifel、Ruby和TCL等。
  7. Mysql对PHP有很好的支持,PHP是目前最流行的Web开发语言。
  8. MySQL支持大型数据库,支持5000万条记录的数据仓库,32位系统表文件最大可支持4GB,64位系统支持最大的表文件为8TB。
  9. Mysql是可以定制的,采用了GPL协议,你可以修改源码来开发自己的Mysql系统。

MySQL 高手是怎样练成的?

想要熟练掌握mysql需要有以下技能:

  1. mysql内核
  2. sql优化工程师
  3. mysql服务器的优化
  4. 查询语句优化
  5. 主重复制
  6. 软硬件升级
  7. 容灾备份
  8. sql编程

2、MySQL 逻辑架构介绍

mysql 的分层思想

  1. 和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上。
  2. 插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。

mysql 四层架构

  1. 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcp/ip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。

  2. 服务层:第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化及部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定查询表的顺序,是否利用索引等,最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存。如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。

    Management Serveices & Utilities 系统管理和控制工具
    SQL Interface SQL 接口。 接受用户的 SQL 命令, 并且返回用户需要查询的结果。 比如 select from 就是调用 SQL Interface
    Parser 解析器。 SQL 命令传递到解析器的时候会被解析器验证和解析
    Optimizer 查询优化器。 SQL 语句在查询之前会使用查询优化器对查询进行优化, 比如有 where 条件时, 优化器来决定先投影还是先过滤。
    Cache 和 Buffer 查询缓存。 如果查询缓存有命中的查询结果, 查询语句就可以直接去查询缓存中取 数据。 这个缓存机制是由一系列小缓存组成的。 比如表缓存, 记录缓存, key 缓存, 权限缓存等
  3. 引擎层:存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过APl与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。后面介绍MyISAM和InnoDB

  4. 存储层:数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互。

MySQL 部件

  1. Connectors:指的是不同语言中与SQL的交互
  2. Management Serveices & Utilities: 系统管理和控制工具
  3. Connection Pool:连接池
    • 管理缓冲用户连接,线程处理等需要缓存的需求。负责监听对 MySQL Server 的各种请求,接收连接请求,转发所有连接请求到线程管理模块。
    • 每一个连接上 MySQL Server 的客户端请求都会被分配(或创建)一个连接线程为其单独服务。而连接线程的主要工作就是负责 MySQL Server 与客户端的通信。接受客户端的命令请求,传递 Server 端的结果信息等。线程管理模块则负责管理维护这些连接线程。包括线程的创建,线程的 cache 等。
  4. SQL Interface:SQL接口。接受用户的SQL命令,并且返回用户需要查询的结果。比如select from就是调用SQL Interface
  5. Parser:解析器
    • SQL命令传递到解析器的时候会被解析器验证和解析。解析器是由Lex和YACC实现的,是一个很长的脚本。
    • 在 MySQL中我们习惯将所有 Client 端发送给 Server 端的命令都称为 Query,在 MySQL Server 里面,连接线程接收到客户端的一个 Query 后,会直接将该 Query 传递给专门负责将各种 Query 进行分类然后转发给各个对应的处理模块。
    • 解析器的主要功能:
      • 将SQL语句进行语义和语法的分析,分解成数据结构,然后按照不同的操作类型进行分类,然后做出针对性的转发到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。
      • 如果在分解构成中遇到错误,那么就说明这个sql语句是不合理的
  6. Optimizer:查询优化器
    • SQL语句在查询之前会使用查询优化器对查询进行优化。就是优化客户端发送过来的 sql 语句 ,根据客户端请求的 query 语句,和数据库中的一些统计信息,在一系列算法的基础上进行分析,得出一个最优的策略,告诉后面的程序如何取得这个 query 语句的结果
    • 他使用的是“选取-投影-联接”策略进行查询。
      • 用一个例子就可以理解: select uid,name from user where gender = 1;
      • 这个select 查询先根据where 语句进行选取,而不是先将表全部查询出来以后再进行gender过滤
      • 这个select查询先根据uid和name进行属性投影,而不是将属性全部取出以后再进行过滤
      • 将这两个查询条件联接起来生成最终查询结果
  7. Cache和Buffer:查询缓存
    • 他的主要功能是将客户端提交 给MySQL 的 Select 类 query 请求的返回结果集 cache 到内存中,与该 query 的一个 hash 值 做一个对应。该 Query 所取数据的基表发生任何数据的变化之后, MySQL 会自动使该 query 的Cache 失效。在读写比例非常高的应用系统中, Query Cache 对性能的提高是非常显著的。当然它对内存的消耗也是非常大的。
    • 如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等
  8. 存储引擎接口
    • 存储引擎接口模块可以说是 MySQL 数据库中最有特色的一点了。目前各种数据库产品中,基本上只有 MySQL 可以实现其底层数据存储引擎的插件式管理。这个模块实际上只是\一个抽象类,但正是因为它成功地将各种数据处理高度抽象化,才成就了今天 MySQL 可插拔存储引擎的特色。
    • 从上图还可以看出,MySQL区别于其他数据库的最重要的特点就是其插件式的表存储引擎。MySQL插件式的存储引擎架构提供了一系列标准的管理和服务支持,这些标准与存储引擎本身无关,可能是每个数据库系统本身都必需的,如SQL分析器和优化器等,而存储引擎是底层物理结构的实现,每个存储引擎开发者都可以按照自己的意愿来进行开发。
    • 注意:存储引擎是基于表table的,而不是数据库Database。

SQL 大致的查询流程

mysql 的查询流程大致是:

  1. mysql 客户端通过协议与 mysql 服务器建连接, 发送查询语句, 先检查查询缓存, 如果命中, 直接返回结果,否则进行语句解析,也就是说, 在解析查询之前, 服务器会先访问查询缓存(query cache)——它存储 SELECT 语句以及相应的查询结果集。 如果某个查询结果已经位于缓存中, 服务器就不会再对查询进行解析、 优化、 以及执行。 它仅仅将缓存中的结果返回给用户即可, 这将大大提高系统的性能。
  2. 语法解析器和预处理: 首先 mysql 通过关键字将 SQL 语句进行解析, 并生成一颗对应的“解析树”。 mysql 解析器将使用 mysql 语法规则验证和解析查询; 预处理器则根据一些 mysql 规则进一步检查解析数是否合法。
  3. 查询优化器当解析树被认为是合法的了, 并且由优化器将其转化成执行计划。 一条查询可以有很多种执行方式,最后都返回相同的结果。 优化器的作用就是找到这其中最好的执行计划。
  4. 然后, mysql 默认使用的 BTREE 索引, 并且一个大致方向是:无论怎么折腾 sql, 至少在目前来说, mysql 最多只用到表中的一个索引。

3、MySQL 存储引擎

查看 mysql 存储引擎

  • 查看 mysql 支持的存储引擎
show engines;

自整理---Mysql高级笔记_第1张图片

  • 查看 mysql 默认的存储引擎
show variables like '%storage_engine%';

image-20200803211257403

MyISAM 引擎和 InnoDb 引擎的对比

自整理---Mysql高级笔记_第2张图片

阿里巴巴用的是啥数据库?

  1. Percona为MySQL数据库服务器进行了改进,在功能和性能上较MySQL有着很显著的提升。该版本提升了在高负载情况下的InnoDB的性能、为DBA提供一些非常有用的性能诊断工具;另外有更多的参数和命令来控制服务器行为。
  2. 该公司新建了一款存储引擎叫xtradb完全可以替代innodb,并且在性能和并发上做得更好,阿里巴巴大部分mysql数据库其实使用的percona的原型加以修改。

自整理---Mysql高级笔记_第3张图片

Innodb和myisam的文件存储格式

innodb

  • xxx.frm: 保存了每个表的表结构以及元数据。
  • xxx.ibd : 既存储索引又存储表数据

myisam

  • xxx.frm
  • xxx.MYD :表的数据文件
  • xxx.MYI: 表的索引文件

第 2 章 索引优化分析

1、慢 SQL

性能下降、 SQL 慢、执行时间长、等待时间长的原因分析

  1. 查询语句写的烂
  2. 索引失效:
    • 单值索引:在user表中给name属性建个索引,create index idx_user_name on user(name)
    • 复合索引:在user表中给name、email属性建个索引,create index idx_user_nameEmail on user(name,email)
  3. 关联查询太多join(设计缺陷或不得已的需求)
  4. 服务器调优及各个参数设置(缓冲、线程数等)

2、join 查询

2.1、SQL 执行顺序

我们手写的 SQL 顺序

image-20200803212731183

MySQL 实际执行 SQL 顺序

  1. mysql 执行的顺序:随着 Mysql 版本的更新换代, 其优化器也在不断的升级, 优化器会分析不同执行顺序产生的性能消耗不同而动态调整执行顺序。
  2. 下面是经常出现的查询顺序:

image-20200803212826429

  • 总结:mysql 从 FROM 开始执行~

image-20200803212936111

2.2、JOIN 连接查询

常见的 JOIN 查询图

image-20200803213106434

建表 SQL

CREATE TABLE tbl_dept(
	id INT(11) NOT NULL AUTO_INCREMENT,
	deptName VARCHAR(30) DEFAULT NULL,
	locAdd VARCHAR(40) DEFAULT NULL,
	PRIMARY KEY(id)
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

CREATE TABLE tbl_emp (
	id INT(11) NOT NULL AUTO_INCREMENT,
	NAME VARCHAR(20) DEFAULT NULL,
	deptId INT(11) DEFAULT NULL,
	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;

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

7 种 JOIN 示例

笛卡尔积

  1. tbl_emp 表和 tbl_dept 表的笛卡尔乘积:select * from tbl_emp, tbl_dept;
  2. 其结果集的个数为:5 * 8 = 40
mysql> select * from tbl_emp, tbl_dept;
40 rows in set (0.00 sec)

inner join

  1. tbl_emp 表和 tbl_dept 的交集部分(公共部分)
  2. select * from tbl_emp e inner join tbl_dept d on e.deptId = d.id;
mysql> select * from tbl_emp e inner join tbl_dept d on e.deptId = d.id;

left join

  1. tbl_emp 与 tbl_dept 的公共部分 + tbl_emp 表的独有部分
  2. left join:取左表独有部分 + 两表公共部分
  3. select * from tbl_emp e left join tbl_dept d on e.deptId = d.id;
mysql> select * from tbl_emp e left join tbl_dept d on e.deptId = d.id;

right join

  1. tbl_emp 与 tbl_dept 的公共部分 + tbl_dept表的独有部分
  2. right join:取右表独有部分 + 两表公共部分
  3. select * from tbl_emp e right join tbl_dept d on e.deptId = d.id;
mysql> select * from tbl_emp e right join tbl_dept d on e.deptId = d.id;

自整理---Mysql高级笔记_第4张图片

LEFT JOIN: 左边表是主表,会显示左边表的所有部分,且左边表对应的右边表虽然不存在但是也会显示,此时显示为null

RIGHT JOIN: 右边表是主表

INNER JOIN: 取两表的公共部分


left join without common part

  1. tbl_emp 表的独有部分:将 left join 结果集中的两表公共部分去掉即可:where d.id is null
  2. select * from tbl_emp e left join tbl_dept d on e.deptId = d.id where d.id is null;
mysql> select * from tbl_emp e left join tbl_dept d on e.deptId = d.id where d.id is null;
+----+------+--------+------+----------+--------+
| id | NAME | deptId | id   | deptName | locAdd |
+----+------+--------+------+----------+--------+
|  8 | s9   |     51 | NULL | NULL     | NULL   |
+----+------+--------+------+----------+--------+
1 row in set (0.00 sec)

right join without common part

  1. tbl_dept表的独有部分:将 right join 结果集中的两表公共部分去掉即可:where e.id is null
  2. select * from tbl_emp e right join tbl_dept d on e.deptId = d.id where e.id is null;
mysql> select * from tbl_emp e right join tbl_dept d on e.deptId = d.id where e.id is null;
+------+------+--------+----+----------+--------+
| id   | NAME | deptId | id | deptName | locAdd |
+------+------+--------+----+----------+--------+
| NULL | NULL |   NULL |  5 | FD       | 15     |
+------+------+--------+----+----------+--------+
1 row in set (0.00 sec)

full join

  1. 兄弟,mysql 不支持 full join ,但是我们可以通过骚操作实现 full join ,union 关键字用于连接结果集,并且自动去重

  2. tbl_emp 与 tbl_dept 的公共部分 + tbl_emp 表的独有部分 + tbl_dept表的独有部分:将 left join 的结果集和 right join 的结果集使用 union 合并即可

  3.  select * from tbl_emp e left join tbl_dept d on e.deptId = d.id
     union 
     select * from tbl_emp e right join tbl_dept d on e.deptId = d.id;
    
mysql> select * from tbl_emp e left join tbl_dept d on e.deptId = d.id
    -> union
    -> select * from tbl_emp e right join tbl_dept d on e.deptId = d.id;
+------+------+--------+------+----------+--------+
| id   | NAME | deptId | id   | deptName | locAdd |
+------+------+--------+------+----------+--------+
|    1 | z3   |      1 |    1 | RD       | 11     |
|    2 | z4   |      1 |    1 | RD       | 11     |
|    3 | z5   |      1 |    1 | RD       | 11     |
|    4 | w5   |      2 |    2 | HR       | 12     |
|    5 | w6   |      2 |    2 | HR       | 12     |
|    6 | s7   |      3 |    3 | MK       | 13     |
|    7 | s8   |      4 |    4 | MIS      | 14     |
|    8 | s9   |     51 | NULL | NULL     | NULL   |
| NULL | NULL |   NULL |    5 | FD       | 15     |
+------+------+--------+------+----------+--------+
9 rows in set (0.00 sec)

full join without common part

  1. tbl_emp 表的独有部分 + tbl_dept表的独有部分
mysql> select * from tbl_emp e left join tbl_dept d on e.deptId = d.id where d.id is null
    -> union
    -> select * from tbl_emp e right join tbl_dept d on e.deptId = d.id where e.id is null;
+------+------+--------+------+----------+--------+
| id   | NAME | deptId | id   | deptName | locAdd |
+------+------+--------+------+----------+--------+
|    8 | s9   |     51 | NULL | NULL     | NULL   |
| NULL | NULL |   NULL |    5 | FD       | 15     |
+------+------+--------+------+----------+--------+
2 rows in set (0.00 sec)

3、索引简介

3.1、索引是什么

索引是个什么东东?

  1. MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。可以得到索引的本质:索引是数据结构

  2. 你可以简单理解为"排好序的快速查找数据结构",即索引 = 排序 + 查找

  3. 一般来说索引本身占用内存空间也很大,不可能全部存储在内存中,因此索引往往以文件形式存储在硬盘上

  4. 我们平时所说的索引,如果没有特别指明,都是指B树(多路搜索树,并不一定是二叉树)结构组织的索引。

  5. 聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。当然,除了B+树这种类型的索引之外,还有哈希索引(hash index)等

3.2、索引原理

将索引理解为"排好序的快速查找数据结构"

  1. 在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
  2. 下图就是一种可能的索引方式示例:
    • 左边是数据表,一共有两列七条记录,最左边的十六进制数字是数据记录的物理地址
    • 为了加快col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到相应数据,从而快速的检索出符合条件的记录。
自整理---Mysql高级笔记_第5张图片

举例:如果我们想要找23这个数据,没有索引的情况下需要遍历整张表才能找到对应的数据,有索引的情况下只需要两次即可找到23,起到快速定位和查找对应数据的作用

索引的结构:磁盘块(数据页)+ 数据块+ 指针

3.3、索引优劣势

索引的优势

  1. 类似大学图书馆的书目索引,提高数据检索效率,通过减少磁盘IO次数来达到降低数据库的IO成本的目的
  2. 通过索引列对数据进行排序,降低数据排序成本,降低了CPU的消耗
  3. 加快了查询速度
自整理---Mysql高级笔记_第6张图片

索引的劣势

  1. 实际上索引也是一张表,该表保存了主键和索引字段,并指向实体表的记录,所以索引也占用空间
  2. 虽然索引大大提高了查询速度,同时却会降低更新表的速度(增删改)。因为更新表时,Mysql不仅要更新数据,还会更新索引
  3. 索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立优秀的索引,或优化查询语句

3.4、MySQL 索引分类

  1. 普通索引(单值索引):是最基本的索引,即一个索引只包含单个列,一个表可以有多个单列索引

  2. 唯一索引:索引的每个值都是唯一的,但允许有空值。

  3. 主键索引:设置主键时数据库会自动创建

  4. 复合索引:即一个索引包含多个列

  5. 全文索引:主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配

    `ALTER TABLE tbl_name ADD FULLTEXT index_name(column_list)`# 该语句指定了索引为FULLTEXT,用于全文索引。
    

覆盖索引

表结构如下:

CREATE TABLE `tuser` (
  `id` int(11) NOT NULL,
  `id_card` varchar(32) DEFAULT NULL,
  `name` varchar(32) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `ismale` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id_card` (`id_card`),
  KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB

如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。

创建一个身份证号和姓名的联合索引,此时通过身份证号查询姓名就是覆盖索引,不再需要回表查整行记录,减少语句的执行时间。

select name from tuser where id_card =  230227200308082328;

使用覆盖索引的前提是要有复合索引。

索引下推

还是以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:

select * from tuser where name like '张 %' and age=10 and ismale=1;

在 MySQL 5.6 之前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。

而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数

无索引下推执行流程

自整理---Mysql高级笔记_第7张图片

有索引下推执行流程

自整理---Mysql高级笔记_第8张图片

可见,在无索引下推中直接通过第一个索引name找到符合条件的记录后就回表了,有索引下推通过第一个索引name找到符合条件的记录后发现后面的条件是联合索引中的下一个索引,因此接着通过索引过滤另一个结果。将所有结果过滤完后再回表,明显减少回表次数

3.5、MySQL 索引语法

查看索引(\G表示将查询到的横向表格纵向输出,方便阅读)

SHOW INDEX FROM table_name\G 

带你看看 mysql 索引:Index_type 为 BTREE

​ show index from t_emp;image-20210712122808095

3.6、MySQL 索引结构

3.6.1、Btree 索引

Btree 索引搜索过程

【初始化介绍】

  1. 一颗 b 树, 蓝色的块我们称之为一个磁盘块, 可以看到每个磁盘块包含几个数据项 和指针(黄色所示)
  2. 如磁盘块 1 包含数据项 17 和 35, 包含指针 P1、 P2、 P3
  3. P1 表示小于 17 的磁盘块, P2 表示在 17 和 35 之间的磁盘块, P3 表示大于 35 的磁盘块
  4. 真实的数据存在于叶子节点和非叶子节点中

【查找过程】

  1. 如果要查找数据项 29, 那么首先会把磁盘块 1 由磁盘加载到内存, 此时发生一次 IO, 在内存中用二分查找确定 29在 17 和 35 之间, 锁定磁盘块 1 的 P2 指针, 内存时间因为非常短(相比磁盘的 IO) 可以忽略不计
  2. 通过磁盘块 1的 P2 指针的磁盘地址把磁盘块 3 由磁盘加载到内存, 发生第二次 IO, 29 在 26 和 30 之间, 锁定磁盘块 3 的 P2 指针
  3. 通过指针加载磁盘块 8 到内存, 发生第三次 IO, 同时内存中做二分查找找到 29, 结束查询, 总计三次 IO。

image-20200804093236612

3.6.2、B+tree 索引

原理图:自整理---Mysql高级笔记_第9张图片

【B+Tree 与 BTree 的区别】

B树的关键字(数据项)和记录是放在一起的; B+树的非叶子节点中只有关键字和指向下一个节点的索引, 记录只放在叶子节点中。

【B+Tree 与 BTree 的查找过程】

  1. 在 B 树中, 越靠近根节点的记录查找时间越快, 只要找到关键字即可确定记录的存在; 而 B+ 树中每个记录的查找时间基本是一样的, 都需要从根节点走到叶子节点, 而且在叶子节点中还要再比较关键字。
  2. 从这个角度看 B 树的性能好像要比 B+ 树好, 而在实际应用中却是 B+ 树的性能要好些。 因为 B+ 树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比 B 树多, 树高比 B 树小, 这样带来的好处是减少磁盘访问次数。
  3. 尽管 B+ 树找到一个记录所需的比较次数要比 B 树多, 但是一次磁盘访问的时间相当于成百上千次内存比较的时间, 因此实际中B+ 树的性能可能还会好些, 而且 B+树的叶子节点使用指针连接在一起, 方便顺序遍历(范围搜索), 这也是很多数据库和文件系统使用 B+树的缘故。

【性能提升】

真实的情况是, 3 层的 B+ 树可以表示上百万的数据, 如果上百万的数据查找只需要三次 IO, 性能提高将是巨大的,如果没有索引, 每个数据项都要发生一次 IO, 那么总共需要百万次的 IO, 显然成本非常非常高。

【思考: 为什么说 B+树比 B-树更适合实际应用中操作系统的文件索引和数据库索引?】

  1. B+树的磁盘读写代价更低:B树无论叶子节点和非叶子节点上都存放数据,而且每个节点都是一个磁盘块,而B+树只在叶子节点上存放数据,一个磁盘块可以存放大量的数据,因此可以明显的降低磁盘的io,要知道一次I/O可是非常耗时的。
  2. B+树的查询效率更加稳定:因为B+树非叶子节点上没有数据,通过B+树每次查询所走的路径长度相同,查询时间更稳定

3.7、何时需要/不需要创建索引

哪些情况下适合建立索引

  1. 频繁作为查询的条件的字段应该创建索引
  2. 查询中与其他表关联的字段,外键关系建立索引
  3. 查询中排序、统计、或者分组字段

哪些情况不要创建索引

  1. 表记录太少
  2. 经常增删改的表字段频繁的更新:因为每次增删改的同时也会修改数据的索引,从而降低增删改的效率,但这也视情况而定
  3. 数据重复且分布平均的表字段: 因此应该只为经常查询和经常排序的数据列建立索引。注意,如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果。

注:索引左前缀原则。对于组合索引(多个字段中使用同一个索引),我们在查询过程中要使用左前缀原则,即:查询中要用到组合索引中的最左字段

自整理---Mysql高级笔记_第10张图片


案例分析:

  1. 假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度。
  2. 索引的选择性是指索引列中不同值的数目与表中记录数的比。如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99。
  3. 一个索引的选择性越接近于1,这个索引的效率就越高。

3.8、聚簇索引和非聚簇索引

非聚簇索引(辅助索引)

MyIsam的B+树实现示意图:

自整理---Mysql高级笔记_第11张图片

可以看出,MyIsam的叶子节点只存储了数据对应的引用地址,并指向了真实数据,此为非聚簇索引

聚簇索引

InnoDB的主键索引的B+树实现示意图:

自整理---Mysql高级笔记_第12张图片

InnoDB的叶子节点存储的就是真实数据本身,此为聚簇索引

聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引

Innodb通过主键聚集数据,如果没有定义主键,innodb会选择非空的唯一索引代替如果没有这样的索引,innodb会隐式的定义一个主键来作为聚簇索引。因此innodb上必有且仅有一个聚簇索引

对比

MyISM主键索引使用的是非聚簇索引,在myisam中主键索引和非主键索引的节点结构完全一致,只是存储的内容不同而已,主键索引B+树的节点存储了主键的值,非主键索引B+树存储了非主键的值。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。因此在myisam不存在回表的现象

Innodb主键索引使用的是聚簇索引,在Innodb中主键索引和非主键索引结构上是不同的,主键索引上寸的是真实数据,非主键索引对应的键上寸的是其对应的id值,因此通过非主键索引查询数据会产生回表(注:通过非主键索引查询也有不回表的情况:例如 select id, coloum from table where coloum = '' 此时就直接通过非主键索引查到数据)

自整理---Mysql高级笔记_第13张图片

自整理---Mysql高级笔记_第14张图片

3.9、索引模型

  1. 哈希索引

    哈希表这种结构只适用于等值查询精确匹配的场景,不适合使用范围查询,哈希索引用在mysql的memory引擎中或者Memcached 及其他一些 NoSQL 引擎。

哈希索引自身只需存储对应的hash值,所以索引的结构十分紧凑,这让哈希索引查找的速度非常快

哈希索引的缺点

  1. 利用hash存储的话需要将所有的数据文件添加到内存,耗费内存空间

  2. 如果搜有的查询时等只查询就会很快,但是实际开发中使用范围查找的sql更多,因此hash索引就不合适

    案例

    当需要存储大量的URL,并且根据URL进行搜索查找,如果使用B+树,存储的内容就会很大 select id from url where url="" 也可以利用将url使用CRC32做哈希,可以使用以下查询方式: select id fom url where url="" and url_crc=CRC32("") 此查询性能较高原因是使用体积很小的索引来完成查找

    自整理---Mysql高级笔记_第15张图片
  3. 有序数组索引

    有序数组在等值查询和范围查询场景中的性能就都非常优秀,但是有序数组索引只适用于静态存储引擎,比如你要保存的是 2021 年某个城市的所有人口信息,这类不会再修改的数据。因为一旦添加和删除数据,就会造成数据的挪动,成本太高

    自整理---Mysql高级笔记_第16张图片
  4. 树形索引

    自整理---Mysql高级笔记_第17张图片

4、性能分析

4.1、性能优化概述

MySQL Query Optimizer 的作用

  1. MySQL 中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供他认为最优的执行计划(MySQL认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)
  2. 当客户端向MySQL 请求一条Query,命令解析器模块完成请求分类,区别出是SELECT并转发给MySQL Query Optimizer时,MySQL Query Optimizer 首先会对整条Query进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对Query中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析 Query中的Hint信息(如果有),看显示Hint信息是否可以完全确定该Query的执行计划。如果没有Hint 或Hint 信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据Query进行写相应的计算分析,然后再得出最后的执行计划。

MySQL 常见瓶颈

  1. CPU 瓶颈:CPU在饱和的时候一般发生在数据装入在内存或从磁盘上读取数据时候
  2. IO 瓶颈:磁盘I/O瓶颈发生在装入数据远大于内存容量时
  3. 服务器硬件的性能瓶颈:top、free、iostat和vmstat来查看系统的性能状态

4.2、Explain 概述

Explain

是什么?Explain 是查看执行计划

  1. 使用EXPLAIN关键字可以模拟优化器执行SQL语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是结构的性能瓶颈
  2. 官网地址:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html

能干嘛?

  1. 分析表的读取顺序(id 字段)
  2. 分析数据读取操作的操作类型(select_type 字段)
  3. 分析哪些索引可以使用(possible_keys 字段)
  4. 分析哪些索引被实际使用(keys 字段)
  5. 分析表之间的引用(ref 字段)
  6. 分析每张表有多少行被优化器查询(rows 字段)

怎么玩?

  • Explain + SQL语句
EXPLAIN SELECT	deptId,	name FROM	tbl_emp HAVING deptId IS NOT NULL;

image-20210712151955272

4.3、Explain 详解

①. id

select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序

这里的id并不是所谓的主键

id 取值的三种情况:

  1. id相同,执行顺序由上至下执行

    image-20200804101016101

  2. id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行

    image-20200804101005684

  3. 即存在id相同和不不同:先执行id优先级高的,优先级相同的从上至下执行

    image-20200804101502048

②select_type

查询的类型,主要用于区别普通查询、联合查询、子查询等复杂查询

参数讲解:

  1. SIMPLE:简单的select查询,查询中不包含子查询或者UNION
  2. PRIMARY:主要的查询,一般在查询的最外层
  3. SUBQUERY:见名知义,包含在 select 中的子查询(不在 from 子句中)
  4. DERIVED:在 from 子句中子查询,MySQL 会将结果存放在一个临时表中,也称为派生表(derived 的英文含义)
  5. UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
  6. UNION RESULT:从UNION表获取结果的SELECT

举栗

explain select (select 1 from actor where id = 1) from (select * from film where id = 1) alias;

自整理---Mysql高级笔记_第18张图片

UNION 和 UNION RESULT举例

EXPLAIN
select * from t_emp e LEFT JOIN t_dept d on e.deptId = d.id
union
select * from t_emp e RIGHT JOIN t_dept d on e.deptId = d.id;

自整理---Mysql高级笔记_第19张图片

③table

表示 explain 的一行访问的表是哪一个

④type

访问类型排列,显示查询使用了何种类型

从最好到最差依次是:system>const>eq_ref>ref>range>index>ALL一般来说,得保证查询至少达到range级别,最好能达到ref。

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

  2. const:该表至多有一个匹配行,const 用于在和 primary key 或 unique 索引中有固定值比较的情形。

    EXPLAIN select * from t_emp where id = 1;
    

    image-20210712154934978

  3. eq_ref:最多只返回一条符合条件的记录。在使用唯一性索引或主键查找时会出现该值,非常高效。

    EXPLAIN select * from t_emp e LEFT JOIN t_dept d on e.deptId = d.id where e.deptId is NULL;
    # 查询e表的独有值,表中只有一个记录
    
    
  4. ref:索引查找,不使用唯一索引,使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。

    image-20200804103959011

    注:eq_ref 和ref的区别是都是使用到索引,前者是通过指定条件(如 where col = ‘’)检索索引并只返回一条符合条件的记录,后者通过指定条件检索索引并返回多条符合条件的记录

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

    image-20200804104130086

  6. index:Full Index Scan,index与ALL区别为index类型只遍历索引树。这通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然all和index都是读全表,但index是从索引中读取的,而all是从硬盘数据库文件中读的

    create index idx_emp_name on tbl_emp(name);  # 对t_emp表中的name字段创建了索引
    explain select name from tbl_emp;
    
  7. all:FullTable Scan,将遍历全表以找到匹配的行(全表扫描)

    explain select * from t_emp;
    

    备注:一般来说,得保证查询只是达到range级别,最好达到ref

⑤possible_keys

  1. 显示可能会在这个表中使用的一个或多个索引

  2. 若查询涉及的字段上存在索引,则该索引将被列出,但不一定被查询实际使用

⑥key

  1. 实际使用的索引,如果为null,则没有使用索引

  2. 若查询中使用了覆盖索引,则该索引仅出现在key列表中

    image-20200804105225100

⑦key_len

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

image-20200804105833936

⑧ref

  1. 显示索引哪一列被使用了,如果可能的话,最好是一个常数。哪些列或常量被用于查找索引列上的值
  2. 由key_len可知t1表的索引idx_col1_col2被充分使用,t1表的col1匹配t2表的col1,t1表的col2匹配了一个常量,即ac。

注意区分type参数中的ref非唯一索引扫描

⑧rows

rows列显示Mysql执行查询时认为它执行查询时必须检查的行数

通俗的讲:就是显示查过了多少行!

image-20200804111249713

⑨Extra

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

  1. Using filesort(文件排序):

    • MySQL中无法利用索引完成排序操作成为“文件排序”
    • 说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取
    • 出现 Using filesort 是不好的情况(九死一生)!,需要尽快优化 SQL!
    • 示例中第一个查询只使用了 col1 和 col3,原有索引派不上用场,所以进行了外部文件排序
    • 示例中第二个查询使用了 col1、col2 和 col3,原有索引派上用场,无需进行文件排序

    image-20200804111738931

  2. Using temporary(创建临时表):

    • 使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by
    • 出现 Using temporary 超级不好!(十死无生),需要立即优化 SQL!
    • 示例中第一个查询只使用了 col1,原有索引派不上用场,所以创建了临时表进行分组
    • 示例中第二个查询使用了 col1、col2,原有索引派上用场,无需创建临时表

    image-20200804112558915

  3. Using index(覆盖索引):

    表示相应的select操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!

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

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

      image-20200804113450147

    • 覆盖索引(Covering Index)

      • 一句话:查询的所有字段中都用到了索引,前提是必须要有主键索引,避免了回表
      • 如果要使用覆盖索引,一定要注意select列表中只取出需要的列,尽量不要使用select * ,用 ***** 查询必然有的字段用不到索引,导致性能的下降!
  4. Using where:表明使用了where过滤

  5. Using join buffer:表明使用了连接缓存

  6. impossible where:where子句的值总是false,不能用来获取任何元组

    只有错误的查询才会出现此情况!

    explain select id,name,age from t_emp where id = 1 and id = 2; 
    
  7. select tables optimized away:在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。

  8. distinct:优化distinct,在找到第一匹配的元组后即停止找同样值的工作

4.4 Explain 热身 Case

image-20200804114203012

5、索引优化

5.1、单表索引优化分析

创建表

  • 建表 SQL
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 VARCHAR(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'),
(1,1,3,3,'3','3');

查询案例

  • 查询category_id为1且comments 大于1的情况下,views最多的article_id。
mysql> SELECT id, author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;
  • 此时 article 表中只有一个主键索引
SHOW INDEX FROM article;

image-20210712153308003

  • 使用 explain 分析 SQL 语句的执行效率
EXPLAIN SELECT id, author_id FROM article WHERE category_id = 1 AND comments > 1 ORDER BY views DESC LIMIT 1;

image-20210712153324454

  • 结论:
    • 很显然,type是ALL,即最坏的情况。
    • Extra 里还出现了Using filesort,也是最坏的情况。
    • 优化是必须的。

开始优化:新建索引

  • 在 category_id 列、comments 列和 views 列上建立联合索引
create index idx_article_ccv on article(category_id, views, comments);

SHOW INDEX FROM article;

自整理---Mysql高级笔记_第20张图片

  • 再次执行查询:

测试1:

EXPLAIN SELECT id, author_id FROM article WHERE category_id > 1 AND views > 1 ORDER BY comments DESC LIMIT 1;

image-20210712153802114

测试2:

EXPLAIN SELECT id, author_id FROM article WHERE category_id = 1 AND views > 1 ORDER BY comments DESC LIMIT 1;

image-20210712153909742

测试3:

EXPLAIN SELECT id, author_id FROM article WHERE category_id > 1 AND views = 1 ORDER BY comments DESC LIMIT 1;

image-20210712153941543

  • 分析:
    • 在测试中我们已经建立了索引啊!为啥没用呢?
    • 这是因为按照B+Tree索引的工作原理,先排序 category_id,再排序 views,再排序comments。
    • 当comments字段在联合索引里处于中间位置时,因为comments>1条件是一个范围值(所谓 range),MySQL 无法利用索引再对后面的views部分进行检索,即 range 类型查询字段后面的索引无效
  • 将查询条件中改为category_id = 1 ,views = 1 ,发现 Use filesort 神奇地消失了,从这点可以验证:范围后的索引会导致索引失效
EXPLAIN SELECT id, author_id FROM article WHERE category_id = 1 AND views = 1 ORDER BY comments DESC LIMIT 1;

image-20210712154140186


删除索引

  • 删除刚才创建的 idx_article_ccv 索引
DROP INDEX idx_article_ccv ON article;

再次创建索引

  • 由于 range 后(category_id > 1)的索引会失效,这次我们建立索引时,直接抛弃 category_id 列,先利用 views 和comments 的复合索引查询所需要的数据,再从其中取出 category_id > 1的数据(我觉着应该是这样的)
create index idx_article_ccv on article(views,comments);
  • 对比测试2执行查询:可以看到,type变为了ref,Extra中的Using filesort也消失了,结果非常理想
EXPLAIN SELECT id, author_id FROM article WHERE category_id > 1 AND views = 1 ORDER BY comments DESC LIMIT 1;

image-20210713234359730

5.2、两表索引优化分析

创建表

  • 建表 SQL
CREATE TABLE IF NOT EXISTS class(
	id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	card INT(10) UNSIGNED NOT NULL,
	PRIMARY KEY(id)
);

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

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


查询案例

  • 实现两表的连接,连接条件是 class.card = book.card
SELECT * FROM class LEFT JOIN book ON class.card = book.card;
自整理---Mysql高级笔记_第21张图片
  • 使用 explain 分析 SQL 语句的性能,可以看到:驱动表是左表 class 表
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;

image-20210713234610750

  • 结论:
    • type 有 All ,rows 为表中数据总行数,说明 class 和 book 进行了全表检索
    • 即每次 class 表对 book 表进行左外连接时,都需要在 book 表中进行一次全表检索

添加索引:在右表添加索引

  • 在 book 的 card 字段上添加索引
CREATE INDEX idx_card on book(`card`);
  • 测试结果:可以看到第二行的type变为了ref,rows也变成了1,优化比较明显。
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;

image-20210713235630120

  • 分析:
    • 这是由左连接特性决定的。LEFT JOIN条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引。
    • 左表连接右表,则需要拿着左表的数据去右表里面查,索引需要在右表中建立索引

添加索引:在右表添加索引

  • 删除之前右边 book 表中的索引
DROP INDEX Y ON book;
  • 在左边 class 表的 card 字段上建立索引
ALTER TABLE class ADD INDEX X(card);
  • 再次执行左连接,发现执行查询的行数共为41行,比之前的22行多了将近一倍
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;

image-20210713235812043

因此我们可以发现左连接需要在右表的连接字段建立索引

  • 别怕,我们来执行右连接:可以看到第二行的type变为了ref,rows也变成了1优化比较明显。
EXPLAIN SELECT * FROM class RIGHT JOIN book ON class.card = book.card;

image-20210713235847393

  • 分析:
    • 这是因为RIGHT JOIN条件用于确定如何从左表搜索行,使用RIGHT JOIN右边是主表一定都有,所以左边是我们的关键点,一定需要建立索引。
    • RIGHT JOIN book :book 里面的数据一定存在于结果集中,我们需要拿着 book 表中的数据,去 class 表中搜索,所以索引需要建立在 class 表中

结论:左外链接索引建右表,右外连接索引建左表! (索引建在副表,因为主表一定全有)

5.3、三表索引优化分析

创建表

  • 建表 SQL
CREATE TABLE IF NOT EXISTS phone(
	phoneid INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	card INT(10) UNSIGNED NOT NULL,
	PRIMARY KEY(phoneid)
)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)));

查询案例

  • 实现三表的连接查询:

    select * from book LEFT JOIN class on book.card = class.card LEFT JOIN phone on class.card = phone.card;
    
  • 使用 explain 分析 SQL 指令:

    explain select * from book LEFT JOIN class on book.card = class.card LEFT JOIN phone on class.card = phone.card;
    

    image-20210715213855136

  • 结论:

    • type 有All 、rows 为表数据总行数,说明 class、 book 和 phone 表都进行了全表扫描!
    • Extra 中的Using join buffer ,表明连接过程中使用了 join 缓冲区

优化:

创建索引

  • 创建索引,在两个副表上建了索引,此sql中主表book不需要建立索引
alter table class add index idx_card (card) ;
alter table phone add index idx_card (card);
show index from class;
show index from phone;
  • 进行 LEFT JOIN ,永远都在右表的字段上建立索引

  • 执行查询:后2行的type都是ref,且总rows优化很好,效果不错。因此索引最好设置在需要经常查询的字段中。

EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card=book.card LEFT JOIN phone ON book.card = phone.card;

image-20210715214142954

解释:将两个 left join 看作是两层嵌套 for 循环

  1. 尽可能减少Join语句中的NestedLoop的循环总次数;
  2. 永远都要小表驱动大表。原因:驱动表为主表,被驱动表为副表,主表一定会遍历全表,因此要用小表作为驱动,大表作为被驱动表建立索引
  3. 优先优化NestedLoop的内层循环;
  4. 保证Join语句中被驱动表上Join条件字段已经被索引;
  5. 当无法保证被驱动表的Join条件字段被索引且内存资源充足的前提下,不要太吝惜JoinBuffer的设置;

6、索引失效

索引失效(应该避免)

创建表

  • 建表 SQL
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`);
  • staffs 表中的测试数据
select * from staffs;
自整理---Mysql高级笔记_第22张图片
  • staffs 表中的复合索引:name、age、pos
SHOW INDEX FROM staffs;

image-20210715215910810

6.1、索引失效准则判断

  1. 全值匹配我最爱,即where后面的字段全都精确匹配

    自整理---Mysql高级笔记_第23张图片
  2. 最佳左前缀法则:如果索引了多例,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。(带头大哥不能没)

  3. 在索引列上做任何操作,(计算、函数、(自动or手动)类型转换)都会导致索引失效而转向全表扫描

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

  5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select *

  6. mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描

  7. is nullis not null 也无法使用索引(早期版本不能走索引,后续版本应该优化过,可以走索引)

  8. like以通配符开头(’%abc…’)mysql索引失效会变成全表扫描操作

  9. 字符串类型的字段不加单引号索引会失效

  10. 少用or,用它连接时会索引失效,建议使用union代替

最佳左前缀法则:带头大哥不能死,中间兄弟不能断

  • 只有带头大哥 name 时
    • key = index_staffs_nameAgePos 表明索引生效
    • ref = const :这个常量就是查询时的 ‘July’ 字符串常量
EXPLAIN SELECT * FROM staffs WHERE name = 'July';

image-20210715222749092

  • 带头大哥 name 带上小弟 age
    • key = index_staffs_nameAgePos 表明索引生效
    • ref = const,const:两个常量分别为 ‘July’ 和 23
EXPLAIN SELECT * FROM staffs WHERE name = 'July'AND age = 23;

image-20210715222905951

  • 带头大哥 name 带上小弟 age ,小弟 age 带上小小弟 pos
    • key = index_staffs_nameAgePos 表明索引生效
    • ref = const,const,const :三个常量分别为 ‘July’、23 和 ‘dev’
EXPLAIN SELECT * FROM staffs WHERE name = 'July'AND age = 23 AND pos ='dev' 

image-20210715223011981

  • 带头大哥 name 挂了
    • key = NULL 说明索引失效
    • ref = null 表示 ref 也失效
    • type=All 说明使用了全表扫描
EXPLAIN SELECT * FROM staffs WHERE age = 23 AND pos = 'dev';

image-20210715223035168

  • 带头大哥 name 没挂,中间的小弟 age 跑了
    • key = index_staffs_nameAgePos 说明索引没有失效
    • ref = const 表明只使用了一个常量,即第二个常量(pos = ‘dev’)没有生效
EXPLAIN SELECT * FROM staffs WHERE name = 'July'AND pos = 'dev';

image-20210715223107167

在索引列上进行计算,会导致索引失效,进而转向全表扫描

  • 不对带头大哥 name 进行任何操作:key = index_staffs_nameAgePos 表明索引生效
EXPLAIN SELECT * FROM staffs WHERE name = 'July';

image-20210715223143824

  • 对带头大哥 name 进行操作:使用 LEFT 函数截取子串
    • key = NULL 表明索引生效
    • type = ALL 表明进行了全表扫描
EXPLAIN SELECT * FROM staffs WHERE LEFT(name,4) = 'July';

image-20210715223740045

范围之后全失效

  • 精确匹配
    • type = ref 表示非唯一索引扫描,SQL 语句将返回匹配某个单独值的所有行。
    • key_len = 140 表明表示索引中使用的字节数
EXPLAIN SELECT * FROM staffs WHERE name = 'July'AND age = 23 AND pos = 'dev';

image-20210715224008782

  • 将 age 改为范围匹配
    • type = range 表示范围扫描
    • key = index_staffs_nameAgePos 表示索引并没有失效
    • key_len = 78 ,ref = NULL 均表明范围搜索使其后面的索引均失效
EXPLAIN SELECT * FROM staffs WHERE name = 'July'AND age > 23 AND pos = 'dev';

image-20210715224027868

尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少 select *

  • SELECT * 的写法
EXPLAIN SELECT * FROM staffs WHERE name = 'July'AND age > 23 AND pos = 'dev';
  • 覆盖索引的写法:Extra = Using where; Using index ,Using index 表示使用索引列进行查询,将大大提高查询的效率
EXPLAIN SELECT name, age, pos FROM staffs WHERE name = 'July'AND age = 23 AND pos = 'dev';
  • 覆盖索引中包含 range 条件:type = ref 并且 Extra = Using where; Using index ,虽然在查询条件中使用了 范围搜索,但是由于我们只需要查找索引列,所以无需进行全表扫描

一句话:用什么查什么,尽量少查,用不到的不要查!

mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描

  • 在使用 != 和 <> 时会导致索引失效:
    • key = null 表示索引失效
    • rows = 3 表示进行了全表扫描
EXPLAIN SELECT * FROM staffs WHERE name = 'July';
EXPLAIN SELECT * FROM staffs WHERE name != 'July';

image-20210715225246578

image-20210715225256256

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

  • is null,is not null 会导致索引失效:key = null 表示索引失效
EXPLAIN SELECT * FROM staffs WHERE name is null;
EXPLAIN SELECT * FROM staffs WHERE name is not null;

image-20210715225452022

image-20210715225508864

like % 要写在最右

  • like % 写在左边的情况
    • type = All ,rows = 3 表示进行了全表扫描
    • key = null 表示索引失效
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高级笔记_第24张图片

可见,将like % 写在右边效果最佳

如果业务需求非得要将like左右两遍都加%,那就得用到覆盖索引了!

创建表

  • 建表 SQL
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;

INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('1aa1',21,'[email protected]');
INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('2bb2',23,'[email protected]');
INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('3cc3',24,'[email protected]');
INSERT INTO tbl_user(`name`,`age`,`email`)VALUES('4dd4',26,'[email protected]');
  • tbl_user 表中的测试数据
select * from tbl_user;
自整理---Mysql高级笔记_第25张图片

创建索引

  • 在 tbl_user 表的 name 字段和 age 字段创建联合索引
CREATE INDEX idx_user_nameAge ON tbl_user(name, age);
SHOW INDEX FROM tbl_user;

image-20210715225907421


测试覆盖索引

  • 如下 SQL 的索引均不会失效:
    • 只要查询的字段能和覆盖索引扯得上关系,并且没有多余字段,覆盖索引就不会失效
    • id是主键,本身也是一种索引!
EXPLAIN SELECT name, age FROM tbl_user WHERE NAME LIKE '%aa%';

EXPLAIN SELECT id FROM tbl_user WHERE NAME LIKE '%aa%';  #id是主键索引
EXPLAIN SELECT name FROM tbl_user WHERE NAME LIKE '%aa%';
EXPLAIN SELECT age FROM tbl_user WHERE NAME LIKE '%aa%';

EXPLAIN SELECT id, name FROM tbl_user WHERE NAME LIKE '%aa%';
EXPLAIN SELECT id, age FROM tbl_user WHERE NAME LIKE '%aa%';
EXPLAIN SELECT id, name, age FROM tbl_user WHERE NAME LIKE '%aa%';
  • 如下 SQL 的索引均会失效:但凡有多余字段,覆盖索引就会失效
EXPLAIN SELECT * FROM tbl_user WHERE NAME LIKE '%aa%';
EXPLAIN SELECT id, name, age, email FROM tbl_user WHERE NAME LIKE '%aa%';

EXPLAIN SELECT * FROM tbl_user WHERE NAME LIKE '%aa%';

EXPLAIN SELECT id, name, age, email FROM tbl_user WHERE NAME LIKE '%aa%';

字符串不加单引号索引失效

  • 正常操作,索引没有失效
explain select * from staffs where name='2000';
  • 如果字符串忘记写 ‘’ ,那么 mysql 会为我们进行隐式的类型转换,但凡进行了类型转换,索引都会失效
explain select * from staffs where name=2000;

使用or导致索引失效

  • 使用 or 连接,会导致索引失效
explain select * from staffs where name='z3' or name = 'July';

image-20210715230914784

6.2、索引优化面试题

索引优化面试题

创建表

  • 建表 SQL
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');

create index idx_test03_c1234 on test03(c1,c2,c3,c4);

问题:我们创建了复合索引idx_test03_c1234,根据以下SQL分析下索引使用情况?

  • 全值匹配
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' AND c3='a3' AND c4='a4';

image-20210715231801080

  • mysql 优化器进行了优化,所以我们的索引都生效了
EXPLAIN SELECT * FROM test03 WHERE c4='a4' AND c3='a3' AND c2='a2' AND c1='a1';

image-20210715231840112

  • c3 列使用了索引进行排序,并没有进行查找,导致 c4 无法用索引进行查找,即验证了索引之后全失效
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' AND c3>'a3' AND c4='a4'; 

image-20210715231916777

  • mysql 优化器进行了优化,所以我们的索引都生效了,在 c4 时进行了范围搜索
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' AND c4>'a4' AND c3='a3'; 

image-20210715232756900

  • c3 列将索引用于排序,而不是查找,c4 列没有用到索引
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' AND c4='a4' ORDER BY c3; 

image-20210715232900592

​ 因为去掉c4项key_len 的长度相同,没有用到c4索引!

EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' ORDER BY c3; 

image-20210715233031454

  • 妈耶,因为索引建立的顺序和使用的顺序不一致,导致 mysql 动用了文件排序
  • 看到 Using filesort 就要知道:此句 SQL 必须优化
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' ORDER BY c4; 

image-20210715233103431

  • 只用 c1 一个字段索引,但是c2、c3用于排序,无filesort
  • 难道因为排序的时候,c2 紧跟在 c1 之后,所以就不用 filesort 吗?
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c5='a5' ORDER BY c2, c3; 

image-20210715233418101

  • 出现了filesort,我们建的索引是1234,因为它没有按照顺序来,32颠倒了
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c5='a5' ORDER BY c3, c2; 

image-20210715233639334

  • 用c1、c2两个字段索引,但是c2、c3用于排序,无filesort
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' ORDER BY c2, c3; 

image-20210715233716144

  • 和 c5 这个坑爹货没啥关系
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' AND c5='a5' ORDER BY c2, c3; 

image-20210715233800901

  • 注意查询条件 c2=‘a2’ ,我都把 c2 查出来了(c2 为常量),我还给它排序作甚,所以没有产生 filesort
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2='a2' AND c5='a5' ORDER BY c3, c2; 

image-20210715233833537

  • group by 分组之前必排序,group by 和 order by 在索引上的问题基本是一样的
  • Using temporary; Using filesort 两个都有,Using temporary的产生那就是灭绝师太了
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c4='a4' GROUP BY c3, c2; 
  • 结论:
    • group by 基本上都需要进行排序,但凡使用不当,就会产生临时表!
    • 定值为常量、范围之后失效,最终看排序的顺序

6.3、索引失效总结

一般性建议

  1. 对于单键索引,尽量选择针对当前查询过滤性更好的索引
  2. 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
  3. 在选择组合索引的时候,尽量选择可以能包含当前查询中的where子句中更多字段的索引**(where中要多用索引)**
  4. 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的

索引优化的总结

  • like 后面以常量开头,比如 like ‘kk%’ 和 like ‘k%kk%’ ,可以理解为就是常量,因为上来就已经选择好了

自整理---Mysql高级笔记_第26张图片


like SQL 实测

  • = ‘kk’ :key_len = 93 ,请记住此参数的值,后面有用

  • like ‘kk%’:

    • key_len = 93 ,和上面一样,说明 c1 c2 c3 都用到了索引
    • type = range 表明这是一个范围搜索
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2 like 'kk%' AND c3='a3';

image-20210715234441631

  • like ‘%kk’ 和 like ‘%kk%’ :key_len = 31 ,表示只有 c1 用到了索引
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2 like '%kk' AND c3='a3';

EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2 like '%kk%' AND c3='a3';
  • like ‘k%kk%’ :key_len = 93 ,表示 c1 c2 c3 都用到了索引
EXPLAIN SELECT * FROM test03 WHERE c1='a1' AND c2 like 'k%kk%' AND c3='a3';

索引优化的总结

  • 全值匹配我最爱, 最左前缀要遵守;
  • 带头大哥不能死, 中间兄弟不能断;
  • 索引列上少计算, 范围之后全失效;
  • LIKE 百分写最右, 覆盖索引不写 *;
  • 不等空值还有OR, 索引影响要注意;
  • VAR的引号不可丢, SQL 优化有诀窍。

第 3 章 查询截取分析

1、查询优化

1.1、MySQL 优化原则

mysql 的调优大纲

  1. 慢查询的开启并捕获

  2. explain+慢SQL分析

  3. show profile查询指定SQL语句在Mysql服务器里面的执行细节和生命周期情况

    show profiles
    show PROFILE for QUERY id
    
  4. SQL数据库服务器的参数调优

永远小表驱动大表,类似嵌套循环 Nested Loop

  1. EXISTS 语法:
    • SELECT ... FROM table WHERE EXISTS(subquery)
    • 该语法可以理解为:将查询的数据,放到子查询中做条件验证,根据验证结果(TRUE或FALSE)来决定主查询的数据结果是否得以保留。
  2. EXISTS(subquery) 只返回TRUE或FALSE,因此子查询中使用SELECT *也可使用以SELECT 1`或其他,官方说法是实际执行时会忽略SELECT清单,因此没有区别
  3. EXISTS子查询的实际执行过程可能经过了优化而不是我们理解上的逐条对比,如果担忧效率问题,可进行实际检验以确定是否有效率问题。
  4. EXISTS子查询往往也可以用条件表达式、其他子查询或者JOIN来替代,何种最优需要具体问题具体分析
自整理---Mysql高级笔记_第27张图片

结论:

  1. 永远记住小表驱动大表
  2. 当 A 表数据集大于 B 表数据集时,使用 in
  3. 当 A 表数据集小于 B 表数据集时,使用 exist

in 和 exists 的用法

  • in 的写法
select * from tbl_emp e where e.deptId in (select id from tbl_dept);
  • exists 的写法
select * from tbl_emp e where exists (select 1 from tbl_dept d where e.deptId = d.id);

1.2、ORDER BY 优化

ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序

创建表

  • 建表 SQL
create table tblA(
    #id int 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);

CASE1:能使用索引进行排序的情况

  • 只有带头大哥 age
EXPLAIN SELECT * FROM tblA where age>20 order by age;

EXPLAIN SELECT * FROM tblA where birth>'2016-01-28 00:00:00' order by age;

image-20210716101733914

image-20210716101746948

  • 带头大哥 age + 小弟 birth
EXPLAIN SELECT * FROM tblA where age>20 order by age,birth;

image-20210716101835175

  • mysql 默认升序排列,全升序或者全降序,都扛得住
EXPLAIN SELECT * FROM tblA ORDER BY age ASC, birth ASC;

EXPLAIN SELECT * FROM tblA ORDER BY age DESC, birth DESC;

CASE2:不能使用索引进行排序的情况

  • 带头大哥 age 挂了
EXPLAIN SELECT * FROM tblA where age>20 order by birth;

image-20210716101948086

  • 小弟 birth 居然敢在带头大哥 age 前面
EXPLAIN SELECT * FROM tblA where age>20 order by birth,age;

image-20210716102014531

  • mysql 默认升序排列,如果全升序或者全降序,都 ok ,但是一升一降就不行!
EXPLAIN SELECT * FROM tblA ORDER BY age ASC, birth DESC;

image-20210716102051203


结论

  1. MySQL支持二种方式的排序,FileSort和Index,Index效率高,它指MySQL扫描索引本身完成排序,FileSort方式效率较低。

  2. ORDER BY满足两情况(最佳左前缀原则),会使用Index方式排序

    • ORDER BY语句使用索引最左前列
    • 使用where子句与OrderBy子句条件列组合满足索引最左前列
  3. 尽可能在索引列上完成排序操作,遵照索引建的最佳左前缀

如果未在索引列上完成排序,mysql 会启动 filesort 的两种算法:双路排序和单路排序

filesort的两种算法

  1. 双路排序

    ​ 扫描两次磁盘

    读取行指针和将要进行orderby操作的列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据传输

    • 从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。

      总结:先将要排序的字段取出来order by,再将排好序的结果按照需要去读取数据行

  2. 单路排序

    ​ 扫描一次磁盘

    • 从磁盘读取查询需要的所有列,按照将要进行orderby的列,在sort buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据,并且把随机IO变成顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。

      总结:先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果

结论及引申出的问题

由于单路是改进的算法,总体而言好过双路但是单路排序存在问题

更深层次的优化策略

  • 增大sort_buffer_size参数的设置

  • 增大max_length_for_sort_data参数的设置

  • 减少select后面的查询字段


Order By 排序索引优化的总结

1.3、GROUP BY 优化

group by关键字优化

自整理---Mysql高级笔记_第28张图片

​ group by使用索引的原则几乎跟orderby一致,唯一区别是groupby即使没有过滤条件用到索引,也可以直接使用索引。

2、慢查询日志

2.1、慢查询日志介绍

慢查询日志是什么?

  1. MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
  2. long_query_time的默认值为10,意思是运行10秒以上的SQL语句会被记录下来
  3. 由他来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合之前explain进行全面分析。

2.2、慢查询日志开启

怎么玩?

说明:

  1. 默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。
  2. 当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件

查看是否开启及如何开启

  • 查看慢查询日志是否开启:
    • 默认情况下slow_query_log的值为OFF,表示慢查询日志是禁用的
    • 可以通过设置slow_query_log的值来开启
    • 通过SHOW VARIABLES LIKE '%slow_query_log%';查看 mysql 的慢查询日志是否开启
mysql> SHOW VARIABLES LIKE '%slow_query_log%';
+---------------------+-------------------------------+
| Variable_name       | Value                         |
+---------------------+-------------------------------+
| slow_query_log      | OFF                           |
| slow_query_log_file | /var/lib/mysql/Heygo-slow.log |
+---------------------+-------------------------------+
  • 如何开启开启慢查询日志:
    • set global slow_query_log = 1;开启慢查询日志
    • 使用set global slow_query_log=1开启了慢查询日志只对当前数据库生效,如果MySQL重启后则会失效。
mysql> set global slow_query_log = 1;
Query OK, 0 rows affected (0.07 sec)

mysql> SHOW VARIABLES LIKE '%slow_query_log%';
+---------------------+-------------------------------+
| Variable_name       | Value                         |
+---------------------+-------------------------------+
| slow_query_log      | ON                            |
| slow_query_log_file | /var/lib/mysql/Heygo-slow.log |
+---------------------+-------------------------------+	
  • 如果要永久生效,就必须修改配置文件my.cnf(其它系统变量也是如此)

    • 修改my.cnf文件,[mysqld]下增加或修改参数:slow_query_log和slow_query_log_file后,然后重启MySQL服务器。
    • 也即将如下两行配置进my.cnf文件
    [mysqld]
    slow_query_log =1
    slow_query_log_file=/var/lib/mysql/Heygo-slow.log
    
  • 关于慢查询的参数slow_query_log_file,它指定慢查询日志文件的存放路径,系统默认会给一个缺省的文件host_name-slow.log(如果没有指定参数slow_query_log_file的话)


那么开启慢查询日志后,什么样的SQL参会记录到慢查询里面?

  • 这个是由参数long_query_time控制,默认情况下long_query_time的值为10秒,命令:SHOW VARIABLES LIKE 'long_query_time%';查看慢 SQL 的阈值
mysql> SHOW VARIABLES LIKE 'long_query_time%';
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
  • 可以使用命令修改,也可以在my.cnf参数里面修改。
  • 假如运行时间大于long_query_time会被记录,等于不会被记录

2.3、慢查询日志示例

案例讲解

  • 设置慢 SQL 的阈值时间,我们将其设置为 3s
mysql> set global long_query_time=3;
Query OK, 0 rows affected (0.00 sec)
  • 为什么设置后阈值时间没变?
    • 需要重新连接或者新开一个会话才能看到修改值。
    • 查看全局的 long_query_time 值:show global variables like 'long_query_time';发现已经生效
mysql> set global long_query_time=3;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW VARIABLES LIKE 'long_query_time%';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| long_query_time | 3.000000 |
+-----------------+----------+
  • 记录慢 SQL 以供后续分析

    • 怼个 select sleep(4); 超过 3s ,肯定会被记录到日志中
    mysql> select sleep(4); 
    +----------+
    | sleep(4) |
    +----------+
    |        0 |
    +----------+
    
    • 查看慢查询日志中的内容,SHOW VARIABLES LIKE '%slow_query_log%';根据自己电脑中的显示的文件地址查
  • 查询当前系统中有多少条慢查询记录:show global status like '%Slow_queries%';

mysql> show global status like '%Slow_queries%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries  | 1     |
+---------------+-------+

配置版的慢查询日志

在 /etc/my.cnf 文件的 [mysqld] 节点下配置

slow_query_log=1;
slow_query_log_file=/var/lib/mysql/Heygo-slow.log 
long_query_time=3;
log_output=FILE

日志分析命令 mysqldumpslow

mysqldumpslow是什么?

在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow。


查看 mysqldumpslow的帮助信息

[root@Heygo mysql]# mysqldumpslow --help
Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]

Parse and summarize the MySQL slow query log. Options are

  --verbose    verbose
  --debug      debug
  --help       write this text to standard output

  -v           verbose
  -d           debug
  -s ORDER     what to sort by (al, at, ar, c, l, r, t), 'at' is default
                al: average lock time
                ar: average rows sent
                at: average query time
                 c: count
                 l: lock time
                 r: rows sent
                 t: query time  
  -r           reverse the sort order (largest last instead of first)
  -t NUM       just show the top n queries
  -a           don't abstract all numbers to N and strings to 'S'
  -n NUM       abstract numbers with at least n digits within names
  -g PATTERN   grep: only consider stmts that include this string
  -h HOSTNAME  hostname of db server for *-slow.log filename (can be wildcard),
               default is '*', i.e. match all
  -i NAME      name of server instance (if using mysql.server startup script)
  -l           don't subtract lock time from total time

mysqldumpshow 参数解释

  1. s:是表示按何种方式排序
  2. c:访问次数
  3. l:锁定时间
  4. r:返回记录
  5. t:查询时间
  6. al:平均锁定时间
  7. ar:平均返回记录数
  8. at:平均查询时间
  9. t:即为返回前面多少条的数据
  10. g:后边搭配一个正则匹配模式,大小写不敏感的

常用参数手册

  1. 得到返回记录集最多的10个SQL

    mysqldumpslow -s r -t 10 /var/lib/mysql/Heygo-slow.log
    
  2. 得到访问次数最多的10个SQL

    mysqldumpslow -s c- t 10/var/lib/mysql/Heygo-slow.log
    
  3. 得到按照时间排序的前10条里面含有左连接的查询语句

    mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/Heygo-slow.log
    
  4. 另外建议在使用这些命令时结合 | 和more使用,否则有可能出现爆屏情况

    mysqldumpslow -s r -t 10 /var/lib/mysql/Heygo-slow.log | more
    

3、批量数据脚本

创建表

  • 建表 SQL
CREATE TABLE dept
(
    deptno int unsigned primary key auto_increment,
    dname varchar(20) not null default "",
    loc varchar(8) not null default ""
)ENGINE=INNODB DEFAULT CHARSET=utf8;

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;

设置参数

  • 创建函数,假如报错:This function has none of DETERMINISTIC………

  • 由于开启过慢查询日志,因为我们开启了bin-log,我们就必须为我们的function指定一个参数。

    • log_bin_trust_function_creators = OFF ,默认必须为 function 传递一个参数
    mysql> show variables like 'log_bin_trust_function_creators'; 
    +---------------------------------+-------+
    | Variable_name                   | Value |
    +---------------------------------+-------+
    | log_bin_trust_function_creators | OFF   |
    +---------------------------------+-------+
    1 row in set (0.00 sec)
    
  • 通过 set global log_bin_trust_function_creators=1;我们可以不用为 function 传参

  mysql> set global log_bin_trust_function_creators=1; 
  Query OK, 0 rows affected (0.00 sec)
  
  mysql> show variables like 'log_bin_trust_function_creators';
  +---------------------------------+-------+
  | Variable_name                   | Value |
  +---------------------------------+-------+
  | log_bin_trust_function_creators | ON    |
  +---------------------------------+-------+
  1 row in set (0.00 sec)
  • 这样添加了参数以后,如果mysqld重启,上述参数又会消失,永久方法在配置文件中修改‘

    • windows下:my.ini --> [mysqld] 节点下加上 log_bin_trust_function_creators=1
    • linux下:/etc/my.cnf --> [mysqld] 节点下加上 log_bin_trust_function_creators=1

创建函数,保证每条数据都不同

  • 随机产生字符串的函数
delimiter $$ # 两个 $$ 表示结束
create function rand_string(n int) returns varchar(255)
begin
    declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyz';
    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 $$

函数使用 的 目 的 : 我 们 正 常 执 行 s q l 都 是 以 ; 为 结 尾 的 , s q l 默 认 为 每 一 行 的 分 隔 符 都 是 分 号 , 我 们 为 了 让 函 数 里 的 语 句 整 体 执 行 暂 时 改 变 为 以 的目的:我们正常执行sql都是以 ;为结尾的,sql默认为每一行的分隔符都是分号,我们为了让函数里的语句整体执行暂时改变为以 sqlsql为结尾。

  • 随机产生部门编号的函数
delimiter $$
create function rand_num() returns int(5)
begin
    declare i int default 0;
    set i=floor(100+rand()*10);
    return i;
end $$

创建存储过程

  • 创建往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 $$
  • 创建往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 $$

调用存储过程

  • 向 dept 表中插入 10 条记录
DELIMITER ;
CALL insert_dept(100, 10);
mysql> select * from dept;
+--------+---------+--------+
| deptno | dname   | loc    |
+--------+---------+--------+
|    101 | aowswej | syrlhb |
|    102 | uvneag  | pup    |
|    103 | lps     | iudgy  |
|    104 | jipvsk  | ihytx  |
|    105 | hrpzhiv | vjb    |
|    106 | phngy   | yf     |
|    107 | uhgd    | lgst   |
|    108 | ynyl    | iio    |
|    109 | daqbgsh | mp     |
|    110 | yfbrju  | vuhsf  |
+--------+---------+--------+
10 rows in set (0.00 sec)
  • 向 emp 表中插入 50w 条记录
DELIMITER ;
CALL insert_emp(100001, 500000);
mysql> select * from emp limit 20;
+----+--------+-------+----------+-----+------------+---------+--------+--------+
| id | empno  | ename | job      | mgr | hiredate   | sal     | comm   | deptno |
+----+--------+-------+----------+-----+------------+---------+--------+--------+
|  1 | 100002 | ipbmd | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    101 |
|  2 | 100003 | bfvt  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    107 |
|  3 | 100004 |       | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    109 |
|  4 | 100005 | cptas | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    101 |
|  5 | 100006 | ftn   | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    108 |
|  6 | 100007 | gzh   | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    102 |
|  7 | 100008 | rji   | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    100 |
|  8 | 100009 |       | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    106 |
|  9 | 100010 | tms   | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    100 |
| 10 | 100011 | utxe  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    101 |
| 11 | 100012 | vbis  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    104 |
| 12 | 100013 | qgfv  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    104 |
| 13 | 100014 | wrvb  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    105 |
| 14 | 100015 | dyks  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    109 |
| 15 | 100016 | hpcs  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    101 |
| 16 | 100017 | fxb   | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    108 |
| 17 | 100018 | vqxq  | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    102 |
| 18 | 100019 | rq    | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    102 |
| 19 | 100020 | l     | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    106 |
| 20 | 100021 | lk    | salesman |   1 | 2020-08-05 | 2000.00 | 400.00 |    100 |
+----+--------+-------+----------+-----+------------+---------+--------+--------+
20 rows in set (0.00 sec)

4、Show Profile

Show Profile 是什么?

  1. 是mysql提供可以用来分析当前会话中语句执行的资源消耗情况。可以用于SQL的调优测量
  2. 官网:http://dev.mysql.com/doc/refman/5.5/en/show-profile.html
  3. 默认情况下,参数处于关闭状态,并保存最近15次的运行结果

分析步骤

查看是当前的SQL版本是否支持Show Profile

  • show variables like ‘profiling%’; 查看 Show Profile 是否开启
show variables like 'profiling%';

自整理---Mysql高级笔记_第29张图片


开启功能 Show Profile,我这里已经默认开启了

  • set profiling=on; 开启 Show Profile

运行SQL

  • 正常 SQL
select * from tbl_emp;
select * from tbl_emp e inner join tbl_dept d on e.deptId = d.id;
select * from tbl_emp e left join tbl_dept d on e.deptId = d.id;
  • 慢 SQL
select * from emp group by id%10 limit 150000;
select * from emp group by id%10 limit 150000;
select * from emp group by id%10 order by 5;

查看结果

  • 通过 show profiles; 指令查看结果
show profiles;

诊断SQL

  • show profile cpu, block io for query SQL编号; 查看 SQL 语句执行的具体流程以及每个步骤花费的时间
mysql> show profile cpu, block io for query 2;
+----------------------+----------+----------+------------+--------------+---------------+
| Status               | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out |
+----------------------+----------+----------+------------+--------------+---------------+
| starting             | 0.000055 | 0.000000 |   0.000000 |            0 |             0 |
| checking permissions | 0.000007 | 0.000000 |   0.000000 |            0 |             0 |
| Opening tables       | 0.000011 | 0.000000 |   0.000000 |            0 |             0 |
| init                 | 0.000024 | 0.000000 |   0.000000 |            0 |             0 |
| System lock          | 0.000046 | 0.000000 |   0.000000 |            0 |             0 |
| optimizing           | 0.000018 | 0.000000 |   0.000000 |            0 |             0 |
| statistics           | 0.000008 | 0.000000 |   0.000000 |            0 |             0 |
| preparing            | 0.000019 | 0.000000 |   0.000000 |            0 |             0 |
| executing            | 0.000003 | 0.000000 |   0.000000 |            0 |             0 |
| Sending data         | 0.000089 | 0.000000 |   0.000000 |            0 |             0 |
| end                  | 0.000004 | 0.000000 |   0.000000 |            0 |             0 |
| query end            | 0.000003 | 0.000000 |   0.000000 |            0 |             0 |
| closing tables       | 0.000005 | 0.000000 |   0.000000 |            0 |             0 |
| freeing items        | 0.000006 | 0.000000 |   0.000000 |            0 |             0 |
| cleaning up          | 0.000006 | 0.000000 |   0.000000 |            0 |             0 |
+----------------------+----------+----------+------------+--------------+---------------+
15 rows in set, 1 warning (0.00 sec)
  • 参数备注:
  1. ALL:显示所有的开销信息
  2. BLOCK IO:显示块IO相关开销
  3. CONTEXT SWITCHES:上下文切换相关开销
  4. CPU:显示CPU相关开销信息
  5. IPC:显示发送和接收相关开销信息
  6. MEMORY:显示内存相关开销信息
  7. PAGE FAULTS:显示页面错误相关开销信息
  8. SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息
  9. SWAPS:显示交换次数相关开销的信息

日常开发需要注意的结论

  1. converting HEAP to MyISAM:查询结果太大,内存都不够用了往磁盘上搬了。
  2. Creating tmp table:创建临时表,mysql 先将拷贝数据到临时表,然后用完再将临时表删除
  3. Copying to tmp table on disk:把内存中临时表复制到磁盘,危险!!!
  4. locked:锁表

举例

自整理---Mysql高级笔记_第30张图片

5、全局查询日志

永远不要在生产环境开启这个功能。

配置启用全局查询日志

  • 在mysql的my.cnf中,设置如下:
# 开启
general_log=1

# 记录日志文件的路径
general_log_file=/path/logfile

# 输出格式
log_output=FILE

编码启用全局查询日志

  • 执行如下指令开启全局查询日志
set global general_log=1;
set global log_output='TABLE';
  • 此后,你所执行的sql语句,将会记录到mysql库里的general_log表,可以用下面的命令查看
select * from mysql.general_log;

第 4 章 MySQL 锁机制

1、概述

1.1、锁的定义

锁的定义

  1. 锁是计算机协调多个进程或线程并发访问某一资源的机制
  2. 在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。
  3. 如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
  4. 从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

1.2、锁的分类

锁的分类

  1. 从数据操作的类型(读、写)分
    • 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响
    • 写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁。
  2. 从对数据操作的颗粒度
    • 表锁
    • 行锁

2、表锁

表锁的特点

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

2.1、表锁案例

表锁案例分析

创建表

  • 建表 SQL:引擎选择 myisam
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');

select * from mylock;
  • mylock 表中的测试数据
mysql> select * from mylock;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
|  3 | c    |
|  4 | d    |
|  5 | e    |
+----+------+

手动加锁和释放锁

  • 查看当前数据库中表的上锁情况:show open tables;,0 表示未上锁
show open tables;
  • 添加锁
lock table 表名1 read(write), 表名2 read(write), ...;
  • 释放表锁
unlock tables;

2.1.1、读锁示例

读锁示例

  • 在 session 1 会话中,给 mylock 表加个读锁
lock table mylock read;
  • 在 session1 会话中能不能读取 mylock 表:可以读
################# session1 中的操作 #################

mysql> select * from mylock;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
|  3 | c    |
|  4 | d    |
|  5 | e    |
+----+------+
5 rows in set (0.00 sec)
  • 在 session1 会话中能不能读取统一数据库的其他表:不能!
################# session1 中的操作 #################
 select * from book;
  • 在 session2 会话中能不能读取 mylock 表:可以读
################# session2 中的操作 #################

mysql> select * from mylock;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
|  3 | c    |
|  4 | d    |
|  5 | e    |
+----+------+
  • 在 session1 会话中能不能修改 mylock 表:不能!
################# session1 中的操作 #################

update mylock set name='a2' where id=1;
  • 在 session2 会话中能不能修改 mylock 表:阻塞,一旦 mylock 表锁释放unlock tables,则会解除阻塞执行修改操作
################# session2 中的操作 #################

mysql> update mylock set name='a2' where id=1;
# 在这里阻塞着呢~~~

结论

  • 在读取方面:

    当前 session 可以读取加了读锁的表,但是不能读取加锁之外的其他表

    其他session任何表都可以读

  • 在修改方面:

    当前 session 不能修改加了读锁的表

    其他 session 想要修改加了读锁的表,必须等待其读锁释放

读锁总结:加了读锁当前表只允许被访问,也不允许访问其他表(共享读锁)

2.1.2、写锁示例

写锁示例

  • 在 session 1 会话中,给 mylock 表加个写锁
lock table mylock write;
  • 在 session1 会话中能不能读取 mylock 表? 可以!
################# session1 中的操作 #################

mysql> select * from mylock;
+----+------+
| id | name |
+----+------+
|  1 | a2   |
|  2 | b    |
|  3 | c    |
|  4 | d    |
|  5 | e    |
+----+------+
5 rows in set (0.00 sec)	
  • 在 session1 会话中能不能读取该数据库的其他表?:不阔以
################# session1 中的操作 #################

select * from book;
  • 在 session1 会话中能不能修改 mylock 表:当然可以啦,加写锁就是为了修改呀
################# session1 中的操作 #################

update mylock set name='a2' where id=1;
  • 在 session2 会话中能不能读取 mylock 表:
################# session2 中的操作 #################

select * from mylock;
# 在这里阻塞着呢~~~

结论

  • 在读取方面:

    当前session可以读取加了写锁的表,不能读取加锁以外的其他表

    其他session不能读取当前session加写锁的表,必须等待其写锁的释放,但当前session可以读取其他表

  • 在写方面:

    当前session可以修改加了写锁的表,无法修改其他表

    其他session不能修改加了写锁的表,可以修改其他表

    结论:一个线程获取了一个表的写锁之后,只有持有锁的线程才能对该表进行更新操作,其他线程的读写操作都会等待直到读锁释放为止(独占写锁)

案例结论

  1. MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁
  2. MySQL的表级锁有两种模式:
    • 表共享读锁(Table Read Lock)
    • 表独占写锁(Table Write Lock)

image-20200805154049814


结论:结合上表,所以对MyISAM表进行操作,会有以下情况:

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

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

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

2.2、表锁分析

表锁分析

  • 查看哪些表被锁了,0 表示未锁,1 表示被锁
show open tables;

【如何分析表锁定】可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定,通过 show status like 'table%'; 命令查看

  1. Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1;
  2. Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在着较严重的表级锁争用情况;
mysql> show status like 'table%';
+----------------------------+--------+
| Variable_name              | Value  |
+----------------------------+--------+
| Table_locks_immediate      | 500440 |
| Table_locks_waited         | 1      |
| Table_open_cache_hits      | 500070 |
| Table_open_cache_misses    | 5      |
| Table_open_cache_overflows | 0      |
+----------------------------+--------+
5 rows in set (0.00 sec)
  • 此外,Myisam的读写锁调度是写优先,这也是myisam不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞

3、行锁

行锁的特点

  1. 偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
  2. InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁

3.1、事务复习

行锁支持事务,复习下老知识

事务(Transation)及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。

  1. 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
  2. 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
  3. 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
  4. 持久性(Durability):事务院成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发事务处理带来的问题

  1. 更新丢失

    (Lost Update):

    • 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题一一最后的更新覆盖了由其他事务所做的更新。
    • 例如,两个程序员修改同一java文件。每程序员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖前一个程序员所做的更改。
    • 如果在一个程序员完成并提交事务之前,另一个程序员不能访问同一文件,则可避免此问题。
  2. 脏读

    (Dirty Reads):

    • 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”。
    • 一句话:事务A读取到了事务B已修改但尚未提交的的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
  3. 不可重复读

    (Non-Repeatable Reads):

    • 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
    • 一句话:事务A读取到了事务B已经提交的修改数据,不符合隔离性
  4. 幻读

    (Phantom Reads)

    • 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读一句话:事务A读取到了事务B体提交的新增数据,不符合隔离性。
    • 多说一句:幻读和脏读有点类似,脏读是事务B里面修改了数据,幻读是事务B里面新增了数据。

事物的隔离级别

  1. 脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
  2. 数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
  3. 同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
  4. 查看当前数据库的事务隔离级别:show variables like 'tx_isolation'; mysql 默认是可重复读

自整理---Mysql高级笔记_第31张图片

3.2、行锁案例

行锁案例分析

创建表

  • 建表 SQL
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 表中的测试数据
mysql> select * from test_innodb_lock;
+------+------+
| a    | b    |
+------+------+
|    1 | b2   |
|    3 | 3    |
|    4 | 4000 |
|    5 | 5000 |
|    6 | 6000 |
|    7 | 7000 |
|    8 | 8000 |
|    9 | 9000 |
|    1 | b1   |
+------+------+
9 rows in set (0.00 sec)
  • test_innodb_lock 表中的索引
SHOW INDEX FROM test_innodb_lock;

操作同一行数据

  • session1 开启事务,修改 test_innodb_lock 中的数据,但是不提交
set autocommit=0;
update test_innodb_lock set b='4002' where a=4;
  • session2 开启事务,修改 test_innodb_lock 中同一行数据,将导致 session2 发生阻塞,一旦 session1 提交事务,session2 将执行更新操作
set autocommit=0;

update test_innodb_lock set b='4003' where a=4;
# 在这儿阻塞着呢~~~,除非session1进行commit!

操作不同行数据

  • session1 开启事务,修改 test_innodb_lock 中的数据
set autocommit=0;

update test_innodb_lock set b='4001' where a=4;
  • session2 开启事务,修改 test_innodb_lock 中不同行的数据
  • 由于采用行锁,session2 和 session1 互不干涉,所以 session2 中的修改操作没有阻塞
set autocommit=0;

update test_innodb_lock set b='9001' where a=9;

无索引导致行锁升级为表锁

  • session1 开启事务,修改 test_innodb_lock 中的数据,varchar 不用 ’ ’ ,导致系统自动转换类型,导致索引失效
set autocommit=0;

update test_innodb_lock set a=44 where b=4000;
  • session2 开启事务,修改 test_innodb_lock 中不同行的数据
  • 由于发生了自动类型转换,索引失效,导致行锁变为表锁
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> update test_innodb_lock set b='9001' where a=9;
# 在这儿阻塞着呢~~~

3.3、间隙锁

什么是间隙锁

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

间隙锁的危害

  1. 因为Query执行过程中通过过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。
  2. 间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害

间隙锁示例

  • test_innodb_lock 表中的数据
mysql> select * from test_innodb_lock;
+------+------+
| a    | b    |
+------+------+
|    1 | b2   |
|    5 | 5000 |
|    6 | 6000 |
|    7 | 7000 |
|    8 | 8000 |
|    9 | 9000 |
+------+------+
9 rows in set (0.00 sec)
  • session1 开启事务,执行修改 a > 1 and a < 6 的数据,这会导致 mysql 将 a =2,3,4 的数据行都被锁住(虽然表中并没有这行数据)
set autocommit=0;

update test_innodb_lock set b='Heygo' where a>1 and a<6;
  • session2 开启事务,修改 test_innodb_lock 中不同行的数据,也会导致阻塞,直至 session1 提交事务
set autocommit=0;

update test_innodb_lock set b='9001' where a=9;
# 在这儿阻塞着呢~~~

3.4、手动行锁

如何锁定一行

  • select xxx ... for update 锁定某一行后,其它的操作会被阻塞,直到锁定行的会话提交
  • session1 开启事务,手动执行 for update 锁定指定行,待执行完指定操作时再将数据提交
set autocommit=0;

select * from test_innodb_lock  where a=8 for update;
  • session2 开启事务,修改 session1 中被锁定的行,会导致阻塞,直至 session1 提交事务
set autocommit=0;

update test_innodb_lock set b='XXX' where a=8;
# 在这儿阻塞着呢~~~

3.5、行锁分析

案例结论

  1. Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。
  2. 当系统并发量较高的时候,Innodb的整体性能和MyISAM相比就会有比较明显的优势了
  3. 但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候(索引失效,导致行锁变表锁),可能会让Innodb的整体性能表现不仅不能比MyISAM高,甚至可能会更差。

行锁分析

如何分析行锁定

  • 通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
show status like 'innodb_row_lock%';
自整理---Mysql高级笔记_第32张图片

对各个状态量的说明如下:

  1. Innodb_row_lock_current_waits:当前正在等待锁定的数量;
  2. Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
  3. Innodb_row_lock_time_avg:每次等待所花平均时间;
  4. Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
  5. Innodb_row_lock_waits:系统启动后到现在总共等待的次数;

对于这5个状态变量,比较重要的主要是

  1. Innodb_row_lock_time_avg(等待平均时长)
  2. Innodb_row_lock_waits(等待总次数)
  3. Innodb_row_lock_time(等待总时长)

尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。

3.6、行锁优化

优化建议

  1. 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  2. 合理设计索引,尽量缩小锁的范围
  3. 尽可能较少检索条件,避免间隙锁
  4. 尽量控制事务大小,减少锁定资源量和时间长度
  5. 尽可能低级别事务隔离

4、页锁

  1. 开销和加锁时间界于表锁和行锁之间:会出现死锁;
  2. 锁定粒度界于表锁和行锁之间,并发度一般。
  3. 了解即可

第 5 章 主从复制

binlog日志文件

binlog日志的介绍

最开始的mysql没有innodb引擎,innodb引擎是其他公司以插件形式插入mysql的。mysql自带的引擎是myisam,但是我们知道redolog是innodb引擎特有的,其他存储引擎都没有,这就会导致没有cash-safe的能力(cash-safe指即使数据库发生异常重启,之前提交的记录都不会丢失)。

binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如 “ 给 ID=2 这一行的 c 字段加 1 ”。

binlog 记录的都是事务操作内容,binlog 有三种模式:

  • Statement(基于 SQL 语句的复制)
  • Row(基于行的复制)
  • 以及 Mixed(混合模式)

binlog日志的使用

查看binlog是否开启以及binlog目录

SHOW GLOBAL VARIABLES like 'log_bin%'
自整理---Mysql高级笔记_第33张图片

binlog日志格式

查看binlog的二进制日志格式

SHOW GLOBAL VARIABLES LIKE 'binlog_format%'

自整理---Mysql高级笔记_第34张图片

自整理---Mysql高级笔记_第35张图片

redolog、undolog日志文件

redolog的介绍

binlog是mysql的server层特有的日志 (归档日志) 是逻辑日志而redo log 是InnoDB 引擎特有的日志,也是物理日志,记录的是 “ 在某个数据页上做了什么修改 ”

redolog的作用

redolog和binlog都可以做数据的恢复

自整理---Mysql高级笔记_第36张图片

redolog的两阶段提交

可以理解为:redolog写到内存后先等着,准备写入磁盘(此时没写入磁盘,处于prepare阶段),然后等binlog也写入内存之后,提交事务commit,将redolog和binlog一起写入磁盘。简单来说就是如果没有两阶段提交就会一个一个的写入磁盘,有了两阶段提交一起写入磁盘

为了让两份日志之间的逻辑一致, redo log 的写入拆成了两个步骤: prepare 和commit ,这就是 " 两阶段提交 "。要注意:

redo log prepare: 是“ 当前事务提交 ” 的一个阶段,也就是说,在事务 A 提交的时候,我们才会走到事务 A 的 redo log prepare 这个阶段。

由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写binlog ,或者采用反过来的顺序,会产生日志与数据不一致的问题,例如

  • 先写redolog直接提交,然后写binlog,假设写完redolog后,机器挂了,binlog日志没有被写入,那么机器重启后,这台机器本机可以通过redolog恢复数据,但是binlog并没有记录该数据,后续进行数据备份的时候,造成数据不同步的问题。
  • 先写binlog,然后写redolog,假设写完了binlog,机器异常重启了,由于没有redolog,本机是无法恢复这条记录的,但是binlog又有记录,后序进行机器备份的时候,造成数据不同步的问题。

通过一种特殊的xid,可以在两阶段提交的不同时刻发生异常重启时,将redo和binlog联系起来

自整理---Mysql高级笔记_第37张图片

时序上 redo log 先 prepare , 再写 binlog ,最后再把 redo log commit。

如果已经写完了binlog,redolog处于预提交的状态,这个时候发生了异常重启,mysql的处理机制如下:

  • 判断redolog是否完整,如果判断是完整的,就立即提交
  • 如果redolog只是预提交但不是commit状态,这个时候就会去判断binlog的完整性,如果完整就提交redolog,不完整就回滚事务

因为Inoodb引擎是通过redolog来支持事务的

undolog的作用

undolog是一个很牛逼的用来支持事务的日志,undolog又称作回滚日志

在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。假如我们的事务中有5条sql,其中有2条执行失败了,3条执行成功了,mysql就会通过undolog将数据回滚到最初状态,保证了事务的原子性。

querylog日志文件

记录mysql的操作信息

SHOW VARIABLES like 'general_log_file%' #查看querylog日志文件的存放位置信息

自整理---Mysql高级笔记_第38张图片

什么是主从复制?

主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;主数据库一般是准实时的业务数据库

主从复制的作用(好处,或者说为什么要做主从)重点!

1、做数据的热备,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。
2、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。
3、读写分离,使数据库能支撑更大的并发。在报表中尤其重要。由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度。

三、主从复制的原理(重中之重,面试必问):

1.数据库有个bin-log二进制文件,记录了所有sql语句。
2.我们的目标就是把主数据库的bin-log文件的sql语句复制过来,并让其在从数据的relay-log重做日志文件中再执行一次这些sql语句即可。

下面的主从配置就是围绕这个原理配置,具体需要三个线程来操作:

  1. **Binlog-Output-Thread:**每当有从库连接到主库的时候,主库都会创建一个线程然后发送binlog内容到从库。在从库里,当复制开始的时候,从库就会创建两个线程进行处理:
  2. **从库IO-Thread:**当START SLAVE语句在从库开始执行之后,从库创建一个I/O线程,该线程连接到主库并请求主库发送binlog里面的更新记录到从库上。从库I/O线程读取主库的binlog输出线程发送的更新并拷贝这些更新到本地文件,其中包括relay log文件。
  3. **从库的SQL-Thread:**从库创建一个SQL线程,这个线程读取从库I/O线程写到relay log的更新事件并执行。

可以知道,对于每一个主从复制的连接,都有三个线程。拥有多个从库的主库为每一个连接到主库的从库创建一个binlog输出线程,每一个从库都有它自己的I/O线程和SQL线程。

image-20210718143840053

2、复制的基本原则

  1. 每个slave只有一个master
  2. 每个slave只能有一个唯一的服务器ID
  3. 每个master可以有多个salve

3、复制最大问题

因为发生多次 IO, 存在延时问题

4、一主一从常见配置

前提:mysql 版本一致,主从机在同一网段下

ping 测试

  • Linux 中 ping Windows

  • Windows 中 ping Linux

主机修改 my.ini 配置文件(Windows)

主从都配置都在 [mysqld] 节点下,都是小写,以下是老师的配置文件

自整理---Mysql高级笔记_第39张图片


以下两条为必须配置

  • 配置主机 id
server-id=1
  • 启用二进制日志
log-bin=C:/Program Files (x86)/MySQL/MySQL Server 5.5/log-bin/mysqlbin

以下为非必须配置

  • 启动错误日志
log-err=C:/Program Files (x86)/MySQL/MySQL Server 5.5/log-bin/mysqlerr
  • 根目录
basedir="C:/Program Files (x86)/MySQL/MySQL Server 5.5/"
  • 临时目录
tmpdir="C:/Program Files (x86)/MySQL/MySQL Server 5.5/"
  • 数据目录
datadir="C:/Program Files (x86)/MySQL/MySQL Server 5.5/Data/"
  • 主机,读写都可以
read-only=0
  • 设置不要复制的数据库
binlog-ignore-db=mysql
  • 设置需要复制的数据
binlog-do-db=需要复制的主数据库名字

从机修改 my.cnf 配置文件(Linux)

  • 【必须】从服务器唯一ID
server-id=2
  • 【可选】启用二进制文件

修改配置文件后的准备工作

因修改过配置文件,主机+从机都重启 mysql 服务

  • Windows
net stop mysql
net start mysql
  • Linux
service mysqld restart

主机从机都关闭防火墙

  • Windows 手动关闭防火墙
  • 关闭虚拟机 linux 防火墙
service iptables stop

在 Windows 主机上简历账户并授权 slave

  • 创建用户, 并授权
GRANT REPLICATION SLAVE ON *.* TO '备份账号'@'从机器数据库 IP' IDENTIFIED BY '账号密码';

GRANT REPLICATION SLAVE ON *.* TO 'Heygo'@'192.168.152.129' IDENTIFIED BY '123456';
  • 刷新权限信息
flush privileges;

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
  • 查询 master 的状态,将 File 和 Position 记录下来,在启动 Slave 时需要用到这两个参数

mysql> show master status;
+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 |      107 | mysql        |                  |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

在 Linux 从机上配置需要复制的主机

  • 从机进行认证
CHANGE MASTER TO 
MASTER_HOST='主机 IP',
MASTER_USER='创建用户名',
MASTER_PASSWORD='创建的密码',
MASTER_LOG_FILE='File 名字',
MASTER_LOG_POS=Position数字;
123456
CHANGE MASTER TO 
MASTER_HOST='10.206.207.131',
MASTER_USER='Heygo',
MASTER_PASSWORD='123456',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=107;
  • 启动从服务器复制功能
start slave;
  • 查看从机复制功能是否启动成功:Slave_SQL_Running:Yes 和 Slave_IO_Running:Yes 说明从机连接主机成功
show slave status\G;
  • 如何停止从服务复制功能
stop slave;

备注

  1. 从机连不上啊。。。一直显示 Slave_IO_Running: Connecting
  2. 我 Windows 中的 mysql 版本为 5.5,Linux 中的 mysql 版本为 5.6

验证主从配置

SHOW GLOBAL STATUS LIKE	'innodb_rows%'; # 验证数据库中受影响的行数

自整理---Mysql高级笔记_第40张图片

通过查看主库和从库数据的变化,来分析主从配置是否生效

数据库设计的三大范式

为了建立冗余较小、结构合理的数据库,设计数据库时必须遵循一定的规则。在关系型数据库中这种规则就称为范式。范式是符合某一种设计要求的总结。要想设计一个结构合理的关系型数据库,必须满足一定的范式。

在实际开发中最为常见的设计范式有三个:

1.第一范式(确保每列保持原子性)

第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。

第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分为省份、城市、详细地址等多个部分进行存储,这样在对地址中某一部分操作的时候将非常方便。这样设计才算满足了数据库的第一范式,如下表所示。

自整理---Mysql高级笔记_第41张图片

上表所示的用户信息遵循了第一范式的要求,这样在对用户使用城市进行分类的时候就非常方便,也提高了数据库的性能。

2.第二范式(确保表中的每列都和主键相关)

第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。

比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键,如下表所示。

订单信息表

自整理---Mysql高级笔记_第42张图片

这样就产生一个问题:这个表中是以订单编号和商品编号作为联合主键。这样在该表中商品名称、单位、商品价格等信息不与该表的主键相关,而仅仅是与商品编号相关。所以在这里违反了第二范式的设计原则。

而如果把这个订单信息表进行拆分,把商品信息分离到另一个表中,把订单项目表也分离到另一个表中,就非常完美了。如下所示。

自整理---Mysql高级笔记_第43张图片

这样设计,在很大程度上减小了数据库的冗余。如果要获取订单的商品信息,使用商品编号到商品信息表中查询即可。

3.第三范式(确保每列都和主键列直接相关,而不是间接相关)

第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。

比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。如下面这两个表所示的设计就是一个满足第三范式的数据库表。

自整理---Mysql高级笔记_第44张图片

这样在查询订单信息的时候,就可以使用客户编号来引用客户信息表中的记录,也不必在订单信息表中多次输入客户信息的内容,减小了数据冗余。

mysql分区

分区表

对于用户而言,分区表是一个独立的逻辑表,但是底层是由多个物理子表组成。分区表对于用户而言是一个完全封装底层实现的黑盒子,对用户而言是透明的,从文件系统中我们可以看到多个使用#分隔命名的表文件,这就是分区表文件。分区表在执行查询的时候,优化器会根据分区定义过滤掉那些我们不需要数据的分区,这样查询就无须扫描所有分区。 分区的主要目的是将数据安好一个较粗的力度分在不同的表中,这样可以将相关的数据存放在一起。

分区表的应用场景

  • 表非常大以至于无法全部都放在内存中,或者只有表的最后部分有热点数据,其他均是历史数据(区分热点和非热点数据)
  • 批量删除大量数据可以使用清除整个分区的方式
  • 可以备份和恢复独立的分区
  • 对一个独立分区进行优化、检查、修复等操作
  • 分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备
  • 可以使用分区表来避免某些特殊的瓶颈
  • innodb的单个索引的互斥访问
  • ext3文件系统的inode锁竞争

分区表的限制

  • 一个表最多只能有1024个分区,在5.7版本的时候可以支持8192个分区
  • 在早期的mysql中,分区表达式必须是整数或者是返回整数的表达式,在mysql5.5中,某些场景可以直接使用列来进行分区
  • 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
  • 分区表无法使用外键约束

分区表的原理

分区表的类型

  1. 范围分区

​ 根据列值在给定范围内将行分配给分区,很好理解

  1. 列表分区

​ 类似于按范围分区,区别在于 :list分区是基于列值匹配一个离散值集合中的某些指定值来进行选择

```sql
CREATE TABLE employees
(
    id        INT  NOT NULL,
    fname     VARCHAR(30),
    lname     VARCHAR(30),
    hired     DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code  INT,
    store_id  INT
) PARTITION BY LIST(store_id) (
    PARTITION pNorth VALUES IN (3,5,6,9,17),
    PARTITION pEast VALUES IN (1,2,10,11,19,20),
    PARTITION pWest VALUES IN (4,12,13,14,18),
    PARTITION pCentral VALUES IN (7,8,15,16) );
```
  1. 列分区

​ mysql从5.5开始支持column分区,可以认为是范围分区和列表分区的升级版,在5.5之后,可以使用column分区替代range和list,但是column分区只接受普通列不接受表达式

CREATE TABLE `list_c`
(
    `c1` int(11) DEFAULT NULL,
    `c2` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1
/*!50500 PARTITION BY RANGE COLUMNS(c1) (PARTITION p0 VALUES LESS THAN (5) ENGINE = InnoDB,  PARTITION p1 VALUES LESS THAN (10) ENGINE = InnoDB) */
CREATE TABLE `list_c`
(
    `c1` int(11) DEFAULT NULL,
    `c2` int(11) DEFAULT NULL,
    `c3` char(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 /*!50500 PARTITION BY RANGE COLUMNS(c1,c3) (PARTITION p0 VALUES LESS THAN (5,'aaa') ENGINE = InnoDB,  PARTITION p1 VALUES LESS THAN (10,'bbb') ENGINE = InnoDB) */
CREATE TABLE `list_c`
(
    `c1` int(11) DEFAULT NULL,
    `c2` int(11) DEFAULT NULL,
    `c3` char(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 /*!50500 PARTITION BY LIST COLUMNS(c3) (PARTITION p0 VALUES IN ('aaa') ENGINE = InnoDB,  PARTITION p1 VALUES IN ('bbb') ENGINE = InnoDB)
  1. hash分区

​ 基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含myql中有效的、产生非负整数值的任何表达式

CREATE TABLE employees
(
    id        INT  NOT NULL,
    fname     VARCHAR(30),
    lname     VARCHAR(30),
    hired     DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code  INT,
    store_id  INT
) PARTITION BY HASH(store_id) PARTITIONS 4;  
--  通过StoreId进行分区,将StoreId对4取模,这样就能放到模为0,1,2,3 这四个不同的分区中

CREATE TABLE employees
(
    id        INT  NOT NULL,
    fname     VARCHAR(30),
    lname     VARCHAR(30),
    hired     DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code  INT,
    store_id  INT
) PARTITION BY LINEAR HASH(YEAR(hired)) PARTITIONS 4;
  1. key分区

​ 类似于hash分区,区别在于key分区只支持一列或多列,且mysql服务器提供其自身的哈希函数,必须有一列或多列包含整数值

CREATE TABLE tk (
      col1 INT NOT NULL,
      col2 CHAR(5),
      col3 DATE ) PARTITION BY LINEAR KEY (col1) PARTITIONS 3;
  1. 子分区

    在基于某个分区的基础之上,再进行分区

CREATE TABLE `t_partition_by_subpart`
(
    `id`     INT AUTO_INCREMENT,
    `sName`  VARCHAR(10) NOT NULL,
    `sAge`   INT(2) UNSIGNED ZEROFILL NOT NULL,
    `sAddr`  VARCHAR(20) DEFAULT NULL,
    `sGrade` INT(2) NOT NULL,
    `sStuId` INT(8) DEFAULT NULL,
    `sSex`   INT(1) UNSIGNED DEFAULT NULL,
    PRIMARY KEY (`id`, `sGrade`)
) ENGINE = INNODB PARTITION BY RANGE(id) SUBPARTITION BY HASH(sGrade) SUBPARTITIONS 2 ( PARTITION p0 VALUES LESS THAN(5), PARTITION p1 VALUES LESS THAN(10), PARTITION p2 VALUES LESS THAN(15) );

如何使用分区表,应用场景

如果需要从非常大的表中查询出某一段时间的记录,而这张表中包含很多年的历史数据,数据是按照时间排序的,此时应该如何查询数据呢? 因为数据量巨大,肯定不能在每次查询的时候都扫描全表。考虑到索引在空间和维护上的消耗,也不希望使用索引,即使使用索引,会发现会产生大量的碎片,还会产生大量的随机IO,但是当数据量超大的时候,索引也就无法起作用了,此时可以考虑使用分区来进行解决

全量扫描数据,不要任何索引

使用简单的分区方式存放表,不要任何索引,根据分区规则大致定位需要的数据为止,通过使用where条件将需要的数据限制在少数分区中,这种策略适用于以正常的方式访问大量数据

索引数据,并分离热点

如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将这部分热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在内存中,这样查询就可以只访问一个很小的分区表,能够使用索引,也能够有效的使用缓存

mysql服务器参数设置

通用参数

自整理---Mysql高级笔记_第45张图片

字符参数

自整理---Mysql高级笔记_第46张图片

连接参数

自整理---Mysql高级笔记_第47张图片

日志参数

自整理---Mysql高级笔记_第48张图片

缓存参数

自整理---Mysql高级笔记_第49张图片 自整理---Mysql高级笔记_第50张图片 自整理---Mysql高级笔记_第51张图片

Innodb参数

自整理---Mysql高级笔记_第52张图片

innodb_flush_log_at_trx_commit

  • 如果innodb_flush_log_at_trx_commit设置为0,log buffer将每秒一次地写入log file中,并且log file的flush(刷到磁盘)操作同时进行.该模式下,在事务提交的时候,不会主动触发写入磁盘的操作。

  • 如果innodb_flush_log_at_trx_commit设置为1,每次事务提交时MySQL都会把log buffer的数据写入log file,并且flush(刷到磁盘)中去.

  • 如果innodb_flush_log_at_trx_commit设置为2,每次事务提交时MySQL都会把log buffer的数据写入log file.但是flush(刷到磁盘)操作并不会同时进行。该模式下,MySQL会每秒执行一次 flush(刷到磁盘)操作。

注意: 由于进程调度策略问题,这个“每秒执行一次 flush(刷到磁盘)操作”并不是保证100%的“每秒”。

sync_binlog

sync_binlog 的默认值是0,像操作系统刷其他文件的机制一样,MySQL不会同步到磁盘中去而是依赖操作系统来刷新binary log。

当sync_binlog =N (N>0) ,MySQL 在每写 N次 二进制日志binary log时,会使用fdatasync()函数将它的写二进制日志binary log同步到磁盘中去。

注:

如果启用了autocommit,那么每一个语句statement就会有一次写操作;否则每个事务对应一个写操作。

根据上述描述,我做了一张图,可以方便大家查看。
自整理---Mysql高级笔记_第53张图片

二 性能

两个参数在不同值时对db的纯写入的影响表现如下:

自整理---Mysql高级笔记_第54张图片

测试场景1

innodb_flush_log_at_trx_commit=2

sync_binlog=1000

测试场景2

innodb_flush_log_at_trx_commit=1

sync_binlog=1000

测试场景3

innodb_flush_log_at_trx_commit=1

sync_binlog=1

测试场景4

innodb_flush_log_at_trx_commit=1

sync_binlog=1000

测试场景5

innodb_flush_log_at_trx_commit=2

sync_binlog=1000

场景 TPS
场景1 41000
场景2 33000
场景3 26000
场景4 33000

由此可见,当两个参数设置为双1的时候,写入性能****最差,sync_binlog=N (N>1 ) innodb_flush_log_at_trx_commit=2 时,(在当前模式下)MySQL的写操作才能达到最高性能。

三 安全

当innodb_flush_log_at_trx_commit和sync_binlog 都为 1 时是最安全的,在mysqld 服务崩溃或者服务器主机crash的情况下,binary log 只有可能丢失最多一个语句或者一个事务。但是鱼与熊掌不可兼得,双11 会导致频繁的io操作,因此该模式也是最慢的一种方式。

当innodb_flush_log_at_trx_commit设置为0,mysqld进程的崩溃会导致上一秒钟所有事务数据的丢失。
当innodb_flush_log_at_trx_commit设置为2,只有在操作系统崩溃或者系统掉电的情况下,上一秒钟所有事务数据才可能丢失。

双1适合数据安全性要求非常高,而且磁盘IO写能力足够支持业务,比如订单,交易,充值,支付消费系统。双1模式下,当磁盘IO无法满足业务需求时 比如11.11 活动的压力。推荐的做法是 innodb_flush_log_at_trx_commit=2 ,sync_binlog=N (N为500 或1000) 且使用带蓄电池后备电源的缓存cache,防止系统断电异常。

复习大纲

你可能感兴趣的:(笔记)