一、重新认识mysql

MySQL它的服务器程序直接和我们存储的数据打交道,然后可以有好多客户端程序连接到这个服务器程序,发送增删改查的请求,然后服务器就响应这些请求,从⽽操作它维护的数据。
我们⽇常使⽤ MySQL 的情景⼀般是这样的:

  1. 启动 MySQL 服务器程序。
  2. 启动 MySQL 客户端程序并连接到服务器程序。
  3. 在客户端程序中输⼊⼀些命令语句作为请求发送到服务器程序,服务器程序收到这些请求后,会根据请求的内容来操作具体的数据并向客户端返回操作结果。
    MySQL是单进程多线程的。

客户端与服务器连接的过程

运⾏着的服务器程序和客户端程序本质上都是计算机上的⼀个进程,所以客户端进程向服务器进程发送请求并得到回复的过程本质上是⼀个进程间通信的过程! MySQL ⽀持下边三种客户端进程和服务器进程的通信⽅式。

  1. TCP/IP
    真实环境中,数据库服务器进程和客户端进程可能运⾏在不同的主机中,它们之间必须通过⽹络来进⾏通讯。 MySQL 采⽤ TCP 作为服务器和客户端之间的⽹络通信
    协议。在⽹络环境下,每台计算机都有⼀个唯⼀的 IP地址 ,如果某个进程有需要采⽤ TCP 协议进⾏⽹络通信⽅⾯的需求,可以向操作系统申请⼀个 端⼝号 ,这是⼀
    个整数值,它的取值范围是 0~65535 。这样在⽹络中的其他进程就可以通过 IP地址 + 端⼝号 的⽅式来与这个进程连接,这样进程之间就可以通过⽹络进⾏通信了。
    MySQL 服务器启动的时候会默认申请 3306 端⼝号,之后就在这个端⼝号上等待客户端进程进⾏连接,⽤书⾯⼀点的话来说, MySQL 服务器会默认监听 3306 端⼝。
  2. 命名管道和共享内存
    如果你是⼀个 Windows ⽤户,那么客户端进程和服务器进程之间可以考虑使⽤ 命名管道 或 共享内存 进⾏通信。不过启⽤这些通信⽅式的时候需要在启动服务器程序和
    客户端程序时添加⼀些参数
  3. Unix域套接字⽂件
    如果我们的服务器进程和客户端进程都运⾏在同⼀台操作系统为类 Unix 的机器上的话,我们可以使⽤ Unix域套接字⽂件 来进⾏进程间通信。

服务器处理客户端请求

其实不论客户端进程和服务器进程是采⽤哪种⽅式进⾏通信,最后实现的效果都是:客户端进程向服务器进程发送⼀段⽂本(MySQL语句),服务器进程处理后再向客户端进程发送⼀段⽂本(处理结果)。
服务器程序处理来⾃客户端的查询请求⼤致需要经过三个部分,分别是 连接管理 、 解析与优化 、 存储引擎
图片说明

连接管理

  1. 客户端进程可以采⽤我们上边介绍的 TCP/IP 、 命名管道或共享内存 、 Unix域套接字 这⼏种⽅式之⼀来与服务器进程建⽴连接,每当有⼀个客户端进程连接到服务器进程时,服务器进程都会创建⼀个线程来专⻔处理与这个客户端的交互,当该客户端退出时会与服务器断开连接,服务器并不会⽴即把与该客户端交互的线程销毁掉,⽽是把它缓存起来,在另⼀个新的客户端再进⾏连接时,把这个缓存的线程分配给该新客户端。这样就起到了不频繁创建和销毁线程的效果,从⽽节省开销。从这⼀点⼤家也能看出, MySQL 服务器会为每⼀个连接进来的客户端分配⼀个线程,但是线程分配的太多了会严重影响系统性能,所以我们也需要限制⼀下可以同时连接到服务器的客户端数量
  2. 在客户端程序发起连接的时候,需要携带主机信息、⽤户名、密码,服务器程序会对客户端程序提供的这些信息进⾏认证,如果认证失败,服务器程序会拒绝连接。另外,如果客户端程序和服务器程序不运⾏在⼀台计算机上,我们还可以采⽤使⽤了 SSL (安全套接字)的⽹络连接进⾏通信,来保证数据传输的安全性。当连接建⽴后,与该客户端关联的服务器线程会⼀直等待客户端发送过来的请求, MySQL 服务器接收到的请求只是⼀个⽂本消息,该⽂本消息还要经过各种处理

解析与优化

查询缓存

MySQL 服务器程序处理查询请求的过程也是这样,会把刚刚处理过的查询请求和结果 缓存起来,如果下⼀次有⼀模⼀样的请求过来,直接从缓存中查找结果就好了,就不⽤再傻呵呵的去底层的表中查找了。这个查询缓存可以在不同客户端之间共享,也就是说如果客户端A刚刚查询了⼀个语句,⽽客户端B之后发送了同样的查询请求,那么客户端B的这次查询就可以直接使⽤查询缓存中的数据。当然, MySQL 服务器并没有⼈聪明,如果两个查询请求在任何字符上的不同(例如:空格、注释、⼤⼩写),都会导致缓存不会命中。另外,如果查询请求中包含某些系统函数、⽤户⾃定义变量和函数、⼀些系统表,如 mysql 、information_schema、 performance_schema 数据库中的表,那这个请求就不会被缓存(这些系统函数每次请求结果可能都不一样,没有缓存的必要。)

不过既然是缓存,那就有它缓存失效的时候。MySQL的缓存系统会监测涉及到的每张表,只要该表的结构或者数据被修改,如对该表使⽤了 INSERT 、 UPDATE 、DELETE 、 TRUNCATE TABLE 、 ALTER TABLE 、 DROP TABLE 或 DROP DATABASE 语句,那使⽤该表的所有⾼速缓存查询都将变为⽆效并从⾼速缓存中删除!
【虽然查询缓存有时可以提升系统性能,但也不得不因维护这块缓存⽽造成⼀些开销,⽐如每次都要去查询缓存中检索,查询请求处理完需要更新查询缓存,维护该查询缓存对应的内存区域。从MySQL 5.7.20开始,不推荐使⽤查询缓存,并在MySQL 8.0中删除。】

语法解析

如果查询缓存没有命中,接下来就需要进⼊正式的查询阶段了。因为客户端程序发送过来的请求只是⼀段⽂本⽽已,所以 MySQL 服务器程序⾸先要对这段⽂本做分析,判断请求的语法是否正确,然后从⽂本中将要查询的表、各种查询条件都提取出来放到 MySQL 服务器内部使⽤的⼀些数据结构上来。【这个从指定的⽂本中提取出我们需要的信息本质上算是⼀个编译过程,涉及词法解析、语法分析、语义分析等阶段】

查询优化

语法解析之后,服务器程序获得到了需要的信息,⽐如要查询的列是哪些,表是哪个,搜索条件是什么等等,但光有这些是不够的,因为我们写的 MySQL 语句执⾏起来效率可能并不是很⾼,MySQL 的优化程序会对我们的语句做⼀些优化,如外连接转换为内连接、表达式简化、⼦查询转为连接吧啦吧啦的⼀堆东⻄。优化的结果就是⽣成⼀个执⾏计划,这个执⾏计划表明了应该使⽤哪些索引进⾏查询,表之间的连接顺序是啥样的。我们可以使⽤ EXPLAIN 语句来查看某个语句的执⾏计划

存储引擎

截⽌到服务器程序完成了查询优化为⽌,还没有真正的去访问真实的数据表, MySQL 服务器把数据的存储和提取操作都封装到了⼀个叫 存储引擎 的模块⾥。我们知道 表 是由⼀⾏⼀⾏的记录组成的,但这只是⼀个逻辑上的概念,物理上如何表示记录,怎么从表中读取数据,怎么把数据写⼊具体的物理存储器上,这都是 存储引擎 负责的事情。为了实现不同的功能, MySQL 提供了各式各样的 存储引擎 ,不同 存储引擎 管理的表具体的存储结构可能不同,采⽤的存取算法也可能不同。它的功能就是接收上层传下来的指令,然后对表中的数据进⾏提取或写⼊操作。
各种不同的存储引擎向上边的 MySQL server 层提供统⼀的调⽤接⼝(也就是存储引擎API),包含了⼏⼗个底层函数,像"读取索引第⼀条内容"、"读取索引下⼀条内容"、"插⼊记录"等等。所以在 MySQL server 完成了查询优化后,只需按照⽣成的执⾏计划调⽤底层存储引擎提供的API,获取到数据后返回给客户端就好了

常见的存储引擎

mysql高级里面提及了很多,可以去看一下。

InnoDB 记录结构

MySQL 服务器上负责对表中数据的读取和写⼊⼯作的部分是 存储引擎 ,⽽服务器⼜⽀持不同类型的存储引擎,⽐如InnoDB 、 MyISAM 、 Memory 啥的,不同的存储引擎⼀般是由不同的⼈为实现不同的特性⽽开发的,真实数据在不同存储引擎中存放的格式⼀般是不同的,甚⾄有的存储引擎⽐如 Memory 都不⽤磁盘来存储数据,也就是说关闭服务器后表中的数据就消失了。

InnoDB⻚简介

InnoDB 是⼀个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。⽽真正处理数据的过程是发⽣在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写⼊或修改请求的话,还需要把内存中的内容刷新到磁盘上。⽽我们知道读写磁盘的速度⾮常慢,和内存读写差了⼏个数量级,所以当我们想从表中获取某些记录时, InnoDB 存储引擎需要⼀条⼀条的把记录从磁盘上读出来么?不,那样会慢死, InnoDB 采取的⽅式是:将数据划分为若⼲个⻚,以⻚作为磁盘和内存之间交互的基本单位,InnoDB中⻚的⼤⼩⼀般为 16 KB。也就是在⼀般情况下,⼀次最少从磁盘中读取16KB的内容到内存中,⼀次最少把内存中的16KB内容刷新到磁盘中。

InnoDB⾏格式

我们平时是以记录为单位来向表中插⼊数据的,这些记录在磁盘上的存放⽅式也被称为 ⾏格式 或者 记录格式 。设计 InnoDB 存储引擎的⼤叔们到现在为⽌设计了4种不同类型的 ⾏格式 ,分别是 Compact 、 Redundant 、 Dynamic 和 Compressed ⾏格式,随着时间的推移,他们可能会设计出更多的⾏格式,但是不管怎么变,在原理上⼤体都是相同的。
不具体介绍有机会再补充

  1. ⻚是 MySQL 中磁盘和内存交互的基本单位,也是 MySQL 是管理存储空间的基本单位。
  2. 指定和修改⾏格式的语法如下:
    CREATE TABLE 表名 (列的信息) ROW_FORMAT=⾏格式名称
    ALTER TABLE 表名 ROW_FORMAT=⾏格式名称
  3. InnoDB ⽬前定义了4种⾏格式
    COMPACT⾏格式
    具体组成如图:
    图片说明
    Redundant⾏格式
    具体组成如图:
    图片说明
    Dynamic和Compressed⾏格式
    这两种⾏格式类似于 COMPACT⾏格式 ,只不过在处理⾏溢出数据时有点⼉分歧,它们不会在记录的真实数据处存储字符串的前768个字节,⽽是把所有的字节都存储到其他⻚⾯中,只在记录的真实数据处存储其他⻚⾯的地址。
    图片说明
    另外, Compressed ⾏格式会采⽤压缩算法对⻚⾯进⾏压缩。
    ⼀个⻚⼀般是 16KB ,当记录中的数据太多,当前⻚放不下的时候,会把多余的数据存储到其他⻚中,这种现象称为 ⾏溢出 。

    InnoDB 数据⻚结构

它是 InnoDB 管理存储空间的基本单位,⼀个⻚的⼤⼩⼀般是 16KB 。 InnoDB 为了不同的⽬的⽽设计了许多种不同类型的 ⻚ ,⽐如存放表空间头部信息的⻚,存放 Insert Buffer 信息的⻚,存放 INODE 信息的⻚,存放 undo ⽇志信息的⻚等等等
存放记录的⻚为索引( INDEX )⻚,也可以成为数据页。
图片说明
在⻚的7个组成部分中,我们⾃⼰存储的记录会按照我们指定的 ⾏格式 存储到 User Records 部分。但是在⼀开始⽣成⻚的时候,其实并没有 User Records 这个部分,每当我们插⼊⼀条记录,都会从 Free Space 部分,也就是尚未使⽤的存储空间中申请⼀个记录⼤⼩的空间划分到 User Records 部分,当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个⻚使⽤完了,如果还有新的记录插⼊的话,就需要去申请新的⻚了,这个过程的图示如下
图片说明

我们上面学习了行格式,进行具体细化一下。
图片说明
图片说明
图片说明
图片说明
delete_mask
这个属性标记着当前记录是否被删除,占⽤1个⼆进制位,值为 0 的时候代表记录并没有被删除,为 1 的时候代表记录被删除掉了。被删除的记录还在 ⻚ 中。这些被删除的记录之所以不⽴即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打⼀个删除标记⽽已,所有被删除掉的记录都会组成⼀个所谓的 垃圾链表 ,在这个链表中的记录占⽤的空间称之为所谓的 可重⽤空间 ,之后如果有新记录插⼊到表中的话,可能把这些被删除的记录占⽤的存储空间覆盖掉。【有点逻辑删除的意思哈】

min_rec_mask
B+树的每层⾮叶⼦节点中的最⼩记录都会添加该标记,min_rec_mask 值都是 0 ,意味着它们都不是 B+ 树的⾮叶⼦节点中的最⼩记录。
heap_no
这个属性表示当前记录在本 ⻚ 中的位置
他们⾃动给每个⻚⾥边⼉加了两个记录,由于这两个记录并不是我们⾃⼰插⼊的,所以有时候也称为 伪记录 或
者 虚拟记录 。这两个伪记录⼀个代表 最⼩记录 ,⼀个代表 最⼤记录
记录也可以⽐⼤⼩,对于⼀条完整的记录来说,⽐较记录的⼤⼩就是⽐较 主键 的⼤⼩
图片说明
图片说明
从图中我们可以看出来,最⼩记录和最⼤记录的 heap_no 值分别是 0 和 1 ,也就是说它们的位置最靠前。
record_type
这个属性表示当前记录的类型,⼀共有4种类型的记录, 0 表示普通记录, 1 表示B+树⾮叶节点记录, 2 表示最⼩记录, 3 表示最⼤记录。从图中我们也可以看出来,我们⾃⼰插⼊的记录就是普通记录,它们的 record_type 值都是 0 ,⽽最⼩记录和最⼤记录的 record_type 值分别为 2 和 3 。
next_record
这玩意⼉⾮常重要,它表示从当前记录的真实数据到下⼀条记录的真实数据的地址偏移量。⽐⽅说第⼀条记录的 next_record 值为 32 ,意味着从第⼀条记录的真实数据的地址处向后找 32 个字节便是下⼀条记录的真实数据。如果你熟悉数据结构的话,就⽴即明⽩了,这其实是个 链表 ,可以通过⼀条记录找到它的下⼀条记录。但是需要注意注意再注意的⼀点是, 下⼀条记录 指得并不是按照我们插⼊顺序的下⼀条记录,⽽是按照主键值由⼩到⼤的顺序的下⼀条记录。⽽且规定 Iǹf?ḿùḿ 记录(也就是最⼩记录) 的下⼀条记录就是本⻚中主键值最⼩的⽤户记录,⽽本⻚中主键值最⼤的⽤户记录的下⼀条记录就是 Sùṗřeḿùḿ 记录
图片说明
从图中可以看出来,我们的记录按照主键从⼩到⼤的顺序形成了⼀个单链表。 最⼤记录 的 next_record 的值为 0 ,这也就是说最⼤记录是没有 下⼀条记录 了,它是这个单链表中的最后⼀个节点。如果从中删除掉⼀条记录,这个链表也是会跟着变化的。
图片说明
图片说明
图片说明
从图中可以看到, InnoDB 并没有因为新记录的插⼊⽽为它申请新的存储空间,⽽是直接复⽤了原来被删除记录的存储空间。

Page Directory(⻚⽬录)

按照上述的描述我们进行查询的时候需要根据链表进行一条一条的查询,效率比较低。
我们可以创建类似目录的东西。
1. 将所有正常的记录(包括最⼤和最⼩记录,不包括标记为已删除的记录)划分为⼏个组。
2. 每个组的最后⼀条记录(也就是组内最⼤的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记录,也就是该组内共有⼏条记录。

3. 将每个组的最后⼀条记录的地址偏移量单独提取出来按顺序存储到靠近 ⻚ 的尾部的地⽅,这个地⽅就是所谓的 Page Directory ,也就是 ⻚⽬录 (此时应该返回头看看⻚⾯各个部分的图)。⻚⾯⽬录中的这些地址偏移量被称为 槽 (英⽂名: Slot ),所以这个⻚⾯⽬录就是由 槽 组成的。
⽐⽅说现在的 page_demo 表中正常的记录共有6条, InnoDB 会把它们分成两组,第⼀组中只有⼀个最⼩记录,第⼆组中是剩余的5条记录,看下边的示意图:
图片说明
现在 ⻚⽬录 部分中有两个槽,也就意味着我们的记录被分成了两个组, 槽1 中的值是 112 ,代表最⼤记录的地址偏移量(就是从⻚⾯的0字节开始数,数112个字
节); 槽0 中的值是 99 ,代表最⼩记录的地址偏移量。
注意最⼩和最⼤记录的头信息中的 n_owned 属性
最⼩记录的 n_owned 值为 1 ,这就代表着以最⼩记录结尾的这个分组中只有 1 条记录,也就是最⼩记录本身。
最⼤记录的 n_owned 值为 5 ,这就代表着以最⼤记录结尾的这个分组中只有 5 条记录,包括最⼤记录本身还有我们⾃⼰插⼊的 4 条记录。
图片说明
图片说明
对于最⼩记录所在的分组只能有 1 条记录,最⼤记录所在的分组拥有的记录条数只能在 1~ 8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间
所以分组是按照下边的步骤进⾏的:
初始情况下⼀个数据⻚⾥只有最⼩记录和最⼤记录两条记录,它们分属于两个分组。
之后每插⼊⼀条记录,都会从 ⻚⽬录 中找到主键值⽐本记录的主键值⼤并且差值最⼩的槽,然后把该槽对应的记录的 n_owned 值加1,表示本组内⼜添加了⼀条记录,直到该组中的记录数等于8个。
在⼀个组中的记录数等于8个后再插⼊⼀条记录时,会将组中的记录拆分成两个组,⼀个组中4条记录,另⼀个5条记录。这个过程会在 ⻚⽬录 中新增⼀个 槽 来记录这个新增分组中最⼤的那条记录的偏移量。**
当我们插入较多记录的时候
图片说明

现在看怎么从这个⻚⽬录中查找记录。因为各个槽代表的记录的主键值都是从⼩到⼤排序的,所以我们可以使⽤所谓的⼆分法来进⾏快速查找。4个槽的编号分别是:0、1、2、3、4,所以初始情况下最低的槽就是low=0,最⾼的槽就是high=4。⽐⽅说我们想找主键值为5的记录,过程是这样的:
1. 计算中间槽的位置:(0+4)/2=2,所以查看槽2对应记录的主键值为8,⼜因为8 > 5,所以设置high=2,low保持不变。
2. 重新计算中间槽的位置:(0+2)/2=1,所以查看槽1对应的主键值为4。所以设置low=1,high保持不变。
3. 因为high - low的值为1,所以确定主键值为5的记录在槽2对应的组中,接下来就是通过遍历槽2对应的组的链表来进⾏查找了。由于⼀个组中包含的记录条数只能是1~8条,所以遍历⼀个组中的记录的代价是很⼩的。

所以在⼀个数据⻚中查找指定主键值的记录的过程分为两步:1. 通过⼆分法确定该记录所在的槽。2. 通过记录的next_record属性遍历该槽所在的组中的各个记录。

Page Header(⻚⾯头部)

设计InnoDB的⼤叔们为了能得到⼀个数据⻚中存储的记录的状态信息,⽐如本⻚中已经存储了多少条记录,第⼀条记录的地址是什么,⻚⽬录中存储了多少个槽等等,特意在⻚中定义了⼀个叫PageHeader的部分,它是⻚结构的第⼆部分,这个部分占⽤固定的56个字节,专⻔存储各种状态信息。
图片说明

File Header(⽂件头部)

Page Header 是专⻔针对 数据⻚ 记录的各种状态信息,⽐⽅说⻚⾥头有多少个记录了呀,有多少个槽了呀。我们现在描述的 File Header 针对各种类型的⻚都通⽤,也就是说不同类型的⻚都会以 File Header 作为第⼀个组成部分,它描述了⼀些针对各种⻚都通⽤的⼀些信息,⽐⽅说这个⻚的编号是多少,它的上⼀个⻚、下⼀个⻚是谁啦吧啦吧啦~ 这个部分占⽤固定的 38 个字节
图片说明
图片说明
通过上面的一些属性或者指针指向,上一页下一页,不是每种类型的文件都有。
图片说明

File Trailer

InnoDB 存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以 ⻚ 为单位把数据加载到内存中处理,如果该⻚中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了⼀半的时候中断电了咋办,为了检测⼀个⻚是否完整(也就是在同步的时候有没有发⽣只同步⼀半的尴尬情况),设计 InnoDB 的⼤叔们在每个⻚的尾部都加了⼀个 File Trailer 部分,这个部由 8 个字节组成可以分成2个⼩部分:

  1. 前4个字节代表⻚的校验和
  2. 后4个字节代表⻚⾯被最后修改时对应的⽇志序列位置(LSN)

    总结

  3. InnoDB为了不同的⽬的⽽设计了不同类型的⻚,我们把⽤于存放记录的⻚叫做 数据⻚ 。
  4. ⼀个数据⻚可以被⼤致划分为7个部分,分别是
    File Header ,表示⻚的⼀些通⽤信息,占固定的38字节。
    Page Header ,表示数据⻚专有的⼀些信息,占固定的56个字节。
    Infimum + Supremum ,两个虚拟的伪记录,分别表示⻚中的最⼩和最⼤记录,占固定的 26 个字节。
    User Records :真实存储我们插⼊的记录的部分,⼤⼩不固定。
    Free Space :⻚中尚未使⽤的部分,⼤⼩不确定。
    Page Directory :⻚中的某些记录相对位置,也就是各个槽在⻚⾯中的地址偏移量,⼤⼩不固定,插⼊的记录越多,这个部分占⽤的空间越多。
    File Trailer :⽤于检验⻚是否完整的部分,占⽤固定的8个字节。
  5. 每个记录的头信息中都有⼀个 next_record 属性,从⽽使⻚中的所有记录串联成⼀个 单链表 。
  6. InnoDB 会为把⻚中的记录划分为若⼲个组,每个组的最后⼀个记录的地址偏移量作为⼀个 槽 ,存放在 Page Directory 中,所以在⼀个⻚中根据主键查找记录是⾮常快的,分为两步:
    通过⼆分法确定该记录所在的槽。
    通过记录的next_record属性遍历该槽所在的组中的各个记录。
  7. 每个数据⻚的 File Header 部分都有上⼀个和下⼀个⻚的编号,所以所有的数据⻚会组成⼀个 双链表 。
  8. 为保证从内存中同步到磁盘的⻚的完整性,在⻚的⾸部和尾部都会存储⻚中数据的校验和和⻚⾯最后修改时对应的 LSN 值,如果⾸部和尾部的校验和和 LSN 值校验不成功的话,就说明同步过程出现了问题。

B+ 树索引

前边我们详细唠叨了 InnoDB 数据⻚的7个组成部分,知道了各个数据⻚可以组成⼀个 双向链表 ,⽽每个数据⻚中的记录会按照主键值从⼩到⼤的顺序组成⼀个 单向链表 ,每个数据⻚都会为存储在它⾥边⼉的记录⽣成⼀个 ⻚⽬录 ,在通过主键查找某条记录的时候可以在 ⻚⽬录 中使⽤⼆分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
图片说明
其中⻚a、⻚b、⻚c ... ⻚n 这些⻚可以不在物理结构上相连,只要通过双向链表相关联即可。

没有索引的查找

在⼀个⻚中的查找

假设⽬前表中的记录⽐较少,所有的记录都可以被存放到⼀个⻚中,在查找记录的时候可以根据搜索条件的不同分为两种情况:

  1. 以主键为搜索条件
    这个查找过程我们已经很熟悉了,可以在 ⻚⽬录 中使⽤⼆分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
  2. 以其他列作为搜索条件
    对⾮主键列的查找的过程可就不这么幸运了,因为在数据⻚中并没有对⾮主键列建⽴所谓的 ⻚⽬录 ,所以我们⽆法通过⼆分法快速定位相应的 槽 。这种情况下只能从 最⼩记录 开始依次遍历单链表中的每条记录,然后对⽐每条记录是不是符合搜索条件。很显然,这种查找的效率是⾮常低的。

在很多⻚中查找

⼤部分情况下我们表中存放的记录都是⾮常多的,需要好多的数据⻚来存储这些记录。在很多⻚中查找记录的话可以分为两个步骤:

  1. 定位到记录所在的⻚。
  2. 从所在的⻚内中查找相应的记录。
    在没有索引的情况下,不论是根据主键列或者其他列的值进⾏查找,由于我们并不能快速的定位到记录所在的⻚,所以只能从第⼀个⻚沿着双向链表⼀直往下
    找,在每⼀个⻚中根据我们刚刚唠叨过的查找⽅式去查找指定的记录。因为要遍历所有的数据⻚,所以这种⽅式显然是超级耗时的,如果⼀个表有⼀亿条记录,
    使⽤这种⽅式去查找记录那要等到猴年⻢⽉才能等到查找结果。

索引

图片说明

图片说明

因为各个⻚中的记录并没有规律,我们并不知道我们的搜索条件匹配哪些⻚中的记录,所以 不得不 依次遍历所有的数据⻚.

我们也可以想办法为快速定位记录所在的数据⻚⽽建⽴⼀个别的⽬录,建这个⽬录必须完成下边这些事⼉:
下⼀个数据⻚中⽤户记录的主键值必须⼤于上⼀个⻚中⽤户记录的主键值。
图片说明
假设我们的每个数据⻚最多能存放3条记录(实际上⼀个数据⻚⾮常⼤,可以存放下好多记录)
图片说明
图片说明
这个过程表明了在对⻚中的记录进⾏增删改操作的过程中,我们必须通过⼀些诸如记录移动的操作来始终保证这个状态⼀直成⽴:下⼀个数据⻚中⽤户记录的主键值必须⼤于上⼀个⻚中⽤户记录的主键值。这个过程我们也可以称为 ⻚分裂 。
图片说明

因为这些 16KB 的⻚在物理存储上可能并不挨着,所以如果想从这么多⻚中根据主键值快速定位某些记录所在的⻚,我们需要给它们做个⽬录,每个⻚对应⼀个⽬录项,每个⽬录项包括下边两个部分:
⻚的⽤户记录中最⼩的主键值,我们⽤ key 来表示。
⻚号,我们⽤ page_no 表示。
图片说明
以 ⻚28 为例,它对应 ⽬录项2 ,这个⽬录项中包含着该⻚的⻚号 28 以及该⻚中⽤户记录的最⼩主键值 5 。我们只需要把⼏个⽬录项在物理存储器上连续存储,⽐如把他们放到⼀个数组⾥,就可以实现根据主键值快速查找某条记录的功能了。⽐⽅说我们想找主键值为 20 的记录,具体查找过程分两步:
1. 先从⽬录项中根据⼆分法快速确定出主键值为 20 的记录在 ⽬录项3 中(因为 12 < 20 < 209 ),它对应的⻚是 ⻚9 。
2. 再根据前边说的在⻚中查找记录的⽅式去 ⻚9 中定位具体的记录。
⾄此,针对数据⻚做的简易⽬录就搞定了。不过忘了说了,这个 ⽬录 有⼀个别名,称为 索引 。

InnoDB中的索引⽅案

上边之所以称为⼀个简易的索引⽅案,上边的解决方案存在着问题,不够灵活。
所以,设计 InnoDB 的⼤叔们需要⼀种可以灵活管理所有 ⽬录项 的⽅式。他们灵光乍现,忽然发现这些 ⽬录项 其实⻓得跟我们的⽤户记录差不多,只不过 ⽬录项 中的两个列是 主键 和 ⻚号 ⽽已,所以他们复⽤了之前存储⽤户记录的数据⻚来存储⽬录项,为了和⽤户记录做⼀下区分,我们把这些⽤来表示⽬录项的记录称为 ⽬录项记录 。InnoDB 怎么区分⼀条记录是普通的 ⽤户记录 还是 ⽬录项记录 呢?别忘了记录头信息⾥的 record_type 属性,它的各个取值代表的意思如下:
0 :普通的⽤户记录
1 :⽬录项记录
2 :最⼩记录
3 :最⼤记录

图片说明
图片说明
数据再多还会往下再分
图片说明
图片说明
如果数据太多目录项页也太多在网上提,生成新的,形成多级。
图片说明
图片说明
图片说明

聚簇索引

图片说明

⼆级索引

上边介绍的 聚簇索引 只能在搜索条件是主键值时才能发挥作⽤,因为 B+ 树中的数据都是按照主键进⾏排序的。那如果我们想以别的列作为搜索条件就不适用了。
我们可以多建⼏棵 B+ 树,不同的 B+ 树中的数据采⽤不同的排序规则。⽐⽅说我们⽤ c2 列的⼤⼩作为数据⻚、⻚中记录的排序规则,再建⼀棵 B+ 树
图片说明
图片说明
图片说明

联合索引

图片说明
图片说明
图片说明

InnoDB的B+树索引的注意事项

图片说明

节点的唯一性

b+树索引,聚簇索引是里面是主键 + 页号 主键是唯一的,能保证目录项记录唯一。

图片说明
图片说明
如果我们想新插⼊⼀⾏记录,其中 c1 、 c2 、 c3 的值分别是: 9 、 1 、 'c' ,那么在修改这个为 c2 列建⽴的⼆级索引对应的 B+ 树时便碰到了个⼤问题:由于 ⻚3 中存储
的⽬录项记录是由 c2列 + ⻚号 的值构成的, ⻚3 中的两条⽬录项记录对应的 c2 列的值都是 1 ,⽽我们新插⼊的这条记录的 c2 列的值也是 1 ,那我们这条新插⼊的记录到底应该放到 ⻚4 中,还是应该放到 ⻚5 中啊?

我们需要保证在B+树的同⼀层内节点的⽬录项记录除 ⻚号 这个字段以外是唯⼀的
所以对于⼆级索引的内节点的⽬录项记录的内容实际上是由三个部分构成的:
索引列的值
主键值
⻚号
图片说明

⼀个⻚⾯最少存储2条记录
图片说明
图片说明
图片说明
InnoDB 和 MyISAM 会⾃动为主键或者声明为 UNIQUE 的列去⾃动建⽴ B+ 树索引,但是如果
我们想为其他的列建⽴索引就需要我们显式的去指明
每建⽴⼀个索引都会建⽴⼀棵 B+ 树,每插⼊⼀条记录都要
维护各个记录、数据⻚的排序关系,这是很费性能和存储空间的。

好东⻄也得先学会怎么⽤ —— B+ 树索引的使⽤

B+树索引的使用

基本的结论:

  1. 每个索引都对应⼀棵 B+ 树, B+ 树分为好多层,最下边⼀层是叶⼦节点,其余的是内节点。所有 ⽤户记录 都存储在 B+ 树的叶⼦节点,所有 ⽬录项记录 都存储在内节点。
  2. InnoDB 存储引擎会⾃动为主键(如果没有它会⾃动帮我们添加)建⽴ 聚簇索引 ,聚簇索引的叶⼦节点包含完整的⽤户记录。
  3. 我们可以为⾃⼰感兴趣的列建⽴ ⼆级索引 , ⼆级索引 的叶⼦节点包含的⽤户记录由 索引列 + 主键 组成,所以如果想通过 ⼆级索引 来查找完整的⽤户记录的话,需要通过 回表 操作,也就是在通过 ⼆级索引 找到主键值之后再到 聚簇索引 中查找完整的⽤户记录。
  4. B+ 树中每层节点都是按照索引列值从⼩到⼤的顺序排序⽽组成了双向链表,⽽且每个⻚内的记录(不论是⽤户记录还是⽬录项记录)都是按照索引列的值从⼩到⼤的顺序⽽形成了⼀个单链表。如果是 联合索引 的话,则⻚⾯和记录先按照 联合索引 前边的列排序,如果该列值相同,再按照 联合索引 后边的列排序。
  5. 通过索引查找记录是从 B+ 树的根节点开始,⼀层⼀层向下搜索。由于每个⻚⾯都按照索引列的值建⽴了 Page Directory (⻚⽬录),所以在这些⻚⾯中的查找⾮常快。

    索引的代价

    虽然索引是个好东⻄,可不能乱建:
    空间上的代价
    每建⽴⼀个索引都要为它建⽴⼀棵 B+ 树,每⼀棵 B+ 树的每⼀个节点都是⼀个数据⻚,⼀个⻚默认会占⽤ 16KB 的存储空间,⼀棵很⼤的B+ 树由许多数据⻚组成,那可是很⼤的⼀⽚存储空间呢
    时间上的代价
    每次对表中的数据进⾏增、删、改操作时,都需要去修改各个 B+ 树索引。⽽且我们讲过, B+ 树每层节点都是按照索引列的值从⼩到⼤的顺序排序⽽组成了双向链表。不论是叶⼦节点中的记录,还是内节点中的记录(也就是不论是⽤户记录还是⽬录项记录)都是按照索引列的值从⼩到⼤的顺序⽽形成了⼀个单向链表。⽽增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进⾏⼀些记录移位,⻚⾯分裂、⻚⾯回收啥的操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的 B+ 树都要进⾏相关的维护操作,这还能不给性能拖后腿么?

⼀个表上索引建的越多,就会占⽤越多的存储空间,在增删改记录的时候性能就越差。⼀个表中有多少索引就会建⽴多少棵 B+ 树。
全值匹配
图片说明
图片说明
先按照 name 列的值进⾏排序。
如果 name 列的值相同,则按照 birthday 列的值进⾏排序。
如果 birthday 列的值也相同,则按照 phone_number 的值进⾏排序
如果我们的搜索条件中的列和索引列⼀致的话,这种情况就称为全值匹配 ,我们建立的索引就是下面查询的那三个索引。
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';
WHERE ⼦句中的⼏个搜索条件的顺序对查询结果有啥影响么?也就是说如果我们调换 name 、 birthday 、 phone_number 这⼏个搜索列的顺
序对查询的执⾏过程有影响么?⽐⽅说写成下边这样:
SELECT * FROM person_info WHERE birthday = '1990-09-27' AND phone_number = '15123983239' AND name = 'Ashburn';
没影响哈。 MySQL 有⼀个叫查询优化器的东东,会分析这些搜索条件并且按照可以使⽤的索引中列的顺序来决定先使⽤哪个搜索条件,后使⽤哪个搜索条件。

匹配左边的列 (最左匹配原则)(列匹配)

其实在我们的搜索语句中也可以不⽤包含全部联合索引中的列,只包含左边的就⾏,⽐⽅说下边的查询语句:
SELECT * FROM person_info WHERE name = 'Ashburn';
或者包含多个左边的列也⾏:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';
图片说明

最左前缀匹配 (值匹配)

图片说明
图片说明
MySQL 就⽆法快速定位记录位置了,因为字符串中间有 'As' 的字符串并没有排好序,所以只能全表扫描了
用通配符只能使用在最左前缀匹配上。 有时候我们想匹配后缀 就需要将字符串逆序然后存储成索引,然后就可以使用最左前缀索引来进行匹配了。

匹配范围值

图片说明

图片说明

这就是为什么 前边有索引还好,只能识别前面的 后面的就识别不到了

精确匹配某⼀列并范围匹配另外⼀列

图片说明

用于排序

图片说明
对于 联合索引 有个问题需要注意, ORDER BY 的⼦句后边的列的顺序也必须按照索引列的顺序给出,
图片说明
ORDER BY⼦句后的列如果不加ASC或者DESC默认是按照ASC排序规则排序的,也就是升序排序的
排序列使⽤了复杂的表达式,被修饰过的,是无法使用索引的。
图片说明

回表的代价

图片说明
图片说明

覆盖索引

图片说明

如何挑选索引

图片说明

图片说明
图片说明
图片说明
【前缀索引】
图片说明
图片说明
图片说明
主键插入顺序
对于⼀个使⽤ InnoDB 存储引擎的表来说,在我们没有显式的创建索引时,表中的数据实际上都是存储在 聚簇索引 的叶⼦节点的。⽽记录⼜是存储在数据⻚中的,数据⻚和记录⼜是按照记录主键值从⼩到⼤的顺序进⾏排序,所以如果我们插⼊的记录的主键值是依次增⼤的话,那我们每插满⼀个数据⻚就换到下⼀个数据⻚继续插,⽽如果我们插⼊的主键值忽⼤忽⼩的话,这就⽐较麻烦了。
图片说明

减少冗余和重复索引

总结

图片说明

Mysql的数据目录

数据库和⽂件系统的关系

图片说明

MySQL数据⽬录

安装⽬录 下⾮常重要的 bin ⽬录,它⾥边存储了许多关于控制客户端程序和服务器程序的命令(许多可执⾏⽂件,⽐如 mysql , mysqld , mysqld_safe 等等等等好⼏⼗个)。⽽ 数据⽬录 是⽤来存储 MySQL 在运⾏过程中产⽣的数据

数据库在文件系统中的表示

图片说明

表在文件系统中的表示

图片说明

Inodb存储表

图片说明
图片说明

文件系统堆数据库的影响

图片说明

存放⻚⾯的⼤池⼦ —— InnoDB 的表空间

表空间 是⼀个抽象的概念,对于系统表空间来说,对应着⽂件系统中⼀个或多个实际⽂件;对于每个独⽴表空间来说,对应着⽂件系统中⼀个名为 表名.ibd 的实际⽂件。⼤家可以把表空间想象成被切分为许许多多个 ⻚ 的池⼦,当我们想为某个表插⼊⼀条记录的时候,就从池⼦中捞出⼀个对应的⻚来把数据写进去。

页面类型

图片说明

页面通用部分

数据⻚,也就是 INDEX 类型的⻚由7个部分组成,其中的两个部分是所有类型的⻚⾯都通⽤的
图片说明
图片说明
图片说明