|
|
@ -1,4 +1,4 @@
|
|
|
|
# MySQL 高级
|
|
|
|
# MySQL 架构
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 系统是怎么和 MySQL 打交道 ?
|
|
|
|
## 1. 系统是怎么和 MySQL 打交道 ?
|
|
|
@ -185,6 +185,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 事物和锁
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 索引优化
|
|
|
|
# 索引优化
|
|
|
|
|
|
|
|
|
|
|
|
## 64. 深入研究索引之前,先来看看磁盘数据页的存储结构
|
|
|
|
## 64. 深入研究索引之前,先来看看磁盘数据页的存储结构
|
|
|
@ -249,4 +255,132 @@
|
|
|
|
- 这就是一个页分裂的过程,核心目标就是保证下一个数据页里的主键值都比上一个数据页里的主键值要大。
|
|
|
|
- 这就是一个页分裂的过程,核心目标就是保证下一个数据页里的主键值都比上一个数据页里的主键值要大。
|
|
|
|
- 保证了每个数据页的主键值,就能为后续的 索引打下基础
|
|
|
|
- 保证了每个数据页的主键值,就能为后续的 索引打下基础
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 67. 基于主键的索引是如何设计的,以及如何根据主键索引查询?
|
|
|
|
|
|
|
|
- 搜id=4的数据,你怎么知道在哪个数据页里?没有任何证据可以告诉你他到底是在哪个数据页里,也就只能全表扫描了
|
|
|
|
|
|
|
|
- 针对主键的索引实际上就是主键目录,这个主键目录呢,就是把每个数据页的页号,还有数据页里最小的主键值放在一起,组成一个索引的目录
|
|
|
|
|
|
|
|
![主键索引1](pic/主键索引1.png)
|
|
|
|
|
|
|
|
- 有了主键目录,直接就可以到主键目录里去搜索,比如你要找id=3的数据,此时就会跟每个数据页的最小主键来比,首先id=3大于了数据页2里的最小主键值1,接着小于了数据页8
|
|
|
|
|
|
|
|
里的最小主键值4。
|
|
|
|
|
|
|
|
- 直接就可以定位到id=3的数据一定是在数据页2里的
|
|
|
|
|
|
|
|
- 假设你有很多的数据页,在主键目录里就会有很多的数据页和最小主键值,此时你完全可以根据二分查找的方式来找你要找的id到底在哪个数据页里
|
|
|
|
|
|
|
|
- 数据页都是一坨一坨的连续数据放在很多磁盘文件里的,所以只要你能够根据主键索引定位到数据所在的数据页,此时假设我们有别的方式存储了数据页跟磁盘文件的对应关系,此时你
|
|
|
|
|
|
|
|
就可以找到一个磁盘文件。
|
|
|
|
|
|
|
|
- 假设数据页在磁盘文件里的位置也就是offset偏移量,你也是可以知道的,此时就可以直接通过随机读的方式定位到磁盘文件的某个offset偏移量的位置,然后就可以读取连续的一大坨数据页了
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 68. 索引的页存储物理结构,是如何用B+树来实现的?
|
|
|
|
|
|
|
|
- 解决几百万,几千万,甚至单表几亿条数据,所以此时可能有大量的数据页,主键目录明显不够用
|
|
|
|
|
|
|
|
- 在考虑这个问题的时候,实际上是采取了一种把索引数据存储在数据页里的方式来做的
|
|
|
|
|
|
|
|
- 也就是说,你的表的实际数据是存放在数据页里的,然后你表的索引其实也是存放在页里的,此时索引放在页里之后,就会有索引页,假设你有很多很多的数据页,那么此时你就可以有很多的索引页
|
|
|
|
|
|
|
|
![主键索引2](pic/主键索引2.png)
|
|
|
|
|
|
|
|
- 你现在有很多索引页,但是此时你需要知道,你应该到哪个索引页里去找你的主键数据,是索引页20?还是索引页28?这也是个大问题
|
|
|
|
|
|
|
|
- 于是接下来我们又可以把索引页多加一个层级出来,在更高的索引层级里,保存了每个索引页和索引页里的最小主键值,如下图所示
|
|
|
|
|
|
|
|
![主键索引3](pic/主键索引3.png)
|
|
|
|
|
|
|
|
- 假设我们要查找id=46的,直接先到最顶层的索引页35里去找,直接通过二分查找可以定位到下一步应该到索引页20里去找,接下来到索引页20里通过二分查找定位,也很快可以定位到数据应
|
|
|
|
|
|
|
|
该在数据页8里,再进入数据页8里,就可以找到id=46的那行数据了。
|
|
|
|
|
|
|
|
- 问题再次来了,假如你最顶层的那个索引页里存放的下层索引页的页号也太多了,怎么办呢?
|
|
|
|
|
|
|
|
- 此时可以再次分裂,再加一层索引页,比如下面图里那样子
|
|
|
|
|
|
|
|
![主键索引4](pic/主键索引4.png)
|
|
|
|
|
|
|
|
- 这就是一颗B+树,属于数据结构里的一种树形数据结构,所以一直说MySQL的索引是用B+树来组成的,其实就是这个意思。
|
|
|
|
|
|
|
|
- 当你为一个表的主键建立起来索引之后,其实这个主键的索引就是一颗B+树,然后当你要根据主键来查数据的时候,直接就是从B+树的顶层开始二分查找,一层
|
|
|
|
|
|
|
|
一层往下定位,最终一直定位到一个数据页里,在数据页内部的目录里二分查找,找到那条数据
|
|
|
|
|
|
|
|
- 这就是索引最真实的物理存储结构,采用跟数据页一样的页结构来存储,一个索引就是很多页组成的一颗B+树
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 69. 更新数据的时候,自动维护的聚簇索引到底是什么?
|
|
|
|
|
|
|
|
![主键索引4](pic/主键索引4.png)
|
|
|
|
|
|
|
|
- 首先呢,现在假设我们要搜索一个主键id对应的行,此时你就应该先去顶层的索引页88里去找,通过二分查找的方式,很容易就定位到你应该去下层哪个索引页里继续找
|
|
|
|
|
|
|
|
- 比如现在定位到了下层的索引页35里去继续找,此时在索引页35里也有一些索引条目的,分别都是下层各个索引页(20,28,59)和他们里面最小的主键值,
|
|
|
|
|
|
|
|
此时在索引页35的索引条目里继续二分查找,很容易就定位到,应该再到下层的哪个索引页里去继续找,
|
|
|
|
|
|
|
|
- 可能从索引页35接着就找到下层的索引页59里去了,此时索引页59里肯定也是有索引条目的,这里就存放了部分数据页页号(比如数据页2和数据页8)和每个数据页里最小的主键值
|
|
|
|
|
|
|
|
- 此时就在这里继续二分查找,就可以定位到应该到哪个数据页里去找
|
|
|
|
|
|
|
|
- 接着比如进入了数据页2,里面就有一个页目录,都存放了各行数据的主键值和行的实际物理位置
|
|
|
|
|
|
|
|
- 此时在这里直接二分查找,就可以快速定位到你要搜索的主键值对应行的物理位置,然后直接在数据页2里找到那条数据即可了。
|
|
|
|
|
|
|
|
- PS: 其实最下层的索引页,都是会有指针引用数据页的,所以实际上索引页之间跟数据页之间是有指针连接起来的
|
|
|
|
|
|
|
|
- PS: 其实索引页自己内部,对于一个层级内的索引页,互相之间都是基于指针组成双向链表的
|
|
|
|
|
|
|
|
- 总结: 假设你把索引页和数据页综合起来看, 他们都是连接在一起的,看起来就如同一颗完整的大的B+树一样,从根索引页88开始,一直到所有的
|
|
|
|
|
|
|
|
数据页,其实组成了一颗巨大的B+树。在这颗B+树里,最底层的一层就是数据页,数据页也就是B+树里的叶子节点了!
|
|
|
|
|
|
|
|
- 所以,如果一颗大的B+树索引数据结构里,叶子节点就是数据页自己本身,那么此时我们就可以称这颗B+树索引为聚簇索引
|
|
|
|
|
|
|
|
- 上图中所有的索引页+数据页组成的B+树就是聚簇索引!
|
|
|
|
|
|
|
|
- 在InnoDB存储引擎里,你在对数据增删改的时候,就是直接把你的数据页放在聚簇索引里的,数据就在聚簇索引里,聚簇索引就包含了数据!比如你插入数据,那么就是在数据页里插入数据
|
|
|
|
|
|
|
|
- 如果你的数据页开始进行页分裂了,他此时会调整各个数据页内部的行数据,保证数据页内的主键值都是有顺序的,下一个数据页的所有主键值大于上一个数据页的所有主键值
|
|
|
|
|
|
|
|
- 同时在页分裂的时候,会维护你的上层索引数据结构,在上层索引页里维护你的索引条目,不同的数据页和最小主键值
|
|
|
|
|
|
|
|
- 然后如果你的数据页越来越多,一个索引页放不下了,此时就会再拉出新的索引页,同时再搞一个上层的索引页,上层索引页里存放的索引条目就是下层索引页页号和最下主键值。
|
|
|
|
|
|
|
|
- 按照这个顺序,以此类推,如果你的数据量越大,此时可能就会多出更多的索引页层级来,不过说实话,一般索引页里可以放很多索引条目,所以通常而言,即使你是亿级的大表,基本上大表里建的索引
|
|
|
|
|
|
|
|
的层级也就三四层而已。
|
|
|
|
|
|
|
|
- 这个聚簇索引默认是按照主键来组织的,所以你在增删改数据的时候,一方面会更新数据页,一方面其实会给你自动维护B+树结构的聚簇索引,给新增和更新索引页,这个聚簇索引是默认就会给你建立的
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 70. 针对主键之外的字段建立的二级索引,又是如何运作的?
|
|
|
|
|
|
|
|
- 假设你要是针对其他字段建立索引,比如name、age之类的字段,这都是一样的原理,简单来说,比如你插入数据的时候,一方面会把完整数据插入到聚簇索引的叶子节点的数据页里去,同时维护
|
|
|
|
|
|
|
|
好聚簇索引,另一方面会为你其他字段建立的索引,重新再建立一颗B+树。
|
|
|
|
|
|
|
|
- 比如你基于name字段建立了一个索引,那么此时你插入数据的时候,就会重新搞一颗B+树,B+树的叶子节点也是数据页,但是这个数据页里仅仅放主键字段和name字段
|
|
|
|
|
|
|
|
- 独立于聚簇索引之外的另外一个索引B+树了,严格来说是name字段的索引B+树,所以在name字段的索引B+树里,叶子节点的数据页里仅仅放主键和name字段的值,至于排序规则之类
|
|
|
|
|
|
|
|
的,都是跟以前说的一样的。
|
|
|
|
|
|
|
|
- 假设你要根据name字段来搜索数据,那搜索过程简直都一样了,不就是从name字段的索引B+树里的根节点开始找,一层一层往下找,一直找到叶子节点的数据页里,定位到name字段值对应的主键值
|
|
|
|
|
|
|
|
- 此时针对select * from table where name='xx'这样的语句,你先根据name字段值在name字段的索引B+树里找,找到叶子节点也仅仅可以找到对应的主键值,而找不到这行数据完整的所有字段。
|
|
|
|
|
|
|
|
- 所以此时还需要进行“回表”,这个回表,就是说还需要根据主键值,再到聚簇索引里从根节点开始,一路找到叶子节点的数据页,定位到主键对应的完整数据行,此时才能把select *要的全部字段值都拿出来
|
|
|
|
|
|
|
|
- 因为我们根据name字段的索引B+树找到主键之后,还要根据主键去聚簇索引里找,所以一般把name字段这种普通字段的索引称之为二级索引,一级索引就是聚簇索引,这就是普通字段的索引的运行原理。
|
|
|
|
|
|
|
|
- 也可以把多个字段联合起来,建立联合索引,比如name+age
|
|
|
|
|
|
|
|
- 此时联合索引的运行原理也是一样的,只不过是建立一颗独立的B+树,叶子节点的数据页里放了id+name+age,然后默认按照name排序,name一样就按照age排序,不同数据页之间的name+age值
|
|
|
|
|
|
|
|
的排序也如此。
|
|
|
|
|
|
|
|
- 总结: innodb存储引擎的索引的完整实现原理了,其实大家一步一步看下来,会发现索引这块知识也没那么难,不过就是建立B+树,根据B+树一层一层二分查找罢了,然后不同的索引就是建立不同的
|
|
|
|
|
|
|
|
B+树,然后你增删改的时候,一方面在数据页里更新数据,一方面就是维护你所有的索引。后续查询,你就要尽量根据索引来查询。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 71. 插入数据时到底是如何维护好不同索引的B+树的?
|
|
|
|
|
|
|
|
- 其实刚开始你一个表搞出来以后,其实他就一个数据页,这个数据页就是属于聚簇索引的一部分,而且目前还是空的
|
|
|
|
|
|
|
|
- 此时如果你插入数据,就是直接在这个数据页里插入就可以了,也没必要给他弄什么索引页
|
|
|
|
|
|
|
|
- 这个初始的数据页其实就是一个根页,每个数据页内部默认就有一个基于主键的页目录,所以此时你根据主键来搜索都是ok没有问题的,直接在唯一 一个数据页里根据页目录找就行了
|
|
|
|
|
|
|
|
- 然后你表里的数据越来越多了,此时你的数据页满了,那么就会搞一个新的数据页,然后把你根页面里的数据都拷贝过去,同时再搞一个新的数据页,根据你的主键值的大小进行挪动,让两个新的数据页根
|
|
|
|
|
|
|
|
据主键值排序,第二个数据页的主键值都大于第一个数据页的主键值
|
|
|
|
|
|
|
|
- 那么此时那个根页在哪儿呢?
|
|
|
|
|
|
|
|
- 此时根页就升级为索引页了,这个根页里放的是两个数据页的页号和他们里面最小的主键值,根页就成为了索引页,引用了两个数据页
|
|
|
|
|
|
|
|
- 接着你肯定会不停的在表里灌入数据,然后数据页不停的页分裂,分裂出来越来越多的数据页
|
|
|
|
|
|
|
|
- 此时你的唯一 一个索引页,也就是根页里存放的数据页索引条目越来越多,连你的索引页都放不下了,那你就让一个索引页分裂成两个索引页,然后根页继续往上走一个层级引用了两个索引页
|
|
|
|
|
|
|
|
- 接着就是依次类推了,你的数据页越来越多,那么根页指向的索引页也会不停分裂,分裂出更多的索引页,当你下层的索引页数量太多的时候,会导致你的根页指向的索引页太多了,此时根页继续分裂成多
|
|
|
|
|
|
|
|
个索引页,根页再次往上提上去去一个层级。
|
|
|
|
|
|
|
|
- 这其实就是你增删改的时候,整个聚簇索引维护的一个过程,其实其他的二级索引也是类似的一个原理
|
|
|
|
|
|
|
|
- 比如你name字段有一个索引,那么刚开始的时候你插入数据,一方面在聚簇索引的唯一的数据页里插入,一方面在name字段的索引B+树唯一的数据页里插入。
|
|
|
|
|
|
|
|
- 然后后续数据越来越多了,你的name字段的索引B+树里唯一的数据页也会分裂,整个分裂的过程跟上面说的是一样的,所以你插入数据的时候,本身就会自动去维护你的各个索引的B+树。
|
|
|
|
|
|
|
|
- 你的name字段的索引B+树里的索引页中,其实除了存放页号和最小name字段值以外,每个索引页里还会存放那个最小name字段值对应的主键值
|
|
|
|
|
|
|
|
- 这是因为有时候会出现多个索引页指向的下层页号的最小name字段值是一样的,此时就必须根据主键判断一下。
|
|
|
|
|
|
|
|
- 新的name字段值肯定是插入到主键值较大的那个数据页里去的。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 查询语句的执行原理
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 多表join语句的执行原理
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# MySQL执行计划
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# SQL语句调优
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 高可用及部署
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|