查看:find / -name mysql
MySQL数据库文件的存放路径:/var/lib/mysql/
相关命令目录:/usr/bin
(mysqladmin、mysqlbinlog、mysqldump等命令)和/usr/sbin
。
配置文件目录:/usr/share/mysql-8.0(命令及配置文件),/etc/mysql(如my.cnf)
表在文件系统中的表示:
.ibd
全表检索
还是使用索引检索
),表之间的连接顺序
(先左表还是先右表)如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户逻辑查询
优化阶段和 物理查询
优化阶段。简化为三层结构:
数据库缓冲池:
InnoDB 存储引擎是以页为单位
来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。而磁盘 I/O 需要消耗的时间很多,DBMS 会申请 占用内存来作为数据缓冲池
,在真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool
之后才可以访问。
缓冲池如何读取数据:
缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面是否在缓冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行
读取
如果我们执行 SQL 语句的时候更新了缓存池中的数据,那么这些数据会马上同步到磁盘上吗? 不会,过一段时间再写入磁盘
读都是在缓冲池中读取
我更新到一半突然发生错误了,想要回滚到更新之前的版本,该怎么办?连数据持久化的保证、事务回滚都做不到还谈什么崩溃恢复?
答案:Redo Log & Undo Log
InnoDB:事务型引擎,除了增加和查询
外,还需要更新
、删除操作
,那么,应优先选择InnoDB存储引擎
InnoDB和ACID模型:
MyISAM:非事务处理存储引擎: 速度快 ,对事务完整性没有要求或者以SELECT、INSERT为主的应用
内存
的表:
InnoDB与MyISAM对比:
崩溃后无法安全恢复
。优点:
缺点:
二级索引(辅助索引、非聚簇索引):先由二级索引查到主键,再回表,根据主键再查出完整数据
MyISAM 与 InnoDB对比:
① 在InnoDB存储引擎中,我们只需要根据主键值对 聚簇索引 进行一次查找就能找到对应的记录,而在MyISAM 中却需要进行一次 回表 操作,意味着MyISAM中建立的索引相当于全部都是 二级索引 。
② InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是 分离的 ,索引文件仅保存数据记录的地址。
③ InnoDB的非聚簇索引data域存储相应记录 主键的值 ,而MyISAM索引记录的是 地址 。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
④ MyISAM的回表操作是十分 快速 的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
⑤ InnoDB要求表 必须有主键 ( MyISAM可以没有 )。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。
索引的代价:
增、删、改
操作时,都需要去修改各个B+树索引。而且我们讲过,B+树每层节点都是按照索引列的值 从小到大的顺序排序 而组成了双向链表
。不论是叶子节点
中的记录,还是内节点中的记录
(也就是不论是用户记录
还是目录项记录
)都是按照索引列的值从小到大的顺序而形成了一个单向链表
。而增、删、改
操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些 记录移位
, 页面分裂
、 页面回收
等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。Hash结构效率高,那为什么索引结构要设计成树型呢?
采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点,特别是当 B+ 树比较深的时候,通过自适应 Hash 索引可以明显提高数据的检索效率。
磁盘与内存交互的基本单位 : 页
,InnoDB将数据划分为若干个页, InnoDB中页的大小默认为16KB
以页作为磁盘和内存之间交互的基本单位, 也就是一次最少从磁盘中读取
16KB的内容到内存中, 一次最少把内存中的16KB内容刷新
到磁盘中. 也就是说, 在数据库中, 不论读一行还是读多行, 都是将这些行所在的页进行加载. 也就是说, 数据库管理存储空间的基本单位是页(Page), 数据库I/O操作的最小单位是页. 一个页中可以存储多行记录
页a, 页b, 页c… 页n这些页可以不在物理结构上相连
, 只要通过双向链表
相关联即可. 每个数据页中的记录会按照主键值
从小到大
的顺序组成一个单向链表
, 每个数据页都会为存储在它里面的记录生成一个页目录
, 在通过主键查找某条记录的时候可以在页目录中使用二分法
快速定位到对应的槽位
, 然后再遍历该槽对应分组
中的记录即可快速找到指定的记录
段
, 段又分为了索引段
和数据段
, 数据段就是索引结构的叶子结点层
, 所以说其实叶子结点层的页是相连的另外在数据库中, 还存在着区(Extent), 段(Segment)和表空间(Tablespace)的概念. 行, 页, 区, 段, 表空间的关系如下图所示:
区(Extent)是比页大一级的存储结构, 在InnoDB存储引擎中, 一个区会分配64个连续的页
. 因为InnoDB中的页的大小默认是16KB, 所以一个区的大小是64*16KB = 1MB.
段(Segment) 由一个或者多个区组成, 区在文件系统是一个连续分配
的空间(在InnoDB中是连续的64个页), 不过在段中不要求区域区是相邻的. 段是数据库中的分配单位, 不同类型的数据库对象以不同的段形式存在, 当我们创建数据表, 索引的时候, 就会相应创建对应的段, 比如创建一张表时会创建一个表段
, 创建一个索引时会创建一个索引段
表空间(Tablespace) 是一个逻辑容器, 表空间存储的对象是段
, 在一个表空间中可以有一个或者多个段, 但是一个段只能属于一个表空间. 数据库由一个或者多个表空间组成, 表空间从管理上可以划分为系统表空间
, 用户表空间
, 撤销表空间
, 临时表空间
等
页如果按照类型划分的话, 常见的有数据页
(保存B+树结点), 系统页
, Undo页
和事物数据页
等, 数据页是我们最常使用的页
数据页的16KB大小的存储空间被划分为了7个部分, 分别是文件头
(File Header), 页头
(Page Header), 最大最小记录
(Infimum+supremum), 用户记录
(User Records), 空闲空间
(Free Space), 页目录
(Page Directory)和文件尾
(File Tailer)
作用 : 描述各种页的通用信息(比如页的编号
, 其上一页
, 下一页
是谁等等)
可以看到页其实是不区分数据页和索引页的, 我们说索引页也好, 说数据页也好都是代表的是索引 或者 数据
段是分为索引段
和数据段
的, 因为我们要区分索引和数据进行分开存储
, 至于好处就是为了能多一点顺序IO
, 减少随机IO
fil_page_prev(4字节)和fil_page_next(4字节)
InnoDB都是以页为单位来存放数据的, 如果数据分散到多个不连续的页中存储的话需要把这些页关联起来, fil_page_prev和fil_page_next就分别代表本页的上一个和下一个页的页号. 这样通过建立一个双向链表把许许多多的页就串联起来了
fil_page_space_or_chksum(4字节)
代表当前页面的校验和(checksum)
校验和的作用:
每当一个页面在内存中修改了, 在同步之前就要把它的校验和算出来. 因为File Headeer在页面的前面, 所以校验和会被首先同步到磁盘, 当完全写完时, 校验和也会被写到页的尾部, 如果完全同步成功, 则页的首部和尾部的校验和应该是一致的. 如果写一般断电了, 那么在File Header中的校验和就代表着已经修改过的页, 而在File Trailer中的校验和代表着原先的页, 二者不同则意味着同步中间出了错. 这里, 校验方式就是采用Hash算法进行校验
前4个字节代表页的校验和
这个部分是和文件头(File Header)中的校验和相对应的
后4个字节代表页面被最后修改时对应的日志序列位置(LSN):
这个部分也是为了校验页的完整性的, 如果首部和尾部的LSN值校验不成功的话, 就说明同步过程出现了问题
我们自己存储的记录会按照指定的行格式存储到User Records(用户记录)部分, 但是在一开始生成页的时候, 其实并没有User Records这个部分, 每当我们插入一条记录, 都会从Free Space部分, 也就是尚未使用的存储空间申请一个记录大小的空间划分到User Records部分, 当Free Space部分的空间全部被User Records部分代替掉之后, 也就意味着这个页使用完了, 如果还有新的记录插入的话, 就需要去申请新的页了
User Records中的这些记录按照指定的行格式一条一条摆在User Records部分, 相互之间形成单链表
InnoDB规定了最小记录与最大记录这两条记录的构造十分简单, 都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成的, 如图所示:
这两条记录不是我们自己定义的记录, 所以它们并不存放在页的User Records部分, 它们被单独放在了一个称为Infimum+Supremum的部分, 如图所示:
有了 Infimum Record 和 Supremum Record ,现在查询不需要将某一页的 User Records 全部遍历完,只需要将这两个记录和待查询的目标记录进行比较。比如我要查询的数据 id = 101 ,那很明显不在当前页。接下来就可以通过下一页指针跳到下页进行检索。
可能有人又会说了,你这 User Records 里不也全是单链表吗?即使我知道我要找的数据在当前页,那最坏的情况下,不还是得挨个挨个的遍历100次才能找到我要找的数据?你管这也叫效率高?
不得不说,这的确是个问题,不过是一个 MySQL 已经考虑到的问题。不错,挨个遍历确实效率很低。为了解决这个问题,MySQL 又在页中加入了另一个区域 Page Directory
。
顾名思义,Page Directory 是个目录
,里面有很多个槽位
(Slots),每一个槽位都指向了一条 User Records
中的记录。大家可以看到,每隔几条数据,就会创建一个槽位
。
MySQL 会在新增数据的时候就将对应的 Slot
创建好,有了Page Directory
,就可以对一张页的数据进行粗略的二分查找
。至于为什么是粗略,毕竟 Page Directory 中不是完整的数据,二分查找出来的结果只能是个大概的位置,找到了这个大概的位置之后,还需要回到 User Records 中继续的进行挨个遍历匹配。
在页中, 记录是以单向链表的形式进行存储的, 单向链表
的特点就是插入, 删除非常方便, 但是检索效率不高
, 最差的情况下需要遍历链表上的所有结点才能完成检索. 因此在页结构中专门设计了页目录
, 专门给记录做一个目录, 通过二分查找法
的方式进行检索, 提升效率
由于槽指向的是一组中最大的值, 所以如果我们判断到某个值比我们的上一个槽值大, 比下一个槽值小的时候, 那么我们就应该到上一个槽的位置, 上一个槽指向的就是上一个页目录中最大的, next_record指针指向的就是下一个槽中的最小值了, 因为页内是单向指针, 所以我们必须要从前往后找
为了能得到一个数据页中存储的状态信息, 比如本页中已经存储了多少条记录, 第一条记录的地址值是什么, 页目录中存储了多少个槽等等, 特意在页中定义了一个叫做Page Header的部分, 这部分占用固定的56个字节, 专门存储各种状态信息
删除标记
而已, 所有被删除掉的记录都会组成一个所谓的垃圾链表
, 之后如果有新记录插入到表中的话, 可能把这些被删除的记录占用的存储空间覆盖
掉min_rec_mask
B+树的每层非叶子结点
中最小记录
都会添加该标记, min_rec_mask值为1
record_type
这个属性表示当前记录的类型,
一共有4种类型的记录:
0 : 表示普通记录
1 : 表示B+树非叶子结点记录
2 : 表示最小记录
3 : 表示最大记录
heap_no
这个属性表示当前记录在本页中的位置
我们插入的4条记录在本页中的位置分别是: 2,3,4,5最小记录和最大记录的heap_no值分别是0和1
n_owned
页目录中每个组最后一条记录的头信息中会存储该组一共多少条记录, 作为n_owned字段
可以看到n_owned和页目录是密切相关的
next_record
记录头信息里该属性非常重要, 它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量
当数据页中存在多条被删除掉的记录时, 这些记录了的next_record属性将会把这些被删除掉的记录组成一个垃圾链表, 以备之后重用这部分存储空间
B+树是如何进行记录检索的:
如果通过B+树
的索引查询行记录, 首先是从B+树的根
开始, 逐层检索, 直到找到叶子结点
, 也就是找到对应的数据页
位置, 将数据页加载到内存
中, 页目录中的槽
(slot)采用二分查找
的方式先找到一个粗略的记录分组
, 然后再在分组中通过链表遍历
的方式查找记录
普通索引和唯一索引在查询效率上有什么不同?
我们创建索引的时候可以是普通索引, 也可以是唯一索引, 那么这两个索引在查询效率上有什么不同?
唯一索引
就是普通索引上增加了约束性, 也就是关键字唯一
, 找到了关键字就停止检索, 因为关键字不重复. 而普通索引, 可能会存在用户记录中的关键字相同的情况
, 根据页结构的原理, 当我们读取一条记录的时候, 不是单独将这条记录从磁盘中读取出去, 而是将这个记录所在页加载到内存中进行读取, InnoDB存储引擎的页大小为16KB, 在一个页中可能存储着上千条记录, 因此在普通索引的字段上进行查找也就是在内存中多几次判断(判断下一条记录是不是值也满足)操作, 对于cpu来说, 这些操作锁耗费的时间是可以忽略不计的. 所以对一个索引字段进行检索, 采用普通索引还是唯一索引在检索效率上基本没有差别
.
我们平时的数据以行为单位来向表中插入数据, 这些记录在磁盘上的存储方式也被称之为是"行格式
"或者"记录格式
". InnoDB存储引擎设计了4中不同类型的"行格式", 分别是’Compact’
, ‘Redundant’
, ‘Dynami
c’, 'Compressed’
行格式
一条完整的记录其实可以被分为记录的额外信息
和记录的真实数据
两大部分:
但是其实记录的真实数据部分还有隐藏字段: row_id(可能有), tri_id, roll_point
变长字段长度列表:
比如varchar(M), varbinary(M), text类型, blob类型, 变长字段中存储多少字节的数据不是固定的, 在Compact行格式中, 把所有变长字段的真实数据占用的字节长度都存放在了记录的开头不为, 从而形成了一个变长字段长度列表.
NULL值列表:
Compact行格式会把可以为NULL的列统一管理起来, 存在一个标记为NULL值的列表
中.
如果表中没有允许存储为NULL的列, 则NULL列值列表也不存在了(所有列都是非空列的时候
二进制位的值为1时, 表示该列的值为NULL
二进制位的值为0时,代表该列的值不为NULL
记录的真实数据:
记录的真实数据除了我们自己定义的列的数据以外, 还会有三个隐藏列:
没有手动定义主键
, 则会选择一个not NULL and Unique
键作为主键, 如果也没有非空且唯一键, 则会为表默认添加一个名为row_id的隐藏列作为主键. 所以row_id是在没有自定义主键以及非空且唯一的情况下才会存在的 事物ID
和回滚指针
在后面事物日志
笔记中会详细讲解找主键也罢, 找唯一非空也罢, 生成隐藏列(非空且唯一的row_id)也罢, 其实都是为了生成聚簇索引
, 有聚簇索引才能存储数据
行溢出:一个页的大小一般是16KB, 也就是16384字节, 而一个varchar(M)类型的列就可以存储65533个字节的真实数据, 当然如果加上变长字段列表, 最终就是65535个字节, 这样就可能出现一个页存放不了一条记录, 这种现象称之为 : 行溢出
Dynamic和Compressed行格式:
MySQL5.7和8.0中默认行格式都是Dynamic
, Dynamic, Compressed行格式和Compact行格式都很像, 只不过在处理行移除数据时有分歧:
完全的行溢出
的方式. 在数据页中只存放20个字节的指针(溢出页的地址和长度), 实际的数据都存放在了Off page(移除页)中
为什么要有区?:让一部分的页连续, 也就是让一个区中的页都连续存储
B+树的每一层
的页
都会形成一个双向链表
, 如果是以页为单位来分配存储空间的话, 双向链表相邻的两个页之间的物理位置可能会离得非常远
. 我们介绍B+树索引的使用场景的时候特别提到范围查询只需要定位到最左边的记录
和最右边的记录
, 然后沿着双向链表一直扫描就可以了, 而如果链表中相邻的两个页物理位置离得非常远, 这就时所谓的随机I/O. 再一次强调, 磁盘的速度和内存速度差了好几个数量级, 随机I/O是非常慢的
, 所以我们应该尽量让链表中相邻的页的物理位置也相邻, 这样进行范围查询的时候才可以使用所谓的顺序I/O
一个区就是在物理位置上连续的64个页. 1MB. 在表中数据量大的时候, 为某个索引分配空间的时候就不再按照页为单位分配了
, 而是按照区为单位分配
, 甚至在表中的数据特别多的时候, 可以一次性分配多个连续的区
, 虽然可能造成一点点空间的浪费(数据不足以填充满整个区), 但是从性能角度看,
可以消除很多随机I/
O, 功大于过!
为什么要有段?:
对于范围查询, 而如果不区分叶子节点和非叶子节点, 统统把节点代表的页面放到申请到一个区中的话, 进行范围扫描的效果就大打折扣了
所以InnoDB对B+树的叶子结点和非叶子结点进行了区别对待, 也就是说叶子结点有自己的独有的区, 非叶子结点也有自己独有的区.
常见的段有数据段
, 索引段
, 回滚段.
数据段即为B+树的叶子结点, 索引段即为B+树的非叶子结点
为什么要有碎片区?:
碎片区中的页可以用于不同的目的, 比如有些页用于段A
, 有些页用于段B
, 有些页甚至哪个段都不属于. 碎片区直属于表空间
, 并不属于任何一个段
所以以后为某个段分配存储空间的策略是这样的:
在刚开始向表中插入数据的时候, 段是从某个碎片区
以单个页面
为单位来分配存储空间的
当某个段已经占用了32个碎片区页面
之后, 就会申请以完整的区为单位来分配存储空间.
也就是某个段已经是在32个碎片区中分配了页面了, 这个时候就会以完整的区为单位来分配存储空间
所以现在段不能仅仅定义为是某些区的集合, 更精确的说应该是某些零散的页面以及一些完整的区的集合
表空间和段都是逻辑概念, 区和页和行是物理结构, 段是以区和零散的页面为分配单位的
表空间可以看做是InnoDB存储引擎逻辑结构的最高层, 所有的数据都存放在表空间中.
表空间从管理上划分为系统表空间
(System tablespace), 独立表空间
(File-per-table tablespace), 撤销表空间
(Undo Tablespace)和临时表空间(
Temporary Tablespace)等