《MYSQL是怎样运行的》笔记
前记:
- 历时15天,笔记+看书。完成于2022.2.5.
- 本书是讲具体的数据库实现,而数据库系统概念见:。可以发现两者是相辅相成的。
文章目录
- 《MYSQL是怎样运行的》笔记
- @[toc]
- -----------第一部分:基础知识-----------
- 一、初识MySQL
- 1基础知识
- 2存储引擎
- 二、启动选项和系统变量
- 1启动选项
- 2配置文件
- 路径
- 配置文件的格式
- 版本专用选项组
- 多个配置文件优先级
- 多个组优先级
- defaults-file
- 命令行与配置文件
- 3系统变量
- 查看方法
- 设置系统变量的方法
- 4状态变量
- 三、字符集与比较规则
- 1字符集和比较规则简介
- 2MySQL中的字符集与比较规则
- 3四个级别的字符集和比较规则
- 4通信过程中的字符集
- -----------第二部分:页面与索引-----------
- 四、InnoDB记录的存储结构
- 1基本知识
- 2InnoDB的行格式
- 3COMPACT行格式
- 变长字段长度列表
- NULL值列表
- 记录头信息
- 真实数据部分
- 4REDUNDANT行格式
- 字段长度偏移列表
- 记录头信息
- NULL比特位
- CHAR(M)的处理
- 5溢出列
- 6DYNAMIC格式 和 COMPRESSED格式
- 五、InnoDB数据页结构
- 1总体结构
- 2User Records部分
- 3Page Dirctory
- 4Page Header
- 5File Header
- 6File Trailer
- 六、B+树索引
- 1基本概念
- 2索引的分类
- 聚簇索引
- 二级索引
- 联合索引
- 3MyISAM的索引方案
- 4MySQL中索引语句
- 七、B+树索引的使用
- 1索引的代价
- 2索引的应用
- 查找
- 排序
- 分组
- 3建立索引的注意事项
- 八、MySQL的数据目录
- 1数据目录
- 2数据目录的结构
- InnoDB
- MyISAM
- 3文件系统对数据库影响
- 4MySQL的系统数据库
- 九、InnoDB的表空间
- 1基本概念与理解
- 2各个结构
- 整体页面类型
- 区extent
- 组
- 段segment
- 链表基结点List Base Node
- 3各个类型的页与位置
- FSP_HDR
- XDES
- IBUF_BITMAP
- INODE
- Segment Header
- 4系统表空间
- 1系统表空间的整体结构
- 2InnoDB数据字典
- -----------第三部分:连表-----------
- 十、单表访问方法
- 1基本概念
- 2访问方法
- 3索引合并
- 十一、连接的原理
- 1连接简介
- 2连接原理
- 嵌套循环连接
- 使用索引加快连接速度
- 基于块的嵌套连接
- 十二、基于成本的优化
- 1基本概念与理解
- 2基于成本的优化步骤与方法
- 根据搜索条件,找出所有可能使用的索引
- 计算全表查询的代价
- 计算其他索引成本
- 比较选取
- 3基本索引统计数据的成本计算
- 4连接查询的成本
- 5调节成本参数
- 十三、InnoDB统计数据的收集
- 1统计数据的存储方式
- 2基于磁盘的永久性统计数据
- innodb_table_stats
- innodb_index_stats
- 3定期与手动更新统计数据
- 4基于内存的非永久统计数据
- 5null重复列的解决
- 十四、子查询优化
- 1条件化简
- 2外连接消除
- 3子查询语法
- 4子查询的运行原理
- 5IN子查询优化
- 物化表
- 物化表转连接
- IN优化--半连接
- 6其他子查询的优化
- 十五、EXPLAIN详解
- 1 基本概念与理解
- 2各列详解
- table
- id
- select_type
- type
- possible_keys / key
- key_len
- ref
- rows
- filtered
- Extra
- 3JSON格式
- 4Extented EXPLAIN
- 十六、optimizer trace
- 十七、InnoDB的Buffer Poll
- 1基本概念与理解
- 2内部结构
- 3LRU链表
- 4刷新页面
- 5大小与组成
- ----------- 第四部分:事务 -----------
- 十八、事务简介
- 十九、redo日志
- 1基本概念与理解
- 2redo日志格式
- 通用格式
- 简单格式
- 复杂格式
- 3Mini-Transaction:保证原子性
- 4redo日志块与写入
- 5redo日志文件
- 6LSN
- 7Checkpoint
- 8相关系统变量
- 9崩溃恢复
- 二十、undo日志
- 1基本概念与理解
- 2事务id
- 3undo日志格式
- INSERT操作
- DELETE操作
- UPDATE操作
- 4undo类型的页面
- 5firstUndoPage页面结构
- 6重用undo页面
- 7回滚段
- 基本结构
- slot释放与分配
- 多个回滚段
- undo的崩溃恢复作用
- 二十一、事务隔离级别与MVCC
- 1事务隔离级别
- 2MVCC原理
- 版本链
- ReadView
- 3Purge原理
- 二十二、锁
- 1锁基本概念
- 2多粒度锁
- 3MySQL中的行锁和表锁
- 其他引擎
- 下面都是针对INNODB的
- 表级锁
- 行级锁
- 锁的内存结构
- 4语句加锁分析
- 5查看事务加锁的情况
-----------第一部分:基础知识-----------
一、初识MySQL
1基础知识
- Mysql是 C/S 架构的
- 代表Mysql服务器进程称为mysql数据库实例。(注意和表实例区别)
- 启动mysql服务器程序
- mysqld可执行文件表示Mysql服务器程序,而mysqld_safe, mysql.server, mysqld_multi也是可执行程序,它们会以不同的方式如监控、多服务器等方式调用 mysqld 文件
-
net start mysql
是要在window下注册成服务才能使用
- 启动mysql客户端程序
- 同样也有很多客户端可执行文件,如mysqladmin, mysqldump, mysqlcheck, 常用的是mysql
mysql -u <用户名> -p <密码> -h <服务器>
- 客户端与服务器的连接过程:采用的是TCP进行连接
- 服务器处理客户端请求的过程如下:
- 连接管理:当有客户端和服务器连接时,服务器会专门创建一个线程与之联系。并且以线程池的方式进行,也就是说连接失效后,线程依然存在
- 解析与优化(查询缓存):会先经过缓存。(但是8.0之后已经去除这个功能了)
- 解析与优化(语法解析):是将客户端的命令进行编译的过程
- 解析与优化(查询优化):优化客户端发送来的请求
- 存储引擎:真正查询数据
- 图示:
可以分为两个层:
一个不与数据打交道:server层
一个真正与数据打交道:存储引擎层
2存储引擎
- 存储引擎也称作 表处理器,数据的存储和提取操作都封装在这个模块中
- 作用:在物理上如何表示记录,怎样从表中读取数据,以及怎样把数据写入具体的物理存储器上,都是存储引擎负责的
- 存储引擎与server层的关系:mysql提供有非常多的存储引擎,它们的接口统一,给上层的server层调用
- Mysql支持的存储引擎如下:
- 查看当前服务器支持的存储引擎:
- 设置表的存储引擎:
- 不同的表的存储引擎是可以不一样的(请注意存储引擎原本的名字:表处理器)
- 可以在创建表时指定
- 也可以修改
alter table <table-name> engine = <engine-name>
二、启动选项和系统变量
1启动选项
- 启动选项意义:就是手机上的“设置功能”,给用户一定的自定义自由
- 定义:在程序启动时指定的设置项称为启动选项(startup option)
- 命令格式 :
<命令> <长参数> <短参数>
- 长参数 :
--启动选项1[=值1] -启动选项2[=值2]
- 如 mysql --skip_networking
- mysql --default-stroage_storage-engine-demo=MyISAM
- 短参数:对一些常用的参数进行简化:
2配置文件
- 启动选项只对一次启动有效,而配置文件对所有启动有效
路径
- 当启动时,mysql会从以下路径中一 一加载,注意是全部加载,而不是“找到一个就停止搜索”
在window下:
在linux下
配置文件的格式
- 示例:
[server]
(具体的启动选项...)
[mysqld]
(具体的启动选项...)
[mysqld_safe]
(具体的启动选项...)
[client]
(具体的启动选项...)
[mysql]
(具体的启动选项...)
[mysqladmin]
(具体的启动选项...)
[server]
option1 #这是option1,该选项不需要选项值
option2 = value2 #这是option2,该选项需要选项值
[mysqld]
basedir=D:\0soft\Mysql\mysql-5.7.15-winx64\mysql-5.7.15-winx64\
datadir=D:\0soft\Mysql\mysql-5.7.15-winx64\mysql-5.7.15-winx64\data\
port=3306
- 被划分为多个组,用" [ ] " 包括起来
- 配置文件中只能使用长格式的参数
- 为什么要不同的组?:不同的组是给不同的程序使用的
- 特殊的组:
- [server]:所有服务器程序都生效
- [client]:所有客户端都生效
- 组示例:
注意:如参数 c 是属于 mysqld的,但是配置文件中可以把参数c写在[server]组中。因为mysqld会去同时读取这两个组的。
版本专用选项组
-
[mysqld-5.7]
这样的话,这个组仅仅对5.7版本的mysql生效 - 意义:同一份配置文件可以适应不同的版本
多个配置文件优先级
- 会根据上面罗列的路径进行搜索
- 如果有相同的配置内容,以后面的为准
- 实际上就是后面加载的优先级更高。windows下 BASEDIR最高,linux下**~用户目录**最高。
多个组优先级
- 当一个命令可以读取多个组时,而组有冲突的命令,以位置在后面的为准
defaults-file
-
mysqld --default-file=<路径>
程序启动时,只在指定的路径中查找配置文件,找不到报错 - ``mysqld --default-extra-file=<路径> `程序启动时,会额外在指定的路径中查找配置文件
命令行与配置文件
- 有冲突时,以命令行的为准
- 包含关系如下(没有验证,根据书上说的推导的):
3系统变量
- 定义:MYSQL服务器在运行过程中会用到许多影响程序行为的变量,它们被称为系统变量
- 如同时连入客户端的个数 :max_connections (实际上为max_connections + 1,多出来的是超级管理员vip)
- 默认存储引擎:default_storage_engine
- 查询缓存大小 :query_cahe_size
查看方法
show variables [like 模式]
设置系统变量的方法
- 方法一:启动选项:有一些启动选项就是在设置系统变量
- 方法二:运行过程中设置:全局 与 会话 变量, 不写默认为会话变量
- set[global|session] <系统变量名> = <值>
- set[@@global|session]<系统变量名> = <值>
- 作用范围:
- 解决的的问题:不同客户端的设置冲突问题。可以适应不同用户设置不同的系统变量
- 全局:对所有客户端生效
- 会话:对当前客户端生效
- 带范围的查看方法:
show [global|session] variables [like 模式]
- 启动选项与系统变量的关系:
4状态变量
- 定义:mysql服务器维护了许多关于程序运行状态的变量,称为状态变量
- 作用:可以让用户更好了解服务器的运行状态
- 查看状态变量:
show [golbal|session] status [like 模式]
, 不定范围默认为session
三、字符集与比较规则
1字符集和比较规则简介
- 字符集:规定能表示什么字符
编码:以什么样的方式来表示这个字符 - 编码 与 解码:
- 把字符映射成进制数据的过程叫做编码
- 将二进制数据映射到字符的过程叫做解码
- 常用的字符集:ascll, iso8859-1, gb2312等
- 用不同字节数来表示一个字符的编码方式称为变长编码方式。
- utf-8 utf-16 utf-32是unicode字符集的一种编码方式
- 比较规则:在同一个字符集内,有着不同的排序方式,如区分大小写,不区分大小写 ,还可以根据每个国家的语言排序规则来分等,这些规则称为排序规则 或者 比较规则
2MySQL中的字符集与比较规则
- mysql不区分字符集与编码方案的概念,也就是说如utf-8, 就同时确定了字符集与编码方案
- utf8 与 utf8mb4
- utf8mb3 :阉割过的utf-8,用1~3字节表示字符。 在mysql中,utf8是utf8mb3的别名
- utf8mb4:正常的utf-8, 用1~4字节表示字符
- 查看字符集:
show (character set | charset) [like 模式]
, 其中,character set 和 charset 是同义词。
maxlen表示这个字符集最大需要多少个字节表示字符。
重要的大小:
- 比较规则的查看:
show collation [like 模式]
(有意思的是,utf8(mb4也没有。。。)并没有关于中文的排序规则。如果在utf8编码下要排序,只能在查询出来的时候转化编码后再排序(那也意味着在utf8下,不能建立基于中文拼音排序的索引)) - 比较规则的命名:utf8_general_ci, utf8_spanish_ci
- utf8:字符集
- general / spanish: 通用比较规则 / 在西班牙语下的比较规则
- ci:后缀含义:
3四个级别的字符集和比较规则
- mysql有4个级别的字符集与比较规则,分别是服务器级别,数据库级别,表级别,列级别
- 从左到右优先级依次增大,也就是后面设置的会覆盖前面设置的。当后面的不设置时,会默认使用前面设置的
- 两大系统变量:
服务器级别的系统变量:
character_set_server | collation_server
数据库级别的系统变量:(只能查看,不能修改)
character_set_database | collation_database - 在建立数据库、建立表、建立列的时候,都可以使用通用的语句指定, 而服务器的设置方法则和修改正常的系统变量方式一样(看第二章)
[[default] character set <字符集名称>]
[[default] collate <比较规则名称>]
- 因为 字符集 和 比较规则 在mysql上是绑定的,如果只设置其中一个,另外一个会自动发生变化
- 只修改字符集:比较规则用默认的
- 只修改比较规则:将字符集设置成和当前比较规则对应的字符集
4通信过程中的字符集
- 字符在客户端和服务器通信过程中的转换:
- 用户输入后,客户端以操作系统的默认字符集进行编码(也可以不用操作系统的)
- 将编码后的字节序列发送到服务器
- 初始化:服务器接收到请求后,将character_set_client ,character_set_connection,character_set_result初始化成客户端的默认字符集
- 服务器根据 character_set_client 变量,对发送来的字节序列进行解读
- 将 character_set_client 编码,转换成 character_set_connection编码,统一用character_set_connection编码进行处理
- 将处理后的结果,以 character_set_result编码方式,返回给客户端
- 客户端还是以操作系统的默认字符集进行解读,呈现给用户(也可以不用操作系统的)
- 上面说的三个变量,都是SESSION级别的,每个客户端不同。
但是这三个变量通常是一样的,防止过多出现编码转化,浪费效率 - 如果处理的时候,列的字符集和排序规则 和 character_set_connection不一样,则列的优先级更高,会进行多一步的编码转化工作
- 思考:为啥要转化成character_set_connection?原因是,character_set_client 和character_set_result是面向客户端的,它们并没有对应的collation变量(面向客户端的,说明字符集就好了,并不用去排序),也就是说客户端发来的字符串的排序规则,服务器是不知道的。所以要用 character_set_connection和collation_connection 进行一个统一的字符转化 和 排序。
总之就是,character_set_client 和character_set_result是面向客户端的没有排序规则,而character_set_connection有排序规则,而排序规则对解释一段查询是重要的。
如 select (A, B)(这是一个由子查询直接生成的临时表) order by xxx , 这个时候就需要排序规则。
(如果面向列就不用了,因为列一定有排序规则,且优先级更高)
-----------第二部分:页面与索引-----------
- 回答问题的时候要多角度,不同存储引擎、同一存储引擎不同存储格式都是不一样的。如果在面试中强调这一点,可以加分
四、InnoDB记录的存储结构
1基本知识
- InnoDB是一个将表中数据存储到磁盘上的存储引擎
- InnoDB将数据划分为若干页,以页作为磁盘和内存之间的交互基本单位
- 页的大小默认为16KB
- 系统变量innodb_page_size表明了页大小,仅仅只能在 启动时修改,运行时不可更改
- 关于B+树的思考:
- 相比于B树,由于B+树是将数据放在了叶子结点,意味着上层的索引而可以容纳更多的索引记录,可以更加快速地查找
- B+树的数据在底层串联成一个链表,更适合用于范围查找。mysql索引的查找时,会形成搜索区间,这个时间这个链表就很有用了
- 内存与硬盘的思考:
- 硬盘中的数据必须被加载到内存才能被程序所使用
- 数据在内存中的组织结构就是数据结构,B树、B+树等主要管理这一块,即B树等是负责数据在内存中的组织结构
- 为了让磁盘内的数据被有效轻松地加载与使用,在内存与磁盘中间又产生了DBMS中间件。即DBMS是提供一种磁盘到内存的数据结构的映射的能力
2InnoDB的行格式
- 在插入时以记录为单位插入表中的,这是逻辑层面的。
在物理上,这些记录在磁盘上的存放形式也被称为 行格式 或者 记录格式 - 有四种行格式:
- compact
- redundant
- dynamic
- compressed
- 可以在创建 表 时定义行格式,或者修改行格式
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
3COMPACT行格式
- compact分为两个部分:记录的额外信息 与 记录的真实信息
变长字段长度列表
思考:我怎么知道变长字段有哪些?
可以在系统表空间的第7号页面进行数据字典的查询得知。
- 记录的是变长类型非null值的长度
- 以逆序的方式存储(原因:这样可以和真实的数据挨得更加近,可以提高缓冲的命中率,使用空间局部性原理)
- 长度:1字节 或者 2字节
规则:当变长字段的最大长度(编码中一个字符所需的最大字节(如utf8是3) * 变长字段的最大字符数(如varchar(10)是10 ) ) 大于 255,并且实际占用的字节数超过127字节,则使用2 字节,否则1字节
NULL值列表
- 用 位图 的方式来记录
- 有多少个允许为null的字段,就是多少个位(不过要向上取到整数个字节)
- 1代表是null,0代表不是
- 逆序存放的
记录头信息
- 由固定的5字节组成,描述当前记录的一些属性
- 记录头的前4个bit称为 info bit
- 图示:
真实数据部分
- 隐藏列:mysql会在真实数据部分加入隐藏列(如果记录本身有主键,则没有row_id)
- 真实的数据就存储在这里:
- char的特殊性:
- char类型如果使用的是定长编码,则不会出现在变长字段长度列表中。且如char(10)会在真实数据部分固定占用10个编码字节(如ascill就是10字节)
- 如果使用的是变长编码(如utf8):则会出现在变长字段列表中,且在真实数据部分至少占用char(M) M个字节。(如果 更新的字符长度小于M,则不用重新分配空间,直接塞进去,不用重新分配空间,防止了空间碎片的产生(有点不理解))
4REDUNDANT行格式
字段长度偏移列表
使用两个相邻偏移量来计算字符的长度。
记录头信息
重点属性:
- 1byte_offs_flags:记录偏移量使用1个字节还是2个字节。(确定方法在P67中)
NULL比特位
- redundant行格式没有null值列表,但偏移量的最高位如果是1,则代表该字符为null
- char两种情况在真实数据的占用字节数的情况:
- 如果是定长编码,则直接占用最大的字节数,用0x00字节填充
- 如果是变长编码,则不在真实数据部分占用字节数
CHAR(M)的处理
- 直接以最大的长度进行空间存储,即 一个字符编码最大长度 * M
5溢出列
- 当一列的字符长度太长,一页无法存放时,会将溢出的字符放在另外一些页面中
- compact 和 redundant格式会只存储前768个字节的数据 和 20字节的指向溢出页的地址(这20字节里面了记录了占用其他页面的字节数)
- 什么时候产生溢出页:有一套计算公式在P70
6DYNAMIC格式 和 COMPRESSED格式
- 其他部分和 compact行格式基本一些,只不过是在溢出列的数据处理上不同
- dynamic:不会记录真实数据的768字节,只记录20字节,全部的数据都存放在溢出页中
- compressed:在dynamic的基础上,会用压缩算法对页面进行压缩
- compact, dynamic, compressed是紧凑的,而redundant是非紧凑的。也就是说同样多的记录,前面三种数据占用页面会相对少一点,从磁盘的交互次数就会少
五、InnoDB数据页结构
innodb有非常多类型的页,专门存放记录的页这里称为数据页。这一章是介绍数据页的(也称为索引页)
1总体结构
2User Records部分
基本
- 当记录要放在页时,会向free space部分申请记录空间,并把free space部分划分为user records。
- infimum 和 supremum也算是一种记录,是一种特殊的记录。把他们提取到另外一个部分中。
- 行格式中的记录头信息就是用户描述“本条记录在整个页中的地位信息”
记录头信息
- deleted_flag:标志当前记录是否被删除了,会形成垃圾链表。思考为啥要这样?原因:如果立马移除,要需要在磁盘上重新排列其他记录,造成性能消耗
- min_rec_flag:B+树,不知道是啥,以后知道~
- n_nowned:带头大哥才拥有的,记录组内的小弟个数
- heap_no:
- 记录的一条一条亲密无间的排列称为堆
- 这个heap_no就是堆的唯一编号
- 0 和 1编号是属于 infimum 记录 和 supremum 记录的
- 任何记录的主键值都比 infimun记录大, 都比 supremum记录小。(它们两个本身没有主键值,是人为规定大小的)
- record_type:记录记录的类型,如 infimum 记录 和 supremum 记录,普通记录,和B+树相关的记录
- next_record:
- 按照主键值的大小,形成一个单向链表
- next_record的值是从当前记录的真实数据到下一条记录的真实数据的距离,可正可负
- 思考:为啥要这样设计距离?原因是这个指针地址的左边就是记录头信息,右边是真实数据,同时记录头中的是逆序存放的,所以可以提高缓冲的命率,充分利用空间局部性原理
- 示意图:
3Page Dirctory
- 就是一个目录的作用,将关键位置的偏移量抽取出来,达到快速查找的能力
- 抽取的过程:
- 将所有记录分为若干组
- 每个组中的最大的记录在页中的偏移地址抽取出来
- 最后一个记录的n_owned属性表示组内有几条记录
- 将抽取出来的偏移地址放在页目录的槽中,每个槽2个字节
- 页目录至少有两个槽,分别是infimum 和 supremum槽
- 查找过程如下:
- 先用二分确定记录在哪个组
- 在组内再用单链表的遍历完成
4Page Header
- 记录数据页中的记录的状态信息
5File Header
Page Header 是专门针对 数据页 记录的各种状态信息,比方说页里头有多少个记录,有多少个
槽。现在描述的 File Header 针对各种类型的页都通用
重要信息:
- fil_page_space_or_chksum:校验和。
- fil_page_offset:页面的唯一id,即页号
- fil_page_type页面的类型:
存放记录的数据页的页类型是 FIL_PAGE_INDEX, 就是索引页。 - fil_page_prev 和 fil_page_next 表示 在溢出页中,本数据页的上一个页和下一个页的页号,形成一个双向链表 (其它无关联的页怎么查找呢 ?所以的页都可以独立查找的,用表空间+页号即可)
6File Trailer
- 8个字节
- 前4个字节:与file header中的校验和相对应,如果页面成功刷新在磁盘,则这两个值一样,代表成功刷新;否则不成功
- 后4个字节:表示页面被最后修改时对应的LSN后4字节
- 意义:当file trailer的前后4个字节校验都成功时,才说明页面被真正成功刷新到页面上。且不用重新计算,直接和file header中的校验和比较即可,速度快。
六、B+树索引
1基本概念
- 索引的灵感:索引(目录)本身可以看成一种特殊的用户记录,于是在mysql中索引目录就利用了数据页结构
- 索引中目录项的组成:页中的最小id + 页号 (注意:槽内是存储组中的最大记录的id)
- 数据页称为B+树的节点;叶子结点是存储数据的;内节点是存放目录记录的
- B+树一般不超过4层,所以最多只需要查找3个存储目录+1个存储用户记录的页(Page header中Page_level就记录层数)
- 根页面是万年不动的,对一个表建立索引,那么根会被记录到一个地方,innodb会到那个地方去固定查询索引的根。(思考:记录到哪个地方呢?还是系统表空间7号页面的元数据表)
插入过程:从0开始,先插入到根结点(把目录当成叶子结点),满了再进行页分裂,将根中的记录复制到新分配的结点上,根结点就升级为一个目录结点(内结点) - 一个页面至少容纳2条记录:防止B+树的层级增长过快
- 回表的代价:在通过id找完整的记录的过程中,会大量随机io。
所以 在全表扫描 和 二级索引+回表 这两种操作中,查询优化处理器会进行一个选择。
当回表操作的记录数越多,就越倾向于全表查询。
2索引的分类
聚簇索引
- 定义:
- 使用主键值作为记录排序的标准
- B+树的叶子结点存放完整的用户记录
满足以上两个条件的,称为聚簇索引
- InnoDB会自动创建聚簇索引
- 聚簇索引是一种数据的存放方式(用户数据在叶子结点上),也就是“索引即数据,数据即索引”
二级索引
- 定义:以非主键列的大小作为排序规则而建立的B+树,需要执行回表操作才可以定位到完整的用户记录,这种B+树称为二级索引(secondary index)或者辅助索引
- 二级索引的内节点的结构组成:
- 索引的列的值
- 主键值(在索引的列的值全部一样的情况下起作用)
- 页号
- 二级索引的叶子结点的组成:
- 索引的列的值
- 主键值 (拿到主键值,进行聚簇索引的回表操作)
- 示意图:
- 查找:建立二级索引的列c可能不具有唯一性,因此定位到第一条符合c的记录,向后进行记录单链表和页面双链表的扫描即可。
- 思考:为啥要进行回表操作?或者说为啥要二级索引?
如果针对某一个非主键列,都要进行完整的用户记录复制的话,浪费空间和效率 - 二级索引本质上是一种联合索引,即(索引列,主键值)的联合索引。即先对索引列进行排序,索引列相同再按照主键进行排序 (查找插入等插入,都可以按照联合索引的思路进行)
联合索引
- 定义:在二级索引的基础上,将 多个非主键列 作为排序规则,即为多个列同时建立索引,也称为复合索引或者多列索引。
- 本质上是一种二级索引(索引列是 c1 c3)
3MyISAM的索引方案
- MyISAM的索引方案也是就树形结构
- 将数据文件和索引文件完全区分开,即 “索引是索引,数据是数据”
- MyISAM的索引全部都是二级索引, 查找过程和innodb差不多,只是回表的时候用的id是 行号 或者 数据文件中的偏移量, 回表操作快速。
- 数据文件的结构:
- 一行一条记录 (二级索引中的叶子结点的主键值就是行号)
- 可变长记录,直接将数据挨在一起(二级索引中的叶子结点的主键值就是 数据在数据文件中的偏移量)
- 索引文件的结点:
- 本质上就是一种二级索引
- 相对于innodb, 叶子结点中的主键值就不是表中的主键了,而是行号或者数据偏移量
4MySQL中索引语句
- KEY 与 INDEX 是同义词
- 自己看以前的笔记
相关文章:http://blog.codinglabs.org/articles/theory-of-mysql-index.html
索引分类:
- 主键索引
- 唯一的标识,不可重复,只能有一个列作为索引
- 唯一索引
- 记录不可重复(除了NULL以外),但一个表中可以有多个 唯一索引
- 常规索引
- 没有任何限制
- 全文索引
- 在特定的数据库引擎都有,MyISAM有
- 快速定位数据
语法:
-- 总共有三种方式可以创建索引
-- ---------------方法一:在创建表的时候定义索引----------------------------
CREATE TABLE `test`(
`id` INT(10) NOT NULL,
`name` VARCHAR(20) NOT NULL,
`age` INT(100) NOT NULL,
`address` VARCHAR(30) NOT NULL,
PRIMARY KEY(`id`), --主键索引
UNIQUE `name`(`name`), --唯一索引
KEY `age`(`age`), --普通索引,也可以用INDEX,等效于KEY
FULLTEXT `address`(`address`)--全文索引用int类型会报错(不知道为什么)
)ENGINE=INNODB DEFAULT CHARSET=utf8
-- --------------------方法二:利用create语句------------------------
CREATE UNIQUE INDEX indexName ON mytable(username(length))
CREATE UNIQUE INDEX `name` ON `test`(`name`(2))
CREATE INDEX `age` ON `test`(`age`)
CREATE FULLTEXT INDEX `name` ON `test`(`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 `test` ADD INDEX `name`(`name`)
ALTER TABLE `test` ADD PRIMARY KEY `id`(`id`)
其它:
- explain 用于分析执行的sql语句
- show index from table_name 显示表中所有的索引
- DROP INDEX [indexName] ON mytable; 删除索引
索引原则:
- 索引不是越多越好
- 不要对经常变动的数据加索引
- 小数据量的表不需要加索引
- 索引一般加在常用来查询的字段上
七、B+树索引的使用
1索引的代价
- 空间代价:每一个索引的建立都要有一棵B+树的空间
- 时间代价:curd要对每一个索引B+树进行维护操作;二级索引还要额外的回表时间;
2索引的应用
思想精髓:精确把握住,叶子结点是从小到大排序的,是按照哪个列的规则排序的就好。
查找
- 全表扫描:对聚簇索引的第一个叶子结点,到最后一个扫描。是万能的
- 扫描区间:扫描记录所在的区间就叫扫描区间
- 边界条件:形成这个扫描区间的搜索条件,称为边界条件
- 单点扫描:只包含一个值的扫描区间
- 重要原则:如果没有办法通过形成合适的扫描区间来减少扫描的数量时,则不考虑用这个索引
- 一个查找中,可以选择使用不同的列索引来查找。查找的核心是可以形成合适的扫描区间来减少扫描的数量
- 索引条件下推(index condition pushdown, ICP):在联合索引中,即使有些列对减少扫描区间没有贡献,但是可以在二级索引中先用他们进行过滤,来减少回表的操作P120
(实际上,下推的真正含义是:将server层才过滤的条件,提前下推到engine层来判断 ,来减少回表的次数)
排序
- 索引中的记录本身是有序的,因此可以用来进行排序
- 文件排序:在内存或者在磁盘中进行排序的方式统称为文件排序
- 联合索引中排序的注意事项:
- 在 order by 语句中,要排序的列的顺序要按照联合索引的顺序;可以对左边一部分连续的非常量的列排序;左边连续的列是常量,对右边部分连续的列进行排序;
- 降序的原理:
索引是升序的自然可以,那么怎样降序呢?
简简单单,逆向遍历记录单链表就可以了?
如何逆向 ?不能逆向。
通过一直遍历,先找到slot大哥,从slot中找到上一个slot大哥,再向前遍历。 - 不用用于排序的情况:
- asc, desc混用:原因:索引数据是从小到大排序的
- 排序列中包含非索引列
- 联合索引中,非连续的列
- 条件中的索引列 和 排序列不同 (因为要用到了不同的索引树)
- 排序列不是以单独列名出现在条件中:如 order by upper(c1)
分组
- 分组的列要和联合索引中的列顺序一致
- 可以对左边连续的列进行分组
3建立索引的注意事项
- 只为只用搜索、排序、分组的列创建索引
- 考虑索引列中不重复的值(疑问P127
- 索引列的类型尽量小 (可以使一页中的记录尽量多,IO会减少)
- 为列前缀建立索引(like的使用)
- 覆盖索引 (select 中的列是二级索引列)
- 让索引列以列名形式在搜索条件中出现 ke1 * 2 < 4 错
- 最好使用自增主键,可以有效减少页分裂 (这就是为什么推荐使用自增主键的原因)
- 去除没必要的索引(如有了联合索引,又为联合索引的第一列建立另外一个索引)
- 其他:
- 索引不是越多越好
- 不要对经常变动的数据加索引
- 小数据量的表不需要加索引
- 索引一般加在常用来查询的字段上
八、MySQL的数据目录
1数据目录
- 什么是数据目录?数据会存储到这个目录,加载时也从这个目录中读取数据
- 数据目录除了存储数据外,还有进程文件、日志文件、SSL证书等
- 查看:
2数据目录的结构
InnoDB
数据库的表示:
- db.opt
表结构的表示:
- 表名.frm
表数据的表示:
- 表空间:为了更好管理页,提出表空间(文件空间)的概念。表空间是一个抽象的概念,对应文件系统一个或者多个真实文件
- 系统表空间:在数据目录下:ibdata1 (只有一份,文件的份数可以修改),所有的数据都在这个文件(一个空间一个或者多个文件)
- 独立表空间:一个表数据一个文件:表名.ibd (一个空间一个文件)
- 其他类型的表空间:如临时表空间、undo表空间等
MyISAM
数据库本身:
数据目录/数据库 目录下:db.opt
在 数据目录/数据库 下:
- 表名.frm:表结构
- 表名.MYD:表数据
- 表名.MYI:表索引
3文件系统对数据库影响
- 数据库的名称和表名称不能超过文件系统允许的最大长度
- 特殊字符:数据库中特殊的字符(数字拉丁字母外)的,会变成@<编码>
- 文件长度:受文件系统最长长度的限制
4MySQL的系统数据库
- mysql:存储用户账号、权限信息、日志信息、存储过程中的定义信息等
- information_schema:有哪些表、视图、触发器、哪些列、哪些索引,就是一些描述性信息,即元数据,也就是数据库系统概念中的数据字典 (这个没有对应的数据库目录,最特殊。本质上是对系统表空间的第7号页面:页面类型为SYS的一个封装)
- performance_schema:保存mysql服务器运行过程中的一些状态信息,算一种性能监控
- sys:通过视图的形式,对information_schema和performance_schema进行封装,让开发人员更方便
九、InnoDB的表空间
1基本概念与理解
- 由于页号FIL_PAGE_OFFSET是4个字节,所以最大可以2^32个页,如果按照16kB算,则一个表空间最多可以有64TB的大小
- 对于16KB的页来说,64个页为一个区extent
- 256区划分为一个组
2各个结构
整体页面类型
区extent
- 对于16KB的页来说,64个页为一个区extent(1MB大小)
- 思考:为什么要分区?
在数据量大的时候,在分配空间的时候以区为单位,可以让相邻的页尽可能在一起,减少随机io - 思考:如果是SSD呢?减少随机io有用吗?当然有。
- M2 NVME SSD随机读大约40M,顺序读大约2000-4000M,相差50-100倍。
- SATA SSD 随机读大约40M,顺序读大约550M,相差14倍。
- 机械硬盘随机读大约 0.5M,顺序读大约 100-200M,相差200-400倍。
- 区的分类:(区的状态)
- 空闲的区:现在还没有用到这个区中的任何页面。
- 有剩余空间的碎片区:表示碎片区中还有可用的页面。
- 没有剩余空间的碎片区:表示碎片区中的所有页面都被使用,没有空闲页面。
- 附属于某个段的区。
- XDES Entry:Extent Descriptor Entry结构:一个区对应一个XEDS Entry, 记录区的一些信息
(就是区不一定是连续的!所以要XDES Entry来串连起来。比如三个状态的XDES Entry, 其相互之间就是不一定连续的) - 各个属性的含义:
- Segment ID每一个段都有一个唯一的编号,用ID表示,此处的 Segment ID 字段表示就是该区所在的段
- List Node:指定页号 与 页号中的偏移量, 形成双向链表
- State:区状态,上面四个区状态(看上面有写)
- Page State Bitmap:这两个比特位的第一个位表示对应的页是否是空闲的,第二个比特位还没有用。
- XDES Entry 链表:
- 表空间直属:把三个直属于表空间的状态(即四个状态中除了FSEG),按照三个状态分类划分成三个链表
- 段内区FSEG:也是按照三个状态,在段内(即根据Segment ID),按照三个状态分类划分成三个链表
组
- 每256个区被划分为一组。
- 思考:**为什么要分组?**因为一个XDES页面放不下太多的XDES Entry, 最多只能256个XDES Entry(即256个区)。所以分组。(笑死?。。。狗头.jpg)
段segment
- 思考:为什么要分段:(感觉就是将不同作用的区进行一个分类)
在索引状态下,如果把叶子结点和非叶子结点放入同一个区,则扫描时会“断层”, 扫描的效果大打折扣。
分段后,叶子结点和非叶子结点就放在了不同的段,也就是不同的区 - 是一种逻辑上的概念
- 对于一个索引:会分为两个段:叶子结点段和非叶子结点段
- 碎片区(fragment):碎片区中的页可以用作不同的目的,可以一部分属于段A,另外一部分属于段B
- 思考:为什么要分碎片区?
如果以完整的区(64页)分给一个段,当段数据太小时,会有很多浪费的情况 - 碎片区的使用:当不满32时,则使用;当一个段满32时,则新分配一个全新的区
- 段的结构图:INODE Entry
- 段结构的属性:
- Segment ID:唯一编号
- 3个 List Base Node:对应上文的段内三种状态的区链表:FREE 链表、 NOT_FULL 链表、 FULL 链表定义了
- Magic Numbe:标记这个 INODE Entry 是否已经被初始化了
- Fragment Array Entry:每个就标记一个零散的页面
链表基结点List Base Node
- 目的:为了寻找表空间中的各个链表的头结点而设立的
- 实际上定位的是一个地址。因为XDES Entry是一个记录;也可以定位到INODE页面, 是一个页面。
定位到链表头,然后再通过 XDES Entry自身的指针,遍历;通过INODE页面本身的指针,遍历 - 示意图:
- 表空间三个区状态的基结点在FSP_HDR中,段的三个区状态的其结点在INODE Entry中
3各个类型的页与位置
FSP_HDR
- 每个表空间只有一个这样的类型,记录表空间整体的一些信息
- 整体结构:
- 其他部分没啥好说的,重点是该类型页面特有的File Space Header:
- seg两个基节点是指向INODE类型的页面的,
XDES
除了没有File Space Header,其他和FSP_HDR一模一样。
IBUF_BITMAP
当修改时,当修改放到这个缓冲中。
当服务器空闲或者其他原因,对应的页面从磁盘上加载到内存中时,再将修改合并到对应的页面
INODE
- INODE页面就是专门用来存放INODE Entry的页面
- 整体结构:
- 结构属性:
- File Header, Empty Space, File Trailer是通用的上面已经说过了
- List Node for INODE pagelist:这里会形成链表。下面两个链表头(指向的是页面)就放在FSP_HDR的头部中
- SEG_INODES_FULL:表示页面已经满了
- SEG_INODES_FREE:表示INODE类型的页面还有空闲的空间放INODE Entry结构
Segment Header
- 在数据页(数据页就是一种页类型,类型为INDEX)中的Page Header部分,有两个属性,可以找到该数据页所属的段:
- PAGE_BTR_SEG_LEAF 和 PAGE_BTR_SEG_TOP是一种结构来着:
真正指示该页所属的 INODE Entry的地址 - 在索引中,一个索引只有两个段,所以这两个字段只有索引根结点才有,而且只用根结点存储就够了。
4系统表空间
1系统表空间的整体结构
可以看到,相对于独立表空间,仅仅是在第一组第一区中多了一些描述系统信息的页面。
其中最重要的是最后一个:SYS类型的,存储着数据字典头部信息的。
2InnoDB数据字典
- 元数据:为了更好好管理用户数据,而不得已引入额外的数据,这些数据称为元数据。就是描述用户数据的数据
- 内部系统表(internal system table):Innodb引擎记录元数据的系统表。
图中前四个称为 基本系统表,是表中之表 - 数据字典:系统表也称为数据字典
- information_schema系统数据库,就是对上述的系统表进行封装
- SYS_TABLE表:
两个索引:
以 NAME 列为主键的聚簇索引
以 ID 列建立的二级索引 - SYS_COLUMNS表:
一个索引:以 (TABLE_ID, POS) 列为主键的聚簇索引 - SYS_INDEXES表:
以 (TABLE_ID, ID) 列为主键的聚簇索引 - SYS_FIELDS
以 (INDEX_ID, POS) 列为主键的聚簇索引 - 上述四个表统一存储在系统表空间的第7号页面:页面类型为SYS
- 重要属性:
- Segment Header:存储对应的段信息。这些有关数据字典的信息当成一个段来分配存储空间,称之为数据字典段。
- Data Dictionary Header各个部分
- Max Row ID:是全局共享的**。就是innodb隐藏列的默认id(**如果没有主键)
- xxxxx_index:记录着上述四个"基本系统表"的索引页面,一共5个。也就是说,系统表空间7号页面并不是直接记录元数据,而是记录着元数据的索引位置
- xxxx ID:一种计数器,用于生成各类ID,包括行、表、索引、表空间的ID
-----------第三部分:连表-----------
十、单表访问方法
1基本概念
- mysql的查询语句是声明式语法,只是告诉服务器要获取的数据符合哪些规则
- 访问方法:把mysql执行查询语句的方式叫作访问方法或者访问类型 (对表的访问方式)
- MRR:多范围读取优化措施:先读取一部分的二级索引记录,将它们的主键值排好序后再统一执行回表,可以减少随机io
2访问方法
- const常量
- 通过主键或者唯一二级索引来定位一条记录的方法
- 如果unique键有多个null,则不能
- ref (单点)
- 搜索条件为二级索引列与常数进行等值比较,形成的扫描区间为单点扫描区间,采用二级索引来执行查询“的访问方法叫做ref
- 有null值不行,这样二级索引就变成了 多点扫描(null + 被索引的列)
- ref_or_null
- 比ref访问方法多了扫描一些值为null的二级索引记录
- 值为null的记录会被放在索引的最左边
- range
- 形成的扫描区间为 ”多个单点区间 或者 范围扫描区间“ 的访问方法称为range
- 一个单点区间不能称为range, (-∞,+∞)也不能用range
- (一般来说等值查找比范围查找需要的扫描记录少,即ref比range少)
- index
- 扫描全部二级索引记录的访问方法称为index
- 当全表扫描时,如果排序中添加了 ”ordery by 主键" , 则也是人为地称为index访问方法
- all
- 全表扫描
3索引合并
概念:把使用多个索引来完成一次查询的执行方法叫做索引合并(index merge)。mysql在一般情况下只会为单个索引生成扫描区间。
所以也是一种访问方法。
- Intersection
- 操作:从不同的索引中扫描到的记录的id值取交集,再回表
- 限制:每个索引获取到的二级索引记录都是按照主键值排序的
- 思考:为啥有这个限制?
- 从有序集合取交集简单
- 如果id值有序,可减少回表时的随机io(要从磁盘中把聚簇索引加入内存)
- 思考:有序集合取交集的步骤:
- 比较,相等留下
- 不相等的,比较小的丢掉
- Union
- 操作:同时使用多个索引来查询,然后根据二级索引记录的id去重合并,再回表
- 限制:每个索引获取到的二级索引记录都是按照主键值排序的
- 思考:为啥有这个限制?和上面差不多
- Sort-Union
- 操作:先将从各个索引中扫描记录的主键值进行排序,再按照执行Union索引合并的方式执行查询。
也就是说,比Union索引多了一步排序的步骤 - 限制:去除了 :每个索引获取到的二级索引记录都是按照主键值排序的
- Sort-Intersection
- mysql中没有
- 原因:因为Intersection本身就是在大量数据背景中回表中提出的,由于大数据的排序可能更花时间,所以没有。(P177成本可能比使用单个二级索引执行查询的成本都要高 ?
十一、连接的原理
1连接简介
- 连接的本质:把各个表的记录都取出来依次匹配,并把匹配后的组合发送给客户端
- 驱动表:第一个需要查询的表,称为驱动表
- 被驱动表:从驱动表发来的结果,再用来过滤。
- 图示:
从下面可以知道:通过单表查询对驱动表的“常数化”,再用“常数化”后的条件去被驱动表再单表查询。
所以本质是多个单表查询。
条件过滤
驱动表
被驱动表
常数 - 连接的过程:
- 从驱动表中查询,得出结果
- 再到被驱动表查询,得出组合
- 驱动表只需访问一次,而被驱动表需要多次(这里的一次是指整体查询,实际上还是一条一条记录进行的)
- 外连接:
- 对于外连接的两个表,即使驱动表中的记录在被驱动表中没有匹配的记录,仍然要加入结果集中
- 左外连接,则左侧为驱动表;右外连接,则右侧为驱动表
- 过滤条件:
- where:对于所有的查询,不论是内连接还是外连接,效果一样,不满足条件则过滤
- on**:对于内连接,on == where;**
对于外连接:即使 驱动表不满足ON条件,也要加入到结果集 (如 ON t1.c = t2.c,t2.c找不到,也要把t1.c加入到结果集)
- ON子句提出的场景:“外连接驱动表中的记录在被驱动表找不到匹配记录时,是否应该把该驱动表加入结果集中”这个场景下提出的
- 外连接必须要用ON来指明连接条件;内连接则不用(on where效果一样)
- 内连接的四种等价语法:对于内连接,驱动表和被驱动表位置可以互换,效果完全一样,等价的,效率不一定
SELECT * FROM t1 JOIN t2;
SELECT * FROM t1 INNER JOIN t2;
SELECT * FROM t1 CROSS JOIN t2;SELECT * FROM t1, t2;
2连接原理
嵌套循环连接
- 对于如三个表,将前两个表连接后的结果作为驱动表,第三个表作为驱动表,循环进行;多个表也一样
- 伪码:就是得到匹配的一条记录,会马上发给客户端
for each row in t1 { #此处表示遍历满足对t1单表查询结果集中的每一条记录
for each row in t2 { #此处表示对于某条t1表的记录来说,遍历满足对t2单表查询结果集中的每一条记录
for each row in t3 { #此处表示对于某条t1和t2表的记录组合来说,对t3表进行单表查询
if row satisfies join conditions, send to client
}
}
}
- 嵌套循环连接(nested-loop join):驱动表只访问一次,但被驱动表却可能访问多次,且访问次数取决于对驱动表执行单表查询后的结果集中有多少条记录。(这个”一次"不是说全部记录查出来后才进行被驱动表的查询,这个“一次”是一种整体的观念。实际上组合出结果,就马上发送给客户端,从上面的伪码也可以看出来)
使用索引加快连接速度
- 连接的本质是多个单表查询的组合。所以在各个单表中,可以应用自己较快的索引访问方法。
考虑的思路和单表一样。 - eq_ref访问方法:连接查询中,对被驱动表的主键或者不允许存储NULL值的唯一二级索引进行等值查找使用的访问方法称为eq_ref (是单点,但是有多个单点,因为有多次查询被驱动表的动作。单表中叫做const)
- 建议最好不用用“*”,而是把真正用到的列作为查询列。
原因:
- 减少回表
- 在连接中P188看不太懂?
- 在基于块的嵌套循环连接算法中,只会记录驱动表中查询列表中的列和过滤条件中的列,所以不用“*“,可以使得连接缓冲区容纳更多的驱动表结果集(下面“基于块的嵌套连接”中有说明)
基于块的嵌套连接
- 背景:如果每组合出一个驱动表记录,就要查询一次被驱动表,会造成大量的随机IO。所以要尽量减少被驱动表的访问次数
- Join Buffer(连接缓冲区):在执行连接查询时,先申请一块内存,用来放驱动表的若干个记录,然后再用被驱动表进行集中的匹配,减少了被驱动表加入内存的次数
- 加入了Join Buffer的嵌套循环连接算法称为基于块的嵌套循环连接(Bolck Nested-Loop Join)算法
- Join Buffer不会存放驱动表记录的所有列,只有查询列表中的列和过滤条件中的列都会被放到Join Buffer中。在基于块的嵌套循环连接算法中,只会记录驱动表中查询列表中的列和过滤条件中的列,所以不用“*“,可以使得连接缓冲区容纳更多的驱动表结果集
十二、基于成本的优化
1基本概念与理解
- 执行成本分为两种:
- IO成本:从磁盘加载到内存的时间称为IO成本
- CPU成本:读取记录以及检测记录等损耗的时间称为CPU成本
- 成本常数:人为规定的,默认值
- 读取一个页面花费的成本默认是1.0
- 读取以及检测一条记录是否符合搜索条件的成本默认是0.2
2基于成本的优化步骤与方法
根据搜索条件,找出所有可能使用的索引
- 把一个查询中可能使用的索引称为possible key
计算全表查询的代价
- 要计算聚簇索引的代价,要需要两个信息:总页面数 及 记录数
- 查看表状态信息:
- Rows:行数
- Data_length:表占用存储字节数
- 行数就是记录数,是一个估计值;Data_lentgh / 16 / 1024 = 16384 / 16 / 1024 = 1页(上面那个表);
- 计算成本 (要加上微调值,这里不列出)
- IO成本:1 * 1.0 (1页)
- CPU成本: 18 * 0.2 (18条记录)
- 加起来就是总成本
计算其他索引成本
- 成本来源:(当然也要加微调值)
- 两次IO(扫描区间数量 + 预估的二级索引条数(有多少条,就要多少次回表,就要载入多少聚聚簇索引页))
- 两次CPU(读取二级索引 + 回表后读取聚簇索引叶子)
- 由于回表的过程是常量级的访问方法Const, 故成本不计
- 各个成本的计算:
- 扫描区间的数量:mysql规定一个扫描区间所用的成本相当于从磁盘中加载一个页,即1.0。有n个扫描区间,就有n * 1.0 的成本
- 预估的二级索引条数(即需要回表的记录数):(一个区间中,就是计算形成区间中的记录数)
- 有两种方法:index dive和 统计数据(只用于单点扫描)
- index dive:先获取区间的最左记录和最右记录,然后统计中间的记录条数
- 中间记录条数的计算方法如下:
- 如果距离较远:则从最低的的id开始,向右读10个页面,求平均值,来算出两个区间边界有多少条记录
- 如果距离较近:可以求出精确值。(两个页面之间的页面数,可以通过上一级的索引来计算出)
- 读取二级索引:就是回表的记录数 * 0.2
- 回表后读取聚簇索引叶子:就是回表的记录数 * 0.2
比较选取
- 比较全表扫描、其他各个索引的成本,选出最低的成本进行
3基本索引统计数据的成本计算
- 场景:如果有很多的 IN(a, b, c,…),形成非常多的单点区间,要进行很多index dive,性能损耗大
- 应用背景:用来计算二级索引中,预估的二级索引条数。(只能用于单点扫描)
- index dive方法:通过直接访问索引对应的B+树来计算某个扫描区间内对应的索引记录条数的方式称为index dive (先获取区间的最左记录和最右记录,然后统计中间的记录条数)
- mysql会为每一个索引维护一些额外的信息:
- Cardinality:记录基数,即记录中不重复的个数,是一个估计值(如果是1,说明记录全部是一样的)
- 结合上面的Rows属性,就可以预估出单点扫描区间记录数:
Rows / Cardinality = 记录重复数(平均值) - 参数调节:IN(1,2,3)的个数,可以通过range_index_dive_limit来限制。超过用统计数据,不超过用index dive
4连接查询的成本
- 计算公式:连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出值 * 单次访问被驱动表的成本
- 扇出:
- 把查询驱动表后得到的记录条数称为驱动表的扇出
- 条件过滤condition filtering:就是计算扇出的过程中,有一些搜索条件的满足比例只能估计出来。这样的猜测过程就叫做条件过滤。(总之就是一种估计方法,最终估计出来的扇出会用于成本计算)
- 内外连接成本:
- 外连接:由于在特定场景下,两个表的顺序是固定的,因此分别算两个表的成本就好了
- 内连接:由于交换两个表的最终效果是一样的,所以表是可以交换的。但时间效率不一定一样,所以要分别计算交换、不交换两个表的位置下的成本,取小的那个。
- 连接优化思路:减少驱动表的扇出;访问被驱动表的成本要低(看公式)(因为占大头的是“驱动表扇出值 * 单次访问被驱动表的成本”)
- 多表连接分析:
- 要在效果一样的情况下,交换各个表的顺序,比较成本,一共 n! 种
- 有一定的规则,去除一些不必要的表顺序
- 提前结束评估:维护一个全局最小值,如分析ABC的过程,AB连表已经大于最小值了,也就没必要继续分析连C了
- 系统变量optimizer_search_depth:限制分析连接表的个数
- 一些启发式规则下的顺序,不考虑
5调节成本参数
- 一个查询分为server层 和 存储引擎层,mysql数据库中的两个表维护着这两个层一些成本常量
- mysql.server_cost表:
- row_evaluate_cost:就是读取并检测一条记录的时间成本,0.2
- cost_value为null代表是默认
- 修改:直接按照update语句对下面的表进行更新并
flush optimizer_costs
- mysql.engine_cost表:
- io_block_read_cost:在innodb中一块就是一页(以块(页)为单位与磁盘交互),默认成本为1.0
- device_type:表示固态硬盘还是机械硬盘。所以成本分析在设备改变时,变化参数就行。
- engine_name:default表示所有引擎都适用
- 可以插入针对某个存储引擎的成本常数并
flush optimizer_costs
十三、InnoDB统计数据的收集
1统计数据的存储方式
- 有两种
- 永久性地存储统计数据:放在磁盘上,服务器重启时加载
- 非永久性:服务器关闭时会被清除,在适当的场景下重新统计数据
- InnoDB是以表为单位来收集和存储数据的,所以可以给每个表设定不同的存储方式
create table <table-name> (...) engine=innodb , stats_persistent = (1|0)
1, 0就是永久和非永久
2基于磁盘的永久性统计数据
- 这种方式的数据是放在表中的,就是永久性存储啦:一共有两种统计表(innodb中)
- 两种状态语句我觉得实际上就是对这两个表进行查询封装,外加一些信息。:
innodb_table_stats
- 查看:
- 表的主键:(database_name, table_name), 也就是一条记录就是关于一个表
- 表各类信息:
- n_rows的计算方法:
从聚簇索引中选取几个叶子页面,统计记录的数量,然后计算一个页面中平均包含的记录,乘以全部的叶子节点的数量(统计值) - 索引占用的页面数量的计算方法:
- 在根页面中找到 page_btr_seg_leaf 和 page_btr_seg_top
- 然后这两个结构中的Segment Header指示各自所属的段INODE Entry
- INODE Entry中有三种区状态的链表头LIST Base Node,从其中的List length中取出求和即可
- 注意:因为链表中的一些页面可能没有使用,所以实际页面数据的比计算出来的要小,这样也会影响到n_rows的计算,因为它的计算也要用到叶子页面的数量
innodb_index_stats
- 查看:
- 主键:(database_name, table_name, index_name, stat_name):一条记录代表着一个索引一种状态的统计项值
- 重要属性:
- n_diff_pfxNN:代表一个列中不重复的值,也就是基数Cardinity
3定期与手动更新统计数据
- 系统变量:innodb_stats_auto_recalc
on表示自动更新,一旦发生变动的记录数量超过表大小的10%,那么服务器会重新计算一次统计数据 - 自动更新是异步的,结果会有延迟
- 可以给每个表都指定是否要自动更新
- 手动更新 :
analyze table <table-name>
会立即更新该表的统计数据,是同步的 - 手动更新 innodb_table_stats和innodb_index_stats统计表的内容也是可以的,要刷新一下表。
4基于内存的非永久统计数据
不怎么用了。
5null重复列的解决
系统变量:innodb_stats_method:会影响一个表的记录基数Cardirity,会影响到“一条记录的平均重复次数”的计算,进而影响成本计算中的“基于索引统计数据的成本计算” 和 连表的成本计算(连表成本这里?)。
- nulls_equals:认为所有的null值是相等的 (是mysql的默认值)
- nulls_unequals:所有的null值是不相等的
- nulls_ignored:直接把null值忽略掉
十四、子查询优化
1条件化简
- 去除不必要的括号
- 常量传递:有一些变量是可以用常量来代替的如 where a = 5 and b < a;
- 移除没用的条件:有一些明显为true或false的条件,会直接用true或false代替
- 表达式计算:有一些可以直接计算出结果的表达式,在执行前就先算出来
- Having和where合并:在没有聚合函数、group by的情况下,having和where的效果一样
- 常量表检测(看不懂)P222
2外连接消除
- 为什么要消除外连接 ?
- 外连接中,被驱动表和驱动表的顺序是固定的,无法优化
- 而转化成内连接后,查询优化器可以考虑表的顺序,选择更加优的方案
- 什么时候外连接可以转化成内连接?
- 外连接的本质:当匹配不到时,保留驱动表,设被驱动表的记录为null
- 时机:当被驱动表被显式地说明不是null时(如 table2.c1 is not null), 或者隐式地被说明不是null时(如 table2.c1 = 3)
- 空值拒绝:指定的where子句中包含被驱动表中的列不为null的条件称为空值拒绝
- 当满足空值拒绝时,外连接和内连接可以互相转化。
- 优化器会把右连接转化为左连接来对待。如 table1 left join table2 => table2 right join table1, 这不是一模一样的吗嘻嘻
3子查询语法
- 在from后面的子查询叫做派生表
- 按返回结果集区分子查询:
- 标量子查询:返回一个单一的值
- 行子查询:只有一行,但是有多列 (即一个对象,很多属性)
- 列子查询:只有一列,但是有多行(即一个属性,但是有很多值)
- 按与外层查询关系分类:
- 不相关子查询:子查询可以单独运行出结果
- 相关子查询:子查询的执行依赖外层查询的值
- ANY 和 SOME 的意思一样
4子查询的运行原理
和《数据库系统概念》中描述得一样
- 不相关子查询:
分成独立的部分,各自执行 - 相关子查询:
外层执行获取值,放到内层再执行。循环。(就是嵌套循环)
5IN子查询优化
物化表
- 背景:在in子查询中,如果一次性全部查询出来,可能结果集太多,内存放不下
- 物化:在不相关子查询中,将子查询的结果放在临时表中,称为物化
- 物化表:那个临时表叫做物化表
- 物化表特征:
- 记录就是子查询的记录
- 去重的:为记录的每个列建立 联合主键
- 位置:在内存中,有哈希;在磁盘上,有B+索引
物化表转连接
- 在不相关子查询进行物化后,就可以把 IN 转化成 物化表与外层表 的 内连接 了
- 转化成内连接后,可以考虑顺序问题,得到不同的方案,选择最优的
IN优化–半连接
- IN子查询直接转化为内连接的问题:如果IN子查询里面有多个重复的值,会导致转化成内连接后,驱动表的多个相同的值重复加入结果集。
本质:含义的不同。IN的含义是:只关心存在不存在; 内连接的含义:有没有,有多少个 - 半连接(semi-join):对于s1中的表的记录来说,只关心有没有与s2表的记录匹配,而不关心有多少条记录匹配。(没有提供语法支持,用户无法书写)
转化例子:
转化前:
select *
from s1
where key1 IN (select common_field from s2 where key3 = 'a');
转化后:(错)
select s1.*
from s1 semi join s2
where s2.key3 = 'a'
and s1.key1 = s2.common_field;
转化后:(对)
select s1.*
from s1 semi join s2
ON s1.key1 = s2.common_field
where s2.key3 = 'a';
- 实现半连接的方法:(核心思想:去除子查询中的重复带来影响)
- Table pullout(子查询表上拉):当子查询中被查询的列是主键或者唯一二级索引时,此时子查询不会有重复的,可以直接把子查询转化成内连接 (含义一样了嘛!)
- duplicate weedout(重复值消除):当s1(外层)的表的记录要添加入结果集时,建立一个临时表保存s1的记录;在s1要加入时,看一下临时表中有没有,有就直接丢掉。
- LooseScan(松散扫描):当子查询的表中有多个重复的索引时,只用最前面的一个(子查询的表作为驱动表)
- Semi-join Materialization(半连接物化):和上面的“物化转连接”一样,就是先物化去重,再直接内连接 (必须是不相关子查询)
- FirstMatch(首次匹配):在相关子查询中,取一条外层 记录,看子查询里面有没有(即最原始的嵌套循环)
- 半连接的条件:(不太懂)P238
- 当不满足半连接的条件:
- 先物化,再查询 (这个时候不能连接)
- 转化成EXISTS子句
- 所有的in都可以转化成exists子句
- 由于in的结果可能为null, true, false, exists子句的结果是true或false;如果IN放在where和ON里面,则没有区别,因为where和on 对null 和 false是相同对待的,只有true才不过滤
- 思考:为啥要转化?如果子查询本身就可以用到索引呢(我猜是对比选择最优)?
转化后,很可能exists里面就可以用到子查询表的索引(被查询的列本身是一个索引列,将外层的条件放在里面后,子查询被查询的列就可以用索引来搜索)
6其他子查询的优化
- ANY/ALL子查询的优化:转化成最大最小值
- [NOT] EXISTS子查询执行
- 不相关:直接将 exists里面的子查询替换成true或者false
- 相关:只能嵌套循环,但是exists里面可能可以用到索引 (in转成exists就是这个道理)
- 派生表:尝试把派生表和外层查询合并,这样可以省去创建和访问临时表的时间。
十五、EXPLAIN详解
1 基本概念与理解
- 作用:用来查询某个查询语句的具体执行计划
- 语法:
- 信息一览:
2各列详解
table
- 说明该条记录是描述哪个表的,即对应单表访问
- 每一张表都是对应一条记录
id
- 每个select会分配唯一的id
- 如果两个表进行了连接,则这两个table对应的select的id是一样的。在记录前面的表示驱动表,在后面的表示被被驱动表
- 查询语句中如果有n个select,那么不一定有n个id。原因是查询优化器会对子查询等进行合并等操作
- 在UNION子句中,explain的记录是这样的: id=null, select_type=union result, table =<union1, 2>
- id为null表示使用了临时表
- table=<union1,2>说明连接了id为1和2对应的表
- 如果是union all, 则没有这条关于临时表的记录
select_type
- 说明这个表所在的小查询在大查询中扮演什么样的角色
- simple:没有union 和 子查询的
- primary:大查询最左边的部分
- union:union/union all 中,除了最左边的其他
- union result:使用临时表的union
- SUBQUERY:
- 不相关子查询
- 不能转为半连接
- 物化方式执行
- 只执行一次
- DEPENDENT SUBQUERY
- 相关子查询
- 不能转为半连接
- 可能执行多次
- DEPENDENT UNION:和外层相关的union的其他部分(即除了最左边)
(疑问:但是这里怎么算是相关 ? )
注意t2表,是DEPENDENT SUBQUERY类型 - DERIVED
- 以物化的方式执行派生表 (from子句中)
- table列显示成 代表物化后的表P251
- MATERIALIZED
- 将子查询物化后,与外层连接进行连接
type
- 表明访问方法
- system:表只有一条记录并使用的存储引擎的统计数据是精确的
innodb下: - const:主键或者唯一二级索引
- eq_ref:连接查询时,被驱动表通过主键或者唯一二级索引(不为null)访问(等值访问)
- ref:普通的二级索引与常量进行匹配 (连接中的被驱动表也可以)(等值访问)
- fulltext
- ref_or_null:通过普通二级索引,并且列可以为null
- index_merge:索引合并
- unique_subquery:
- 只有用在“IN转为EXISTS”的情况
- 转换后,子查询如果用的是eq_ref的访问方法
- index_subquery:和unique_subquery类似,只不过是普通的二级索引(即ref)
- range:多个单值;或者范围
- index:当使用索引覆盖(或者全表扫描时,需要对主键排序)
- all:全表扫描nni
possible_keys / key
- possible_keys:可能用到的key
- key:实际用到的
- possible_keys越多,查询优化时间越长
key_len
- 使用的索引列的最长长度
- 场景:当使用联合主键时,需要区分用到哪些主键(因为key列只会显示联合索引的名称,而不显示具体使用哪几个列)
ref
- 说明等值匹配时,谁与索引列进行匹配
- 如 where key1 = ‘a’, 则 ref 显示const (常量)
- 又如 where key1 = t1.c1; 则 ref显示 .t1.c1
rows
- 使用all执行计划时,该表的估计行数
- 使用二级索引执行计划时,预计扫描的索引记录行数
filtered
- 条件过滤的比例
Extra
- 用来说明一些额外信息
- no table used:没有表使用
- impossible where:where条件永远为false时
- using index:使用索引覆盖时
- using index condition:使用了索引下推
- using where:某个搜索条件需要在server层进行判断 (重点理解P262
- using join buffer :使用基于块的嵌套循环
- using intersect(…) using union(…) using sort_union(…) 说明哪个列进行索引合并
- using filesort:使用文件排序(在内存中排序或者在磁盘中)
- using temporary:使用临时表
- start/end temporary:半连接的执行策略为 Duplicate,( start指示半连接后的驱动表,end指示被驱动表
- LooseScan
- FirstMatch(.)
3JSON格式
- 语法:
- 成本:
"cost_info": {
"read_cost": "1.00",
"eval_cost": "0.20",
"prefix_cost": "2.40", -- 整个单表查询成本各,如果是被驱动表,则是多次连接查询成本和
"data_read_per_join": "1K" --这次查询中需要读取的数据量
},
4Extented EXPLAIN
- 展示查询执行计划的扩展信息
- 语法:
十六、optimizer trace
是一个追踪查询优化过程的工具
十七、InnoDB的Buffer Poll
1基本概念与理解
- 缓冲池:在服务器启动的时候,向操作系统申请的一片连续的空间
- 相关的系统变量:
innodb_buffer_poll_size 大小
innodb_old_bolck_pct 分区的LRU的old区比例
- 查看状态信息:show engine innodb status\G
2内部结构
- 缓冲区被划分为若干个页面,大小和表空间中的页默认大小一样16KB
- 两个组成部分:控制块 和 缓冲页
- 控制块:
- 每个缓冲页对应一个控制块,存放着控制相关信息(大小、位置等)
- 分配空间时,不包含在innodb_buffer_poll_size中
- free链表:
- 把所有的空闲的缓冲页的控制块存放一个链表中(注意,链表的节点是控制块)
- free链表的基节点不包含在“那片连续的空间”中, 是额外的空间
- 结构图:
- flush链表:
- 当缓冲页中的数据被修改时,状态就和磁盘上的不一致了,称之为脏页
- flush链表是存储脏页用的,结构和free链表一样
- 思考:如何快速找到缓冲页?
- 用哈希:表空间+页号为key, 控制块地址为value。找到控制块后,再找对应的缓冲页
3LRU链表
- 简单LRU链表带来的问题:
- 预读加载到缓冲池的页不一定用到
- 如果有非常多的频率不是很高的页加入内存,会把频率高的冲掉
- LRU优化的核心:提高缓冲池的命中率(上述出现的问题,最后会导致命中率低)
- 分区:
- 按比例,将LRU链表分为old(冷)和young(热)区
- young的前1/4,是“非常热区”
- 当old访问时,会移到young头;当young后3/4访问时,会移到young头
- innodb_old_bolck_pct 分区的LRU的old区比例
- 预读加载到缓冲池的页不一定用到:放在old区就好了
- 如果有非常多的频率不是很高的页加入内存,会把频率高的冲掉:设置一个时间间隔,如果时间间隔内多次访问,则不移到young区(全表扫描时,同一个页面会多次读取记录)
- 其他链表:。。。
4刷新页面
- 刷新脏页是后台另外开一个线程的
- BUF_FLUSH_LRU:从LRU尾部开始找脏页(冷区)
- BUF_FLUSH_LIST:从flush链表中刷新
- BUF_FLUSH_SINGLE_PAGE:单个页面刷新(当没有可用的缓冲页时)
5大小与组成
- 在多线程环境下,访问缓冲池的各种链表都要加锁
- 当Buffer Pool特别大时,可以拆分成多个小的缓冲池,称为实例
- 各个实例间是独立申请空间、独立处理的,可以提高并发能力
- 一个实例,是由多个chunk组成的。在服务器运行期间,可以通过调整一个实例中的chunk的数量,来调整实例的大小(可以不用向操作系统申请大的连续空间)(每个实例的chunk是相同的)
----------- 第四部分:事务 -----------
十八、事务简介
本章介绍的东西绝大部分和《数据库系统概念》中的一样~这里摘取出关键点
- 现实世界的业务场景需要映射到数据库世界
- 从现实意义上来对一致性的理解:数据库是现实世界的一个映射,现实世界中存在的约束当然也要在数据库中有所体现。如果数据库中的数据全部符合现实世界中的约束,我们就说这些数据是一致的,或者说符合一致性的(在上文中,也有一段关于一致性的理解,从数据正确性的层面上说的)
- mysql不支持check子句来实现一致性,因为它根本没有实现check
- 原子性和隔离性都是保证一致性的一种手段
- 当事务处于“中止”或者“提交”,一个事务的周期才算真正完成
- 开启事务:
BEGIN [WORK]
START TRANSACTION (可以带修饰符,如规定事务只读、读写)
set autocommit = OFF;(自动地把几条语句当成一个事务)
- Mysql中,只有InnoDB和NDB支持事务。如果对使用了不支持事务的存储引擎的表进行修改,将无法回滚
- 隐式提交:当在事务中,输入了某些语句,会导致前面的事务悄悄地被提交(commit)
- 在事务中使用了DDL语言
- 事务控制语句:当又使用 begin或者start transcation时,相当于将之前的事务提交,并开启一个新事务
- 加载数据的语句
- 关于mysql复制的语句
- 其他语句
- 保存点:
- 在事务中生成保存点:
savepoint <保存点名称>
- 回滚事务至保存点:
roolback [work] to [savepoint] <保存点名称>
- 删除保存点:
release savepoint<保存点名称>
十九、redo日志
1基本概念与理解
- 为啥需要redo日志 ?
- 防止系统崩溃
- 不能同步将buffer pool中的页更新到磁盘(效率慢)
- 相对于在事务提交时,将所有修改过的页面刷新到磁盘中,只将redo日志刷新到磁盘的好处:
- redo日志的大小很小
- redo日志是顺序写入的,可以使用顺序IO
2redo日志格式
通用格式
- type :该条 redo 日志的类型。(5.7.21 有53种)
- space ID :表空间ID。
- page number :页号
- data :该条 redo 日志的具体内容。
简单格式
- 场景:如修改表空间的一个全局常量,不会引起其他值的变化,如MAX Row ID
复杂格式
(举例而已)
- 一些复杂的更新,会带来一系列的牵连更新,如当插入一条记录时,可能更新槽信息、header中的各种统计信息、等等
- 这种类型的页面有两个方面的特点:
- 物理层面上:指明了对哪个表空间的哪个页进行修改
- 逻辑层面上:并不会将所有的牵连更新都写入redo日志,而将那些更新时必要的数据保存起来,在恢复时,当成参数传入系统函数,那些牵连更新的值自然会被更新。
3Mini-Transaction:保证原子性
- 背景:在插入时:
- 乐观插入:当插入一条记录时,页空间充足,直接插入就好了,产生一条MLOG_COMP_REC_INSERT类型的redo日志
- 悲观插入:页空间不足,要进行页的分裂、记录的复制等操作,产生很多的redo日志
如果在悲观插入的过程中,只记录了一部分的redo日志,那么在恢复时,会将B+树恢复成一个不正确的状态。
- 以组形式保存redo日志:
- 在执行一些要保证原子性操作时,要用组的形式来记录redo日志
- 在进行恢复时,要么把组内全部的日志都恢复,要么都不恢复(看起来redo日志也具有一定的原子性)
- 动作实现:
- 如果一组中有多条日志,那么会在最后插入一个MULTI_REC_END类型的结尾日志,标志结束
- 如果只有一条日志,那么type的第1个比特为1
- MTR概念:把对底层页面进行一次原子访问的过程称为一个Mini-Transaction(即是日志层面上的迷你事务)
4redo日志块与写入
- redo日志块:MTR生成的redo日志都放在了大小为512字节的页中,称为redo log block(redo日志块)
- redo日志缓冲区:和buffer pool的作用一样,只不过它是用来保存日志块的
- MTR产生的日志,首先暂存到一个地方,等到MTR结束的时候,再统一将组内redo日志写入log buffer中
- buf_free是指示日志缓冲块中,空闲的地方:
5redo日志文件
- 日志刷盘的时机:
- log buffer空间不足时:当log buffer的容量到达50%左右,就需要刷盘
- 事务提交时
- 某个脏页刷新到磁盘前
- 后台线程,定时
- 关闭服务器时
- checkpoint时
- 日志文件组
- 默认日志文件在数据目录下
- redo文件不止一个,他们组成日志文件组
- 日志组总大小 = 日志组内文件数量 * 一个日志的大小
- 当空间用满时会进行重写覆盖
- 日志文件格式
- 本质就是将日志缓冲区中的log block进行一个映射
- 前4个block用来存放管理信息,其他和普通的block一样
- 前4个block示意图:(前2048B)
checkpoint只有日志文件组的第一个文件有
6LSN
- LSN(log sequence number):是一个全局变量,用来记录当前总共已经写入redo日志的量(指示大小的)
- 初始值为8704。因此和日志文件偏移量的对应关系为:2048-8704, 之后一一对应
- 计算大小的时候,是block的头和尾也是计算进去,就是一整个block啦
- 每一组由MTR生成的redo日志都有唯一的lsn值与其对应,越早产生的越小。也就是说,一组MTR一个LSN号码
- 指针:
- buf_next_to_write:指示当前日志缓冲区中,已经被刷新到磁盘中的位置
- buff_free:空闲位置。如果和buf_next_to_write相同,说明缓冲区中全部的日志已经刷新
- flushed_to_disk_lsn:指示已经被刷新到磁盘中的lsn号
- 两者关系:前者指示地址,后者指示lsn号。其实指向的是“同一个位置” (错,LSN号实际上指的是MTR的号码数)
- flush链表中的lsn(checkpoint的时候用得到)
- flush的结点中,有两个属性:
oldest_modification:第一次修改页面缓冲区时,修改该页面的MTR对应的lsn值
newest_modification:之后每次有MTR修改该页面,则更新该lsn值 - flush前面的脏页第一次修改的时间较晚
- 动作:第一次修改页面时,加入flush链表,并更新oldest_modification值;之后修改,直接更新newest_modification
7Checkpoint
redo被刷新到磁盘,不能确定脏页被刷新到磁盘。
checkpoint就是保证,checkpoint之前,脏页一定被刷新到磁盘。
- 思考:为啥要checkpoint?
- 从物理的角度:日志文件空间是有限的,需要checkpoint,来指示可以覆盖的日志范围
- 从逻辑的角度:系统崩溃时,检查点之前的redo日志就不用考虑了,节省了系统恢复时间
- 全局变量:checkpoint_lsn:表示当前系统中,可以被覆盖的redo日志总量为多少
- 一次checkpoint的步骤
- 内存中:将最早的flush链表结点中的oldest_modification赋值给checkpoint_lsn。
原因:这个是最早修改脏页时的lsn,说明前面的脏页全部已经刷新到磁盘了。由于lsn是顺序增长的,也说明前面的lsn也被刷新到磁盘了 - 磁盘上:更新管理信息。将checkpoint_lsn, checkpoint_no, checkout_offset更新到管理信息块中
- checkpoint_no:表示系统中已经做了多少次checkpoint
- checkpoint_lsn:lsn号
- checkout_offset在文件中的偏移量
- 日志文件中的示意图:
注意:下面三个指针都是在内存中的,这里用文件的形式将其对应起来
8相关系统变量
- show engine innodb status 可以查看各个lsn值
- innodb_flush_log_at_trx_commit:可以控制redo日志与事务提交的刷新时机把控
- 为0:事务提交后,redo不立即刷新
- 为1:事务提交前,redo必须刷新
- 为2:事务提交时,将redo刷新到系统的文件缓冲区,由操作系统控制
9崩溃恢复
- 恢复的起点:日志文件中lsn值为check——lsn的日志开始
- 终点:第一个日志块大小不满512为止 (思考?如果全uuh部满了呢?
- 恢复优化:
- 哈希表,根据redo日志的space ID和page number计算出哈希值,将对同一个页面修改的redo日志放在一起。这样对同一个页面进行修改的操作可以一次性进行,不用重复从磁盘中读取页面
- FIL_PAGE_LSN:在页面头中,有一个记录修改该页面时,最晚的lsn号码。如果当前日志的lsn号码小于该号码,就直接跳过该页面。(因为脏页刷新和redo刷新是不同步的(注意 事务提交和redo才有同步问题),脏页刷新后对应的的lsn如果更大,说明当前日志已经对这个页面已经产生过作用了,不用再再执行)
二十、undo日志
1基本概念与理解
- undo应用场景:当一个事务完成到一半时,需要回滚
2事务id
- 分配事务id的时机:
- 当事务对表中的记录(包括临时表)进行增删改操作时,才会被分配一个唯一的id
- 事务id的生成:有一个全局变量Max Trx ID, 其更新方式和Max Row 采用的策略一样。
- 行格式中事务相关隐藏列:(trx是事务的英文读音缩写)
- trx_id:在innoDb中的行格式中。指示对当前记录更改的最近的事务的id
- roll_pointer:指示修改当前记录的undo日志地址
3undo日志格式
INSERT操作
- 当插入时,其反操作(回滚,undo)就是删除。所以只需要要记录插入的主键就行了,回滚时直接根据主键删除记录
- 格式:TRX_UNDO_INSERT_REC
- 注意:在插入时,二级索引实际上也是要插入的。但在记录undo日志的时候,只记录主键信息即可。在回滚时,删除主键索引的同时也会自动同步删除二级索引。delete和update操作也是一样的
DELETE操作
- delete语句的删除过程:
- 阶段1 delete mark:在这个阶段,仅仅将记录的deleted_flag标志位置为1。(trx_id, roll_pointer的值也要修改的)
- 阶段2 purge:在事务提交后,由专门的线程将记录移动到垃圾链表,并更新一系列的相关值,如可用空间大小等
- 垃圾链表中重用空间的使用问题:
首先明白 PAGE_FREE是垃圾链表,PAGE_GARBAGE是可重用存储空间占用的总字节数。
- 如果新插入的记录小于垃圾链表头,则直接从垃圾链表中分配空间
- 如果大于,则从其他剩余空间中申请空间。
- 当页面趋于满时(剩余空间不够),会根据垃圾链表头和剩余空间计算总和,判断是否要进行页分裂。
- delete操作中,undo只需要记录阶段一。因为阶段二是在事务提交之后的了,所以并不需要回滚。
- 格式:TRX_UNDO_DEL_MARK_REC
不懂:为啥要单独记录索引的删除信息 ?因为delete操作中,undo只需要记录阶段一 - 版本链:上面格式中,有一个roll_pointer记录上一个事务的undo记录:
可以看到,从行格式的隐藏列,到最新记录,到上一次记录,形成一个版本链
UPDATE操作
不更新主键
- 两种更新:
- 就地更新:如果更新后的记录与更新前占用的存储大小一样
- 如果存储大小不一致,则先删除(真正地删除,直接把记录移动垃圾链表),再根据更新的值创建新记录并插入
- undo日志类型:TRX_UNDO_UPD_EXIST_REC
- n_updated:记录被更新的列数,后边跟着<pos, old_len, old_value>
更新主键的情况
- 两个步骤:
- 将旧记录进行 delete mark
- 根据值创建新记录,插入到聚簇索引中
- 产生的undo日志:
- 一条是TRX_UNDO_DEL_MARK_REC
- 另外一条是 TRX_UNDO_INSERT_REC
4undo类型的页面
- 类型为FIL_PAGE_UNDO_LOG类型的页面,是用来专门存放undo日志的页面
- 页面结构如下:
- Undo Page Header结构如下:
- type:表示这个页面的存储undo日志的类型。(只能是两个大类,思考:为啥分类:TRX_UNDO_INSERT在事务提交后可以直接删除,但其他类型的undo日志MVCC服务要用)
- TRX_UNDO_INSERT:insert语句,或者更新主键的update产生的日志,即TRX_UNDO_INSERT_REC
- TRX_UNDO_UPDATE:update语句、delete语句产生的日志,即TRX_UNDO_DEL_MARK_REC,TRX_UNDO_UPD_EXIST_REC
- start 和 Free,共同组成日志记录的边界
- node:即 LIST BASE NODE,与其他页面形成链表(错,不是LIST BASE NODE,只是普通的链表节点,即去除 LIST BASE NODE中的 length部分
- UNDO页面链表:
链表的第一个页面first undo page, 会有一些管理信息在头部 - 一个事务最多可以产生四种类型的链表 :一个页面有两个类型, 对临时表和正常表区分,即2 * 2 = 4
- 四种链表的分配时机:按需分配,在用到时才分配
5firstUndoPage页面结构
- 每一个undo页面都对应一个段,称为 undo log segment
- 段信息存放在 first undo page 的 Undo log segment header部分
- undo log segment header:
- state:标识当面页面链处于什么状态,有活跃,被缓存,等待被释放等
- log:本页面链表中最后一个undo log header位置
- fseg_header:即 Segment Header, 可以找到INODE Entry, 找到本链表对应的段
- list:基结点,即LIST BASE NODE
- undo log header:
- 同一个事务向一个undo页面链表中写入的undo算是一个组
- 一个页面中可以有很多组(页面重用时)
- 存储组的描述信息的地方称为 undo log header (一组一个,所以一个页面内可以有很多个这样的header)
- 结构如下
- 除了first undo page会在写入undo日志前填充undo page header, undo log segment header, undo log header, 其他页面只会填充undo page header。
其他页面没有组信息头undo log header,是因为只有链表只有一个页面时,才可以被重用。所以其他页面一定没有两个以上的组的。
6重用undo页面
- 如果一个事务就要申请自己独有undo页面链表,那么效率低。因此可以在一些情况下重用
- 满足下面两个条件,该undo页面链表就可以重用 (必须在前一个事务提交后)
- 该链表只有一个undo页面
- 该undo页面已经使用的空间不超过 3/4(太小可以用的,也就没有重用的必要了)
- 两种类型的链表的重用策略
- insert undo链表:由于只存储TRX_UNDO_INSERT_REC类型的日志,在事务提交后,就没有用了,因此可以直接把前面一组的undo日志覆盖
- update undo链表:由于MVCC服务要继续用,所以不能覆盖,只能继续写
7回滚段
理解作用:回滚段就是将各个事务的链表统一起来管理。通过集中链表头的方式来。
回滚段本身,由系统表空间的第5号页面进行管理。
基本结构
- 类型为Rollback Segment Header的页面,称为回滚段页面
- 每一个这样的页面都对应一个段
- 结构:
- fseg_header:即存放Segment header的地方
- slots:存放各个first undo page(即链表头)头的地方,一共有1024个slot
slot释放与分配
- 当一个事务被提交时,其slot的变化:
- 如果不满足重用条件,则inert undo链表直接释放;update undo链表要加入到Histroy链表中,才能释放
- 如果满足重用条件,则将两个大类的undo链表分别放到insert undo cached和 update undo cached链表中 (一个回滚段会对应着这两个链表)
- 事务在回滚段中分配slot的策略:
- 首先到该回滚段对应的两个重用缓冲链表中查看,如果有可以重用的,就直接用
- 如果没有,遍历每个slot,直接有空闲的slot(FIL_NULL)
多个回滚段
- 在系统表空间的第5号页面,存放着128个回滚段的地址:
- 回滚段可以在不同的表空间
- 结构图:
- 回滚段的分类:(数量和表空间位置可以在配置文件中修改)(根据回滚段的位置分类)
- 第一类:0号(必须在表空间)、33~127:普通表的undo记录
- 第二类:1~32号:这些回滚段必须在临时表空间中
- 思考:为啥要针对普通表和临时表来划分不同的回滚段?
每个undo日志在记录到undo页面的时候,其本质也是对一个页面进行改写,会产生对应的redo日志。
临时表在服务器关闭后就没用了,所以不用redo日志。即第二类回滚段中的undo日志,不用生成对应的redo日志。 - roll_pointer组成:
作用:根据roll_pointer, 可以很快定位到一条具体的undo日志
(思考:怎么定位?通过rseg id到系统表空间5号页面找到对应的回滚段地址,这个时候可以得到表空间(因为系统表空间的第5号页面结构中带有表空间)。再通过表空间+page number,定位到一个具体的页。再通过offset定位到一条undo日志的地址)
- is_insert:标志两个大类
- rseg id :回滚段编号,0~127
- page number:undo日志所在的页号
- offset:undo日志在页面中的偏移量
undo的崩溃恢复作用
**一:**首先,通过redo日志,将全部操作恢复。(要明白,undo页面的操作也会产生相应的redo日志)
二:通过undo日志,将执行到一半的事务,全部回滚,步骤如下:
- 首先通过系统表空间5号页,遍历每一个回滚段中的每一个slot
- 如果存在值不为FIL_NULL的slot,如果slot中的first undo page中的undo log segment header中的TRX_UNDO_STATE属性为ACTIVE,说明当前undo页面链表处于活跃状态
- 再在undo log segment header中找到TRX_UNDO_LAST_LOG属性,定位到本页面的最后一个undo log header,拿到活跃状态的事务的组信息
(因为一个页面可能有重用的情况,最后一个组之前的事务一定是提交的了(不提交就没有其他组)) - 根据组信息,找到该事务对应的undo日志(日志在同一个链表中),根据undo日志,将该事务全部回滚
下面标红的字段,记录本组日志在页面的开始偏移量。
二十一、事务隔离级别与MVCC
1事务隔离级别
- 并发执行时的一致性问题:
- 脏写:一个事务修改提交事务修改过的数据
- 脏读:一个事务读取了另一个未提交事务修改过的数据
- 不可重复读:一个事务修改了另一个未提交事务读取的数据
- 幻读:事务前后根据相同条件,读取到的记录数目不一样
- 严重性:脏写 > 脏读 > 不可重复读 > 幻读
- 4个隔离级别:
- 任何级别都不允许脏写,因为太严重了,会造成一致性、原子性、持久性问题
- Mysql 默认的是 Repeatable Read
- 可以给不同范围的事务设置不同的隔离级别
2MVCC原理
版本链
- MVCC:利用记录版本链来控制并发事务访问相同记录时的行为,称为多版本并发控制(Multi-Version Concurrency Control)
- 当事务提交后,insert undo类型的undo页面会被释放或者被重用。但是update undo类型的undo页面日志会被用于MVCC
- 注意点:TRX_UNDO_UPDATE日志不会记录全部的列信息,只会记录一些索引列及被更新的列的信息。
如果当前版本的日志没有某个列的值,说明这个值和上一个版本的值相同,去上个版本中找。上个版本中也没有,就上上个版本,一直找到为止。 - 思考:为什么说MVCC只是很大程度上禁止幻读,而不说完全禁止?(repeatable read隔离级别下)
- 很大程度:由于可以通过事务的id来判断记录的可见性,所以通常情况下不会幻读
- 特殊情况:设事务A处于repeatable read隔离级别下,搜索条件为a,假设此时结果集为空。当另外一个事务B插入一条记录r并提交,而事务A对记录r进行修改(MVCC是控制读,不控制写;而且事务B已经提交了,说明记录r上没有X锁)并让它符合条件a。事务A再次搜索时,则结果集就出现了记录r,出现了幻读
ReadView
- 对于 读未提交 级别的事务,直接读取版本链中最新的一个版本就好了
- 对于 串行化 级别的事务,会用加锁的方式访问记录
- 对于 可重复读 和 读已提交,核心问题是需要判断版本链中的哪个版本是当前事务可见的
- ReadView的结构:
- m_ids :活跃事务id列表
- min_trx_id:生成readView时,最小的事务id
- max_trx_id:生成readView时,系统应该分配给下一个事务的id(即比当前系统中所有事务的id大)
- creatro_trx_id:创建本readView的事务id
- 使用ReadView 判断记录 可见性 的步骤: (可见:说明可以访问)
- 如果被访问版本的trx_id和creatro_trx_id相同,则说明在同一个事务内访问,可见
- 如果trx_id < min_trx_id,说明trx_id的事务已经提交,可见
- 如果trx_id >= max_trx_id,说明该事务在ReadView生成之后,不可见 (这里其实是因为判断不了这个trx_id 是否已经被提交,所以一刀切保险)
- 如果 min_trx_id <= trx_id < max_trx_id之间,
- 如果trx_id 在活跃列表m_ids ,说明这个trx_id 事务是活跃的,不可见
- 如果不在,则可见
如果某个版本经过上面的判定后不可见,就沿着版本链继续判断下去。
- 读已提交 和 可重复读 的最大区别:
- 读已提交:每次进行查询时,生成一个独立的新的ReadView, 这样可保证每次查询读取的时候,记录是提交可见的
- 可重复读:只在第一次查询的时候生成ReadView,其他查询沿用这个ReadView。这样可保证,记录版本是和第一次查询时一样的(即同一个时间线),保证这个 可重复读 在事务的任何中间时刻,读到的记录时间线 和 第一次查询时一样。
(时间线是指:当第一次查询时,当前所有的版本链中的状态,即以第一次查询时的版本链状态为基准点)
- 思考:delete mark的作用:
因为delete mark是属于update类型的日志,在一个可重复读事务A时,如果一个其他事务B删除了一条记录,如果真正删除,那么可重复读事务A读不回被删除的那条记录; 如果用delete mark记录一下,则可重复读事务A可以仍然读取到 - 二级索引记录对于当前事务的可见性:(因为二级索引记录没有roll_pointer和trx_id隐藏列,才这么麻烦)
首先定位到需要的二级索引记录
- 判断当前页面的PAGE_MAX_TRX_ID的属性是否小于当前事务readView的min_trx_id ,如果小,说明当前页面之前的事务已经提交,直接读
- 如果大于,则回表到聚簇索引,找到该readView的第一个可见版本,然后判断 该版本中相应的二级索引列的值是否与利用该二级索引查询时的值相同,相同返回,不同跳过这条记录继续查找。
3Purge原理
- 为了支持MVCC,会有两个浪费空间的地方:
- 当事务提交后,update undo日志会加入history链表中
- delete mark仅仅是是打上删除标记
- 对上述两个占用空间的清除回收,就叫做purge操作
- purge的时机:当系统中最早的readView不再访问他们,就可以清除了
什么叫不再访问他们 ?就是这些undo日志会串成版本链,MVCC的过程会进行可见性的判断访问。
如果一个事务提交时间比当前最小的readView还小,说明这个事务在所有readView创建前就已经提交了,那么就不需要这个事务的版本链了,也就是可以清除了 - 事务no:
- 在一个事务提交时,会生成一个no,按照提交时间顺序增长
- ReadView结构中,也会有一个事务no,是生成该readView时当前系统中最大事务no值大1
- histroy链表:其中的日志是按照事务no值来排序各组undo日志的,也就是按照事务提交时间排序的
- readView链表:系统会根据readview创建时间形成一个链表
- purge操作原理:
- 系统取出最早的readView, 拿到其中的事务no ,这里称为A(如果不存在readView,则直接取当前系统中最大事务no值大1)
- 然后遍历各个回滚段的回滚页面的各个histroy链表,如果存在事务no值比A值小的,则清除。
- 如果清除的过程中发现,有TRX_UNDO_DEL_MARKS类型的undo日志,则把对应打上delete mark的记录真正删除。(看,delete mark的purge操作,是在清除undo记录时附带的!)
二十二、锁
1锁基本概念
- 锁结构:
- 当对一条记录加锁时,会在内存中生成一个锁结构与之关联(隐式锁除外)
- trx:事务id;is_waiting:true表示“获取锁失败,需要排除"
- 结构:(只是示意图,不是真正的结构,真正的结构在下面)
- 解决读写、写读的两种方案:
- 读用MVCC控制,写加锁:读写不冲突,性能比较高
- 读、写都加锁 :读一致性更强
两者的不同是,后者写也加锁,用在一些一致性很强的场景中,如读取银行里面的余额。
例如,如读取银行里面的余额时,在可重复读的隔离级别下,则不能用MVCC的版本链的思想了,来读取“历史版本”了。
- 一致性读:(弱一致性)
- 概念:事务利用MVCC进行的读取,称为一致性读(Consistent Read),或者一致性无锁读、快照读。
- 所有普通的select语句(plain select)(注意,这是一个专有名词,而不是形容词),在 已提交读 和 可重复读 都算是一致性读 (不会进行任何加锁,允许其他事务自由对表进行改动)
- 半一致性读(semi-Consisten read)
- 在隔离级别不大于read comitted且执行语句为update时,将使用半一致性读
- 当updata语句读到被加上X锁的记录时,innodb会将记录的最新版本提取出来,判断是否与update搜索条件一致,匹配的话再进行加锁再修改
- 目的:只是为了让update在条件判断时尽量少被其他事务阻塞,但要修改记录的话还是要排队获得锁
- 锁定读:在读取记录之前就为该记录加锁的读取方式称为锁定读(Locking read)
- 锁定读加锁的语句:(不加的话就是默认使用MVCC,即一致性读)
- 加S锁: select… LOCK IN SHARE MODE;
- 加X锁:select… FOR UPDATE;
- 写操作中的加锁:
- delete:是一种获取X锁的锁定读
- update:
- 就地更新:是一种获取X锁的锁定读
- 未修改主键,但列大小超过:先彻底删除,再插入,并将原记录关联的锁转移到新记录
- 修改主键:先delete, 再insert
- insert:新插入的记录受隐式锁保存,不用在内存中生成锁结构
2多粒度锁
目的:为了快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。
具体看以前的笔记。
3MySQL中的行锁和表锁
其他引擎
- 如myisam, memory等只有表级锁,而且这些存储引擎不支持事务
- 一个表同一时刻最多一个会话访问。
下面都是针对INNODB的
表级锁
- 表级S和X锁:
- 在对表执行select, insert, delete, update时,innodb不会自动加表级锁
- 一般情况下不会使用innodb提供的表级锁,原因是其保护作用低,降低并发能力,但可以手动加
- 元数据锁(metadate lock, MDL):在同时对表使用DDL语言(如drop,alter)和DML语言(如select),它们之间会相互阻塞。原因是在server层中使用一种叫元数据锁来实现的
- 表级IS、IX锁:和上述理论一样,在对记录加锁前,给表加意向锁
- 表级别 AUTO-INC锁:
- 作用在使用了 auto_increment 的表上,当插入时加上该锁,其他事务阻塞插入,从而保证一个语句中分配的递增值是连续的
- AUTO-INC锁是语句范围的,插入语句结束后,就释放该锁
- 轻量级锁:在生成本次插入语句需要用到的auto_increment 修饰的列的值之后,就把该轻量锁释放掉(可能生成id是交叉的,在主从复制场景是不安全的)
行级锁
- Record Lock:LOCK_REC_NOT_GAP(正经记录锁
- 即正常的S和X锁
- Gap Lock:LOCK_GAP
- 功能:在一条记录A上加了gap锁,则不能在前面一条记录B和A之间插入记录
- 记录在插入时,会向前面一条记录看一眼,如果有gap锁则阻塞插入
- 不会与其他任何锁发生冲突
- 作用:防止插入幻影记录
- 思考:这怎么能防止幻影记录呢?
因为在对记录搜索读取的时候,是对区间进行扫描的,(如精确匹配会给扫描区间最后一条记录的下一条记录加gap锁,可以防止新事务的记录插入到扫描区间内)
- Next-Key Lock:LOCK_ORDINARY
- 作用:可以锁住某条记录,同时又可以阻止其他事务在该记录前面的间隙插入新记录
- 本质是一个正经记录锁和gap锁的合体
- Insert Intention Lock:LOCK_INSERT_INTENTION
- 功能:当某个记录要在某个间隙插入记录时,但处于等待状态(前面一条记录有gap或者next-key lock),就在内存中生成一个这样的锁
- 不会与其他任何锁发生冲突
- 当gap锁事务提交后,T2,T3就能获得对应的插入意向锁(is_waiting = true)
- 隐式锁:
- 功能:当插入一条记录时,可以不显式地加锁,延迟生成锁结构,可以节约内存。
- 生成场景:
- 聚族索引下:在对一个记录进行加锁时,看这条记录的隐藏列trx_id是否为当前活跃事务(如果小于当前最小活跃事务,应该也是可以读取的,书上没说),如果不是则为它生成一个is_waiting=false的锁,同时为被阻塞的事务生成一个is_waiting=true的锁
- 二级索引下:先看PAGE_MAX_TRX_ID是否小于当前活跃事务的最小id;否则回表,找到聚族索引,重复上面的动作
锁的内存结构
- 可以将具有某些特征的锁放在一起,当满足下面条件时:
- 同一个事务的锁
- 被锁的记录在同一个页面中(因为行锁是通过heap_no来映射比特位的,当然要同一个页面)
- 加锁的类型是一样的
- 等待的状态一样
- 结构图:
- 属性:
- 事务信息:提示当前事务的一些信息呗
- 索引信息:看这个锁是什么索引,是primary呢还是其他
- 表锁信息:记录对哪个表进行加锁
- 行锁信息:记录表空间、页号、n_bits(有多少条记录被加锁)
- type_mode:三个部分,指示锁模式(S、X还是其他)、锁类型(行还是表)、锁具体类型(在行锁上的具体分类,如gap, next-key)
- Lock_wait:等待状态,被隐含在type_mode的某个比特位中
- 一堆比特位:将页面的heap_no映射到比特位上,一个比特位代表一条记录
4语句加锁分析
语句加锁的情况受到事务的隔离级别、语句执行时使用的索引类型、是否精确匹配、是否唯一性搜索、具体执行的语句的类型等情况的制约,具体情况具体分析
5查看事务加锁的情况
- 可以通过information_schema数据库下的一些表查看(Mysql 8.0已经移除)
- show engine innodb status语句可以查看事务的情况和锁相关信息
- 死锁日志可以帮助分析死锁时的情况
后记:看完才发现,自己好像也没长进多少,只是理论上有了一个大概的认识。具体的还是要应用到实践吧。