文章目录
- 1. MySQL简介
- 2. MySQL安装
- 3. MySQL逻辑架构
- 5. 索引
- 6. 索引优化
- 7. sql实战
- 8. View视图
- 9. Mysql日志和锁
- 10. 多版本并发控制
1. MySQL简介
MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司。
MySQL是一种关联数据库管理系统,将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
Mysql是开源的,所以你不需要支付额外的费用。
Mysql是可以定制的,采用了GPL协议,你可以修改源码来开发自己的Mysql系统。
Mysql支持大型的数据库。可以处理拥有上千万条记录的大型数据库。
MySQL使用标准的SQL数据语言形式。
Mysql可以允许安装于多个系统上,并且支持多种语言。这些编程语言包括C、C++、Python、Java、Perl、PHP、Eiffel、Ruby和Tcl等。
MySQL支持大型数据库,支持5000万条记录的数据仓库,32位系统表文件最大可支持4GB,64位系统支持最大的表文件为8TB。
2. MySQL安装
官网下载地址:http://dev.mysql.com/downloads/mysql/
2.1 MySQL8新特性
MySQL 8版本在功能上做了显著的改进与增强
1. 更简便的NoSQL支持
从5.6版本开始,MySQL就开始支持简单的NoSQL存储功能。MySQL 8对这一功能做了优化,以更灵活的方式实现NoSQL功能。
2. 更好的索引
在查询中,正确地使用索引可以提高查询的效率。MySQL 8中新增了隐藏索引
和降序索引
。隐藏索引可以用来测试去掉索引对查询性能的影响。在查询中混合存在多列索引时,使用降序索引可以提高查询的性能。
3.更完善的JSON支持
MySQL从5.7开始支持原生JSON数据的存储,MySQL 8对这一功能做了优化,增加了聚合函数JSON_ARRAYAGG()
和JSON_OBJECTAGG()
,将参数聚合为JSON数组或对象,新增了行内操作符 ->>,是列路径运算符 ->的增强,对JSON排序做了提升,并优化了JSON的更新操作。(开发中一般使用mdb操作json:项目中使用频繁的比较复杂的多表联查数据,如果数据改动不频繁,可以直接以json的方式来保存,优势:查询速度快,代码中不需要对象和json之间转换。 如果数据的字段频繁修改,同一类数据字段不一定一样,表不是特别好设计,可以使用使用json存数据)
4.InnoDB的变化
InnoDB是MySQL默认的存储引擎
,是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键。在MySQL 8 版本中,InnoDB在自增、索引、加密、死锁、共享锁等方面做了大量的改进和优化
,并且支持原子数据定义语言(DDL),提高了数据安全性,对事务提供更好的支持。
5.数据字典
在之前的MySQL版本中,字典数据都存储在元数据文件和非事务表中。从MySQL 8开始新增了事务数据字典,在这个字典里存储着数据库对象信息,这些数据字典存储在内部事务表中。
6.字符集支持
MySQL 8中默认的字符集由latin1
更改为utf8mb4
,并首次增加了日语所特定使用的集合,utf8mb4_ja_0900_as_cs。
7.窗口函数
MySQL 8开始支持窗口函数。在之前的版本中已存在的大部分聚合函数
在MySQL 8中也可以作为窗口函数来使用。
等等…
2.2 安装MySQL
2.2.1 在docker中创建并启动MySQL容器:
docker run -d \
-p 3316:3306 \
-v mysql-master-conf:/etc/mysql/conf.d \
-v mysql-master-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--name mysql-master \
mysql:8
2.2.2 修改mysql密码
如果使用的mysql客户端是新版本的 不需要修改密码
#进入容器:env LANG=C.UTF-8 避免容器中显示中文乱码
docker exec -it atguigu-mysql-master env LANG=C.UTF-8 /bin/bash
#进入容器内的mysql命令行
mysql -uroot -p123456
#修改默认密码插件
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
# 退出容器
2.2.3 重启mysql容器
docker restart atguigu-mysql-master
2.2.4 常见问题解决
如果mysql容器连接不上或者报错WARNING: IPv4 forwarding is disabled. Networking will not work.
先排除防火墙、网络问题
然后尝试如下方案:
# 修改配置文件:
vim /usr/lib/sysctl.d/00-system.conf
# 添加
net.ipv4.ip_forward=1
# 保存退出 重启网络服务
systemctl restart network
# 重启mysql容器
docker restart atguigu-mysql-master
2.3 字符集问题
查看mysql编码(show variables like ‘%character%’
),发现默认编码是utf8,无需修改
mysql8以前的版本默认编码是latin,需要手动修改
2.4 远程访问MySQL(用户与权限管理)
2.4.0 远程连接问题
1、防火墙
接下来用宿主机sqlyog或者navicat客户端访问MySQL服务,如果等待一会才连接失败:一般是防火墙未关闭
# 关闭防火墙或者开放端口号:
systemctl start firewalld.service
systemctl status firewalld.service
systemctl stop firewalld.service
systemctl enable firewalld.service
systemctl disable firewalld.service
-- 查看开放的端口号
firewall-cmd --list-all
-- 设置开放的端口号
firewall-cmd --add-service=http --permanent
firewall-cmd --add-port=3306/tcp --permanent
-- 重启防火墙
firewall-cmd --reload
2、账号不支持远程连接
默认情况下,mysql不允许远程连接。只允许localhost连接
执行:
select host,user,select_priv, plugin,authentication_string from mysql.user
ql.user;
%
表示所有远程通过 TCP方式的连接
IP地址
如 (192.168.1.2,127.0.0.1) 通过制定ip地址进行的TCP方式的连接
机器名
:通过制定i网络中的机器名进行的TCP方式的连接
::1
IPv6的本地ip地址 等同于IPv4的 127.0.0.1
localhost
本地方式通过命令行方式的连接 ,比如mysql -u xxx -p 123xxx 方式的连接。
User
: 表示用户名,同一用户通过不同方式链接的权限是不一样的。
password
:密码,所有密码串通过 password(明文字符串) 生成的密文字符串。加密算法为MYSQLSHA1 ,不可逆 。
mysql 5.7 的密码保存到 authentication_string 字段中不再使用password 字段。
plugin
: 表示生成密码的插件
mysql5.7及之前使用mysql_native_password
mysql8使用caching_sha2_password
select_priv
, insert_priv
等,为该用户所拥有的权限。
解决方案:
-
创建新的用户
-
修改root用户访问权限
2.4.1. 创建新用户
创建新用户并指定密码:create user liuyan identified by 'liuyan';
修改root用户密码:set password =password('123456');
修改某个用户的密码:update mysql.user set authentication_string=password('123456') where user='liuyan';
修改用户名:update mysql.user set user='li4' where user='wang5';
删除用户:drop user li4 ;
flush privileges;
#所有通过user表的修改,必须用该命令才能生效。
授权命令: grant 权限1,权限2,…权限n on 数据库名称.表名称 to 用户名@用户地址 identified by ‘连接口令’; 该权限如果发现没有该用户,则会直接新建一个用户。
授予新创建用户权限:
grant select,insert,delete,drop on testdb.* to liuyan@'%' ; # 给用户liuyan用本地命令行方式下,授予testdb这个库下的所有表的插删改查的权限。
create user zhangsan identified by 'zhangsan';
grant all privileges on *.* to zhangsan@'%'; #授予通过网络方式登录的的zhansan用户,对所有库所有表的全部权限,密码设为zhangsan.
flush privileges;#刷新权限
扩展:
查看当前用户权限:show grants;
查看当前用户的全局权限:select * from user ;
查看某用户的某个表的权限:select * from tables_priv;
收回权限命令:
revoke 权限1,权限2,…权限n on 数据库名称.表名称 from 用户名@用户地址 ;
收回全库全表的所有权限:REVOKE ALL PRIVILEGES ON mysql.* FROM li4@localhost;
收回mysql库下的所有表的插删改查权限:REVOKE select,insert,update,delete ON mysql.* FROM li4@localhost;
用户必须重新登录后才能生效
2.4.2. 修改root账户的host地址
root用户的当前主机配置信息为localhost。只允许本地使用。
修改Host为通配符%
update user set host = '%' where user ='root';
2.4.3 测试连接
连接时出现如下问题:错误号码 2058
mysql5.7之前密码加密使用的插件是mysql_native_password,mysql8修改为caching_sha2_password
解决方法: 一种是升级SQLyog和Navicat(新版SQLyog和Navicat不会出现此问题),另一种是把MySQL用户登录密码加密规则还原成mysql_native_password。
Linux下 mysql -u root -p 登录你的 mysql 数据库,然后 执行这条SQL:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
flush privileges;
重新测试连接
2.5 SQL大小写规范
2.5.1 Windows和Linux的区别
Windows环境:
全部不区分大小写
Linux环境:
1、数据库名、表名、表的别名、变量名严格区分大小写
;
2、列名与列的别名不区分大小写
。
3、关键字、函数名称不区分大小写
;
2.5.2 Linux下大小写规则设置(了解)
在MySQL 8中设置的具体步骤为:
1、停止MySQL服务
2、删除数据目录,即删除 /var/lib/mysql 目录
3、在MySQL配置文件(/etc/my.cnf )的 [mysqld] 中添加 lower_case_table_names=1
4、启动MySQL服务
注意:不建议在开发过程中修改此参数,将会丢失所有数据
2.6 导入数据
把课前资料中的《1.数据准备.sql》导入到mysql数据库
2.7 sql_mode
2.7.1 了解sql_mode
宽松模式:
执行错误的SQL或插入不规范的数据,也会被接受,并且不报错。
严格模式:
执行错误的SQL或插入不规范的数据,会报错。MySQL5.7版本开始就将sql_mode默认值设置为了严格模式。
需求:根据部门id查询每个部门的员工平均年龄。
sql_mode是个很容易被忽视的变量,默认值是空值(mysql5.5),在这种设置下是可以允许一些非法操作的,比如允许一些非法数据的插入。在生产环境必须将这个值设置为严格模式,所以开发、测试环境的数据库也必须要设置,这样在开发测试阶段就可以发现问题。
查看:show variables like ‘sql_mode’;
sql_mode常用值如下:
ONLY_FULL_GROUP_BY:
对于GROUP BY聚合操作,如果在SELECT中的列,没有在GROUP BY中出现,那么这个SQL是不合法的,因为列不在GROUP BY从句中
NO_AUTO_VALUE_ON_ZERO:
该值影响自增长列的插入。默认设置下,插入0或NULL代表生成下一个自增长值。如果用户 希望插入的值为0,而该列又是自增长的,那么这个选项就有用了。
STRICT_TRANS_TABLES:
在该模式下,如果一个值不能插入到一个事务表中,则中断当前的操作,对非事务表不做限制。提高性能
NO_ZERO_IN_DATE:
在严格模式下,不允许日期和月份为零
NO_ZERO_DATE:
设置该值,mysql数据库不允许插入零日期,插入零日期会抛出错误而不是警告。
ERROR_FOR_DIVISION_BY_ZERO:
在INSERT或UPDATE过程中,如果数据被零除,则产生错误而非警告。如果未给出该模式,那么数据被零除时MySQL返回NULL
NO_AUTO_CREATE_USER:
禁止GRANT创建密码为空的用户
NO_ENGINE_SUBSTITUTION:
如果需要的存储引擎被禁用或未编译,那么抛出错误。不设置此值时,用默认的存储引擎替代,并抛出一个异常
PIPES_AS_CONCAT:
将"||"视为字符串的连接操作符而非或运算符,这和Oracle数据库是一样的,也和字符串的拼接函数Concat相类似
ANSI_QUOTES:
启用ANSI_QUOTES后,不能用双引号来引用字符串,因为它被解释为识别符
ORACLE:
设置等同:PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS, NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER
2.7.2 设置sql_mode
1、临时设置
查询sql_mode的值:
SELECT @@session.sql_mode;
SELECT @@global.sql_mode;
-- 或者
SHOW VARIABLES LIKE 'sql_mode'; --session级别
临时设置sql_mode的值:
SET GLOBAL sql_mode = 'mode1,model2,...'; --全局,要重新启动客户端生效,重启MySQL服务后失效
SET SESSION sql_mode = 'mode1,model2,...'; --当前会话生效效,关闭当前会话就不生效了。可以省略SESSION关键字
2、持久化设置
在 /etc/my.cnf 中配置,永久生效:
[mysqld]
sql-mode = "mode1,model2,..."
3. MySQL逻辑架构
和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用,并发挥良好作用。主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。
下图是MySQL的经典架构图
3.0 逻辑架构详解
3.0.1 连接层
最上层是一些客户端和连接服务,包含本地socket通信和大多数基于客户端/服务端工具实现的类似于tcp/ip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
3.0.2 服务层
a) Management Serveices & Utilities: 系统管理和控制工具
b) SQL Interface: SQL接口
接受用户的SQL命令,并且返回用户需要查询的结果。比如select from就是调用SQL Interface
c) Parser: 解析器
SQL命令传递到解析器的时候会被解析器验证和解析。解析器中SQL 语句进行词法分析、语法分析、语义分析
,并为其创建语法树
。
d) Optimizer: 查询优化器。
SQL语句在查询之前会使用查询优化器对查询进行优化。
用一个例子就可以理解: select uid,name from user where gender= 1;
优化器来决定先投影还是先过滤。
这个select 查询先根据where 语句进行选取,而不是先将表全部查询出来以后再进行gender过滤
这个select查询先根据uid和name进行属性投影,而不是将属性全部取出以后再进行过滤
e) Cache和Buffer: 查询缓存。
如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。
这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等
mysql8禁用记录缓存(查询缓存,数据库表一旦修改数据了查询缓存需要删除)
3.0.3 引擎层
存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取。后面介绍MyISAM和InnoDB
3.0.4 存储层
数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互。
3.1. 查询SQL执行流程
首先,mysql的查询流程大致是:
mysql客户端通过协议与mysql服务器建连接,发送查询语句,先检查查询缓存,如果命中,直接返回结果,否则进行语句解析,也就是说,在解析查询之前,服务器会先访问查询缓存(query cache)——它存储SELECT语句以及相应的查询结果集。如果某个查询结果已经位于缓存中,服务器就不会再对查询进行解析、优化、以及执行。它仅仅将缓存中的结果返回给用户即可,这将大大提高系统的性能。
语法解析器和预处理:首先mysql通过关键字将SQL语句进行解析,并生成一颗对应的“解析树”。mysql解析器将使用mysql语法规则验证和解析查询;预处理器则根据一些mysql规则进一步检查解析树是否合法。
当解析树被认为是合法的了,查询优化器将其转化成执行计划。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
3.2. SQL执行计划
利用show profiles
可以查看sql的执行周期。需要先开启该功能:查看参数表中profiling(性能分析工具): sql性能分析工具
show variables like '%profiling%';
# 本次会话开启profiling
set profiling=1;
执行:
select * from t_emp;
select * from t_dept;
- 显示最近几次查询:
show profiles;
-
查看程序的具体执行步骤:
show profile cpu,block io for query 查询id
show profile的常用查询参数。
①ALL:显示所有的开销信息。
②BLOCK IO:显示块IO开销。
③CONTEXT SWITCHES:上下文切换开销。
④CPU:显示CPU开销信息。
⑤IPC:显示发送和接收开销信息。
⑥MEMORY:显示内存开销信息。
⑦PAGE FAULTS:显示页面错误开销信息。
⑧SOURCE:显示和Source_function,Source_file,Source_line相关的开销信息。
⑨SWAPS:显示交换次数开销信息。
**Mysql5.7:使用了查询缓存,执行计划如下**
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/65b8f92fc61e4137b006da9a9e64c338.png)
**Mysql8: 取消了查询缓存,执行计划如下**
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/eb2e66b29be84760a4bd0dec607ea7cb.png)
Executing hook on transaction: 启用事务
checking permissions:检查权限
Opening tables:打开表
init : 初始化
System lock :系统锁
optimizing : 优化sql
statistics : 统计
preparing :准备执行
executing :执行sql
Sending data :发送数据
Sorting result :排序
end :结束
query end :查询 结束
closing tables : 关闭表 /去除TMP 表
freeing items : 释放物品
cleaning up :清理
## 3.3. SQL语法顺序
随着Mysql版本的更新换代,其优化器也在不断的升级,优化器会分析不同执行顺序产生的性能消耗不同而动态调整执行顺序。
需求:查询每个部门年龄高于20岁的人数且高于20岁人数不能少于2人,显示人数最多的第一名部门信息
下面是经常出现的查询顺序:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/00fc24fe4b384035bc50e37c7e1dced4.png)
## 3.4. MySQL存储引擎
如何用命令查看:`show engines;`
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/93c8f546466a4c5a99a5ca6a2940ae29.png)
也可以通过`show variables like '%storage_engine%';`查看默认的存储引擎。
### 3.4.1. 各引擎简介
1. InnoDB存储引擎
InnoDB是MySQL的默认事务型引擎,它被设计用来<font color='red'>处理大量的短期(short-lived)事务</font>。除非有非常特别的原因需要使用其他的存储引擎,否则<font color='red'>应该优先考虑InnoDB引擎。</font>
> 支持事务、回滚、崩溃恢复等功能
> 支持行级锁定,实现高并发读写
> 支持外键约束,保证数据完整性
> 存储的数据有序,支持B+树索引
> 提供存储引擎级别的缓冲池,减少对磁盘IO的访问
2. MyISAM存储引擎
MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但<font color='red'>MyISAM不支持事务和行级锁</font>,有一个毫无疑问的缺陷就是崩溃后无法安全恢复。
3. Archive引擎
<font color='red'>Archive档案存储引擎只支持INSERT和SELECT操作</font>,在MySQL5.1之前不支持索引。
Archive表适合日志和数据采集类应用。
根据英文的测试结论来看,Archive表比MyISAM表要小大约75%,比支持事务处理的InnoDB表小大约83%。
4. Blackhole引擎
<font color='red'>Blackhole引擎没有实现任何存储机制,它会丢弃所有插入的数据,不做任何保存。</font>但服务器会记录Blackhole表的日志,所以可以用于复制数据到备库,或者简单地记录到日志。但这种应用方式会碰到很多问题,因此并不推荐。
5. CSV引擎
<font color='red'>CSV引擎可以将普通的CSV文件作为MySQL的表来处理,但不支持索引。</font>
CSV引擎可以作为一种数据交换的机制,非常有用。
CSV存储的数据直接可以在操作系统里,用文本编辑器,或者excel读取。
6. Memory引擎
如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,那么使用Memory表是非常有用。Memory表至少比MyISAM表要快一个数量级。
7. Federated引擎
Federated引擎<font color='red'>是访问其他MySQL服务器的一个代理</font>,尽管该引擎看起来提供了一种很好的跨服务器的灵活性,但也经常带来问题,因此默认是禁用的。
8. PERFORMANCE_SCHEMA
PERFORMANCE_SCHEMA是MySQL的一套性能监控与分析系统。它可以监控MySQL服务器上的资源消耗情况。
### 3.4.2. MyISAM和InnoDB的区别(重点)
| **对比项** | **MyISAM** | **InnoDB** |
| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| **外键** | 不支持 | 支持 |
| **事务** | 不支持 | 支持 |
| **行表锁** | 表锁,即使操作一条记录也会锁住整个表 <font color='red'>不适合高并发的操作 </font> | 行锁,操作时只锁某一行,不对其它行有影响 <font color='red'>适合高并发的操作 </font> |
| **缓存** | 只缓存索引,不缓存真实数据 | 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 |
| **关注点** | 节省资源、消耗少、简单业务 | 并发写、事务、更大资源 |
| **默认安装** | Y | Y |
| **用户表默认使用** | N | Y |
| **自带系统表使用** | N | Y |
### 3.4.3. 阿里巴巴、淘宝用哪个
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d6fc61f8c8ab4cd29caf298f93586722.png)
Percona 为 MySQL 数据库服务器进行了改进,在功能和性能上较 MySQL 有着很显著的提升。该版本提升了在高负载情况下的 InnoDB 的性能、为 DBA 提供一些非常有用的性能诊断工具;另外有更多的参数和命令来控制服务器行为。
该公司新建了一款存储引擎叫xtradb完全可以替代innodb,并且在性能和并发上做得更好,
阿里巴巴大部分mysql数据库其实使用的percona的原型加以修改。
# 4. SQL预热
常见七种通用的Join查询
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/cc2a5a8616994ac889005aaa3f28790e.png)
练习:
#1. 所有有门派的人员以及部门信息 ( A、B两表共有)
#2. 列出所有用户,并显示其部门信息 (A的全集)
#3. 列出有部门的人员以及所有部门(B的全集)
#4. 所有不入门派的人员 (A的独有)
#5. 所有没有员工的部门(B的独有)
#6. 列出所有人员和部门的对照关系(AB全有)
#7. 列出所有没部门的人员和没人的部门(A的独有+B的独有)
```sql
#1. 所有有门派的人员信息 ( A、B两表共有)
SELECT * FROM t_emp a INNER JOIN t_dept b ON a.deptId=b.id;
#2. 列出所有用户,并显示其机构信息 (A的全集)
SELECT * FROM t_emp a LEFT JOIN t_dept b ON a.deptId=b.id;
#3. 列出所有门派(B的全集)
SELECT * FROM t_emp a RIGHT JOIN t_dept b ON a.deptId=b.id;
#4. 所有不入门派的人员 (A的独有)
SELECT * FROM t_emp a LEFT JOIN t_dept b ON a.deptId=b.id WHERE a.deptId IS NULL;
#5. 所有没人入的门派(B的独有)
SELECT * FROM t_emp a RIGHT JOIN t_dept b ON a.deptId=b.id WHERE a.deptId IS NULL;
#6. 列出所有人员和机构的对照关系(AB全有)
#MySQL Full Join的实现 因为MySQL不支持FULL JOIN,下面是替代方法
#left join + union(可去除重复数据)+ right join
SELECT * FROM t_emp a LEFT JOIN t_dept b ON a.deptId=b.id
UNION
SELECT * FROM t_emp a RIGHT JOIN t_dept b ON a.deptId=b.id WHERE a.deptId IS NULL;
#7. 列出所有没入派的人员和没人入的门派(A的独有+B的独有)
SELECT * FROM t_emp a LEFT JOIN t_dept b ON a.deptId=b.id WHERE a.deptId IS NULL
UNION
SELECT * FROM t_emp a RIGHT JOIN t_dept b ON a.deptId=b.id WHERE a.deptId IS NULL;
练习:
-
求各个门派对应的掌门人名称
-
求所有当上掌门人的平均年龄
-
求所有人物对应的掌门名称
# 求各个门派对应的掌门人名称
select * from t_dept a left join t_emp b on a.CEO=b.id;
# 求所有当上掌门人的平均年龄
select avg(a.age) from t_emp a inner join t_dept b on a.id=b.CEO;
# 求所有人物对应的掌门名称
# No1
select c.name,c.id,v.name from t_emp c
LEFT JOIN (select a.deptName,a.id,b.name from t_dept a left join t_emp b on a.ceo=b.id) v on c.deptId=v.id
# No2
select c.name,c.id,v.name from (select a.deptName,a.id,b.name from t_dept a left join t_emp b on a.ceo=b.id) v
LEFT JOIN t_emp c on c.deptId=v.id
# No3
select a.name,a.id,c.name from t_emp a
LEFT JOIN t_dept b on a.deptId=b.id
LEFT JOIN t_emp c on b.ceo=c.id
# No4
select a.name,a.id,(select c.name from t_emp c where c.id=b.ceo) from t_emp a
LEFT JOIN t_dept b on a.deptId=b.id
作业:
课前资料中的《2.8个sql.txt》
5. 索引
5.1 索引简介
5.1.1. 什么是索引
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。
可以得到索引的本质:索引是数据结构。
索引的目的在于提高查询效率,可以类比字典,
如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从上往下找到y字母,再找到剩下的sql。
如果没有索引,那么你可能需要a----z,如果我想找到Java开头的单词呢?或者Oracle开头的单词呢?
是不是觉得如果没有索引,这个事情根本无法完成?
你可以简单理解为“排好序的快速查找数据结构”。
在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。下图就是一种可能的索引方式示例:
左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址
为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到相应数据,从而快速的检索出符合条件的记录。
结论:
数据本身之外,数据库还维护着一个满足特定查找算法的数据结构,这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法,这种数据结构就是索引。
一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上
AVLTree 高度平衡树
增加和删除可能需要通过一次或多次树旋转来重新平衡这个树.
具有以下特点:
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
- 并且左右两个子树都是一棵平衡二叉树。
多叉树
多叉树(multiway tree)允许每个节点可以有更多的数据项和更多的子节点
。2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少节点数量,增加分叉,减少树的高度
,能对二叉树进行优化。
5.1.2. 索引的优劣势
优势:
-
类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本。
-
通过索引列对数据进行排序或分组,降低数据排序的成本,降低了CPU的消耗。
劣势:
-
虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。每次更新添加了索引列的字段,都会调整因为更新所带来的键值变化后的索引信息
-
实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的
5.2. MySQL的索引结构
5.2.1. BTree索引
B-Tree即B树,Balance Tree,多路平衡搜索树,它的高度远小于平衡二叉树的高度
。- 2-3树是最简单的B树结构。
B树的阶:
节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4。
【初始化介绍】
一颗b树,浅蓝色的块我们称之为一个磁盘块(innodb默认16kb一个磁盘块),可以看到每个磁盘块包含几个数据项(深蓝色所示)、指向关键字具体信息的指针(红色)和指向其他磁盘块的指针(黄色所示)
上图所表示的 B 树就是一棵 3 阶(叉)的 B 树。假设一个磁盘块可以存储一个节点的数据。我们可以看下磁盘块 2,里面的关键字为(8,12),它有 3 个孩子 (3,5),(9,10) 和 (13,15),你能看到 (3,5) 小于 8,(9,10) 在 8 和 12 之间,而 (13,15)大于 12,三节点的子树的值大小仍然遵守 BST 二叉排序树的规则。
【查找过程】假设我们想要 查找的数据项是 9 ,那么步骤可以分为以下几步:
1、第一次磁盘IO:找到根节点磁盘块1,读入内存,执行二分查找,9 小于 17 ,得到指针 P1
2、第二次磁盘IO:按照指针P1找到磁盘块 2,读入内存,执行二分查找, 9 在 8 和 12 之间,得到指针 P2
3、第三次磁盘IO:按照指针P2找到磁盘块 6,读入内存,执行二分查找, 找到了数据项 9。
B 树在搜索过程中,虽然比较的次数并不少,但把数据读取出来在内存中比较,这个时间要远远小于 I/O操作查找磁盘块的时间。真实的情况是,3层的b树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。
5.2.2. B+Tree索引
1、数据结构
2、B+Tree与B-Tree 的区别
1)B-树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。树的高度会更矮胖,IO次数也会更少。
2)在B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。从这个角度看B-树的性能好像要比B+树好,而在实际应用中却是B+树的性能要好些。因为B+树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比B-树多,树高比B-树小,这样带来的好处是减少磁盘访问次数。尽管B+树找到一个记录所需的比较次数要比B-树多,但是一次磁盘访问的时间相当于成百上千次内存比较的时间,因此实际中B+树的性能可能还会好些,而且B+树的叶子节点使用指针连接在一起,方便顺序遍历(例如查看一个目录下的所有文件,一个表中的所有记录等),这也是很多数据库和文件系统使用B+树的缘故。
思考:为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
- B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。 - B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
3、数据页大小
查看mysql文件页大小(16K):
SHOW GLOBAL STATUS LIKE '%page_size%'
为什么mysql页文件默认16K?
假设我们一行数据大小为1K,那么一页就能存16条数据,也就是一个叶子节点能存16条数据;再看非叶子节点,假设主键ID为bigint类型,那么长度为8Byte,指针大小在Innodb源码中为6Byte,一共就是14Byte,那么一页里就可以存储16K/14B=1170个(主键+指针)
一颗高度为2的B+树能存储的数据为:1170*1170=1,368,900
一颗高度为3的B+树能存储的数据为:1170*1170*16=21902400(千万级)
5.2.3. 聚簇索引与非聚簇索引
聚簇索引:
叶子节点将数据存储与索引放到了一块,找到索引也就找到了数据。
页内的记录
是按照主键
的大小顺序排成一个单向链表
。
页和页之间
也是根据页中记录的主键
的大小顺序排成一个双向链表
。
非叶子节点存储的是记录的
主键+页号
。叶子节点存储的是完整的用户记录
。
非聚簇索引:
叶子节点将数据存储与索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
澄清一个概念:innodb中,非聚簇索引又称辅助索引,辅助索引访问数据总是需要二次查找。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。
InnoDB使用聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。
若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。(重点在于通过其他键需要建立辅助索引)
聚簇索引的好处:
-
由于行数据和叶子节点存储在一起,同一页中会有多条行数据,访问同一数据页不同行记录时,已经把页加载到了Buffer中,再次访问的时候,会在内存中完成访问,不必访问磁盘。这样主键和行数据是一起被载入内存的,找到叶子节点就可以立刻将行数据返回了,如果按照主键Id来组织数据,获得数据更快。
-
聚簇索引适合用在排序的场合,非聚簇索引不适合
-
取出一定范围数据的时候,使用聚簇索引
-
二级索引需要两次索引查找,而不是一次才能取到数据,因为存储引擎第一次需要通过二级索引找到索引的叶子节点,从而找到数据的主键,然后在聚簇索引中用主键再次查找索引,再找到数据
聚簇索引的限制:
- 对于mysql数据库目前只有innodb数据引擎支持聚簇索引,而Myisam并不支持聚簇索引。
- 由于数据物理存储排序方式只能有一种,所以每个Mysql的表只能有一个聚簇索引。
- 一般情况下就是该表的主键。
- 如果没有primary key,会以(not null unique key)非空的唯一索引保存数据
- 内部自己生成一个字段保存数据
- 为了充分利用聚簇索引的聚簇的特性,所以innodb表的主键列尽量选用有序非空的字段,而不建议用无序的id,比如uuid这种。
5.2.4. 回表
通过非聚簇索引查找到主键值之后仍然需要到聚簇索引中再查询一遍,这个过程称为回表
**问题:**为什么我们还需要一次回表
操作呢?直接把完整的用户记录放到叶子节点不OK吗?
回答:
如果把完整的用户记录放到叶子节点是可以不用回表。但是太占地方
了,相当于每建立一棵B+树都需要把所有的用户记录再都拷贝一遍,这就有点太浪费存储空间了。
5.3. MySQL索引分类
- 主键索引:设定为主键后数据库会自动建立索引,innodb为聚簇索引
- 单值索引:即一个索引只包含单个列,一个表可以有多个单列索引
- 唯一索引:索引列的值必须唯一,但允许有空值
- 复合索引:即一个索引包含多个列
5.3.1 索引创建删除
CREATE TABLE customer (
id INT (10) UNSIGNED AUTO_INCREMENT,
customer_no VARCHAR (200),
customer_name VARCHAR (200),
PRIMARY KEY (id), # 主键索引
KEY idx_name (customer_name), # 单值索引
UNIQUE KEY uk_name (customer_name), # 唯一索引
KEY idx_no_name (customer_no, customer_name) # 复合索引
);
单独建索引:
# 使用CREATE语句:CREATE [UNIQUE ] INDEX [indexName] ON table_name(column))
CREATE INDEX idx_customer_name ON customer(customer_name); # 单值索引
CREATE UNIQUE INDEX idx_customer_no ON customer(customer_no); # 唯一索引
CREATE INDEX idx_no_name ON customer(customer_no,customer_name); # 复合索引
# 使用ALTER命令:
ALTER TABLE tbl_name ADD PRIMARY KEY (column_list);
# 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
ALTER TABLE tbl_name ADD UNIQUE index_name (column_list);
# 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
ALTER TABLE tbl_name ADD INDEX index_name (column_list);
# 添加普通索引,索引值可出现多次。
ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list);
# 该语句指定了索引为 FULLTEXT ,用于全文索引。
删除索引:
ALTER TABLE customer drop PRIMARY KEY; # 删除主键索引
DROP INDEX idx_customer_name on customer; # 删除单值、唯一、复合索引
查看索引:
SHOW INDEX FROM table_name;
修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引
5.3.2 联合索引结构
基于多个字段创建的索引就是联合索引,也称为复合索引,比如我们创建索引create index idx on table(a,b,c) 我们称在字段a,b,c上创建了一个联合索引。同时以这三个列的大小作为排序规则。
- 记录先按照a列排序
- a列值相同时使用b列排序
- b列值相同时使用c列排序
然后将排好序的abc三列的值组织到非聚簇索引索引结构中。
联合索引结构:
5.4. 索引的使用场景
哪些情况需要创建索引:
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引
- 查询中与其它表关联的字段,外键关系建立索引
- 单键/组合索引的选择问题, 组合索引性价比更高
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 查询中统计或者分组字段
哪些情况不要创建索引:
-
表记录太少
-
经常增删改的表或者字段。
Why:提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件
-
Where条件里用不到的字段不创建索引
-
过滤性不好的、有大量重复数据的列不适合建索引
6. 索引优化
6.1. 性能分析(explain)
mysql5.6以后优化器做了很多改进,执行时会自动进行大量的优化。
6.1.1. explain是什么?
模拟优化器查看执行计划
使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈
6.1.2. explain能干什么?
-
表的读取顺序
-
那些索引可以使用
-
数据读取操作的操作类型
-
那些索引被实际使用
-
表之间的引用
-
每张表有多少行被物理查询
6.1.3. explain怎么玩?
explain + SQL语句
官方文档:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html#explain-extra-information
数据准备:
CREATE TABLE t1(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));
CREATE TABLE t2(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));
CREATE TABLE t3(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));
CREATE TABLE t4(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id));
# 以下新增sql多执行几次,以便演示
INSERT INTO t1(content) VALUES(CONCAT('t1_',FLOOR(1+RAND()*1000)));
INSERT INTO t2(content) VALUES(CONCAT('t2_',FLOOR(1+RAND()*1000)));
INSERT INTO t3(content) VALUES(CONCAT('t3_',FLOOR(1+RAND()*1000)));
INSERT INTO t4(content) VALUES(CONCAT('t4_',FLOOR(1+RAND()*1000)));
6.1.4. 各字段解释
6.1.4.1. id(重要)
select查询的序列号,表示查询中执行select子句或操作表的顺序
三种情况:
1、id相同,执行顺序由上至下。
explain select * from t1, t2, t3 where t1.id=t2.id and t2.id=t3.id;
2、id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
EXPLAIN SELECT *
FROM t1 WHERE t1.content =(
SELECT t2.content
FROM t2 WHERE t2.content=(
SELECT t3.content
FROM t3
WHERE t3.content=""
)
);
3、id既有相同又有不同
4、id为NULL最后执行。
EXPLAIN SELECT * FROM t1 UNION SELECT * FROM t2;
关注点:每个id号码,表示一趟独立的查询。一个sql 的查询趟数越少越好。
6.1.4.2. select_type
查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询
**SIMPLE:**简单查询,查询中不包含子查询或者UNION。
EXPLAIN SELECT * FROM t1;
**PRIMARY:**主查询,查询中若包含子查询,则最外层查询被标记为PRIMARY。
**SUBQUERY:**子查询,在SELECT或WHERE列表中包含了子查询。
EXPLAIN SELECT * FROM t3 WHERE id = ( SELECT id FROM t2 WHERE content= 'a');
DEPENDENT SUBQUREY:如果包含了子查询,并且查询语句不能被优化器转换为连接查询,并且子查询是相关子查询(子查询基于外部数据列),则子查询就是DEPENDENT SUBQUREY。
EXPLAIN SELECT * FROM t3 WHERE id = ( SELECT id FROM t2 WHERE content = t3.content);
UNCACHEABLE SUBQUREY: 表示一个子查询(subquery)被标记为无法缓存。在某些数据库管理系统中,查询优化器会尝试将查询结果缓存起来以提高性能,但对于被标记为UNCACHEABLE的子查询,查询优化器将不会缓存其结果。
EXPLAIN SELECT * FROM t3 WHERE id = ( SELECT id FROM t2 WHERE content = @@character_set_server);
UNION: 对于包含UNION或者UNION ALL的查询语句,除了最左边的查询是PRIMARY,其余的查询都是UNION。
UNION RESULT: UNION会对查询结果进行查询去重,MYSQL会使用临时表来完成UNION查询的去重工作,针对这个临时表的查询就
是"UNION RESULT"。
EXPLAIN
SELECT * FROM t3 WHERE id = 1
UNION
SELECT * FROM t2 WHERE id = 1;
DEPENDENT UNION: 子查询中的UNION或者UNION ALL,除了最左边的查询是DEPENDENT SUBQUREY,其余的查询都DEPENDENT UNION。
EXPLAIN SELECT * FROM t1 WHERE content IN
(
SELECT content FROM t2
UNION
SELECT content FROM t3
);
DERIVED: 在包含**派生表(子查询在from子句中)**的查询中,MySQL会递归执行这些子查询,把结果放在临时表里。
EXPLAIN SELECT * FROM (
SELECT content, COUNT(*) AS c FROM t1 GROUP BY content
) AS derived_t1 WHERE c > 1;
6.1.4.3. table
显示这一行的数据是关于哪张表的
6.1.4.4. partitions
代表分区表中的命中情况,非分区表,该项为null
6.1.4.5. type(重要)
type显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是:
null>system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
常见:system > const > eq_ref > ref > range > index > ALL
一般来说,得保证查询至少达到range级别,最好能达到ref。
all:Full Table Scan,将遍历全表以找到匹配的行。
explain select * from t1
index:出现index是sql使用了索引但是没用通过索引进行过滤,一般是使用了覆盖索引或者是利用索引进行了排序分组。
explain select id from t1
range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引,一般就是在你的where语句中出现了between、<、>、in等的查询。这种范围扫描索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引.
select * from t1 where t1.id<10
ref: 表示使用了非唯一索引进行的等值比较,可能返回多个匹配的行。
-- t4表 content需要提前创建索引
EXPLAIN SELECT * FROM t4 WHERE content = 'a';
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描。
select * from t1,t2 where t1.id=t2.id
-- 如果连接字段使用的不是唯一索引,是什么效果??
system:表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计。
CREATE TABLE t(i int) Engine=MyISAM;
INSERT INTO t VALUES(1);
EXPLAIN SELECT * FROM t;
const:表示通过索引一次就找到了,const用于比较primary key或者unique索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量。
select * from t1 where t1.id=1
null: MySQL不访问任何表或索引,直接返回结果
select * from t1 where 1=2;
index_merge: 在查询过程中需要多个索引组合使用,通常出现在有 or 的关键字的sql中。
EXPLAIN SELECT * FROM t3 WHERE t3.content IS NULL OR t3.id=10;
ref_or_null: 对于某个字段既需要关联条件,也需要null值得情况下。查询优化器会选择用ref_or_null连接查询。
EXPLAIN SELECT * FROM t3 WHERE t3.content IS NULL OR t3.content='aaaa';
6.1.4.6. possible_keys
显示可能应用在这张表中的索引,一个或多个。
查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用
6.1.4.7. key
实际使用的索引。如果为NULL,则没有使用索引。
查询中若使用了覆盖索引,则该索引和查询的select字段重叠
6.1.4.8. key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。
key_len字段能够帮你检查是否充分的利用上了索引。
如何计算
1 、先看索引上字段的类型+长度比如 int=4 ; varchar(20) =20 ; char(20) =20
2 、如果是varchar或者char这种字符串字段,视字符集要乘不同的值,比如utf8mb3要乘 3(MySQL5.7),如果是utf8mb4要乘4,,GBK要乘2
3 、varchar这种动态字符串要加2个字节
4、 允许为空的字段要加1个字节
索引字段最好不要为NULL,因为NULL让统计更加复杂,并且需要额外一个字节的存储空间。
6.1.4.9. ref
显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值
6.1.4.10. rows
rows列显示MySQL认为它执行查询时必须检查的行数。越少越好
6.1.4.11. filtered
最后查询出来的数据占所有服务器端(server)检查行数(rows)的百分比。值越大越好。
6.1.4.12. extra
包含不适合在其他列中显示但十分重要的额外信息,通过这些额外信息来理解MySQL到底将如何执行当前的查询语句
。MySQL提供的额外信息有好几十个,这里只挑比较重要的介绍。
Using filesort:说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。
MySQL中无法利用索引完成的排序操作称为“文件排序”
EXPLAIN SELECT * FROM t1 ORDER BY content;
这类SQL语句性能极差,需要进行优化。
在一个非索引列上进行了order by,就会触发filesort,常见的优化方案是,在order by的列上添加索引,避免每次查询都全量排序(只查询索引列的值)。
Using temporary:使了用临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。
EXPLAIN SELECT * FROM t1 GROUP BY content;
group by和order by同时存在,且作用于不同的字段时,就会建立临时表,以便计算出最终的结果集。
USING index:利用索引进行了排序或分组。表示相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错!
EXPLAIN select * from t_emp where age=30 ORDER BY name
如果同时出现using where,表明索引被用来执行索引键值的查找;
如果没有同时出现using where,表明索引只是用来读取数据而非利用索引执行查找。
Using where:表明使用了where过滤
using join buffer:使用了连接缓存,非主键关联(mysql8Using join buffer (hash join)
速度要好于 mysql5.7Using join buffer (Block Nested Loop)
)
impossible where:where子句的值总是false,不能用来获取任何元组。(EXPLAIN select * from t_emp where false;)
select tables optimized away:在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。
在innodb中:
Using index condition: Index Condition Pushdown Optimization (索引下推优化)
-- 如果存在,删除t_emp的deptId索引
ALTER TABLE t_emp DROP INDEX idx_dept_id;
-- 查询:结果如下图
EXPLAIN SELECT * FROM t_emp WHERE deptId IN (1,2) AND deptId<2;
-- 添加索引查询测试
ALTER TABLE t_emp ADD INDEX idx_dept_id(deptId);
EXPLAIN SELECT * FROM t_emp WHERE deptId IN (1,2) AND deptId<2;
索引下推就是指在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数来提高查询效率。
1、
如果没有索引下推(ICP)
,那么MySQL在存储引擎层找到满足deptId=1或2
的二级索引记录。使用主键值进行回表
,然后再使用条件deptId<2
对数据进行过滤,最后返回完整的记录给server层。2、
如果使用了索引下推(ICP)
,那么MySQL在存储引擎层找到满足deptId=1或2
二级索引记录。先不执行回表
,而是判断后面的条件中使用了deptId
字段索引的条件(deptId<2
)是否成立。如果这些条件不成立直接过滤;如果条件成立,则执行回表操作,返回完整的记录给server层。3、
deptId
字段条件只有一个时也会执行索引下推,判断条件是否成立(mysql底层代码中的冗余判断)
6.1.5. 小结
表的读取顺序:id
id趟数越少越好,id相同执行顺序由上至下,id不同 大的先执行
查询方式:select_type
那些索引可以使用:possible_keys
数据读取操作的操作类型:type
range index all
那些索引被实际使用:key
创建的索引是否能够被实际应用
使用索引的长度:key_len
命中的索引匹配的长度(用来判断索引是否被完全利用)
计算索引长度:
utf-8
varchar(len) =使用len*3+2 (如果字段可以为null,再+1)
char(len) =使用len*3 (如果字段可以为null,再+1)
int(len) = 4 (如果字段可以为null,再+1)
表之间的引用:table
每张表有多少行被物理查询:rows
行数越少越好(多表联查时 被驱动表的rows如果使用索引了一般非常小)
额外优化信息:extra
using join buffer(多表联查)、using filesort(排序)和 using temporary(分组) 需要考虑优化
其他情况性能都可以无需优化
6.2. 数据准备
在做优化之前,要准备大量数据。接下来创建两张表,并往员工表里插入50W数据,部门表中插入1W条数据。
建表sql:
CREATE TABLE `dept` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`deptName` VARCHAR(30) DEFAULT NULL,
`address` VARCHAR(40) DEFAULT NULL,
ceo INT NULL ,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `emp` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`empno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT(3) DEFAULT NULL,
`deptId` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
#CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
怎么快速插入50w条数据呢? 存储过程
怎么保证插入的数据不重复?函数
以部门表分析:
id:自增长
deptName:随机字符串,允许重复
address:随机字符串,允许重复
CEO:1-50w之间的任意数字
以员工表分析:
id:自增长
empno:可以使用随机数字,或者从1开始的自增数字,不允许重复
name:随机生成,允许姓名重复
age:区间随机数
deptId:1-1w之间随机数
总结:需要产生随机字符串和区间随机数的函数。
6.2.1. 创建函数
set global log_bin_trust_function_creators=1;
# 随机产生字符串
DELIMITER $$
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN
DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
WHILE i < n DO
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
SET i = i + 1;
END WHILE;
RETURN return_str;
END $$
#用于随机产生区间数字
DELIMITER $$
CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN
DECLARE i INT DEFAULT 0;
SET i = FLOOR(from_num +RAND()*(to_num -from_num+1));
RETURN i;
END$$
#假如要删除
#drop function rand_string;
#drop function rand_num;
6.2.2. 存储过程
# 插入员工存储过程
DELIMITER $$
CREATE PROCEDURE insert_emp(START INT, max_num INT)
BEGIN
DECLARE i INT DEFAULT 0;
#set autocommit =0 把autocommit设置成0
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO emp (empno, NAME, age, deptid ) VALUES ((START+i) ,rand_string(6), rand_num(30,50), rand_num(1,10000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END$$
#删除
# DELIMITER ;
# drop PROCEDURE insert_emp;
#往dept表添加随机数据
DELIMITER $$
CREATE PROCEDURE `insert_dept`(max_num INT)
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO dept ( deptname,address,ceo ) VALUES (rand_string(8),rand_string(10),rand_num(1,500000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END$$
#删除
# DELIMITER ;
# drop PROCEDURE insert_dept;
6.2.3. 调用存储过程
#执行存储过程,往dept表添加1万条数据
DELIMITER ;
CALL insert_dept(10000);
#执行存储过程,往emp表添加50万条数据
DELIMITER ;
CALL insert_emp(100000,500000);
6.2.4. 批量删除表索引
批量删除某个表上的所有索引
DELIMITER $$
CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE ct INT DEFAULT 0;
DECLARE _index VARCHAR(200) DEFAULT '';
DECLARE _cur CURSOR FOR SELECT index_name FROM information_schema.STATISTICS WHERE table_schema=dbname AND table_name=tablename AND seq_in_index=1 AND index_name <>'PRIMARY' ;
DECLARE CONTINUE HANDLER FOR NOT FOUND set done=2 ;
OPEN _cur;
FETCH _cur INTO _index;
WHILE _index<>'' DO
SET @str = CONCAT("drop index ",_index," on ",tablename );
PREPARE sql_str FROM @str ;
EXECUTE sql_str;
DEALLOCATE PREPARE sql_str;
SET _index='';
FETCH _cur INTO _index;
END WHILE;
CLOSE _cur;
END$$
执行批量删除:
CALL proc_drop_index("dbname","tablename"); # 库名称和表名称
6.3. 单表优化
6.3.1. 索引优化原则
-
不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
-
like以通配符开头(‘%abc…’)mysql索引失效会变成全表扫描的操作
-
mysql 在使用**不等于(!=或者<>)**的时候无法使用索引会导致全表扫描
-
is not null 也无法使用索引,但是is null是可以使用索引的
-
字符串不加单引号索引失效
-
以下两个sql,那个写法更好:
案例:
#1.1 查找姓名以"abc"开头的员工信息
#1.2 查找姓名含有"abc"的员工信息
#1.3 查找年龄不等于25的员工
#1.4 查找姓名不为空的员工信息
#1.5 查找姓名等于"123"的员工信息
# 创建索引
create index idx_emp_age on emp(age);
create index idx_emp_name on emp(name);
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name LIKE 'abc%';
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE LEFT(emp.name,3)='abc';
sql访问类型range > ALL;使用索引idx_emp_name > NULL; 使用索引长度63 > NULL; 扫描行数25 < 498951
第一个sql用时:0.00s
第二个sql用时:0.37s
由此可见第一个sql优于第二个sql:第一个走了索引,第二个走了全表扫描。
- 把第一个sql的like查询条件改成‘%abc%’,会怎样呢?
可以发现改成’%abc%'之后,第一个sql失去了索引优势,走了全表扫描。
- 再来看这两个sql:不等于(!=或者<>)
EXPLAIN SELECT * FROM emp WHERE emp.age=30;
EXPLAIN SELECT * FROM emp WHERE emp.age!=30;
- is not null和is null
- 字符串加引号
6.3.2. 组合索引原则
- 全值匹配我最爱
- 符合最左原则:不跳过索引中的列。
- 如果where条件中是OR关系,加索引不起作用
- 存储引擎不能使用索引中范围条件右边的列
首先删除之前创建的索引:
CALL proc_drop_index("mydb","emp");
- 全值匹配我最爱
SELECT SQL_NO_CACHE * FROM emp WHERE age=30 and deptId=1 and name='abc';
create index idx_age_deptId_name on emp(age, deptId, name);
SELECT SQL_NO_CACHE * FROM emp WHERE age=30 and deptId=1 and name='abc';
- 最左匹配原则
- OR关联
- 范围条件右边的列
6.3.3. 小结
一般性建议:
- 对于单键索引,尽量选择针对当前query过滤性更好的索引
- 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
- 在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引
- 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面
- 书写sql语句时,尽量避免造成索引失效的情况
假设index(a,b,c) 重要
Where语句 | 索引是否被使用 |
---|---|
where a = 3 | Y,使用到a |
where a = 3 and b = 5 | Y,使用到a,b |
where a = 3 and b = 5 and c = 4 | Y,使用到a,b,c |
where b = 3 或者 where b = 3 and c = 4 或者 where c = 4 | N |
where a = 3 and c = 5 | 使用到a, 但是c不可以,b中间断了 |
where a = 3 and b > 4 and c = 5 | 使用到a和b, c不能用在范围之后,b断了 |
where a is null and b is not null | is null 支持索引 is not null 类似范围查询,ab能使用,b右边的会失效 |
where a <> 3 | 不能使用索引 |
where abs(a) =3 | 不能使用 索引 |
where a = 3 and b like ‘kk%’ and c = 4 | Y,使用到a,b,c |
where a = 3 and b like ‘%kk’ and c = 4 | Y,只用到a |
where a = 3 and b like ‘%kk%’ and c = 4 | Y,只用到a |
where a = 3 and b like ‘k%kk%’ and c = 4 | Y,使用到a,b,c |
6.4. 关联查询优化
接下来再次创建两张表,并分别导入20条数据:
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 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)));
6.4.1. 关联案例
explain分析一下两个sql:
EXPLAIN SELECT * FROM class LEFT JOIN book ON class.card = book.card;
EXPLAIN SELECT * FROM class RIGHT JOIN book ON class.card = book.card;
EXPLAIN SELECT * FROM class INNER JOIN book ON class.card = book.card;
给book.card创建索引:
ALTER TABLE `book` ADD INDEX idx_card ( `card`);
然后explain分析:
删除旧索引,添加新索引:
# 删除旧索引 + 新建 + 第3次explain
drop index idx_card on book;
ALTER TABLE class ADD INDEX index_class_card (card);
再次explain分析:
同时给两张表的card字段添加索引:(class(card)索引已有:index_class_card,只需给book(card)添加索引)
ALTER TABLE `book` ADD INDEX idx_card ( `card`);
最后explain分析:
6.4.2. 优化建议
- 保证被驱动表的join字段已经创建了索引
- left/right join 时,为被驱动表连接字段创建索引。
- inner join 时,mysql会自己帮你把小结果集的表选为驱动表,对被驱动表连接字段创建索引。(5.6已经优化掉了,5.5需要手动编写)
- 子查询尽量不要放在被驱动表,有可能使用不到索引。
- 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
6.4.3. 三种实现的比较
6.5. 子查询优化
尽量不要使用not in 或者 not exists
尽量不要使用子查询
6.6. 排序优化
以下三种情况不走索引:
- 无过滤,不索引
- 顺序错,不索引
- 方向反,不索引
create index idx_age_deptid_name on emp (age,deptid,name)
# 以下 是否能使用到索引,能否去掉using filesort
explain select * from emp order by age,deptid;
-- 强制使用索引
EXPLAIN SELECT * FROM emp FORCE INDEX(idx_age_deptid_name) ORDER BY age,deptid;
explain select * from emp order by age,deptid limit 10;
# 无过滤 不索引 观察extra的值
explain select * from emp where age=45 order by deptid;
explain select * from emp where age=45 order by deptid,name;
explain select * from emp where age=45 order by deptid,empno;
explain select * from emp where age=45 order by name,deptid;
explain select * from emp where deptid=45 order by age;
# 顺序错,不索引
explain select * from emp where age=45 order by deptid desc, name desc ;
# 方向反 不索引
explain select * from emp where age=45 order by deptid asc, name desc ;
6.6.1. 优化演示
ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序
执行案例前先清除emp上的索引,只留主键
# 查询 年龄为30岁的,且员工编号小于101000的用户,按用户名称排序
SELECT SQL_NO_CACHE * FROM emp WHERE age =30 AND empno <101000 ORDER BY NAME;
结论:很显然,执行时间为0.477s,type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。
优化思路: 尽量让where的过滤条件和排序使用上索引。
现在过滤条件使用了两个字段(age,empno)排序使用了name。
我们建一个三个字段的组合索引可否?
CREATE INDEX idx_age_empno_name ON emp(age,empno,NAME);
再次explain测试:
我们发现using filesort 依然存在,所以name 并没有用到索引。
原因是因为empno是一个范围过滤,所以索引后面的字段不会再使用索引了。
所以我们建一个3值索引是没有意义的
那么我们先删掉这个索引:DROP INDEX idx_age_empno_name ON emp
为了去掉filesort我们可以把索引建成
CREATE INDEX idx_age_name ON emp(age,NAME);
也就是说empno 和name这个两个字段只能二选其一。
这样我们优化掉了 using filesort。
执行一下sql:
速度果然提高了4倍。
假如:选择创建age和empno会速度会怎样呢,自己试试有惊喜!
结果竟然有 filesort的 sql 运行速度,超过了已经优化掉 filesort的 sql ,而且快了好多倍。何故?
原因:是所有的排序都是在条件过滤之后才执行的,所以如果条件过滤了大部分数据的话,几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序但实际提升性能很有限。 相对的 empno<101000 这个条件如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比最高,是最优选择。
结论: 当范围条件和group by 或者 order by 的字段出现二选一时 ,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。
6.6.2. 了解filesort算法
如果不在索引列上,filesort有两种算法:mysql就要启动双路排序和单路排序
6.6.2.1. 双路排序
MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据,读取行指针和orderby列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
也就是:从磁盘取排序字段,再buffer进行排序,再从磁盘取其他字段。
取一批数据,要对磁盘进行了两次扫描,众所周知,I\O是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。
6.6.2.2. 单路排序
从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。
结论及引申出的问题:由于单路是后出的,总体而言好过双路。
但是用单路有问题:在sort_buffer中,比双路排序要多占用很多空间,因为单路排序是把所有字段都取出, 所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取取sort_buffer容量大小,再排……从而多次I/O。
本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。
6.6.2.3. 优化策略
- Order by时select * 是一个大忌,只Query需要的字段, 这点非常重要。在这里的影响是
- 当Query的字段大小总和小于max_length_for_sort_data 而且排序字段不是 TEXT|BLOB 类型时,会用改进后的算法——单路排序, 否则用老算法——多路排序。
- 两种算法的数据都有可能超出sort_buffer的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort_buffer_size。
- 尝试提高 sort_buffer_size
不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程的 1M-8M之间调整
- 尝试提高 max_length_for_sort_data
提高这个参数, 会增加用改进算法的概率。但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是高的磁盘I/O活动和低的处理器使用率。1024-8192之间调整
6.7. 分组优化
group by 使用索引的原则几乎跟order by一致 ,唯一区别是groupby 即使没有过滤条件用到索引,也可以直接使用索引。
-
group by 先排序再分组,遵照索引建的最佳左前缀法则
-
当无法使用索引列,增大max_length_for_sort_data和sort_buffer_size参数的设置
-
where高于having, 能写在where限定的条件就不要写在having中了
只要对分组列创建索引即可
6.8. 覆盖索引
最后使用索引的手段:覆盖索引
什么是覆盖索引?
简单说就是,select 到 from 之间查询的列 <=使用的索引列+主键
explain select * from emp where name like '%abc';
使用覆盖索引后
理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引
理解方式二:非聚集复合索引的一种形式,它包括在查询里的Select、Join和Where子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段,也即,索引包含了查询正在查找的数据)。
6.9. 索引无效说明
创建索引后,用不用索引,最终是优化器说了算。优化器会基于开销选择索引
,怎么开销小就怎么来。不是基于规则,也不是基于语义。
另外SQL语句是否使用索引,和数据库的版本、数据量、数据选择度(查询中选择的列数)运行环境都有关系。
所有创建索引后需要结合explain进行分析索引是否有效
6.10 优化总结
MySQL的性能调优主要集中在以下几个方面:
1、查询优化:
通过分析和优化查询语句,包括使用合适的索引、避免全表扫描、优化JOIN操作等,以提高查询性能。
2、索引优化:
合理设计和使用索引,包括选择合适的列作为索引、创建复合索引、删除不必要的索引等,以加快数据检索速度。
3、配置优化:
调整MySQL的配置参数,如缓冲区大小、并发连接数、线程池大小等,以适应不同的工作负载和硬件环境。
4、内存管理:
合理配置MySQL的内存使用,包括设置合适的缓冲池大小、排序缓冲区大小、临时表空间大小等,以提高内存利用率和减少磁盘IO。
5、存储引擎选择:
根据应用需求选择合适的存储引擎,如InnoDB、MyISAM等,并针对不同存储引擎进行相应的优化。
6、数据库设计优化:
合理设计数据库表结构、字段类型和关系,以减少数据冗余和提高查询效率。
以上只是MySQL性能调优的一些常见方面,具体的调优策略和方法需要根据具体情况进行分析和优化。通过综合考虑硬件、操作系统、网络和应用程序等因素,可以全面提升MySQL数据库的性能和可伸缩性。
7. sql实战
7.1 实现并优化8个SQL
#删除两个表的所有索引
#1、列出自己的掌门比自己年龄小的人员
EXPLAIN SELECT * FROM t_emp c
INNER JOIN (SELECT a.id, a.deptName, b.age FROM t_dept a INNER JOIN t_emp b ON a.CEO=b.id) v ON c.deptId=v.id
WHERE c.age > v.age;
EXPLAIN SELECT SQL_NO_CACHE * FROM dept a
INNER JOIN t_emp b ON a.CEO=b.id
INNER JOIN t_emp c ON c.deptId=a.id
WHERE c.age > b.age;
create index idx_deptId on t_emp(deptId);
#2、列出所有年龄低于自己门派平均年龄的人员
EXPLAIN select SQL_NO_CACHE * from emp c
LEFT JOIN (select b.deptId,AVG(b.age) avgage from emp b GROUP BY b.deptId
) v on c.deptId=v.deptId
where c.age < v.avgage;
EXPLAIN select * from (select b.deptId, AVG(b.age) avgage from emp b GROUP BY b.deptId) v
LEFT JOIN emp c on c.deptId=v.deptId
where c.age < v.avgage;
CREATE INDEX idx_deptId on emp(deptId);
#3、列出至少有2个年龄大于40岁的成员的门派
explain select SQL_NO_CACHE a.deptId, b.deptName, count(*) cou
from t_emp a
INNER JOIN t_dept b on a.deptId=b.id
where a.age>40
GROUP BY a.deptId
HAVING cou >= 2;
# 使用数据量少的表的字段分组
explain select SQL_NO_CACHE b.id, b.deptName, count(*) cou
from t_dept b
STRAIGHT_JOIN t_emp a on a.deptId=b.id
where a.age>40
GROUP BY b.id
HAVING cou >= 2;
create INDEX idx_dept on t_emp(deptId, age);
#4、至少有2位非掌门人成员的门派
EXPLAIN select SQL_NO_CACHE a.deptId,b.deptName,count(*) cou
from t_emp a
INNER JOIN t_dept b on a.deptId=b.id
where a.id!=b.CEO
GROUP BY a.deptId
HAVING cou >= 2;
explain select a.deptId,c.deptName,count(*) cou from t_emp a LEFT JOIN t_dept b on a.id=b.ceo INNER JOIN t_dept c on a.deptId=c.id
where b.ceo is null
GROUP BY a.deptId
HAVING cou>=2
CREATE INDEX idx_deptId on t_emp(deptId);
CREATE INDEX idx_ceo on t_dept(ceo);
#5、列出全部人员,并增加一列备注“是否为掌门”,如果是掌门人显示是,不是掌门人显示否
select a.id, a.`name`, CASE WHEN b.CEO IS NULL THEN '是' ELSE '否' END '是否掌门人'
from t_emp a LEFT JOIN t_dept b on a.id=b.CEO;
#6、列出全部门派,并增加一列备注“老鸟or菜鸟”,若门派的平均值年龄>50显示“老鸟”,否则显示“菜鸟”
select a.id,a.deptName,AVG(b.age),IF(AVG(b.age)>50, '老鸟', '菜鸟') '老鸟or菜鸟'
from t_dept a INNER JOIN t_emp b on a.id=b.deptId GROUP BY a.id;
#7、显示每个门派年龄最大的人
CREATE INDEX idx_deptId on t_emp(deptId);
CREATE INDEX idx_age on t_emp(age);
#8、显示每个门派年龄第二大的人·
SET @last_deptid=0;
SELECT a.id,a.deptid,a.name,a.age,a.rk
FROM(
SELECT t.*,
IF(@last_deptid=deptid,@rank:=@rank+1,@rank:=1) AS rk,
@last_deptid:=deptid AS last_deptid
FROM t_emp t
ORDER BY deptid,age DESC
)a WHERE a.rk=2;
UPDATE t_emp SET age=100 WHERE id = 2
SET @rank=0;
SET @last_deptid=0;
SET @last_age=0;
SELECT t.*,
IF(@last_deptid=deptid, IF(@last_age = age, @rank, @rank:=@rank+1),@rank:=1) AS rk,
@last_deptid:=deptid AS last_deptid,
@last_age :=age AS last_age
FROM t_emp t
ORDER BY deptid,age DESC
CALL proc_drop_index('mydb', 't_emp');
CALL proc_drop_index('mydb', 't_dept');
CALL proc_drop_index('mydb', 'emp');
CALL proc_drop_index('mydb', 'dept');
7.2 计算并指定索引长度
阿里开发手册:
【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区
分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名,索引长度)) / count(*) 的区分度来确定。
测试:
-- address长度为10,当截取到5的时候查询区分度高达0.9572(dept表是随机数据 根据自己的情况判断)
SELECT COUNT(DISTINCT LEFT(address,5)) / COUNT(*) FROM dept;
-- 创建address列的索引并指定长度为5(address可以为空 varchar类型,字节数为:5*3+3 = 18)
ALTER TABLE dept ADD INDEX idx_address(address(5));
-- 可以看到address使用的索引长度为18
EXPLAIN SELECT * FROM dept WHERE address IS NULL;
7.3. 时间日期处理(了解)
SELECT DATE_FORMAT(NOW() , '%Y年%m月%d日 %H时%i分%s秒');
SELECT NOW();
SELECT * FROM ucenter_member WHERE DATE(gmt_create) = '2019-01-02';
SELECT * FROM ucenter_member WHERE DATE_FORMAT(gmt_create , '%Y-%m-%d') = '2019-01-02';
7.4 行转列
测试表:
CREATE TABLE t_score(
id INT(11) NOT NULL auto_increment,
stuid VARCHAR(20) NOT NULL COMMENT 'id',
subject VARCHAR(20) COMMENT '科目',
score DOUBLE COMMENT '成绩',
PRIMARY KEY(id)
)
测试数据:
INSERT INTO t_score(stuid,subject,score) VALUES ('001','Java基础',90);
INSERT INTO t_score(stuid,subject,score) VALUES ('001','mysql',92);
INSERT INTO t_score(stuid,subject,score) VALUES ('001','Javaweb',80);
INSERT INTO t_score(stuid,subject,score) VALUES ('002','Java基础',88);
INSERT INTO t_score(stuid,subject,score) VALUES ('002','mysql',90);
INSERT INTO t_score(stuid,subject,score) VALUES ('002','Javaweb',75.5);
INSERT INTO t_score(stuid,subject,score) VALUES ('002','ssm',100);
INSERT INTO t_score(stuid,subject,score) VALUES ('003','Java基础',70);
INSERT INTO t_score(stuid,subject,score) VALUES ('003','mysql',85);
INSERT INTO t_score(stuid,subject,score) VALUES ('003','Javaweb',90);
INSERT INTO t_score(stuid,subject,score) VALUES ('003','ssm',82);
SELECT * FROM t_score;
需求:行转列显示学生直观显示学生各科成绩
SELECT stuid ,
SUM(IF(SUBJECT = 'Java基础' , score , NULL)) 'Java基础',
SUM(IF(SUBJECT = 'mysql' , score , NULL)) 'mysql',
SUM(IF(SUBJECT = 'Javaweb' , score , NULL)) 'Javaweb',
SUM(IF(SUBJECT = 'ssm' , score , NULL)) 'ssm'
FROM t_score
GROUP BY stuid;
7.5 group_concat
聚合函数:
group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator '分隔符'] )
-- 查询每个学科及格的学生的id集合
SELECT SUBJECT,GROUP_CONCAT(DISTINCT stuid ORDER BY score DESC separator ':')
FROM t_score
WHERE score>=60
GROUP BY subject;
7.6 删除重复行
插入重复数据:
INSERT INTO t_score(stuid,SUBJECT,score) VALUES ('001','Java基础',5);
INSERT INTO t_score(stuid,SUBJECT,score) VALUES ('001','mysql',90);
INSERT INTO t_score(stuid,SUBJECT,score) VALUES ('001','Javaweb',1);
INSERT INTO t_score(stuid,SUBJECT,score) VALUES ('002','Java基础',22);
INSERT INTO t_score(stuid,SUBJECT,score) VALUES ('002','mysql',55);
INSERT INTO t_score(stuid,SUBJECT,score) VALUES ('002','Javaweb',1.5);
INSERT INTO t_score(stuid,SUBJECT,score) VALUES ('002','ssm',2);
SELECT * FROM t_score ORDER BY stuid,SUBJECT;
需求:每个学生同一学科有多个成绩的,保留分数高的
DELETE FROM t_score WHERE id NOT IN(
SELECT tmp.id FROM
(SELECT id FROM t_score t1 JOIN (
SELECT stuid , SUBJECT , MAX(score) m_score
FROM t_score
GROUP BY stuid , SUBJECT) t2
ON t1.`stuid` = t2.stuid
AND t1.`subject` = t2.subject
AND t1.`score` = t2.m_score)tmp
);
SET @stuid:=0;
SET @subject:='';
SET @rank:= 1;
DELETE FROM t_score WHERE id IN(
SELECT id
FROM(
SELECT * , IF(@stuid = stuid , IF(@subject = SUBJECT , @rank:=@rank+1 ,@rank:=1) , @rank:=1) 'rank',
@stuid:=stuid , @subject:=SUBJECT
FROM t_score
ORDER BY stuid , SUBJECT ,score DESC) tmp
WHERE tmp.rank !=1);
7.7 窗口函数
窗口函数和普通聚合函数很容易混淆,二者区别如下:
Ø 聚合函数是将多条记录聚合为一条
Ø 窗口函数是每条记录都会执行,有几条记录执行完还是几条
按照功能划分,可以把MySQL支持的窗口函数分为如下几类:
# 序号函数:没有参数
row_number()/rank()/dense_rank()
# 分布函数:没有参数
percent_rank():所在行索引/总行数的百分比
cume_dist():累积分布值
# 前后函数:参数有3个(expr:列名;n:偏移量;default_value:超出记录窗口的默认值)
lag(): 从当前行开始往前获取第N行,缺失则使用默认值
lead():从当前行开始往后获取第N行,缺失则使用默认值
# 头尾函数: 参数1个(expr:列名)
first_value():返回分组内第一行的值
last_value():返回分组内最后一行的值
# 其他函数:
-- 参数有2个(expr:列名;n:偏移量)
nth_value():返回分组内截止当前行的第N行
-- 参数有1个(n: 切片数)
ntile():返回当前行在分组内的分桶号
/*
语法结构:
window_function ( expr ) OVER (
PARTITION BY ...
ORDER BY ...
)
其中,window_function 是窗口函数的名称;
expr 是参数,有些函数不需要参数;OVER子句包含三个选项:
1、分区(PARTITION BY)
PARTITION BY选项用于将数据行拆分成多个分区(组),它的作用类似于GROUP BY分组。如果省略了 PARTITION BY,所有的数据作为一个组进行计算
2、排序(ORDER BY)
OVER 子句中的ORDER BY选项用于指定分区内的排序方式,与 ORDER BY 子句的作用类似
OVER后面括号中的内容可以抽取:
WINDOW w AS (
PARTITION BY ...
ORDER BY ...
)
*/
测试窗口函数的使用:
-- 1、查询员工信息和他部门年龄升序排列前一名员工的年龄
SELECT * , lead(age , 1,-1) over(
PARTITION BY deptId
) last_emp_age
FROM t_emp;
-- 2、查询每个员工在自己部门由大到小的年龄排名
select * ,
row_number() over(PARTITION BY deptid ORDER BY age DESC) as row_num,
from t_emp;
# 或者
SELECT * ,
row_number() over w AS row_num # w代表使用的
FROM t_emp
WINDOW w AS(PARTITION BY deptid ORDER BY age DESC);
-- 3、查询每个员工在自己部门年龄排序第一个的员工姓名
SELECT * ,
first_value(`name`) over(
PARTITION BY deptId ORDER BY age
)
FROM t_emp;
8. View视图
视图是将一段查询sql封装为一个虚拟的表。 这个虚拟表只保存了sql逻辑,不会保存任何查询结果。
主要作用:
- 封装复杂sql语句,提高复用性
- 逻辑放在数据库上面,更新不需要发布程序,面对频繁的需求变更更灵活
常用场景:
- 共用查询结果
- 报表
语法:
创建
CREATE VIEW view_name AS SELECT column_name(s) FROM table_name WHERE condition
使用
#查询
select * from view_name
#更新
CREATE OR REPLACE VIEW view_name
AS SELECT column_name(s) FROM table_name WHERE condition
#删除
DROP VIEW view_name;
9. Mysql日志和锁
9.1 Mysql基础日志
9.1.1 日志分类
MySQL可以通过各种各样的日志帮助数据库管理员追踪数据库发生过的各种事件。MySQL有多种类型的日志,用于记录数据库的操作和状态。常见的MySQL日志有错误日志、查询日志、慢查询日志、二进制日志、事务日志
9.1.2 错误日志
错误日志(Error Log)是 MySQL 中最重要的日志之一,用来记录MySQL启动、运行过程中发生的错误和异常情况,如启动错误、语法错误等。
my.cnf中可以添加配置设置保存错误日志文件位置:
vim /var/lib/docker/volumes/mysql-master-conf/_data/my.cnf
[mysqld]
log_error = /var/lib/mysql/mysql-master-error.err
重启mysql
docker restart mysql-master
**查看日志:**建议view/tail查看
tail -f /var/lib/docker/volumes/mysql-master-data/_data/mysql-master-error.err
重启mysql容器可以查看到日志
9.1.3 查询日志
查询日志(Query Log)记录所有执行的sql语句,包括SELECT、INSERT、UPDATE、DELETE等操作。可以用于分析查询性能和调试问题,但需要注意对于高负载的系统,开启查询日志可能会对性能产生影响。
my.cnf中可以添加以下配置设置保存查询日志文件位置:
vim /var/lib/docker/volumes/mysql-master-conf/_data/my.cnf
[mysqld]
# 该选项用来开启查询日志 , 可选值 : 0 或者 1 ; 0 代表关闭, 1 代表开启
general_log=1
# 设置日志的文件名 , 如果没有指定, 默认的文件名为 host_name.log
general_log_file=/var/lib/mysql/mysql-master-query.log
重启mysql容器:
docker restart mysql-master
查看查询日志:
tail -f /var/lib/docker/volumes/mysql-master-data/_data/mysql-master-query.log
执行CRUD sql后再看控制台查询日志:
注意:
1、开启查询日志会影响MySQL性能,生产环境中慎用。
2、查询日志会记录大量的sql语句,导致文件过大。可以通过定期清理或限制日志文件大小来解决。
3、查询日志可能会记录敏感信息(如密码),因此要确保只有授权的人员可以访问查询日志文件。
9.1.4 慢查询日志
慢查询日志(Slow Query Log)记录执行时间超过参数 long_query_time值 (默认10秒,可以精确到微秒)的查询语句。分析慢查询日志可以找出执行时间较长的查询,进行性能优化。
9.1.4.1 开启慢查询日志
MySQL数据库默认没有开启慢查询日志,非调优不启用。
my.cnf中可以添加以下配置设置开启慢查询日志:
vim /var/lib/docker/volumes/mysql-master-conf/_data/my.cnf
# 该参数用来控制慢查询日志是否开启, 可取值: 1 和 0 , 1 代表开启, 0 代表关闭
slow_query_log=1
# 该参数用来指定慢查询日志的文件名
slow_query_log_file=/var/lib/mysql/mysql-master-slow-query.log
# 该选项用来配置查询的时间限制, 超过这个时间将认为值慢查询, 将需要进行日志记录, 默认10s
long_query_time=1
重启mysql容器:
docker restart mysql-master
9.1.4.2 查看慢查询日志
慢查询日志和上面的日志一样也是纯文本记录,可以被直接读取。
1、查询慢查询是否开启以及日志文件位置
SHOW VARIABLES LIKE '%slow_query_log%';
2、查询long_query_time 的值。
SHOW VARIABLES LIKE '%long_query_time%'; -- 查看值:默认10秒 单位秒
3、查看慢查询日志文件
tail -f /var/lib/docker/volumes/mysql-master-data/_data/mysql-master-slow-query.log
4、执行sql再次查看日志
-- 由于在查询的执行时间小于配置的1s,因此该查询不会记录到慢查询日志中。
select * from t_emp ;
再次执行以下sql:
-- sleep函数可以让查询休眠指定时间
SELECT * FROM t_emp WHERE id = 1 AND SLEEP(1.1) ;
查看控制台慢查询日志:
9.2 Mysql事务日志
事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?
-
事务的隔离性由 锁机制 实现。
-
事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
- REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
- UNDO LOG 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
9.2.1 事务回顾
组成一个逻辑单元的多个操作要么都成功要么都失败,保证数据一致性。
ACID四大特性
A:原子性(Atomicity)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
C:一致性(Consistency)
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。
如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。
如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
I:隔离性(Isolation)
当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。
事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,不会查看到中间状态的数据。
D:持久性(Durability)
保证事务提交后不会因为宕机等原因导致数据丢失。
事务的隔离性由 锁机制 实现,事务的原子性、一致性和持久性由事务的 redo日志和undo日志来保证。
9.2.2 redo log日志(了解)
9.2.2.1 innodb写数据过程
innodb存储引擎是以页为单位来管理存储空间的。
查询记录时,会从硬盘把记录所在的一整页数据加载出来缓存到内存(Buffer Pool
)中,后续都是先Buffer Pool
中查找,没有命中再去硬盘加载,减少硬盘IO
开销提升性能。
9.2.2.2 redo log日志介绍
Innodb引擎采用的是WAL技术(write-ahead logging)让MySQL拥有了崩溃恢复能力 , 也就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是redo log(重做日志)
。
有了redo log
就算MySQL
实例崩溃宕机,重启时,InnoDB
存储引擎也能使用redo log
恢复数据,保证数据的持久性与完整性。
原理:
更新表数据的时候,Buffer Pool
里如果存在要更新的数据,就直接在Buffer Pool
里更新。然后将“某个数据页上做了哪些修改”记录到重做日志缓存(redo log buffer
)里,接着刷盘到redo log
文件里。
重做日志的缓冲 ( redo log buffer )
存在内存中,是易失的。redo log buffer 大小,默认 16M ,最大值是4096M,最小值为1M。
show variables like '%innodb_log_buffer_size%';
重做日志文件 (redo log file)
保存在硬盘中,是持久的。
重做日志文件保存位置:
ll /var/lib/docker/volumes/mysql-master-data/_data
9.2.2.3 redo log整体流程
第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值 。(redo记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成)
第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中
宕机且数据未刷新到磁盘的时候,可以通过redo log来恢复保证持久化。
优点:
1、redo日志降低了刷盘频率
2、redo日志占用的空间非常小
特点:
1、redo日志是顺序写入磁盘的
2、事务执行过程中,redo log不断记录
9.2.2.4 redo log的刷盘策略
1. 介绍
redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后按照刷盘策略配置将redo log buffer刷入到redo log file 中。
InnoDB可以通过 innodb_flush_log_at_trx_commit 参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。
-- 查看innodb_flush_log_at_trx_commit变量的值
SHOW VARIABLES LIKE '%innodb_flush_log_at_trx_commit%';
2. 刷盘策略
- 设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)。性能最佳、数据风险较高。
- 设置为1 :表示每次事务提交时都将执行刷盘操作( 默认值 )。数据安全性较高、性能稍差。
- 设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由系统自己决定什么时候同步到磁盘文件。**性能较高、数据安全性较高。**在发生数据库故障时,可能会丢失最近提交的事务的数据,因为尚未刷新到磁盘上的日志文件中。
修改方法:my.cnf中添加配置
[mysqld]
...
innodb_flush_log_at_trx_commit=2
9.2.3 undo log日志(了解)
9.2.3.1 简介
事务需要保证 原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半 会出现一些情况,比如:
情况一:事务执行过程中可能遇到各种错误,比如 服务器本身的错误 , 操作系统错误,甚至是突然断电导致的错误。
情况二:程序员可以在事务执行过程中手动输入ROLLBACK 语句结束当前事务的执行。
以上情况出现,需要把数据改回原先的样子保证原子性要求,这个过程称之为回滚 。
MySQL为回滚
记录了增删改操作的记录,这些内容称为撤销日志或者回滚日志(undo log)
1、插入一条记录,至少会记录主键值,之后回滚时只需要把这个主键值对应的记录删除(对于INSERT,INNODB存储引擎会完成一个DELETE)。
2、删除一条记录,至少会记录整条数据,之后回滚时再把这些内容插入到表中就好了(对于每个DELETE,INNODB存储引擎会执行一个INSERT)。
3、修改一条记录,至少要记录更新前的旧值,之后回滚时把这条记录更新为旧值就好了(对于每个UPDATE,INNODB存储引擎会执行一个反向的UPDATE,将修改之前的行放回去)。
redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中更新数据的前置操作就是写入一个undo log日志。
9.2.3.2 undo log存储结构
InnoDB
对undo log
采用段的方式管理,也叫回滚段(rollback segment
),最多支持128个rollback segment
,每个回滚段记录了1024
个undo log segment
,所以最大支持同时在线的事务为128*1024
个。undo
页(详解参考9.2.3.3
)是在undo log segment
段中申请。
# 通过如下的SQL语句查询回滚段的大小
SHOW VARIABLES LIKE 'innodb_rollback_segments';
9.2.3.3 undo 页
1、介绍
Undo页是用于实现事务的回滚和MVCC(多版本并发控制)机制的关键组成部分。
在MySQL中,undo页在事务提交或回滚后,使用过的undo页可以被重新利用存储新事务的undo信息。这个过程称为undo页的重用
。
当一个事务提交或回滚后,其对应的undo页就不再需要了。为了节省空间和提高性能,MySQL会将这些undo页标记为可重用状态,然后添加到undo页的空闲链表中。当新的事务需要分配undo页时,MySQL会首先尝试从空闲链表中获取可重用的undo页,而不是分配新的页。
通过重用undo页,可以减少对磁盘空间的需求,提高系统性能。然而,如果系统中存在长时间运行的读事务或长时间运行的只读事务,可能会导致undo页无法及时重用,从而增加了undo段的大小和磁盘空间的占用。
2、回滚段与事务的关系
1、每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
2、当一个事务开始的时候,会指定一个回滚段,事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
3、当事务提交时,InnoDB存储引擎会做以下两件事情:
- 将undo log放入列表中,以便之后的purge(清理)操作
- 判断undo log所在的页是否可以重用,若可以分配给下个事务使用
9.2.3.4 回滚段中的数据分类
1、uncommitted undo information
未提交的回滚数据(uncommitted undo information)
是指在事务执行过程中所做的修改,但尚未提交的数据。这些数据包括已插入、已更新或已删除的记录。在数据库中,当一个事务开始时,它可能会对数据库中的数据进行修改。这些修改操作会生成相应的undo信息,用于在事务回滚时撤销这些修改。这些undo信息被存储在回滚段(rollback segment)中。
当一个事务还没有提交时,其所做的修改被视为未提交的回滚数据。这意味着其他事务无法看到这些修改,因为它们尚未被永久保存到数据库中。如果该事务被回滚,那么这些未提交的回滚数据将被撤销,数据库恢复到事务开始之前的状态。需要注意的是,未提交的回滚数据只存在于回滚段中,并且只对当前正在执行的事务可见。其他事务无法读取或修改这些未提交的数据。只有在事务成功提交后,这些数据才会成为已提交的数据,对其他事务可见。
2、committed undo information
已经提交但未过期的回滚数据(committed undo information)
是指在事务执行过程中所做的修改,并且已经成功提交到数据库中的数据。这些数据包括已插入、已更新或已删除的记录。在数据库中,当一个事务成功提交后,其所做的修改会被永久保存到数据库中。同时,相应的undo信息也会保留在回滚段中,以便在需要时进行回滚操作。
已经提交的回滚数据对其他事务可见,可以被读取和修改。这意味着其他事务可以看到并访问这些已提交的数据,而不仅仅局限于当前事务的范围内。需要注意的是,已经提交的回滚数据只有在没有过期的情况下才能被保留。过期数据是指由于某些原因(如长时间未使用)而被标记为可回收的数据。
3、expired undo information
事务已经提交并过期的数据(expired undo information)
是指在事务执行过程中所做的修改,并且已经成功提交到数据库中,但由于某些原因被标记为可回收的数据。这些数据包括已插入、已更新或已删除的记录。在数据库中,当一个事务成功提交后,其所做的修改会被永久保存到数据库中。同时,相应的undo信息也会保留在回滚段中,以便在需要时进行回滚操作。
然而,由于一些策略或配置的原因,一些已经提交的回滚数据可能会被标记为过期。过期数据通常是由于长时间未使用或其他管理策略而被认为是可以回收的数据。过期的回滚数据可能会被后续的事务覆盖或清理,以释放存储空间或提高性能。这意味着虽然这些数据已经成功提交,但它们可能不再对其他事务可见或可访问。
需要注意的是,过期的回滚数据的处理方式可能因数据库管理系统而异。一些数据库系统会自动清理过期数据,而另一些则需要手动管理和清理。
9.2.3.5 undo log类型
在InnoDB存储引擎中,undo log分为:insert undo log和update undo log
1、insert undo log
Insert undo log(插入撤销日志)是数据库中用于记录插入操作的一种撤销日志。因为insert操作的记录,只对事务本身可见,对其他事务不可见(这是事务的隔离性的要求),因此 undo log可以在事务提交之后删除。
2、update undo log
Update undo log(更新撤销日志)是数据库中用于记录**更新操作(delete、update)**的一种撤销日志。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
9.2.3.6 undo log的生命周期
1、undo log简要生成过程:
假设有2个数值,分别为A=1和B=2, 然后将A修改为3,B修改为4
1、start transaction ;
2、记录A=1到undo log ;
3、update A = 3 ;
4、记录A=3到redo log ;
5、记录B=2到undo log ;
6、update B = 4 ;
7、记录B=4到redo log ;
8、将redo log刷新到磁盘
9、commit
异常情况分析:
1、在1~8步骤的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。
2、如果在8~9之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时redo log已经持久化。
3、若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘。
只有Buffer Pool没有Redo Log和Undo log的流程:
有了Redo Log和Undo Log之后:
在更新Buffer Pool中数据之前,我们需要先将该数据事务开始之前的状态写入Undo log中。假设更新到一半出错了,我们就可以通过Undo log来回滚到事务开始前。
2、详细生成过程
对于InnoDB引擎来说,每个行记录除了记录本身的数据之外,还有几个隐藏的列:
1、DB_ROW_ID: 如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键。
2、DB_TRX_ID: 每个事务都会分配一个事务的ID,当对某条记录发生变更时,就会将这个事务的事务ID写入trx_id中。
3、DB_ROLL_PTR: 回滚指针,本质上就是指向undo log的指针。
当我们执行INSERT时:
begin;
INSERT INTO user (name) VALUES ("tom");
插入的数据都会生成一条insert undo log , 并且数据的回滚指针会指向它。undo log会记录undo log的序号,插入主键的列和值…, 那么在进行rollback的时候,通过主键直接把对应的数据删除即可。
当我们执行UPDATE时:
对于更新的操作会产生update undo log,并且会分更新主键的和不更新主键的,假设现在执行:
update user set name = 'Sun' where id = 1 ;
这时会把老的记录写入新的undo log,让回滚指针指向定的undo log,它的undo log是1,并且新的undo log会指向老的undo log(undo no = 0)。
假设现在执行:
update user set id = 2 where id = 1 ;
对于更新主键的操作,会先把原来的数据deletemark标识打开,这是并没有真正的删除数据,真正的删除会交给清理线程去判断,然后在后面插入一条新的数据,新数据也会产生undo log,并且undo log的序号会递增。
可以发现每次对数据的变更都会产生一个undo log,当一条记录被变更多次时,那么就会产生多条undo log,undo log记录的是变更前的日志,并且每个undo log的序号是递增的,那么要当回滚的时候,按照序号依次向前,这样就会找到原始数据了。
3、回滚流程
以上面的例子来说,假设执行rollback,那么对应的流程应该是这样:
1、通过undo no=3的日志把id=2的数据删除
2、通过undo no=2的日志把id=1的数据的deletemark还原成0
3、通过undo no=1的日志把id=1的数据的name还原成Tom
4、通过undo no=0的日志把id=1的数据删除
4、undo log的删除
1、针对于insert undo log 因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。
2、针对于update undo log 该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
9.3 并发事务问题
9.3.1 写写并发问题
第一类更新丢失:回滚丢失
A事务和B事务同时执行操作同一条记录,B事务先于A事务提交,A事务回滚时导致B事务提交的数据丢失。
第二类更新丢失:提交覆盖
A事务和B事务同时执行操作同一条记录,B事务先于A事务提交,A事务提交时覆盖了B事务提交的数据。
解决:加锁
9.3.2 读写并发问题
**脏读:**一个事务读取到另一个事务未提交的数据
**不可重复读:**一个事务读取到另一个事务已提交(更新)的数据。
**虚读、幻读:**一个事务读取到另一个事务已提交(新增、删除)的数据。
解决:设置事务隔离级别
不可重复度,Repeatable Read
幻读,Serializable
脏读,Read Committed
9.4 Mysql锁
9.4.1 介绍
锁是协调多个进程或线程并发访问某一资源的机制。数据库中的数据是需要用户共享的资源。我们需要保证数据并发访问的一致性,可以合理使用mysql锁。
锁机制也为实现MySQL的各个隔离级别提供了保证。 锁冲突 也是影响数据库 并发访问性能 的一个重要因素。
9.4.2 并发事务
9.4.2.1 问题
多事务并发操作相同数据库相同记录时可能会导致以下问题:
脏读: 一个事务可以读取另一个事务未提交的数据
事务A读取了事务B更新的数据,事务B未提交并回滚数据,那么A读取到的数据是脏数据
丢失修改(Lost to modify):指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
**不可重复读:**一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配
事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
幻读(虚读): 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点
系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A更改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
9.4.2.2 解决方案
事务隔离:MySQL支持四种隔离级别
读未提交(READ UNCOMMITTED):配置后其他事务可以看到本事务没有提交的部分修改,会造成脏读问题。开发中不允许脏读出现,所以不会使用此隔离级别。
sql演示:
# 创建数据库表
create table goods_innodb(
id int NOT NULL AUTO_INCREMENT,
name varchar(20) NOT NULL,
primary key(id)
)ENGINE=innodb DEFAULT CHARSET=utf8;
# 插入数据
insert into goods_innodb(name) values('华为');
insert into goods_innodb(name) values('小米');
# 查询事务隔离级别
SHOW VARIABLES LIKE '%isolation%';
# 会话一
# 设置事务的隔离级别为read uncommitted
set session transaction isolation level read uncommitted ;
# 开启事务
start transaction ;
# 查询数据
select * from goods_innodb ;
# 会话二
# 设置事务的隔离级别为read uncommitted;
set session transaction isolation level read uncommitted ;
# 开启事务
start transaction ;
# 修改数据
update goods_innodb set name = 'ZETC' where id = 2 ;
# 会话一
# 查询数据:可以查询到会话2未提交的数据
select * from goods_innodb ;
# 会话二
# 回滚事务
rollback;
# 会话一
# 再次查询测试
# 提交事务
commit;
读已提交(READ COMMITTED):其他事务只能读取到本事务已经提交的部分。这个隔离级别解决了脏读问题,但是有不可重复读的问题。
sql演示:
# 会话一
# 设置事务的隔离级别为read committed
set session transaction isolation level read committed ;
# 开启事务
start transaction ;
# 查询数据
select * from goods_innodb ;
# 会话二
# 设置事务的隔离级别为read committed
set session transaction isolation level read committed ;
# 开启事务
start transaction ;
# 修改数据
update goods_innodb set name = '荣耀' where id = 2 ;
# 会话一
# 查询数据
select * from goods_innodb ;
# 会话二
# 提交事务
commit;
# 会话一
# 查询数据
select * from goods_innodb ;
REPEATABLE READ(可重复读):可重复读隔离级别解决了上面不可重复读的问题,但是不能完全解决幻读。MySql默认的事务隔离级别就是:REPEATABLE READ
SELECT @@global.transaction_isolation ;
sql演示(解决不可重复读):
# 会话一
set session transaction isolation level REPEATABLE READ ;
# 开启事务
start transaction ;
# 查询数据
select * from goods_innodb ;
# 会话二
set session transaction isolation level REPEATABLE READ ;
# 开启事务
start transaction ;
# 修改数据
update goods_innodb set name = '一加' where id = 1 ;
# 会话一
# 查询数据
select * from goods_innodb ;
# 会话二
# 提交事务
commit;
# 会话一
# 查询数据
select * from goods_innodb ;
sql演示(测试不会出现幻读的情况):
# 会话一
set session transaction isolation level REPEATABLE READ ;
# 开启事务
start transaction ;
# 查询数据
select * from goods_innodb ;
# 会话二
set session transaction isolation level REPEATABLE READ ;
# 开启事务
start transaction ;
# 插入数据
insert into goods_innodb(name) values('小米');
# 提交事务
commit;
# 会话一
# 查询数据
select * from goods_innodb ;
sql演示(测试出现幻读的情况):
# 表结构进行修改
ALTER TABLE goods_innodb ADD version int(10) NULL ;
# 会话一
set session transaction isolation level REPEATABLE READ ;
# 开启事务
start transaction ;
# 查询一条不满足条件的数据
select * from goods_innodb where version = 1;
# 会话二
set session transaction isolation level REPEATABLE READ ;
# 开启事务
start transaction ;
# 插入一条满足条件的数据
insert into goods_innodb(name, version) values('vivo', 1);
# 提交事务
commit;
# 会话一
# 将version为1的数据更改为'金立'
update goods_innodb set name = '金立' where version = 1;
# 查询一条不满足条件的数据
select * from goods_innodb where version = 1;
SERIALIZABLE(可串行化):这是最高的隔离级别,可以解决上面提到的所有问题,因为他强制将所以的操作串行执行,这会导致并发性能极速下降,因此不会使用到。
9.4.2.3 并发事务分析
并发事务访问相同记录的情况大致可以划分为3种:读-读情况、写-写情况、读-写或写-读情况
1、 读-读情况
读-读情况,即并发事务相继读取相同的记录 。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。
2、 写-写情况
写-写 情况,即并发事务相继对相同的记录做出改动。 在这种情况下会发生 脏写(脏写读取、脏写覆盖) 的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行 ,这个排队的过程其实是通过锁来实现的。这个所谓 的锁其实是一个 内存中的结构 ,在事务执行前本来是没有锁的,也就是说一开始是没有 锁结构 和记录进 行关联的,如图所示:
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构 ,当没有的时候 就会在内存中生成一个 锁结构 与之关联。比如,事务 T1 要对这条记录做改动,就需要生成一个 锁结构 与之关联:
在锁结构中存在很多的信息,为了简化理解,只把两个比较重要的属性拿出来:
1、trx信息:代表这个锁结构是哪一个事务生成的
2、is_waiting: 代表当前事务是否在线等待
当事务T1改动了这条记录后,就生成了一个锁结构与该条记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,我们把这个场景就称之为获取锁成功,或者加锁成功。然后就可以继续进行操作了。
在事务T1提交之前,另外一个事务T2也想对该记录做更改,那么先看看有没有锁结构与该条记录关联,发现有一个锁结构与之关联,然后也生成了一个锁结构与这条记录关联,不过锁结构的is_waiting属性就是true,表示当前事务需要等待,我们把这个场景就称之为获取锁失败,或者加锁失败。如下图所示:
当事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让他继续执行,此时事务T2就算获取到了锁,效果如下所示:
小结:
1、不加锁 意思就是不需要在内存中生成对应的 锁结构 ,可以直接执行操作。
2、获取锁成功,或者加锁成功 意思就是在内存中生成了对应的 锁结构 ,而且锁结构的 is_waiting 属性为 false ,也就是事务 可以继续执行操作。
3、获取锁失败,或者加锁失败,或者没有获取到锁 意思就是在内存中生成了对应的 锁结构 ,不过锁结构的 is_waiting 属性为 true ,也就是事务 需要等待,不可以继续执行操作。
3、 读-写情况
读-写 或 写-读 ,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、 不可重 复读 、 幻读 的问题。
要想解决这些问题就需要使用到到事务的隔离级别,而事务的隔离性的实现原理有两种:
1、使用MVCC:读操作利用多版本并发控制( MVCC ),写操作进行加锁 。
普通的SELECT语句在
READ COMMITTED
和REPEATABLE READ
隔离级别下会使用到MVCC读取记录。1、在 READ COMMITTED 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一 个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改 ,也就 是避免了脏读现象;
2、在 REPEATABLE READ 隔离级别下,一个事务在执行过程中只有 第一次执行SELECT操作 才会 生成一个ReadView,之后的SELECT操作都复用 这个ReadView,这样也就避免了不可重复读 和幻读的问题。
2、读、写操作都采用 加锁 的方式。
小结对比发现:
1、采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。
2、采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。
一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况 下,要求必须采用 加锁 的方式执行。
9.4.3 锁分类
从对数据操作的粒度分 : 开发中mysql锁慎用,容易导致死锁(锁升级)
1) 表锁:操作时,会锁定整个表。
2)页面锁:操作时,会锁定某一页的数据。
3) 行锁:操作时,会锁定当前操作行。
从对数据操作的类型分:
1) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。
2) 写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。下表中罗列出了各存储引擎对锁的支持情况:
存储引擎 | 表级锁 | 行级锁 | 页面锁 |
---|---|---|---|
MyISAM | 支持 | 不支持 | 不支持 |
InnoDB | 支持 | 支持 | 不支持 |
MEMORY | 支持 | 不支持 | 不支持 |
BDB | 支持 | 不支持 | 支持 |
MySQL这3种锁的特性可大致归纳如下 :
锁类型 | 特点 |
---|---|
表级锁 | 偏向MyISAM 存储引擎,开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 |
行级锁 | 偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 |
页面锁 | 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 |
9.4.4 表锁
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
一般用在整表数据迁移的场景。
-- 加锁
lock table 表1 read(write),表2 read(write);
-- 查看表锁
show open tables where in_use=1;
-- 删除本次session的表锁
unlock tables;
案例:
一个session添加A表的写锁,另一个session添加B表的写锁,第一个session测试A、B表数据读取
-- Session_1执行:
lock table t_emp write;
-- Session_2执行:
select * from t_emp;
-- Session_1执行:
unlock tables;
9.4.5 行锁 Record Locks
每次操作锁住一行数据,锁定粒度最小,发生锁冲突的概率最低,并发能力强。但是开销大、加锁慢、可能会出现死锁。InnoDB存储引擎在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
查看行锁:
show status like 'innodb_row_lock%';
对各个状态量的说明如下:
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待所花平均时间(*)
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
Innodb_row_lock_waits:系统启动后到现在总共等待的次数(*)
当等待的次数很高,而且每次等待的时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
**测试行锁:**必须在事务中使用
-- Session_1执行:
start transaction;
select * from t_emp where id=1 for update;
-- Session_2执行:
start transaction;
select * from t_emp where id=2 for update;
-- Session_1执行:
select * from t_emp where id=2 for update;
-- Session_2执行:
select * from t_emp where id=1 for update;
注意:如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。
9.4.6 间隙锁 Gap Locks
间隙锁 是 Innodb 为了解决在可重复读下的 幻读问题
引入的锁机制。间隙锁是行锁的一种。
使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
对于键值在条件范围内但并不存在的记录,叫做 “间隙(GAP)” , InnoDB也会对这个 “间隙” 加锁,这种锁机制就是所谓的 间隙锁(Gap Locks)。
9.4.7 临键锁 Next-Key Locks
Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。
也可以理解为一种特殊的间隙锁。通过临键锁可以解决幻读
的问题,也是行锁
select * from t_emp where id <=8 and id > 3 for update;
9.4.7 总结
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
合理设计索引,尽量缩小锁的范围
尽可能减少索引条件,及索引范围,避免间隙锁
尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
尽可能低级别事务隔离
10. 多版本并发控制
10.1 MVCC概述
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
在数据库中,对数据的操作主要有2种,分别是读和写,而在并发场景下,就可能出现以下三种情况:
读-读并发
写-写并发
读-写并发
我们都知道,在没有写的情况下发读-读并是不会出现问题的,而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么,读-写并发则可以通过MVCC的机制解决。
10.2 快照读和当前读
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读写冲突时,也能做到 不加锁 ,非阻塞并发读 ,而这个读指的就是快照读 , 而非当前读 。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
10.2.1 快照读
快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞 读;比如这样:
SELECT * FROM xx_table WHERE ...
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下, 避免了加锁操作,降低了开销。 既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。 快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
10.2.2 当前读
当前读要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,读取的是记录的最新版本(最新数据,而不是历史版本的数据)。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:
# 共享锁
SELECT * FROM xx_table LOCK IN SHARE MODE;
# 排它锁
SELECT * FROM xx_table FOR UPDATE;
# 排它锁
INSERT INTO xx_table ...
# 排它锁
DELETE FROM xx_table ...
# 排它锁
UPDATE xx_table ...
10.3 隐藏字段以及Undo Log版本链
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。
1、trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给 trx_id 隐藏列。
2、roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然 后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)。
假设 trx_id = 100 的事务 A 插入一条行记录(id = 1, username = “Jack”, age = 18),那么,这行记录的两个隐藏字段 trx_id = 100 和 roll_pointer指向一个空的 undo log,因为在这之前并没有事务操作 id = 1 的这行记录。如图所示:
然后,trx_id = 200 的事务 B 修改了这条行记录,把 age 从 18 修改成了 20,于是,这条行记录的 trx_id就变成了 200,roll_pointer就指向事务 A 生成的 undo log :
接着,trx_id = 300 的事务 C 再次修改了这条行记录,把 age 从 20 修改成了 30,如下图:
可以看到,每次修改行记录都会更新 trx_id 和 roll_pointer 这两个隐藏字段,之前的多个数据快照对应的 undo log 会通过 roll_pointer 指针串联起来,从而形成一个版本链。MVCC 这个机制,其实就是靠 update undo log 实现的。
那么,一条记录在同一时刻可能有多个事务在执行,那么,undo log会有一条记录的多个快照,那么在这一时刻发生SELECT要进行快照读的时候,要读哪个快照呢?
10.4 MVCC之ReadView
MVCC 的实现依赖于:Undo Log、隐藏字段、Read View。
10.4.1 ReadView简介
Read View 主要来帮我们解决可见性的问题的, 也就是他会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。
MVCC主要针对的是READ COMMITTED、REPEATABLE READ
1、READ UNCOMMITTED 隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
2、SERIALIZABLE 隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
3、READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到 已经提交了的 事务修改过的记录。
这个ReadView中主要包含4个比较重要的内容,分别如下:
1、creator_trx_id ,创建这个 Read View 的事务 ID。
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
2、m_ids,生成 ReadView 时有哪些事务在执行但是还没提交的(称为 ”活跃事务“),这些活跃事务的 id 就存在这个字段里。
3、min_trx_id:m_ids 里最小的值
4、max_trx_id:生成 ReadView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务 ID 越大)max_trx_id是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
**注意:**max_trx_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时, m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
10.4.2 ReadView工作原理
通过一个例子来理解下 ReaView 机制是如何做到判断当前事务能够看见哪些版本的:
假设表中已经被之前的事务 A(id = 100)插入了一条行记录(id = 1, username = “Jack”, age = 18),如图所示:
接下来,有两个事务 B(id = 200) 和 C(id = 300)过来并发执行,事务C 想要更新(update)这行 id = 1 的记录,而事务 B(select)想要查询这行数据,这两个事务都执行了相应的操作但是还没有进行提交:
如果现在事务 B 开启了一个 ReadView,在这个 ReadView 里面:
1、m_ids 就包含了当前的活跃事务的 id,即事务 B 和事务 C 这两个 id,200 和 300
2、min_trx_id就是 200
3、max_trx_id是下一个能够分配的事务的 id,那就是 301
4、creator_trx_id是当前创建 ReadView 事务 B 的 id 200
现在事务 B 进行第一次查询,会使用行记录的隐藏字段 trx_id 和 ReadView 的 min_trx_id 进行判断,此时,发现 trx_id 是100,小于 ReadView 里的 min_trx_id(200),这说明在事务 B 开始之前,新增这行记录的事务 A 已经提交了,所以开始于事务 A 提交之后的事务 B、是可以查到事务 A 对这行记录的更新的。
row.trx_id < ReadView.min_trx_id
接着事务 C 过来修改这行记录,把 age = 18 改成了 age = 20,所以这行记录的 trx_id就变成了 300,同时 roll_pointer指向了事务 C 修改之前生成的 undo log:
那这个时候事务 B 再次进行查询操作,会发现这行记录的 trx_id(300)大于 ReadView 的 min_trx_id(200),并且小于 max_trx_id(301)。
row.trx_id > ReadView.min_trx_id && row.trx_id < max_trx_id
这说明一个问题,就是更新这行记录的事务很有可能也存在于 ReadView 的 m_ids(活跃事务)中。所以事务 B 会去判断下 ReadView 的 m_ids 里面是否存在 trx_id = 300的事务,显然是存在的,这就表示这个 id = 300 的事务是跟自己(事务 B)在同一时间段并发执行的事务,也就说明这行 age = 20 的记录事务 B 是不能查询到的。
既然无法查询,那该咋整?事务 B 这次的查询操作能够查到啥呢? 没错,undo log 版本链!
这时事务 B 就会顺着这行记录的 roll_pointer 指针往下找,就会找到最近的一条trx_id = 100 的 undo log,而自己的 id 是 200,即说明这个 trx_id = 100 的 undo log 版本必然是在事务 B 开启之前就已经提交的了。所以事务 B 的这次查询操作读到的就是这个版本的数据即 age = 18。
通过上述的例子,我们得出的结论是,通过 undo log 版本链和 ReadView 机制,可以保证一个事务不会读到并发执行的另一个事务的更新。
那自己修改的值,自己能不能读到呢?肯定可以读到。不过上面的例子我们只涉及到了 ReadView 中的前三个字段,而 creator_trx_id 就与自己读自己的修改有关,所以这里还是图解出来让大家更进一步理解下 ReadView 机制:
假设事务 C 的修改已经提交了,然后事务 B 更新了这行记录,把 age = 20 改成了 age = 66,如下图所示:
然后,事务 B 再来查询这条记录,发现 trx_id = 200与 ReadView 里的 creator_trx_id = 200 一样,这就说明这是我自己刚刚修改的啊,当然可以被查询到。
row.trx_id = ReadView.creator_trx_id
那如果在事务 B 的执行期间,突然开了一个 id = 500 的事务 D,然后更新了这行记录的 age = 88 并且还提交了,然后事务 B 再去读这行记录,能读到吗?
答案是不能的。
因为这个时候事务 B 再去查询这行记录,就会发现 trx_id = 500大于 ReadView 中的 max_trx_id = 301,这说明事务 B 执行期间,有另外一个事务更新了数据,所以不能查询到另外一个事务的更新。
row.trx_id > ReadView.max_trx_id
那通过上述的例子,我们得出的结论是,通过 undo log 版本链和 ReadView 机制,可以保证一个事务只可以读到该事务自己修改的数据或该事务开始之前的数据。
10.4.3 ReadView总结
10.4.3.1 ReadView的规则
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。
1、如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问 它自己修改过的记录,所以该版本可以被当前事务访问。
2、如果被访问版本的trx_id属性值小于ReadView中的 min_trx_id 值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
3、如果被访问版本的trx_id属性值大于或等于ReadView中的 max_trx_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
4、如果被访问版本的trx_id属性值在ReadView的 min_trx_id 和 max_trx_id 之间,那就需要判 断一下trx_id属性值是不是在 trx_ids 列表中。
- 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
- 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
10.4.3.2 MVCC整体操作流程
了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:
1、首先获取事务自己的版本号,也就是事务 ID;
2、获取 ReadView;
3、查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
4、如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
最后返回符合规则的数据。 在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次 Read View。
注意:此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况。
当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会 获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View,如下表所示:
10.5 总结
MVCC
在 READ COMMITTD
、 REPEATABLE READ
这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。核心点在于 ReadView
的原理, READ COMMITTD
、 REPEATABLE READ
这两个隔离级别的一个很大不同就是生成ReadView的时机不同:
-
READ COMMITTD
在每一次进行普通SELECT操作前都会生成一个ReadView -
REPEATABLE READ
只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。