Mysql高手之路

举报
yd_278904034 发表于 2024/07/08 20:19:18 2024/07/08
【摘要】 本人在阅读《Mysql是怎样运行的》相关读书笔记,图片来源知乎上原作者。本文设计Mysql数据库记录行格式,Innodb存储引擎下索引,事务,redoLog,undoLog,锁相关的知识。

……
[MySQL是怎样运行的]
……

第一章 初识MySQL

MySQL的服务器程序直接与要存储的数据打交道,多个客户端程序可以连接到这个服务器程序,向服务器发送增删改查的请求,服务器程序根据这些请求对存储的数据进行相应的处理。与微信一样,MySQL的每一个客户端都需要使用用户名和密码才能登陆服务器,而且只有在登录之后才能向服务器发送某些请求来操作数据。MySQL的日常使用场景是下面这样的。

  1. 启动MySQL服务器程序。
  2. 启动MySQL客户端程序,并连接到服务器程序
  3. 在客户端程序中输入命令语句,并将其作为请求发送给服务器程序。服务器程序在收到这些请求后,根据请求内容来操作具体的数据并将处理结果返回给客户端。

众所周知,现在计算机的功能都很强大,一台计算机上可以运行多个程序。比如微信,QQ,LoL.计算机上运行的每一个程序也称为一个进程。运行过程中的MySQL服务器程序和客户端程序在本质上来说都算是计算机中的进程,其中代表MySQL服务器程序的进程成为Mysql数据库实例(instance)。

总结

MySQL采用客户端/服务器架构,用户通过客户端程序发送增删改查请求,服务器程序收到请求后处理,并且把处理结果返回给客户端。
MySQL安装目录的bin目录下存放了许多可执行文件,其中有一些是服务器程序(比如mysqld,mysqld_safe),有一些是客户端程序(比如mysql,mysqladmin).
在类unix系统上启动服务器程序的方式有下面这些:

  • msqld:
  • mysqld_safe:
  • mysql.server:
  • mysql_multi。

在windows系统上启动服务器的方式有下面这些:

  • mysqld:
  • 将mysqld注册为Windows服务

启动客户端程序时常用的语法如下:

  mysql -h主机名  -u用户名  -p密码

客户端进程和服务器进程通信时采用下面几种方式:

  • TCP/IP
  • 命名管道或共享内存
  • UNIX域套接字

以查询请求为例,服务器程序在处理客户端发送过来的请求时,大部分为以下几个部分。

  • 连接管理:主要负责连接的建立与信息的认证。
  • 解析与优化:主要进行查询缓存,语法解析,查询优化。
  • 存储引擎:主要负责读取和写入地城表中的数据。

MySQL支持的存储引擎有好多种,它们的功能各有侧重,我们常用的就是InnoDB和MyISAM,其中InnoDB为服务器程序的默认存储引擎。

  • 查看当前服务器程序支持的存储引擎:show engines;
  • 创建表时指定表的存储引擎: create table 表明() engine = 存储引擎名称;
  • 修改表的存储引擎:alter table 表明 engine = 存储引擎名称;

第二章 MySQL启动选项和系统变量

总结

1.启动选项可以调整服务器启动后的一些行为。它们可以在命令行中指定,也可以将它们写入配置文件中。在命令行中指定启动选项时,可以将各个启动选项写到一行中,每个启动选项名称前面添加–,而且各个启动选项之间使用空白字符隔开。有一些启动选项不需要指定选项值,有一些选项需要指定选项值。在命令行中指定有值的启动选项时要注意,选项名,=,选项值之间不可以有空白字符。一些常用的启动选项具有段形式的选项名,使用短形式选项时在选项名前只加一个短划线 - 前缀。

//在命令行上使用选项 比如我们想在启动服务器程序时就禁止各客户端使用TCP/IP网络进行通信 
  mysql --skip-networking 或者 mysql --ship_networking
//在使用mysql来启动客户端程序把服务器主机名指定为127.0.0.1的话会显示连接失败:
  mysql -h127.0.0.1 -uroot -p
//在举一个例子 如果我们在创建表时没有显式指定表的存储引擎,那就会默认使用InnoDB作为表的存储引擎。如果我们想改变表的默认存储引擎可以输入下面的启动服务器的命令:
mysqld --default-storage-engine=MyISAM

2.服务器程序在启动时将会在一些给定的路径下搜索配置文件,不同操作系统的搜索路径是不同的。
配置文件中的启动选项被划分为若干个组,每个组有一个组名,用中括号[]扩起来。在配置文件中指定的启动选项不允许添加 – 前缀,并且每行至指定一个选项,而且等号 = 周围可以有空白字符。我们可以使用#来添加注释。
3.系统变量是服务器程序中维护的一些变量,这些变量影响着服务器的行为。修改系统变量的方式如下。

  • 在服务器启动时通过添加相应的启动选项进行修改
  • 在运行时使用set语句修改,下面两种方式都可以:
  • set [global|session] 系统变量名 = 值;
  • set[@@(global|session).]系统变量名 = 值;
//比如我们想在服务器的运行过程中把作用范围为global的系统变量default_storage_engine的值
//修改为MyISAM,也就是想让之后新连接到服务器的客户端都用MyISAM作为默认的存储引擎:
 - set global default_storage_engine = MyISAM;
 - set @@global.default_storage_engine = MyISAM;
//如果只想对本客户端生效,也可以选择下面3条语句中的任意一条来设置。
 - set session default_storage_engine = MyISAM;
 - set @@session.default_storage_engine = MyISAM;
 - set default_storage_engine = MyISAM;
//查看系统变量
 - SHOW [GLOBAL|SESSION] VARIABLES [LIKE %]
 注意事项:
 并不是所有的系统变量都具有GLOBALSESSION的作用范围。
 - 有一些系统变量只具有GLOBAL作用范围,比如max_connections,
 - 它表示服务器程序支持同时最多有多少个客户端程序进行连接。
有一些系统变量只具有session作用范围,比如insert_id,它表示在对某个包含AUTO_INCREMENT列的表进行插入时,该列初始的值。

4.状态变量是用来显示服务器程序运行状态的,我们可以使用下面的命令来查看,而且只能查看:
SHOW [GLOBAL | SESSION] STATUS [LIKE 匹配的模式];

第三章 字符集与比较规则(排序规则)

总结

1.字符集指的是某个字符范围的编码规则,包括不限于以下几种:

 - ASCII 字符集:共收录了128个字符,包括空格,标点符号,数字,大小写字母和一些不可见字符。
   由于ASCII字符集总共才128个字符,所以可以使用一个字节(8位)来进行编码。
 - ISO 8859-1 字符集:共收录256个字符,它在ASCII字符集的基础上有扩充了128个西欧常用字符(包括德法两国的字母)。ISO 8859-1 字符集也可以使用一个字节来进行编码。(别名:Latin1)。
 - GB2312 字符集:收录了汉字及拉丁字母,希腊字母,平假名以及片假名字母,俄语西里尔字母,收录汉字6763个,收录其他文字符号682个。这种字符集同时兼容ASCII字符集,所以在编码上有一些奇怪:如果该字符在ASCII字符集上,则采用一个字节编码;否则采用两个字节编码。
 - GBK字符集:GBK字符集只是在收录的字符范围上对GB2312字符集进行了扩充,编码方式兼容GB2312字符集。
 - UTF-8 字符集:几乎收录了当今世界各个国家/地区使用的字符,而且还在不断扩充。这种字符集兼容ASCII字符集,采用边长编码方式,编码一个字符时需要使用1~4字节。
MySQL中的utf8为阉割过的UTF-8字符集,只使用1~3个字节表示字符。(utf8mb3) 
      utf8mb4:正宗的UTF-8字符集,使用1~4个字节表示字符。

2.比较规则是对某个字符集中的字符比较大小的一种规则,也叫排序规则。
MySQL中,一个字符集可以有若干种比较规则,其中有一个默认的比较规则。一个比较规则必须对应一个字符集。

//比较规则名称后缀英文释义及描述
 - _ai : accent insensitive 不区分重音
 - _as :accent sensitive 区分重音
 - _ci : case_insensitive 不区分大小写
 - _cs : case_sensitive 区分大小写
 - _bin : binary 以二进制方式比较
//在执行SHOW COLLATION语句中,default列的值为YES的比较规则就是该字符集的默认比较规则,比如utf8字符集默认的比较规则就是utf8_general_ci。

3.在MySQL中查看支持的字符集与比较规则的语句如下:

  • show (character set | charset) [like 匹配的模式];
  • show collation [like 匹配的模式];
    4.MySQL有4个级别的字符集和比较规则,具体如下。
 1. 服务器级别:character_set_server 表示服务器级别的字符集,collation_server表示服务器级别的比较规则。
 2. 数据库级别:create database 数据库名
               character set 字符集名称
               collate 比较规则名称
// character_set_database 表示当前数据库的字符集,collation_datebase表示当前数据库的比较规则。
//这两个系统变量只用来读取,修改他们并不会改变当前数据库的字符集和比较规则。
//如果没有指定当前数据库,则这两个系统变量与服务器级别相应的系统变量具有相同的值。
3.MySQL有4个级别的字符集和比较规则
服务器级别:
character_set_server 表示服务器级别的字符集,
collation_server表示服务器级别的比较规则。
数据库级别:
创建和修改数据库时可以指定字符集和比较规则:
character_set_database表示当前数据库的字符集
collation_database表示当前数据可的比较规则。这两个系统变量只用来读取,修改他们并不会改变当前数据库的字符集和比较规则。如果没有指定当前数据库,则这两个系统变量与服务器级别相应的系统变量具有相同的值。
表级别:
create table (列的信息) 
     character set 字符集名称,
     collate 比较规则名称;
列级别:
 create table 表名(
      列名 字符串类型 character set 字符集名称 collate 比较规则名称...)


发送请求到接受响应的过程中发生的字符集转换如下所示。
 1. 客户端发送的请求字节序列是采用哪种字符集进行编码的。
 这一步骤主要取决于当前操作系统当前使用的字符集;对windows操作系统来说
 还与客户端启动时设置的default-character-set启动项有关。 
 2. 服务器接受字节序列后会认为它是采用哪种字符集进行编码的
 这取决于系统变量character_set_client的值
 3. 服务器在运行过程中会把请求的字节序列转换为以哪种字符集编码的字节序列
 这取决于系统变量character_set_connection的值
 4. 服务器在向客户端返回字节序列时采用哪种字符集进行编码。
 这取决于系统变量character_set_result的值
 5. 客户端在接收到响应字节序列后怎么把它们写道黑框框中的。
 这取决于当前操作系统所使用的字符集;windows操作系统来说还与客户端启动时设置的dafault-character-set有关。
 
 比较规则通常用来比较字符串的大小预计对某些字符串进行排列。

第四章 InnoDB记录的存储结构

总结

从简单的来说:页是InnoDB中磁盘和内存交互的基本单位,也是InnoDB管理存储空间的基本单位,默认大小为16KB。
指定和修改行格式的语法如下:
create table 表名(列的信息) row_format = 行格式名称;
alter table 表名 row_format = 行格式名称;

InnoDB 目前定义了4种行格式:

  1. COMPAC行格式:
    在这里插入图片描述
1.记录的额外信息
 (1)变长字段长度列表
   MySQL支持一些变长类型:varchar,varbinary,各种text类型,blob类型。
  变长字段占用的存储空间为两部分:真正的数据内容,该数据占用的字节数
  在COMPACT行格式中,所有变长字段的真实数据占用的字节数都存在记录的
  开头位置,从而形成一个变长字段长度列表,各变长字段的真实数据占用的字节数
  按照列的顺寻逆序存放。
  变长字段其真实内容占用的字节数可能用1个字节或者2个字节来表示:
 - 假设某个字符集中最多需要W字节来表示一个字符,比如utf8mb4字符集中的w就
  是4,utf8中的w就是3,gbk中的w为2,ascii字符集中w为1.
 - 对于变长类型varchar(M)来说,这种类型表示最多能存储M个字符,所以这种类型能表示的字符串最多占用的字节数就是W*M.
 - 假设该变长字段实际存储的字符串占用的字节数是L.
总结一下就是:如果该变长字段允许存储的最大字节数(M*W)超过255字节,并且真实数据所占用的字节数(L)超过127字节,则使用2字节来表示真实数据占用的字节数,否则用1字节。另外还需要注意一点是,变长字段长度列表中只存储值为非NULL的列的内容长度,不存储值为Null的的列的内容长度。
(2NULL值列表
 COMPACT行格式把一条记录中值为null的所有列统一管理起来。
 - 首先统计表中允许存储NULL的列有哪些
 - 如果表中没有允许存储NULL的列,那NULL值列表也就不存在了,否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的书讯逆序排列。二进制位为1时则表示该列的值为NULL,二进制位为0时代表该列的值不为NULL- MySQL中规定NULL值列表必须用整个字节的位表示,如果二进制位数不是整个字节则高位补0.3)记录头信息
 - 整个记录头信息由5个字节组成,40个二进制位。
 - deleted_flag:标记该记录是否被删除,min_rec_flag:B+树的每层非叶子节点中最小的目录项记录都会添加该标记,n_owned:一个页中的记录会分为若干个组,每个组中有一条记录中的n_owned值代表该组中所有记录条数,heap_no:表示当前记录在页面堆中的相对位置,record_type:表示当前记录的类型 0:为普通记录,1表示B+树非叶子节点的目录项记录,2:表示Infimum记录,3:表示Supremum记录,next_record表示下一条记录的相对位置。
对于除我们自己定义的列的数据外,MySQL会为每个记录默认地添加一些列。比如:row_id, trx_id(事务ID), roll_pointer(回滚指针)
InnoDB优先使用用户自定义的主键作为主键;如果用户没有定义主键,则选取一个不为NULLUNIQUE键作为主键,否则InnoDB会默认添加一个名为row_id的隐藏列作为主键。

2.REDUNDANT行格式

在这里插入图片描述

REDUNDANT行格式是MySQL5.0之前在使用的一种行格式。
(1)字段长度偏移列表:没有个变长意味着REDUNDANT行格式把该条记录的所有列包括隐藏列的长度信息按照逆序存储到字段长度偏移列表,它是采用两个相邻偏移量的差值来计算各个列值的长度。
 (2) 1byte_offs_flag : 标记字段长度偏移列表中每个列对应的偏移量是使用1个字节还是2个字节。
   - 当记录的真实数据占用的字节数不大于127,用1字节表示。
   - 当记录的真实数据占用的字节数大于127但不大于32767时,每个列对应的偏移量占用2字节
   - 当记录的真实数据大于32767时,将一部分记录放到溢出页中,在本页中只保留前768字节和20字节的溢出页面地址,此时用2个字节来存储每个列对应的偏移量就够了。
(3)没有NULL值列表,我们将第一个比特位作为NULL值比特位,如果为1则该列的值为NULL,否则不是NULL。(此时也就说明了第二条只要记录的真实数据大于127就用2个字节了)

3.DYNAMIC行格式与COMPRESSED行格式
这两个行格式与COMPACT行格式类似,只不过再处理溢出列的数据时有些分歧:它们不会在记录的真实数据存储溢出列真实数据的前768字节,而是把该列的所有真实数据都放到溢出页中,只在记录的真实数据存储20字节大小的指向溢出页的地址。COMPRESSED行格式会采用压缩算法对页面进行压缩,以节省空间。
溢出列的临界值(132 + 2(27 + n ) < 16384)即一条记录的某个列中存储的数据占用的字节数非常多时(8099)该列可能会变为溢出列。

第五章 InnoDB数据页结构

总结

1.我们最关心的就是那些存放表中记录的那种类型的页,官方称为索引页。(数据页)
数据页代表的16KB大小的存储空间可以划分为多个部分:

InnoDB数据页结构示意图

 - File Header:文件头部 页的一些通用信息 38字节
 - Page Header:页面头部 数据页专有的一些信息 56字节
 - Infimum+Supremum:页面中最小记录和最大记录 26字节
 - User Records:用户记录 用户存储的记录内容 
 - Free Space:空闲空间 页中尚未使用的空间
 - Page Directory:页目录  页中某些记录的相对位置(存放每个组槽的位置)
 - File Trailer:文件尾部 校验页是否完整(内存与磁盘之间交互是通过页,校验是否为一个完整页的依据)

2.记录在页中的存储
从最开始生成页的时候其实并没有UserRecords部分,每当插入一条记录都会从FreeSpace部分申请一个记录的大小的空间并将这个空间划分到UserRecords部分。当Free Space部分的空间全部被UserRecords部分顶替后也就说明当前页用完了,在插入记录时要申请新的页。
记录在页中的存储

记录头信息
COMPACT行格式示意图

特意把记录头信息的5字节的数据给标出来了,下面是记录头信息中各个属性的大体意思:
 - deleted_flag:1bit 标记该记录是否被删除(值为1时表示纪录被删除,被删除的记录不从磁盘上移除,因为在移除它们值后,还需要在磁盘上重新排列其他记录。所有被删除掉的记录会形成一个垃圾链表,记录在这个链表所占的空间成为可重用空间,之后有新的记录插入到表中就有可能覆盖掉被删除的这些记录占用的存储空间。)
 - min_rec_flag: 1bit B+树中每层非叶子节点中的最小的目录项记录都会添加这个标记(也就是索引目录项)
 - n_owned: 4bit 一个页面中的记录会被分成若干组,每个组中有一条最大记录使用n_owned记录该组中有几条数据
 - heap_no:13bit 当前记录在页面堆中的相对位置(我们向表中插入的记录从本质上说都是放到数据页的UserRecords部分,这些记录一条一条紧密排列。这个结构我们称为堆,每新申请一条记得存储空间时,该条记录比物理位置在它前面的那条记录的heap_no值大1。heap_no值为01的两条记录就是Infimum与Supremum,无论我们向页中插入了多少条记录,都规定任何记录都比Infimum记录大,比Supremum小,这两条记录只包含5字节大小的记录头信息和8字节大小的一个固定单词组成),值得注意的是即使之后删除了除堆中的某条记录,这条被删除记录的heap_no值也依旧保持不变。
 - record_type: 3bit 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点目录项,2表示Infimum记录,3表示Supremum记录
 - next_record: 16bit 表示下一条记录的相对位置(它表示从当前记录的真实数据到下一条记录的真实数据的距离,如果该属性值为正数,说明当前记录的吓一跳记录在当前记录的后面;如果该属性值为负数,说明当前记录的下一条记录在当前记录的前面,比如第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址向后找32字节便是下一条记录的真实数据。比如第4条记录的next_record值为-111,意味着从第4条记录的真实数据的地址处向前找111字节遍试下一条记录的真实数据。需要注意的一点是下一条记录的真实数据不是指插入顺寻的下一条记录,而是主键值由小到大的顺序排列的下一条记录。并且规定Infimum记录的下一条记录就是本页中主键值最小的用户记录,本页中主键值最大的用户记录的下一条记录就是Supremum记录,为了更形象的表示next_record,如下第二幅图所示。)

可以看到图中record_type为2的是Infimum记录,为3的是Supremum记录,0代表我们插入的记录
记录存放方式
注意:箭头指向的位置,每个箭头都指向记录的真实数据开始的地方,可以看出来记录按照主键大小的顺寻形成了一个单向链表。Supremum记录的next_record值为0,也就是说Supremum记录之后就没有下一条记录了,这也意味着Supremum记录就是这个单向链表中的最后一个节点。如果从表中删除一条记录,单向链表也是发生变化的,如下图删掉第二条记录后的示意图所示。
next_record用箭头代替
删除掉第二条记录后的示意图

//从上图可以看到删除第二条记录后的示意图
 1. 第二条记录其实并没有从存储空间中移除,而是把deleted_flag值设置为1
 2. 第二条记录的next_record值变为0,意味着该记录没有下一条记录
 3. 第一条记录的next_record指向了第三条记录
 4. Supremum记录的o_owned值从5变成了4(就是该组中所有记录的条数,按分组的规矩来定就是Infimum记录自己一个组,Supremum记录与用户记录一个组最多8条数据)
补充:next_record指针指向记录头信息和真实数据之间的位置的原因:向左读取记录头的信息,向右读取真实数据。并且变长字段长度列表,null值列表中的信息都是逆序存放的,这样可以使记录中位置靠前的字段和他们对应的字段长度信息在内存中靠的更近,提高高速缓存的命中率。
其实还有一个特别有意思的地方:主键为2的记录被删除掉了,但是却没有回收存储空间,并且heap_no的值也未发生改变,我们重新插入这条数据后InnoDB并没有因为新记录的插入而为它申请新的存储空间,而是直接复用了原来被删除记录的存储空间。
当数据页中存在多条被删除的记录时,可以使用这些记录的next_record属性将这些被删除的记录组成一个垃圾链表,以备之后重用这部分空间。

重新插入后记录的存储情况
3.页目录

 1. 将所有正常的记录(包括Infimum和Supremum,不包括已经删除的记录)划分为几个组 Infimum记录所在的分组只能有一条记录,Supremum记录所在的分组拥有的记录条数只能在1~8条之间,剩下分组的记录只能在4~8条之间
 2. 每个组的最后一条记录(也就是组内最大的记录)n_owned属性表示该组内共有几条记录
 3. 每个组中最后一条记录的地址偏移量(就是该记录的真实数据与页面中第0个字节之间的距离)单独提取出来,按顺序存储到靠近页尾的地方。这个地方就是PageDirectory,页目录中的这些偏移量成为槽(Slot)每个槽占用两个字节。(一个页就16KB的大小,即16384字节,而2字节可以表示的地址偏移量范围是0~65535

比如现在表中的记录共有6条。InnoDB会把它们分成2个组,第一组只有Infimum记录,第二组这是剩余的5条记录,则就有两个槽,每个槽存放着每个组中最大记录在页面中的地址偏移量。

  1. 初始情况下,一个数据页中只有Infimum记录和Supremum记录这两条,它们分属于两个分组。
  2. 之后每插入一条新纪录都会从页目录中找到比主键值大并且差值最小的的槽,然后更该该槽对应那条记录的n_owned值加1,直到该组中的记录数等于8个。
  3. 当一个组中的记录数等于8后,再插入一条记录,会将组中的记录拆分成两个组,其中一个组中4条记录,另一个5条记录。拆分的过程中会在页目录中新增加一个槽,记录这个新增分组中最大的那条记录的偏移量。

记录与页目录的关系
当我们在向表中继续插入12条数据后,现在这个页中就一共有了18条记录了(包括Infimum和Supremum记录),这些记录被分成了5个组。

INSERT INTO page_demo VALUES(5, 500, 'eeee'), (6, 600, 'ffff'), (7, 700, 'gggg'), (8, 800, 'hhhh'), (9, 900, 'iiii'), (10, 1000, 'jjjj'), (11, 1100, 'kkkk'), (12, 1200, 'llll'), (13, 1300, 'mmmm'), (14, 1400, 'nnnn'), (15, 1500, 'oooo'), (16, 1600, 'pppp');
Query OK, 12 rows affected (0.00 sec)
Records: 12  Duplicates: 0  Warnings: 0

对应的记录及分组

我们用非常间的编号代表槽,假如我们想找主键值为6的记录,过程是这样的(二分):

 1. 计算中间槽:即2,槽2对应的最大记录的主键值为8,所以设置high = 2。然后继续查找,最后我们确定主键值为6的记录在槽2对应的组中。但是我们怎么沿着单向链表遍历操2的记录?(槽对应的为组中最大记录)因为各个槽都是挨着的,所以可以轻松的找到槽1对应最大记录然后它下一条记录就是槽2的最小记录,直到我们找到主键为6的记录即可。(每个组中的数据最多8条所以代价很小)
 2. 注意是通过next_record属性遍历该槽所在的组中的记录

4.Page Header
该部分主要存储的就是页面中已经存储了多少条记录,FreeSpace在页面中的地址偏移量,页目录中存了多少个槽等状态信息。

//pageHeader的结构及描述
名称	       大小(单位:byte)	 描述
PAGE_N_DIR_SLOTS	2	在页目录中的槽数量
PAGE_HEAP_TOP	2	第一个记录的地址
PAGE_N_HEAP	2	本页中的记录的数量(包括最小和最大记录以及标记为删除的记录)
PAGE_FREE	2	指向可重用空间的地址(就是标记为删除的记录地址)
PAGE_GARBAGE	2	已删除的字节数,行记录结构中delete_flag为1的记录大小总数
PAGE_LAST_INSERT	2	最后插入记录的位置
PAGE_DIRECTION	2	最后插入的方向
PAGE_N_DIRECTION	2	一个方向连续插入的记录数量
PAGE_N_RECS	2	该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录)
PAGE_MAX_TRX_ID	2	修改当前页的最大事务ID,该值仅在二级索引中定义
PAGE_LEVEL	2	当前页在索引树中的位置,高度
PAGE_INDEX_ID	8	索引ID,表示当前页属于哪个索引
PAGE_BTR	10	非叶节点所在段的segment header,仅在B+树的Root页定义
PAGE_LEVEL	10	B+树所在段的segment header,仅在B+树的Root页定义

PAGE_DIRECTION:假如新插入的一条巨鹿的主键值比上一条记录的主键值大,我们说这条记录的插入方向是右边,反之则是左边。用来表示最后一条记录插入方向的状态。
PAGE_N_DIRECTION:假设连续插入新纪录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来。如果最后一条记录的插入方向发生了改变则这个状态的值会被清零然后重新计算、

5.File Header
FileHeader通用于各种类型的页,也就是说各种类型的页都会以FileHeader最为一个组成部分,它描述了一些通用于各种页的信息,比如这个页的编号是多少,它的上一个页和下一个页是谁;

File Header的结构及描述
FIL_PAGE_SPACE_OR_CHKSUM	4	页的校验和(checksum值)
FIL_PAGE_OFFSET	4	页号
FIL_PAGE_PREV	4	上一个页的页号
FIL_PAGE_NEXT	4	下一个页的页号
FIL_PAGE_LSN	8	最后被修改的日志序列位置(英文名是:Log Sequence Number)
FIL_PAGE_TYPE	2	该页的类型(之前我们说的是数据页)
FIL_PAGE_FILE_FLUSH_LSN	8	仅在系统表空间的一个页中定义,代表文件至少被更新到了该LSN值,独立表空间中都是0
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID	4	页属于哪个表空间

FIL_PAGE_SPACE_OR_CHKSUM:代表当前页的校验和,我们会通过某种凡是计算一个比较短的值来代替很长的字节串,这个比较短的值就是校验和。这样在比较两个较长的字节串之前我们先比较其校验和,若是校验和都不相等则不去比较两个字节串,这样就省去多余的时间损耗。
FIL_PAGE_OFFSET:每一个页都有一个单独的页号,如同我们的身份证号码一样。InnoDB通过页号来唯一定位一个页。
FIL_PAGE_TYPE:表示当前页的类型,我们用来存放记录的数据页类型就是FIL_PAGE_INDEX也就是索引页。
FIL_PAGE_PREV 和 FIL_PAGE_NEXT:InnoDB是以页为单位存放数据的,有时候在存放某种类型的数据时,占用的空间非常大,InnoDB无法一次性为这么多数据分配一个特别大的存储空间,分散到多个不连续的页中进行存储,则需要把这些页关联起来,FIL_PAGE_PREV,FIL_PAGE_NEXT就分别代表本数据页的上一个页和下一个页的页号,这样通过双向链表就把许许多多的页串联起来,无需这些页在物理上真正连着。

数据页组成的双向链表
6.File Trailer
InnoDB存储引擎会把数据存储到磁盘上,但磁盘的效率太慢,需要以页为单位把数据加载到内存中处理。为了检测一个页是否完整,File Trailer部分由8个字节组成

 -4个字节代表检验和。与File_Header中的校验和相对应。每当一个页面在内存中发生修改时,在刷新之间就要把页面的校验和计算出来。因为FileHeader的校验和首先会被刷新到磁盘,当完全写完后校验和也会被写到页的尾部。如果刷新成功应该收尾一致,如果刷新了一部分后断电了,那么FileHeader中的校验和就代表着已经修改过的页,而FileTrailer中的校验和代表着原先的页,二者不同则代表了刷新期间发生了错误。
 -4个字节代表页面被最后修改时对应的LSN的后4字节,正常情况下应该与FileHeader的FIL_PAGE_LSN的后4字节相同,也是用于校验页的完整性。

第九章 InnoDB的表空间

总结

一.独立表空间结构

对于16KB的页来说,64个页就是一个区(extent),一个区默认占用1MB的空间大小。无论是系统表空间还是独立表空间都是由若干个连续的区组成的,每256个区被划分成一组。其中extent0~extent256这256个区算是第一个组,extent256到extent511这256个区算一个组,可以划分更多的组,这些组的头几个页面的类型都是类似的。

 - 第一个组的最开始的3个页面的类型是固定的,也就是说extent0这个区最开始的3个页面的类型是固定的,分别如下:
   1.FSP_HDR:这个类型的页面用来登记整个表空间的一些整体属性以及本组所有区的所有属性,需要注意的是整个表空间只有一个FSP_HDR类型的页面。
   2.IBUF_BITMAP:这个类型的页面用来存储关于Change Buffer的一些信息
   3.INODE:这个类型的页面存储了称为INODE Entry的数据结构。
 - 其余各组最开始的2个页面的类型是固定的。也就是说extent256,extent512这些区最开始的两个页面的类型是固定的。
   1.XDES:全程是extent descriptor,用来登记本组256个区的属性。也就是说对于在extent256区中的该类型的页面来说,存储的就是extent256~extent511这些区的属性;与前面FSP_HDR类型的页面相似只不过FSP_HDR类型的页面还会额外存储一些表空间的属性。
   2.IBUF_BITMAP:

1.段的概念
假如表中的数据很少,只有几十条几百条数据,的确用不到区的概念。我们每向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引所代表的B+树的节点中插入数据。而B+树每一层中的页都会形成一个双向链表,如果也页为单位来分配存储空间,双向链表相邻的两个也之间的物理位置可能离的比较远。我们提到过使用B+树来减少记录的扫描行数的过程是通过一些搜索条件到B+树的叶子节点中定位到第一条符合条件的记录(对于全表扫描就是定位到第一个叶子节点的第一条记录),然后沿着由记录组成的单向链表以及数据也组成的双向链表一直想后扫描就可以了,如果双向链表中相邻的两个页物理位置不连续,传统的机械硬盘来说需要重新定位磁头位置,也就是会产生随机I/0,这样会影响磁盘的性能,所以才有了区(extent)的概念,我们尽量让页面链表中相邻的页的物理位置也相邻,这样再扫描叶子节点中大量的记录时才会使用顺序I/O。一个区就是在物理位置上连续存储的64个页(页号是连续的)。在表中的数据量很大时,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区为单位进行分配。甚至在表中的数据非常非常多的时候,就可以一次行分配多个连续的区(可能会造成一些空间上的浪费,数据不足以填满整个区,从性能的角度来说减少随机 I/O)。
当我们使用B+树执行查询时只是在扫描叶子结点的记录,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请的区中,扫描效果就大打折扣了。所以InnoDB的设计者将B+树的叶子节点和非叶子节点进行了区分对待,叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也是一个段。也就是说一个索引会分为两个段。
针对于“以完整的区为单位分配给某个段时,对于数据量较小的表来说太浪费存储空间”,提出了碎片区(fragment)的概念,也就是说在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,碎片区中页可以用于不同的目的,有些页属于段A,有些属于段B,有些不属于任何段。碎片区直属于表空间,并不属于任何一个段。此后向某个段分配存储空间的策略如下:

  • 在刚开始向表中插入数据时,段是从某个碎片区以单个页面为单位来分配存储空间;

  • 当某个段已经占用了32个碎片页面之后,就会以完整的区为单位来分配存储空间(原先占用的碎片区页面并不会被复制到新申请的完整的区中)

  • 也就是说段是一些零散的页面以及一些完整的区的集合
    2.区的分类

  • 空闲的区:现在没有用到这个区中的任何页面 FREE

  • 有剩余空闲页面的碎片区:表示碎片区中还有可被分配的空闲页面 FREE_FRAG

  • 没有剩余页面的碎片区:表示碎片区所有的页面都被分配使用没有空闲页面 FULL_FRAG

  • 附属于某个段的区:这些区的页面完全用于存储该段中的数据(碎片区可以存储不同段的数据)FSEG
    注意:前三者直属于表空间;而处于FSEG状态的区是附属于某个段的。

XDES Entry结构
3.每一个区对应一个XDEX Entry结构:

  • Segment ID :表示的就是该区所在的段,前提是该区已经配分配给某个段
  • List Node:这个部分可以将若干个XDES Entry结构串连成一个链表。List Node的结构有上图所示,如果我们像定位表空间内的某一个位置,只需要指定页号以及该位置在指定页号中的页内偏移量即可。
    Pre Node Page Number 和 Pre Node Offset的组合就是指向前一个XDES Entity的指针。(next就是下一个)
  • State:这个字段表明区的状态。可选的值分别是FREE,FREE_FRAG,FULL_FRAG和FSEG。
  • Page State Bitmap:16字节,128位,一个区有64个页,每个页对应2位。这2位中的第1位表示对应的页是否是空闲的,第二位还没有用到。

!!!!XDES Entry详解
现在回到最初的起点,捋一捋向某个段中插入数据时,申请新页面的过程。当段中数据较少时,首先会查看表空间中是否有状态为FREE_FRAG的区,如果找到了那么从该区中取一个零散页把数据插进去;否则到表中申请一个状态为FREE的区(也就是空闲的区),把该区的状态变为FREE_FRAG,然后从该新申请的区中去一个零散页把数据插进去。之后在不同的段使用零散页时都从该区中取,直到该去中没有空闲页面;然后该区的状态就变成了FULL_FRAG。
List Node指针分别把状态位FREE,FREE_FRAG,FULL_FRAG对应的区连接成一个链表,当想查找FREE_FRAG状态的区时,直接把FREE_FRAG链表的头节点拿出来,从这个节点对应的区中取一些零散页来插入数据,当这个节点对应的区中没有空闲的页面时,就修改他的State值然后将其从FREE_FARG链表中移到FULL_FRAG链表中。同理如果FREE_FRAG链表中一个节点都没有,那么直接从FREE链表中去一个节点移动到FREE_FRAG链表并修改该节点的STATE字段值位FREE_FRAG,然后再从去中获取零散页就好了。
当段中的数据占满了32个零散页后,直接申请完整的区来插入数据。
然后我们基于段号(Segment ID)来建立链表,为每个段中的区对应的XDES Entry结构建立了三个链表:
1.FREE链表:同一个段中,所有页面都是空闲页面的区对应的XDES Entry结构会加入这个链表中(与表空间的FREE链表区分开)
2.NOT_NULL链表:同一个段中,仍有空闲页面的区对应的XDES Entry结构会加入这个链表中
3.FULL链表:同一个段中,已经没有空闲页面的区对应的XDES Entry结构会加入这个链表中

每个索引都对应两个段,每个段都会维护上述的三张表,除此外还有表空间的3个链表。
**4.**链表基节点
前提:上述了许多链表,我们怎么找到这些链表?或者说怎么找到链表头节点或者尾节点在表空间的位置?
List Base Node 链表基节点的结构:

  • List Length:表明该链表一共有多少个节点
  • First Node Page Number 和First Node Offset:表明链表的头节点在表空间中的位置
  • Last Node Page Number 和Last Node Offset:表明链表的头、尾节点在表空间中的位置

总结:表空间是由若干个区组成,每个区对应一个XDEX Entru结构,直属于表空间的区对应的XDES Entry结构可以分为FREE,FREE_FRAG,FULL_FRAG这三个链表。每个段可以拥有若干个区,每个段中的区对应的XDES Entry结构可以构成FREE,NOT_FULL,FULL这三个链表。每个链表都对应一个List Base Node结构。
5. 段的结构
每个段有一个INODE Entry结构:

  • SegmentID:对应段的编号
  • NOT_FULL_N_USED:在NOT_FULL链表中已经使用了多少个页面
  • 3个List Base Node:分别为段的FREE链表,NOT_FULL,FULL联保定义了List Base Node
  • Magic Number:用来标记这个INODE Entry是否已经被初始化(值为97,937,874)
  • Fragment Array Entry:共有32个,每个对应着一个零散的页面(每个4字节)

各类型页面详细情况
1.FSP_HDR类型
它是表空间第一个页面也是第一组的第一个页面,主要保存了表空间的一些整体属性以及第一个组内256个区对应的XDES Entry结构。
FSP_HDR类型结构示意图
(1)File Space Header
File Space Header结构示意图

 - Space ID:表空间ID
 - Not Used:未被使用
 - Size:当前表空间拥有页面数
 - List Base Node for FREE List, List Base Node for FREE_FRAG List,List Node for FULL_FRAG List:分别是直属于表空间的FREE,FREE_FRAG,FULL_FRAG链表的基节点,此位置固定就是在白空间的第一个页面(FSP_HDR类型的页面)
 - FREE_N_Used:表明在FREE_FRAG链表中已经使用的页面数量
 - FREE Limit:表空间对应着具体的磁盘文件(一般自增长),创建表空间时需要对区建立对应的XDES Entry结构,为各个段简历INODE Entry结构,建立各种链表在内的操作。对于非常大的磁盘文件,有大部分空间是空闲的,我们可以选择把所有空闲的区对应的XDES Entry结构加入FREE链表中,亦可以选择一部分加入,等区不够用的时候再把之前没有加入FREE链表空闲区对用的XDES Entry结构加入到FREE链表。对于自增涨文件来说,可能发生一次自增长时分配的磁盘空间非常大同样可以采用上述操作。中心思想就是等用的时候再把他们加入到FREE链表,FREE Limit字段表示的页号之后的区都未被使用,尚未被加入FREE链表中
 - Next Unused Segment ID:表示当前表空间中最大的段ID的下一个值,这样在创建新段时为其赋予唯一ID。
 - Space Flags:表空间中一些与布尔类型相关的属性。
 - List Base Node for SEG_INODES_FULL List 与 List Base Node for SEG_INODES_FREE List:每个段对应的INODE Entry结构会集中存放到一个类型为INODE的页,这些也会构成上述两种链表,分别为没有空闲空间与仍有空闲空间存储INODE Entry结构。

(2)XDES Entry部分:一个大小为40字节,应为一个页面大小为16KB,只能存放数量有限的XDEXEntry结构(256个)所以256个区划为一组。

2.XDES 类型
XDES 类型页面示意图
将表空间分为若干个组,每组开头的一个页面记录着本组内所有的区对应的XDES Entry结构,因为第一组第一个页面也是表空间第一个页面,需要额外记录其他表空间信息也就是FSP_HDR类型的页面,除第一组外的,我们把之后的每个分组第一个页面类型定义为XDES.
3.IBUF_BITMAP类型
每个组的第二个页面类型均为此,主要记录了一些有关Change Buffer的东西。我们往数据库表中插入一条记录本质上就是向每个索引对应的B+树种插入记录。这些页面在表空间中随机分布会造成大量I/O,严重影响性能。对于Update与Delete,在修改非唯一二级索引时,如果该页面尚未加载到内存中,修改将被暂时缓存到Change Buffer中,之后服务器空闲或者其他原因导致对应的页面从磁盘上加载到内存中时,在将修改合并的对应页面。
4.INODE类型
INODE类型页面结构示意图

  • INODE Entry部分:对应段内零散的页面以及属于该段的FREE,NOT_FULL,FULL链表的基节点,每个占192字节,一个页面可以存储85个。
  • List Node for INODE Page List:一个表空间的段超过85个时,就需要多个INODE类型的页面,为了管理这些页面区分为两个链表:
    SEG_INODES_FULL链表:在此链表中INODE类型的页面中已经没有空闲时间来存储额外的INODE Entry结构
    SEG_INODES_FREE链表:INODE类型的页面中还有空闲时间类存储额外的INODE Entry结构。
    此时就与上边FSP_HDR类型页面的FIle Space Header中存放的两个链表基节点关联上了。以后每新创建一个段,都会创建一个INODE Entry结构,存储此结构的过程是这样的:先看 SEG_INODES_FULL链表是否为空,如果不为空,直接从此链表中获取一个仍有空闲空间的INODE类型的页面,然后把该INODE Entry结构放到该页面中。当该页面中没有剩余空间则将该页面放到SEG_INODES_FULL链表中。
    如果SEG_INODES_FREE链表为空,则从表空间的FREE_FRAG链表中申请一个页面,并将该页面的类型修改为INODE,把该页面放到SEG_INODES_FREE链表中同时把INODE Entry结构放到申请的页面中。

Segment Header结构的运用
Segment Header结构

在Page Header中有两个结构:PAGE_BTR_SEG_LEAF 与 PAGE_BTR_SEG_TOP,它们其实对应的就是Segment Header结构。

  • Space ID of INODE Entry :INODE Entry结构所在的表空间ID
  • Page Number of the INODE Entry:INODE Entry结构所在页面页号
  • Byte Offset of the INODE Entry:在页面中的偏移量

这样就很清晰了:PAGE_BTR_SEG_LEAF 记录着叶子节点段对应的INODE Entry结构地址是在哪个表空间哪个页面哪个偏移量,
PAGE_BTR_SEG_TOP记录着非叶子节点段对应的INODE Entry结构的地址是那个表空间中哪个页面的那个偏移量。一个索引对应两个段,只在索引跟页面中记录这个两个结构就可以了。

二.系统表空间结构

MySQL只用一个系统表空间,系统表空间中需要记录一些与整个系统相关的信息,表空间ID为0相当于所有表空间的大哥。
1.系统表空间的整体结构
整体结构梳理:
整体概述
可以看出系统表空间跟独立表空间前3个页面(页号为0,1,2)的类型为一致,但是页号为3 到7页面是系统表空间独有的。

  • SYS:insert Buffer Header 存储Change Buffer的头部信息
  • INDEX :insert Buffer Root 存储Change Buffer的根页面
  • TRX_SYS:Transction System 事务系统的相关信息
  • SYS:First Rollback Segment 第一个回滚段的信息
  • SYS:Data Dictionary Header 数据字典头部信息
    除了这几个记录系统属性的页面之外系统表空间的extent1和extent2这两个区也就是页号从64-191这128个页面称为Doublewrite Buffer(双写缓冲区)

InnoDB数据字典
MySQL除了保存我们插入的用户数据还需要保存许多额外的信息,这些数据实际上是为了更好地管理用户数据而不得已引入的额外数据,简称元数据。比如:某个表属于那个表空间,表里有多少列,每个列的类型,该表有多少索引,每个索引对应多少字段,该索引对应的跟页面在哪个表空间的哪个页面,该表有哪些外键等等。

  • SYS_TABLES 整个InnoDB存储引擎中所有表的信息
  • SYS_COLUMNS 整个InnoDB存储引擎中所有列的信息
  • SYS_INDEXES 整个InnoDB存储引擎中所有索引的信息
  • SYS_FIELDS 整个InnoDB存储引擎中所有索引对应的列的信息
  • SYS_FOREIGN 整个InnoDB存储引擎中所有外键的信息
  • SYS_FOREIGN_COLS 整个InnoDB存储引擎中所有外键对应的列的信息
  • SYS_TABLESPACES 整个InnoDB存储引擎中所有的表空间信息
  • SYS_DATAFILES 整个InnoDB存储引擎中所有表空间对用的文件系统的文件路径信息
  • SYS_VIRTUAL 整个InnoDB存储引擎中所有虚拟生成的列的信息
    这些系统表页称为数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中,其中SYS_TABLES,SYS_COLUMNS,SYS_INDEXES,SYS_FIELDS这4个表尤其重要,称为基本系统表。

Data Dictionnary Header结构
Data Dictionary Header
设计InnoDB的大叔用一个固定的页面来记录这4个表的聚簇索引跟二级索引对应的B+树位置,就是系统表空间第一组内的7号页面(从0开始)类型为SYS 记录了Data Dictionary Header(数据字典的头部信息),所有信息如图所示。
Data Dictionary Header:记录了一些基本系统表的跟页面位置以及InnoDB存储引擎的一些全局信息。我们看到还有Segment Header部分,这意味这把这些有关数据字典信息当成一个段来分配存储空间,但是只是一个碎片页。

  • Max Row ID:没有显示的指定主键时并且表中没有不允许存储NULL值的UNIQUE键,会生成一个row_id的列作为主键。Max Row ID 全局共享,无论哪个拥有row_id的表插入一条数据该记录的row_id的值就是Max Row ID 对应的值,然后再把Max Row ID 对应的值加1.
  • Max table ID:所有表都对应唯一的ID
  • Max Index ID:每个索引都对应一个唯一的ID
  • Max Space ID:所有的表空间都对应一个唯一的ID
  • Mix ID Low(Unused):
  • Root of SYS_TABLES clust index:表示SYS_TABLE 表聚簇索引的跟页面的页号
  • Root of SYS_TABLE_IDS clust index:表示SYS_TABLES表为ID列建立的二级索引的跟页面页号
  • Root of SYS_TABLE_COLUMNS clust index:表示SYS_COLUMNS表聚簇索引的跟页面页号
  • Root of SYS_INDEXES clust index:表示SYS_INDEXS表聚簇索引跟页面页号
  • Root of SYS_FIELDS clust index:表示SYS_FIELDS表聚簇索引跟页面页号

information——schema 系统数据库
用户不能直接访问InnoDB的这些内部系统表,所以在这个系统库内提供了一些以INNODE_SYS开头的表,这些表并不是真正的1内部系统表,而是在存储引擎启动时读取这些以SYS开头的系统表,然后填充到INNODB_SYS开头的表中。

第十章 单表访问方法

总结

1.const:通过主键或者唯一二级索引列与常数的等值比较来定位一条记录。如果主键或者唯一二级索引的索引列由多个列构成,需要在索引列中的每一个列都与常熟进行等值比较时,这个const访问方法才有效。

select * from single_table where id = 1438;//id为主键
select * from single_table where key2 = 3841;// key2列为唯一二级索引
注意:对于唯一二级索引列在查询列为NULL时,它并不限制NULL值的数量,所以会有多条记录。(不是const访问方法)
select * from single_table where key2 is NULL;

2.ref:搜索条件为二级索引列于常熟进行等值比较,形成的扫描区间是单点扫描区间,采用二级索引来执行查询。(可能有多列)
采用二级索引执行查询时,每获得一条二级索引记录就会执行一次回表操作,这种ref访问方法比const差了一点。

select * from single_table where key1 = 'abc'//key1 为二级索引列
注意:1.在二级索引列允许存储NULL值时,无论普通的二级索引还是唯一二级索引并不限制null值的数量(即key is null 这种形式),这种查询时最多只能使用ref方式,不能使用const
     2.对于包含多个列的二级索引来说,只要最左边连续的列是与常数进行等值比较,就可以采用ref访问方法:
     select * from single_table where key_part1 = 'god like';
     select * from single_table where key_part1 = 'god like' and key_part2 ='legendary';
     select * from single_table where key_part1 = 'god like' and key_part2 ='legendary' and key_part3 ='penta kill';
     (idx_key_part 索引是上述三个列组成的联合索引)
 如果索引列中最左边连续的列不全部是等值比较的话,那它的访问方法就不能称为ref,而是range,例如:
   select * from single_table where key_part1 = 'god like' and key_part2 > 'legendary'

3.ref_or_null:我们不仅想找出某个二级索引列的值为常数还想把该列中的值为null的记录也找出来。(值为null的记录会放啊在索引的最左边)

select * from single_table where key1= 'abc' or key1 is null;
当使用二级索引而不是全表扫描时对应的扫描空间就是[NULL,NULL],['abc','abc']

4.range:使用索引执行查询时,对应的扫描区间为若干个单点扫描区间或者范围扫描区间的方法。(仅包含一个单点扫描区间不能称为range),扫描区间为负无穷到正无穷的访问方法也不能称为range。

select * from single_table where key2 in (1438, 6328) or (key2 >=38 and key2 <=79)//对应的扫描区间:[1438,1438],[6328,6328],[38,79]

5.index:扫描全部二级索引记录的方法称为index访问方法

select key_part1,key_part2,key_part3 from single_table where key_part2 ='abc';
由于key_part2并不是联合索引idx_key_part1的索引列中最左边的列,无法形成合适的区间来减少需要扫描的记录数量无法使用ref或者range访问方法来执行。
另外如果通过全表扫描对InnoDB存储引擎的表执行查询时,添加了order by 主键时,认定为使用index访问方式。
select * from single_table order by id;

6.all:对于InnoDB表来说也就是直接扫描全部的聚簇索引记录。

注意事项

1.二级索引:在使用索引来减少需要扫描的记录数量时一般情况下只会为单个索引生成扫描区间。

select * from single_table where key1 = 'abc' and key2 > 1000

2.索引合并

(1)Intersection(交集) 索引合并

select * from single_table where key1 = 'a' and key3 = 'b';
有三种执行索引的方案:
 1. 使用idx_key1索引执行查询,对应的扫描区间就是[a,a],对获取的每条二级索引记录都根据id进行回表操作然后在判断key3='b' 是否成立。
 2. 使用idx_key3索引执行查询,对应的扫描区间就是[b,b],对获取的每条二级索引记录都根据id进行回表操作然后在判断key1 = 'a' 是否成立。
 (上述两种情况其实在值固定的情况下是按照主键大小去排序的)
 3.同时使用idx_key1 和 idx_key3执行查询,也就是在idx_key1 扫描key1值在['a','a']区间中的二级索引记录同时idx_key3扫描key3值在['b','b']的二级索引记录,然后从两者的操作结果中找出id列值相同的记录之后在进行回表操作。

也就是说Intersection索引合并是要求从不同二级索引中获取的二级索引记录都是按照主键值排好序的。

select * from single_table where key1 > 'a' and key3 = 'b';//这是不能使用Intersection索引合斌的方式执行查询
select * from single_table where key1 = 'a' and key_part1 = 'a';//在idx_key_part二级索引中 key_part1值为'a'的二级索引记录并不是按照主键值进行排序的
------------------------------------------------------------------------------------------------
select * from single_table where key1 = 'a' and id >9000;
这种情况就是二级索引中key1值为'a'的二级索引记录是按照id值排序的,就可以直接使用idx_key1执行查询。

(2)Union(并集)索引合并

select * from single_table where key1 = 'a' or key3 = 'b';
//上述我们只是用其中一个索引查询对应的扫描区间就是负无穷到正无穷
我们可以采用Union索引合并,也就是在idx_key1中扫描key1的值位于[a,a]区间中的二级索引记录,同时在idx_key3中扫描key3值位于['b','b']区间的二级索引记录,然后根据二级索引记录的id值在两者的结果中去重然后在进行回表操作。同时也要求从不同索引中获取的二级记录是按照主键值排好序的。原因有两点:
   1.从两个有序集合执行去重操作比从两个无序集合中执行去重操作容易一些
   2.如果id值是有序的话,那么根据这些id值进行回表操作会减少随机I/O
 select * from single_table where key1 > 'a' or key3 = 'b';//idx_key1中获取的二级索引记录并不是按照主键排序的
 select * from single_table where key1 = 'a' or id > 9000;//这种是没问题的
 select * form single_table where (key_part1 = 'a' and key_part2 = 'b' and key_part3 = 'c') or (key1 ='a' and key3 ='b');
 上边的这个sql我们可以先通过idx_key1和idx_key3执行Intersection索引合并,这样可以找到与后部分匹配的记录然后再通过idx_key_part执行Union索引合并即可。

(3)Sort -Union索引合并

select * from single_table where key1< 'a' or key3 >'z';
先根据 idx_key1中获取二级索引记录,然后按照主键大小排序,同时 根据key3 > 'z' 条件从idx_key3 二级索引中获取二级索引记录并将其按照主键排序,在Union索引合并方式。
注意:并没有Sort-Intersection索引合并 我猜跟比较方式有关,intersection是一条一条比较而且它是针对 单独根据搜索条件从某个二级索引中获取的记录数太多,导致回表成本太大的使用场景。

第十一章 连接的原理

总结

11.1连接简介

1.驱动表与被驱动表

select * from t1, t2 where t1.m1 > 1 and t1.m1 = t2.m2 and t2.n2 < 'd';
//这个查询中有3个过滤条件

 1. t1.m1 > 1
 2. t1.m1 = t2.m2 
 3. t2.n2 < 'd'
 步骤1:首先确定第一个要查询的表,这个表称为驱动表
 在单表查询语句中只需要选取代价最小的访问方式去执行查询语句(const,ref,ref_or_null,range, index merge,index,all)。此时我们直接查找满足t1.m1<1的所有记录。(没有索引,所以访问方法为all)
 步骤2:步骤1中从驱动表每获取的一条记录都需要到t2表中查找匹配的记录。
 我们可以称t2为被驱动表,步骤1从t1中获取两条记录,根据两个表的过滤条件t1.m1 = t2.m2就派上用场了,对于t1中的第一条记录过滤条件t1.m1 = t2.m2就相当于t2.m2 = 2,然后再对t2进行单表查询。
 从上面两个步骤来看我们查询了一次t1表,查询了两次t2表。也就是说驱动表访问一次,被驱动表可能被访问多次。需要注意的是并不是将所有满足条件的驱动表记录先查询出来放到一个地方,然后再去被驱动表中查询,而是每获得一次驱动表记录就立即到被驱动表中寻找匹配的记录。

2.内连接与外连接

  1. 对于内连接的两个表,若是驱动表中的记录在被驱动表中没有找到与之匹配的记录,则该记录不会加入到最后的结果集中。
  2. 对于外连接的两个表,即使驱动表中的记录在被驱动表中没有匹配的记录也仍需要加入到结果集中
    有时对于外连接来说我们也不想把驱动表的全部记录都加入到最后的结果集,把过滤条件分为两种就可以解决这个问题:
  3. WHERE 子句中的过滤条件:无论是内连接还是外连接凡是不符合where子句中的过滤条件的记录都不会被加入到最后的结果集。
  4. ON子句中的过滤条件:对于外连接的驱动表中的记录老说,如果无法在驱动表中找到匹配ON子句中过滤条件的记录,那么该驱动表的记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用null值填充。
    ON子句是专门为了‘外连接驱动表中的记录在被驱动表找不到匹配记录时是否应该把该驱动表记录加入结果集中’场景提出的。如果把ON子句放到内连接中,MySQL会把它向where一样对待。

11.2连接的原理

1.嵌套循环连接
驱动表只访问一次,但被驱动表却可能访问多次,且访问次数取决于对驱动表执行单表查询后的结果集中有多少条记录。这种方式称为Nested-Loop Join。
2.使用索引加快连接速度
3.基于块的嵌套循环连接

从上述我们了解到,驱动表结果集中有多少条记录,就可能把被驱动表从磁盘加载到内存中多少次。MySQL设计的大叔提出一个Join Buffer(连接缓存区)的概念,它就是在执行连接查询前申请一块固定大小的内存。先把若干条驱动表结果集中的记录装在这个JoinBuffer中,然后开始扫描被驱动表,被驱动表每一条记录一次性的与JoinBuffer中的多条驱动表记录进行匹配。由于匹配的过程都是在内存中完成的,所以这样可以减少被驱动表的I/O代价。JoinBuffer的大小可以通过启动选项或者系统变量join_buffer_size进行配置,默认大小256KB,最小可以设置为128字节,需要注意的是joinBuffer中并不会存放驱动表记录的所有列,只把查询列表的列和过滤条件中的列放到Join buffer中。

第十二章 基于成本的优化

总结

12.1基于成本的优化步骤

 1. 根据搜索条件找出所有可能是用的索引
 2. 计算全表扫描的代价
 3. 计算使用不同索引执行查询的代价
 4. 对比各种执行方案的代价,找出成本最低的那个方案
比如:select * from single_table 
      where key1 in ('a','b','c')
       and key2 > 10 and key2 < 1000 
       and key3 > key2 and key_part1 like '%hello'
       and common_field = '123';  

1.计算全表扫描的代价
全表扫描的意思就是把聚簇索引中的记录都依次与给定的搜索条件进行比较,并把符合搜索条件的记录加入到结果集中。所以需要将聚簇索引对应的页面加载到内存中,然后再检测记录是否符合条件。查询成本 = I/O成本 + CPU成本
通过 SHOW TABLE STATUS语句查看某个表的统计信息 :
SHOW TABLE STATUS

Rows:表示表中的记录条数,对于MyISAM存储引擎的表来说,值是准确的;对于使用InnoDB存储引擎的表来说该值是一个估计值。
Data_length:表示表占用的存储空间字节数,对于InnoDB存储引擎的表来说该值就相当于聚簇索引占用的空间大小:Data_length = 聚簇索引的页面数量 * 每个页面的大小。
I/O成本:97 * 1.0 + 1.1 =98.197是指页面数量,1.0指的是加载一个页面的成本常熟,后边的1.1是一个微调值。
CPU成本:9693 * 0.2 + 1.0 = 1939.69693指的是统计数据中表的记录数,0.2是访问一条记录所需的成本常熟数。

2.计算使用不同索引执行查询的代价(先分析唯一二级索引的成本在分析使用普通索引的成本)
(1)使用 uk_key2执行查询的成本分析:

key2 >10 and key2<1000,扫描区间就是(101000)二级索引+回表方式执行的查询,成本= 扫描区间数量 和 需要回表的记录数。
1.扫描区间数量:无论某个扫描区间的二级索引占用了多少个页面,查询优化器粗暴地认为读取索引的一个扫描区间的I/O成本与读取一个页面的I/O成本是相同的。
2.需要回表的记录数:通过索引定位到左区间记录与右区间记录,性能消耗可以不计(const级别)。如果最左记录跟最右记录相隔不太远,就可以精确的统计出满足条件的二级索引记录的条数,否则沿着区间最左记录向右读10个页面,计算下每个页面平均包含多少记录再乘区间最左记录到区间最右记录之间的页面数量就可以了。(递归找索引两个页之间有多少目录项)此例子中大概有95条记录所以付出的CPU成本:95 * 0.2 +0.01 =19.01(0.2为读取一条记录的成本常数)3.根据这些记录的主键值进行回表操作
我们认为每次回表操作都相当于访问一个页面,也就是说二级索引扫描区中有多少记录就进行多少次回表操作,也就是需要多少次页面I/O操作。
95 * 1.0 = 95.01.0是读取一个页面的I/O成本)。
4.回表操作后得到完整的用户记录然后再检测其他搜索条件是否成立
95 * 0.2 = 19.00.2是检测一条记录是否符合给定搜索条件的成本常数)
所以使用uk_key2的成本:
   I/O成本:1.0 *95 +1.0 = 96.0 (扫描区间的数量+需要回表的二级索引记录数)
   CPU成本:95 * 0.2 +0.01 + 95 * 0.2 = 38.01(读取二级索引记录的成本+读取并检测回表操作后聚簇索引记录的成本)

(2)使用idx_key1执行查询的成本分析:

 1.扫描区间的数量:idx_key1对应的搜索条件是 key1 in'a','b','c'),相当于3个单点扫描区间 I/O成本为3 * 1.0 = 3.0
 2.需要回表的记录数:读取这些二级索引记录的CPU成本就是118 *0.2 +0.01 = 23.61
 3.根据这些记录的主键值进行回表操作:I/O成本:118 * 1.0 = 118.0
 4.回表之后得到完整的用户记录然后再检测其他搜索条件是否成立:CPU成本:118 * 0.2 = 23.60
 所以使用idx_key1执行查询的成本:
    I/O成本:3.0 + 118 *0.1 = 121.0(扫描区间的数量+预估的二级索引记录条数)
    CPU成本:118 * 0.2 +0.01 + 118 * 0.2 = 47.21(读取二级索引记录的成本 + 读取并检测回表操作后聚簇索引记录的成本)
  是否有可能使用索引合并(Index Merge):而对于idx_key1 和 uk_key2 都是范围查询,也就是说二级索引记录并不是按照主键值进行排序的不满足Intersection合并的条件。
 

3.基于索引统计数据的成本计算
有时在使用索引执行查询时会有许多单点扫描区间,使用IN语句就很容易产生非常多的单点扫描区间,由于这个索引并不是唯一二级索引所以并不能确定一个单点扫描区间内对应的二级索引记录的条数有多少,把这种通过直接访问索引对应的B+树来计算某个扫描区间内对应的索引记录条数的方式称为index_dive。如果扫描区间过多这些单点扫描区间对应的索引记录条数的成本比直接全表扫描的成本都大。所以设置一个系统变量:eq_range_index_dive_limit 默认值为200,如果通过IN语句生成的单点扫描区间的数量小于200个,则使用index dive来计算各个单点扫描区间对应的记录条数;如果大于等于200个,则使用索引统计数据(index statistics)来进行估算。

MySQL会为表中的每个索引维护一份统计数据,通过SHOW INDEX FROM 表名来查看: show index from t_osm_objectshow index from

1.Cardinality:该列中不重复值的数量,对于联合索引 idx_key_part 来说:key_part2列的Cardinality代表key_part1,key_part2的组合不重复的数量。
当IN语句中对应的单点区间数量大于或等于系统变量eq_range_index_dive_limit的值时,不会使用index dive来计算各个单点区间对应的索引记录条数,而是使用索引统计数据(index statistics)。这里的索引统计数据指的是下面这两值:

 - 使用SHOW TABLE STATUS语句显示出来的Rows值:表示一个表中有多少条记录。
 - 使用SHOW INDEX 语句显示出来的Cardinality属性。结合ROWS统计数据我们可以计算出在某一个列中一个值平均重复多少次。一个值的重复次数大约等于Rows除以Cardinality的值。
 single_table表的idx_key1索引为例,Rows值是9693,Key1列的Cardinality值是968,所以key1列单个值的平均重复次数 :9693 除以 698 大概10条记录。
 假设IN语句对应着20000个单点扫描区间,就直接使用统计数据来估算这些单点扫描区间对应的记录条数。每个单点扫描区间大约对应10条记录,所以一共需要回表的记录数就是20000 * 10 = 200000.
 使用索引统计数据来计算致命弱点就是不精确!也可能查询成本与实际执行成本可能相差很大。
 
#### 12.2 连接查询的成本
1.条件过滤 (Condition Filtering)
MySQL中连接查询采用的是嵌套循环连接算法,驱动表会被访问一次,被驱动表可能会被访问多次。
 - 单次查询驱动表的成本
 - 多次查询被驱动表的成本
我们把查询驱动表后得到的记录条数称为驱动表的扇出(fanout)。显然驱动表的扇出值越小,对被驱动表的查询次数也就越少,查询链接的总成本也就越低。

```sql
select * from s1 inner join s2 where s1.common_field > 'xyz';//多了一个搜索条件只能猜测9693条记录有多少满足这个条件
select * from s1 inner join s2 where s1.key2 > 10 and s1.key2 < 1000 and s1.common_field > 'xyz';//因为有二级索引uk_key2的缘故,只需要在二级索引扫描区间的记录中猜测有多少条记录符合 s1.common_field > 'xyz'即可。
总结:如果使用全表扫描的方式执行单表查询,那么计算驱动表扇出值时需要猜测满足全部搜索条件的记录到底有多少条。2.如果是使用那个索引来执行单表查询,那么计算驱动表扇出值需要猜测除了满足形成索引扫描区间的搜索条件还满足其他搜索条件的记录有多少条。
2.两表连接的成本分析
连接查询总成本 = 单词访问驱动表的成本 + 驱动表扇出值 * 单词访问被驱动表的成本
优化重点:1.尽量减少驱动表的扇出 2.访问被驱动表的成本尽量要低 其实第二点十分有用,我们需要尽量在被驱动表的连接列上建立索引,这样就可以使用ref访问方法来减低被驱动表的访问成本。(当然最好的是连接列最好是主键索引或者唯一二级索引)
3.多表连接成本分析
多表连接可能会生成多少种连接顺序:n的阶乘种连接顺序
在n个表进行连接时,MySQL查询优化器需要计算每一种连接顺序的成本么?其实真的要计算只不过设计MySQL的大叔想了很多方法来减少因计算不同连接顺序下的查询成本而带来的性能损耗:
 - 提前结束某种连接顺序的成本估算
 - 系统变量optimizer_search_depth(如果连接表的数量小于它则穷举分析每一种连接顺序的成本,否则只进行到相等数量的穷举)
 - 某些规则压根而就不考虑某些连接顺序(系统变量 optimizer_prume_level 来控制是否启用这些规则)

12.3 调节成本常数

1.mysql.server_cost:对应了server层进行的一些操作对应的成本常数
2.mysql.engine_cost:在存储引擎层进行的一些操作所对应的成本常数

第十三章 InnoDB统计数据是如何收集的

总结

13.1基于磁盘的永久性统计数据

统计数据存储在磁盘上,在服务器重启之后这些统计数据依然存在。其实当我们选择把某个表以及该表索引的统计数据存放到磁盘上时实际上是把这些统计数据存储到了两个表中:show tables from mysql like 'innodb%stats'

  • innodb_table_stats存储了关于表的统计数据,每一条记录对应一个表的统计数据
  • innodb_index_stats存储了关于索引的统计数据每一条记录对应着一个索引的一个统计项数据
    1.在innodb_table_stats中几个重要的字段:
  • database_name:数据库名
  • table_name:数据库表名
  • n_rows:表明表中大约有多少条记录
  • clustered_index_size:表中聚簇索引占用多少个页面
  • sum_of_other_index_sizes:其他索引一共占用多少个页面
关于clustered_index_size和sum_of_other_index_sizes统计项的收集
首先我们知道一个索引对应两个段,每个段由一些零散的页面以及一些完整的区构成。分为叶子节点段与非叶子节点段,需要分别统计,主要步骤如下:
1.从数据字典中找到表的各个索引对应的跟页面位置,系统表SYS_INDEXES中存储了各个索引对应的跟页面信息。
2.从根页面的Page Header中找到叶子节点段和非叶子节点段对应的Segment Header
3.从两个Segment Header找到两个段对应的INODE Entry结构。
4.根据INODE Entry结构从中找出该段对应的分散页面以及对应的FREE,NOT FULL,FULL链表的基节点。
5.从上述三个链表中统计区的数量,每个区64个页面,在加上分散的页面就可以统计出整个段占用的页面

2.innodb_index_stats中几个重要的字段:

  • database_name: 数据库名
  • table_name: 表名
  • index_name: 索引名
  • stat_name: 统计项名称
  • stat_value: 统计项的值
  • sample_size:生成统计数据而采样的页面数量
 select * from mysql.innodb_index_stats where table_name = 'single_table'
 步骤如下:
 1.先查看index_name列,这个列说明该记录是哪个索引的统计信息,从结果上看PRIMARY索引(主键)占了三条记录,idx_key_part索引占了6条记录
 2.针对index_name列相同的目录,sta_name表示针对该索引的统计项名称
 具体由哪些统计项呢?
 - n_leaf_pages:表示该索引的叶子节点实际上占用了多少页面
 - size:表示该索引一共占有多少页面(包括已经分配给叶子节点段或者非叶子节点段但是尚未使用的页面)
 - n_diff_prxNN:表示对应的索引列不重复的值有多少,对于主键索引或者唯一二级索引只有一个,但是对于其他联合索引或者普通二级索引则会有多条记录。
 例如 idx_key_part有三个列组成的联合索引,则n_diff_prxNN对应的值则有4个。

3.定期更新统计数据
随着我们不断对表的增删改操作,表中的数据一直在变换,对应的innodb_table_ststs与innodb_index_stats也会跟着变。提供了以下两种更新统计数据的方式:

  • 开启innodb_stats_auto_recalc:如果发生变动的记录数超过表大小的百分之10,则会更新这两张统计数据的表。(异步发生)
  • 手动调用ANALYZE TABLE 语句来更新统计数据:analyze table single_table;(过程可能会比较慢)

13.2 基于内存的非永久性统计数据

当把系统变量innodb_stats_persistent的值设置为OFF时,之后船舰的表的统计数据默认就都是非永久性的了。非永久性的统计数据采样的页面数量是由系统变量innodb_stats_transient_sample_pages来控制的,默认值是8。在每次服务器关闭时以及执行某些操作后会被清楚,并在下次访问表时重新计算。这样就可能导致存在重新计算统计数据时得到不同的结果,从而可能生成非常变化的执行计划。

第十四章 基于规则的优化

总结

14.1 条件简化

1.移除不必要的括号

SELECT * FROM (T1,(T2,T3)) WHERE T1.A = T2.A AND T2.B = T3.B;

2.常量传递

 a = 5 AND b > a 就可以转换为 a = 5 AND b > 5

3.移除没用的条件
4.表达式计算

a = 5 + 1;//这个表达式只包含常量就会被化简成a = 6;

5.HAVING子句和WHERE子句的合并
如果查询语句中没有出现诸如SUM,MAX这样的聚集函数以及GROUP BY 子句,查询优化器就把Having子句和WHERE子句合并起来。
6.常量表检测

  • 查询的表中一条记录都没有,或者只有一条记录(并不适用与InnoDB作为存储引擎的表)
  • 使用主键等值匹配或者唯一二级索引列等值匹配作为搜索条件来查询某个表(查询优化器在分析一个查询语句时,首先执行常量表查询,然后把查询中涉及该表的条件全部替换成常数。)

14.2 外连接消除

在指定的WHERE子句中包含被驱动表中的列不为NULL值的条件称为空值拒绝。在被驱动表的WHERE子句符合空值拒绝的条件后,外连接和内连接可以互相转换。这种转换带来的好处就是优化器可以通过评估不同连接顺序的成本,选出成本最低的连接顺序来执行查询。

14.3 子查询优化

1.子查询语法
  • 在select子句中:SELECT (SELECT m1 FROM t1 LIMIT 1)
  • 在from子句中:SELECT m,n FROM (select......)
  • 在WHERE 或 ON子句的表达式中:SELECT * FROM T1 WHERE m1 IN (SELECT m2 FROM t2)
2.按返回的结果集区分子查询
1.标量子查询:只返回一个单一值的子查询称为标量子查询 selectselect m1 from t1 limit 12.行子查询: 返回一条记录的子查询
3.列子查询:查询一个列的数据(多条)
4.表子查询:子查询的结果既包含很多条记录又包含很多个列
3.按与外层查询的关系来区分子查询
1.不相关子查询:如果子查询可以单独运行出结构,而不依赖于外层查询的值,我们就可以把这个子查询称为不相关子查询。
2.相关子查询:如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询成为相关子查询。(select * from t1 where m1 in (select m2 from t2 where n1 = n2)
4.子查询在布尔表达式中的使用
1.使用 =,>,<,>=,<=,<>,!=,<=>作为操作符:操作数 comparison_operator (子查询) 这里的操作数可以是某个列名,或者是一个常量,或者是一个更复杂的表达式,甚至可以是另一个子查询。但需要注意的是 这里的子查询只能是标量子查询或者行子查询,也就是说子查询的结果只能返回一个单一的值或者只能是一条记录:
select * from where (m1,n1) = (select m2,n2 from t2 limit 1);
2.[NOT] IN/ANY/SOME/ALL子查询 
3.EXISTS子查询:我们不关心查出来的结果到底是什么,只在乎子查询的结果集中是否存在记录

需要注意的几点:1.select子句中子查询必须是标量子查询
2.要想得到标量子查询或者行子查询但又不能保证查询的结果集中只有一条记录时应该使用LIMIT 1语句来限制记录数量。
3.对于[NOT] IN / ANY/SOME /ALL 子查询来说,子查询中不允许有LIMIT语句。(非法:select * from t1 where m1 in (select * from t2 limit 2))
4.不允许在一条语句中增删某个表的记录时同时还对该表进行子查询!

14.4 子查询在MySQL中是怎么执行的

1.标量子查询,行子查询的执行方式

select * from s1 where key1 = (select common_field from s2 where key3 ='a' limit 1);
执行方式大致如下:1.单独执行子查询
                2.然后将子查询得到的结果当作外层查询的参数在执行外层查询
select * from s1 where key1 = (select common_field from s2 where s1.key1 = s2.key3 limit 1);
执行方式大致如下:1.先从外层查询中能够获取一条记录
                2.然后从这条记录中找出子查询中涉及的值,执行子查询
                3.最后根据子查询的查询结果来检查外层查询where子句的条件是否成立,如果成立就把外层查询的那就记录加入到结果集否则就丢弃
                4.重复上述步骤知道外层查询记录中获取不到记录为止。
 
#### IN 子查询优化
 1.物化表:对于不相关的IN子查询比如 select * form s1 where key1 in (select common_field from s2 where key3 = 'a');
 如果单独执行子查询后的结果集太多就会导致下面这些问题:
 - 结果集太多,可能内存中都放不下
 - 对于外层查询来说,如果子查询的结果集太多则意味着IN子句中的参数特别多,这将导致:(1)无法有效使用索引,只能对外层查询进行全表扫描(2)在对外层查询来说如果IN子句中的参数特别多,会导致在检测一条记录的IN表达式是否为TRUE时花费太多的时间。
所以不直接将不相关子查询的结果集当作外层查询的参数,而是将该结果集写入一个临时表中。需要注意两点:(1)该临时表的列就是子查询结果集中的列(2)写入临时表的记录会被去重。(一般情况下会建立基于内存的使用MEMORY存储引擎的临时表并且使用哈希索引。如果子查询的结果集非常大则会转而使用基于磁盘的存储引擎来保存结果集中的记录,索引类型也转成B+树索引)我们称临时表为物化表。(因为有了索引所以查询特别快)
2.物化表转连接
select * from s1 where key1 in (select common_field from s2 where key3 = 'a');
当把子查询物化后,从两个角度看待:
 - 从表s1的角度来看待:对于s1表中的每条记录来说,如果该纪录的key1列的值在子查询对应的物化表中则该记录会被加入到最终结果集
 - 从子查询物化表的角度来看:对于子查询物化表的每个值来说,如果能在s1表中找到对应的key1列的值与该值相等的记录,那么就把这些记录加入到最终结果集中。
 这其实就相当于s1与子查询物化表进行内连接。
 总查询成本有下面及部分组成:
 - s1作为驱动表:物化子查询时需要的成本,扫描s1表时的成本,s1表中的记录 * 通过条件m_val = xxx对materialized_table 表进行单表访问的成本。
 - 使用物化表作为驱动表:.....
3.将子查询转换为半连接
select * from s1 where key1 in (select common_field from s2 where key3 = 'a') ->
select s1.* from s1 inner join s2 on s1.key1 = s2.common_field where s2.key3 = 'a';
这两个sql其实很像,只不过我们不能保证对于s1表中的某条记录来说,在s2表中有多少记录满足s1.key1 = s2.common_field。可能或多次将s1中相同的记录加入到最终结果集中。半连接的意思:对于s1表中的某条记录来说我们只关心在s2表中是否存在与之匹配的记录而不关心具体有多少条记录与之匹配,最终的结果集中只保留s1表的记录。
实现半连接的几种方式:
 - Table pullout(子查询中的表上拉) 
 当子查询的查询列表处只有主键或者唯一索引列时,可以把子查询中的表上拉到外层查询的FROM子句中,并且把子查询中的搜索条件合并到外层查询的搜索条件中:`SELECT * FROM s1 where key2  IN (SELECT key2 FROM s2 WHERE key3 = 'a')`
合并之后就是:`SELECT s1.* FROM s1 inner join s2 ON s1.key2 = s2.key2 WHERE s2.key3 = 'a'` 不可能出现多条记录加入结果集的情况
 - Duplicate Weedout(重复值消除):使用临时表的消除半连接结果中重复值的方式称为Duplicate Weedout。
 - LooseScan(松散扫描):
SELECT * FROM s1 WHERE key3 In (SELECT key1 FROM s2 WHERE key1 > 'a' AND key1< 'b');
在子查询中,对于s2表的访问可以使用到key1列索引,得到的重复二级索引记录可能有多条,此时我们只需要取第一条的值到s1表中查找s1.key3 = *** 的记录。虽然是扫描索引但只取键值相同的同一条记录去执行匹配的操作方式称为LooseScan。
 - Semi-join Materiallization (半连接物化)
 - FirstMatch(首次匹配):先取一条外层查询中的记录然后到子查询的表中寻找符合匹配条件的记录。如果能找到一条则将该外层查询的记录放到最终结果集并且通知查找更多匹配的记录:如果找不到则将外层查询的记录丢弃然后再开始取下一条外层查询的记录,重复上述结果知道外层查询获取不到记录为止。对于相关子查询:
 select * from s1 where key1 in (select common_field from s2 where s1.key3 = s2.key3);转换为半连接
 => select s1.* from s1 SEMT JOIN s2 on s1.key1 = s2.common_field AND s1.key3 = s2.key3;接下来就可以使用前面介绍的几种半连接执行策略来执行查询。需要注意的是由于相关子查询并不是一个独立的查询,所以不能转换为物化表来执行查询。
 4.半连接的使用条件
 并不是所有包含IN子查询的查询语句都可以转换为半连接,只有下面这两种形式:
 SELECT .... FROM outer_tables WHERE expr IN (SELECT ... FROM inner_tables..) AND ..
 SELECT ... FROM outer_tables WHERE (oe1, oe2,...) IN (SELECT ie1,ie2,.. From inner_tables...) AND ...
 总结一下只有符合下面这些条件的子查询才可以转换为半连接:
 
 - 该子查询必须是与IN操作符组成的布尔表达式,并且在外层查询的WHERE 或者ON子句中出现。
 - 外层查询也可以有其他的搜索条件,只不过必须使用AND操作符与IN子查询的所有条件连接起来
 - 该子查询必须是一个单一的查询不能使用union连接起来
 - 该子查询不能包含GROUP BY HAVING语句或者聚集函数
5.不适用于半连接的情况
 - 对于不相关的子查询可以尝试把它们物化后再参与查询
 - 无论是相关子查询还是不相关子查询都可以尝试将IN子查询转换为EXISTS子查询。(转为EXISTS子查询后可能会用到索引)
#### ANY/ALL子查询优化
 < ANY(SELECT ...) => < (SELECT MAX(..))
 > ANY(SELECT ...) => > (SELECT MIN(..))
 < ALL(SELECT ...) => < (SELECT MIN(..))
 > ALL(SELECT ...) => > (SELECT MAX(..))
#### [NOT] EXISTS 子查询优化
如果是不相关子查询可以先执行子查询得出其结果是true还是false,查询优化器会重写查询。
SELECT * FROM s1 WHERE EXISTS (SELECT 1 FROM s2 WHERE key1 = 'a') OR key2 > 100; => SELECT * FROM s1 WHERE true OR key2 > 100;
如果是相关子查询只能先执行外层查询然后在执行子查询,如果子查询中可以使用到索引则查询速度也会加快不少。
#### 对于派生表的优化
把子查询放在外层查询的FROM子句后,这个子查询相当于一个派生表。
 - 把派生表物化:在对派生表进行物化时设计MySQL的大叔使用了一种成为延迟物化的策略,也就是在查询中真正使用到派生表时才会去尝试物化派生表,而不是再执行查询之前就先把派生表物化。
 - 将派生表和外层查询合并

第十五章 EXPLAIN详解

总结

MySQL查询优化器在基于成本和规则对一条查询语句进行优化后,会生成一个执行计划。这个执行计划展示了接下来执行查询的具体方式:比如多表连接的顺序是什么,采用什么访问方法来具体查询每个表等。EXPLAIN语句可以让我们查看某个查询语句的具体执行计划。

15.1执行计划输出中各列详解

1.table:无论多么复杂最后也是对每个表进行单表访问,table代表表明。EXPLAIN SELECT * FROM s1

只涉及单表查询所以XEPLAIN输出中只有一条记录。
2.id
查询语句一般都以SELECT 关键字开头,查询语句中每出现一个SELECT 关键字设计MySQL的大叔就会为它分配一个唯一的id值,对于连接查询来说一个SELECT关键字后面的FROM子句中可以跟随多个表。在连接查询的执行计划中每个表都会对应一条记录但这些记录的id值都是相同的。出现在前面的表表示驱动表,出现在后面的表表示被驱动表。

对于包含子查询的查询语句来说,就可能涉及多个SELECT关键字,所以在包含子查询的查询语句的执行计划中,每个SELECT关键字都会对应一个唯一的id值。比如下面这样:

EXPLAIN SELECT * FROM s1 WHERE key1 INSELECT key1 from s2) or ...


从输出结果中可以看到,s1表在外层查询中,外层查询有一个独立的SELECT 关键字,以及子查询有一个独立的SELECT关键字。需要注意的现在id值是不同的,查询优化器可能对涉及子查询的查询语句进行重写,从而转换为连接查询(半连接),比如将上述查询改为:EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 from s2),此时对应的id值全部都是1这就表明了查询优化器将子查询转为半连接查询。

对于包含union子句的查询语句来说,每个SELECT关键字对应一个id值也是没错的,但还是有点特别的东西。EXPLAIN SELECT * FROMt_userUNION select * from t_user

UNION子句会把多个查询的结果集合并起来并对结果集中的记录进行去重,使用的内部临时表名称就是<union 1,2>,id为NULL表示这个临时表是为了合并两个查询的结果集而创建的。UNION ALL就不需要对最终的结果集去重,它只是单纯的把多个查询结果集中的记录合并成一个并返回给用户,所以不需要临时表。
3.select_type
一条大的查询语句里面可以包含若干个SELECT关键字,每个SELECT关键字代表着一个小的查询语句。每个select语句的FROM子句中都可以包含若干张表(这些表用于连接查询),每一张表都对应着执行计划输出中的一条记录,对于在同一个select关键字中的表来说,id值是相同的。 每一个SELECT关键字代表的小查询都定义了一个名为select_type属性,也就知道了这个小查询在整个大查询中扮演的角色。

select_type的取值大致如下:
1.SIMPLE:查询语句中不包含UNION或者子查询的查询都算作SIMPLE类型。(连接查询或者半连接查询的select_type值也是SIMPLE2.PRIMARY: 对于包含UNIONUNION ALL 或者子查询的大查询来说,它是由几个小查询组成的;其中最左边那个查询的select_type值就是PRIMARY。(即 从左往右的第一个查询)
3.UNION:对于包含UNION或者UNION ALL的大查询来说它是由几个小查询组成的成了最左边的那个小查询外,其余小查询的select_type值就是UNION
4.UNION RESULT:MySQL选择使用临时表完成UNION查询去重工作针对该临时表的查询的select_type就是UNION RESULT。
5.SUBQUERY: 如果包含子查询的查询语句不能转为对应的半连接形式并且该子查询是不相关子查询,而且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个SELECT关键字代表的那个查询的select_type就是SUBQUERY.
6.DEPENDENT SUBQUERY :如果包含子查询的查询语句不能够转为对应的半连接形式,并且该子查询被查询优化器转换为相关子查询的形式,则该子查询的第一个SELECT关键字代表的那个查询select_type就是DEPENDENT SUBQUERY.(此子查询可能被执行多次)
7.DEPENDENT UNION :在包含UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询,则除了最左边的那个小查询之外,其余小查询的select_type的值就是DEPENDENT UNION.
8.DERIVED:在包含派生表的查询中,如果是以物化派生表的方式执行查询,则派生表对应的子查询的select_type就是DERIVED.
EXPLAIN select * from (select key1, count(*) as c from s1 group by key1) as derived_s1 where c > 1.
9.MATERIALIZED:查询优化器在执行包含子查询的语句时,选择将子查询物化之后外层查询进行连接查询,该子查询对应的select_type属性是MATERIALIZED.

4.partitions:分区,如果无分区则值为NULL
5.type:执行计划的一条记录代表着MySQL对某个表执行查询时的访问方法,其中的type列就表明了这个访问方法是啥。

EXPLAIN
 SELECT * FROM `t_osm_object` where id = '000070ad270a44f3bd73d2d8474c0a42'

在这里插入图片描述

 - system:当表中只有一条记录并且该记录使用的存储引擎(MyISAM,MEMORY)的统计数据是精确的,那么对该表的访问方法就是system
 - const:根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const
 - eq_ref:执行连接查询时,如果被驱动表是通过主键或者不允许存储NULL值的唯一二级索引列等值匹配的方式进行访问的(如果该主键或者不允许存储NULL值的唯一二级索引是联合索引,则所有索引列都必须进行等值比较),则对该驱动表的访问方法就是eq_ref.
 - ref:当通过普通的二级索引列与常数进行等值匹配的方式来查询某个表,对该表的访问方法可能就是ref
 - fulltext:全文索引
 - ref_or_null:当对普通二级索引列进行等值匹配且该索引列的值也可以是NULL值时,对表的访问访问方法就可能是ref_or_null
 - index_merge:一般情况下只会为但个索引生成扫描区间,但是在单表访问方法时,特意强调了在某些场景下可以使用Intersection,Union,Sort-Union3中索引合并的方式来执行查询。
 - unique_subquery:类似于两表连接中被驱动表的eq_ref访问方法,unique_subquery针对的是一些包含IN子查询的查询语句。如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询在转换之后可以使用主键或者不允许存储NULL值的唯一二级索引进行等值匹配,那么该子查询执行计划的type列的值就是unique_subquery.
 - index_subquery:在访问子查询中的表时使用的是普通的索引。
 - range:如果使用索引获取某些索引单点扫描区间的记录,那么就可能使用到range访问方法
 - index:当可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index(执行全表扫描并且要对主键进行排序时type列的值也是Index- all:最熟悉的全表扫描

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。