You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

535 lines
58 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# MySQL 架构
## 1. 系统是怎么和 MySQL 打交道 ?
- MySQL不仅仅是CRUD
### 1.1 MySQL 驱动
- mysql 驱动是跟数据库进行的一个网络连接建立工具
- 不同的语言有不同的驱动使用
### 1.2 数据库连接池
- 作用是减少连接的重复建立和销毁让线程去执行SQL语句后, 不要销毁这个数据库连接,而是放在池子中进行复用
- 常见的数据库连接池有: DBCPC3P0Druid
## 2. 执行 SQL 语句, MySQL用了什么架构设计
- 一个不变的原则: 网络连接必须让线程来处理
![MySQL架构设计](pic/MySQL架构设计.png)
- SQL 接口: 负责处理接收到的SQL语句
- 监听请求以及读取请求数据的线程, 把解析好的SQL语句v转交给SQL接口去执行
- 查询解析器: 按照估计的SQL语法, 对我们发送的 SQL 语句进行解析, 理解其要进行的操作
- 查询优化器: 生成查询路径树, 然后从里面选择一条最优的查询路径
- 调用存储引擎, 真正的执行 SQL 语句
- MySQL 的设计架构中, SQL接口, SQL解析器, 查询优化器其实都是通用的,就是一套组件
- 存储引擎可以供我们去选择, 选择那种存储引擎来执行SQL。常见的存储引擎: InnoDB, MyISAM, Memory等。
- 执行器: 执行器会根据我们的优化器生成一套执行计划, 然后不停的调用存储引擎的各种接口去完成SQL语句的执行计划
## 3. 用一次数据更新流程, 了解 InnoDB 引擎的架构设计
### 3.1 InnoDB 的重要内存结构: 缓冲池
![缓冲池](pic/缓冲池.png)
- InnoDB 的很重要放在内存里面的组件, 里面会缓冲很多数据, 在查询时候, 如果查的是内存中的数据, 就不用去查盘了
![缓冲池加载](pic/缓冲池加载.png)
- 引擎在更新语句的时候, 比如 "id=10" 这行数据是否在缓冲池中, 如果不在就从磁盘中加载到缓冲池, 而且还会对这行数据加独占锁
### 3.2 undo日志文件: 如何让你更新的数据可以回滚?
![undo日志](pic/undo日志.png)
- 我们开发中可以轻松地利用事务对数据提交的过程进行回滚操作是因为会把更新前的值写入到undo日志文件中
### 3.3 更新 buffer pool 中的缓存数据
- 当我们要把更新的那行记录从磁盘文件加载到缓冲池, 同时对他加锁, 而且还把更新前的旧值写入undo日志文件之后, 我们才正式开始更新
这行记录, 更新的时候, 先会更新缓冲池中的记录, 此时这个数据是脏数据
- 这里所谓的更新内存缓冲池里的数据, 意思是把内存里的 "id=10" 这行数据的name修改为 "xxx"
- 为什么说是脏数据呢?
- 因为这个时候磁盘上 "id=10" 这行数据的name字段还是以前的值, 但是内存里面这行数据已经改变了, 所以称为脏数据
![更新buffer-pool中的缓存数据](pic/更新bufferpool中的缓存数据.png)
### 3.4 Redo Log Buffer: 万一系统宕机, 如何避免数据丢失?
- 在 3.3 中如果此时宕机, 会导致内存中改过的数据丢失, 怎么办?
- 这个时候,就必须把对内存的修改写到一个Redo Log Buffer 中, 这也是内存里的一个缓冲区, 是用来存放 redo 日志的
- 所谓 redo 日志,就是记录你对数据做了什么修改, 比如对 "id=10这行记录修改了name字段的值为xxx", 这就是一个日志
![redo日志](pic/redo日志.png)
- 这个 redo 日志其实是用来在 MySQL 突然宕机的时候, 用来恢复你更新过的数据, 现在redo日志还仅仅停留在内存缓冲里
### 3.5 如果还没提交事务, MySQL 宕机了怎么办?
- 执行一条SQL语句, 其实也可以是一个独立的事务, 当你提交事务后, SQL语句才算执行结束
- 此时还没有提交事务, 如果此时 MySQL 崩溃, 必然导致内存里 Buffer Pool 中的修改过的数据都丢失,
同时你写入 Redo Log Buffer 中的redo日志也会丢失<br/>
![没提交事务宕机](pic/没提交事务宕机.png)
- 此时, 丢数据不要紧, 因为你一条更新语句, 没提交事务, 就代表他没执行成功, 磁盘上的数据没有改变, mysql 重启后, 你的数据无任何变化
### 3.6 提交事务的时候将redo日志写入磁盘中
- 我们提交一个事务, 此时会根据一定的策略把redo日志从 redo log buffer里刷入到磁盘文件里去
- 这个策略是通过 innodb_flush_log_at_trx_commit 来配置的
- 当这个值为0时候, 你提交事务的时候,不会把redo log buffer里面的数据刷入磁盘文件,此时可能你都提交事务了, 结果MySQL宕机了, 然后此时内存里的数据全部丢失
相当于你提交事务成功了, 但是由于MySQL突然宕机, 导致内存中的数据和redo日志都丢失了<br/>
![redo写磁盘0](pic/redo写磁盘0.png)
- 当这个参数值为1的时候, 你提交事务的时候, 就必须把redo log从内存刷入到磁盘文件中去, 只要事务提交成功, 那么redo log就必然在磁盘里了
![redo写磁盘1](pic/redo写磁盘1.png)
- 只要提交事务成功, redo日志一定在磁盘文件中了, 此时你肯定会有一条redo日志说了, "我对什么数据进行了一个什么操作"
- 然后哪怕此时buffer pool中更新过的数据还没刷到磁盘里面去,此时内存中的数据是已经更新过的"name=xxx",然后磁盘上的数据是还没更新过的"name=zhangsan"
![redo写磁盘3](pic/redo写磁盘3.png)
- 此时不会丢数据, 因为redo日志中已经记录了操作
- 所以此时mysql重启后, 可以根据redo日志去恢复之前做的修改
![redo写磁盘4](pic/redo写磁盘4.png)
- 如果 innodb_flush_log_at_trx_commit 值为2
- 意思是说, 提交事务的时候, 把redo日志写入磁盘文件对应的os cache缓存里去, 而不是直接进入磁盘文件, 可能1秒后才会把 os cache 里的数据写入到磁盘文件中去
- 这种模式下,你提交了事务, redo log可能仅仅停留在os cache内存缓存中, 没实际进入磁盘文件, 万一此时你要是机器宕机了, 那么os cache里的redo log就会丢失
同样让你感觉提交了事务, 数据丢了
![redo写磁盘5](pic/redo写磁盘5.png)
---
- 总结: 对于redo日志的三种刷盘策略, 我们的通常建议是1, 保证事务提交后, 数据绝对不能丢失
## 4. 聊聊binlog是什么?
### 4.1 binlog 日志概念
- binlog 日志叫做归档日志, 他里面记录的是偏向于逻辑性的日志, 类似于“对users表中的id=10的一行数据做了更新操
作,更新以后的值是什么”
- binlog不是InnoDB存储引擎特有的日志文件是属于mysql server自己的日志文件。
### 4.2 提交事务, 同时会写入binlog日志
![写入binlog日志](pic/写入binlog日志.png)
- 跟InnoDB存储引擎进行交互的组件加入了之前提过的执行器这个组件
他会负责跟InnoDB进行交互包括从磁盘里加载数据到Buffer Pool中进行缓存包括写入undo日志包括更新
Buffer Pool里的数据以及写入redo log bufferredo log刷入磁盘写binlog等等。
- 执行器是非常核心的一个组件负责跟存储引擎配合完成一个SQL语句在磁盘与内存层面的全部数据更新操
作。
- 把一次更新语句的执行拆分为了两个阶段上图中的1、2、3、4几个步骤其实本质是你执行这个更新语句的时候干的事。
- 5和6两个步骤是从你提交事务开始的属于提交事务的阶段了。
### 4.3 binlog日志的刷盘策略分析
- binlog日志其实也有不同的刷盘策略有一个**sync_binlog**参数可以控制binlog的刷盘策略他的默认值是0
此时你把binlog写入磁盘的时候其实不是直接进入磁盘文件而是进入os cache内存缓存
- 如果此时机器宕机那么你在os cache里的binlog日志是会丢失的
![binlog日志刷盘](pic/binlog日志刷盘.png)
- 如果要是把sync_binlog参数设置为1的话那么此时会强制在提交事务的时候把binlog直接写入到磁盘文件里去
那么这样提交事务之后哪怕机器宕机磁盘上的binlog是不会丢失的
![binlog日志刷盘1](pic/binlog日志刷盘1.png)
### 4.3 基于binlog和redo log完成事务的提交
- 当我们把binlog写入磁盘文件之后接着就会完成最终的事务提交此时会把**本次更新对应的binlog文件名称**和这次
**更新的binlog日志在文件里的位置**都写入到redo log日志文件里去同时在redo log日志文件里写入一个**commit标记**。
### 4.4 最后一步在redo日志中写入commit标记的意义是什么?
- 用来保持redo log日志与binlog日志一致的
- 完整的事物提交成功, 必须是在redo log中写入最终的事务commit标记了而且redo log里有本次更新对应的日 志binlog里也有本次
更新对应的日志 redo log和binlog完全是一致的。
### 4.5 后台IO线程随机将内存更新后的脏数据刷回磁盘
- 已经提交事务, 他已经把内存里的 buffer pool中的缓存数据更新了同时磁盘里有redo日志和binlog日志都记录的新值,但是磁盘上可能还是旧值?
- 因为MySQL有一个后台的IO线程会在之后某个时间里随机的把内存buffer pool中的修改后的脏数据给刷回到磁 盘上的数据文件里去
![IO线程随机更新](pic/IO线程随机更新.png)
- 在你IO线程把脏数据刷回磁盘之前哪怕mysql宕机崩溃也没关系因为重启之后会根据redo日志恢复之前提交事 务做过的修改到内存里去,
就是id=10的数据的name修改为了xxx然后等适当时机IO线程自然还是会把这个修改后的数据刷到磁盘上的数据文件里去的
### 4.6 总结
- InnoDB存储引擎: buffer pool、redo log buffer等内存里的缓存数据, undo日志文件, redo日志文件, 同时mysql server自己还有 binlog日志文件
- 在你执行更新的时候每条SQL语句都会对应修改buffer pool里的缓存数据、写undo日志、写redo log buffer几个步骤
- 当你提交事务的时候一定会把redo log刷入磁盘binlog刷入磁盘完成redo log中的事务commit标记最后后台的IO线程会随机的把buffer pool里的脏数据刷入磁盘里去。
## 生产经验: 真实生产环境下的数据库机器配置如何规划?
### 普通的Java应用系统部署在机器上能抗多少并发
- Java应用系统部署的时候常选用的机器配置大致是2核4G和4核8G的较多一些数据库部署的时候常选用的机器配置最低在8核16G以上正常在16核32G
- 一台机器能抗下每秒多少请求,往往是跟你每个请求处理耗费多长时间是关联的
- 4核8G的机器部署普通的Java应用系统每秒大致就是抗下几百的并发访问从每秒一两百请求到每秒七八百请求都是有可能的关键是看你每个请求处理需要耗费多长时间。
### 高并发场景下,数据库应该用什么样的机器?
- 往往对一个数据库而言都是选用8核16G的机器作为起步最好是选用16核32G的机器更加合适一些因为数据库需要执行大量的磁盘IO操作
他的每个请求都比较耗时一些,所以机器的配置自然需要高一些了
- 一般8核16G的机器部署的MySQL数据库每秒抗个一两千并发请求是没问题的但是如果你 的并发量再高一些假设每秒有几千并发请求那么可能数据库就会有点危险了因为数据库的CPU、磁盘、IO、内存的负载
都会很高,弄不数据库压力过大就会宕机。
- 对于16核32G的机器部署的MySQL数据库而言每秒抗个两三千甚至三四千的并发请求也都是可以的但是如果你达到每秒上万请求
那么数据库的CPU、磁盘、IO、内存的负载瞬间都会飙升到很高数据库也是可能会扛不住宕机的。
- 对于数据库而言如果可以的话最好是采用SSD固态硬盘而不是普通的机械硬盘因为数据库最大的复杂就在于大量的 磁盘IO他需要大量的读写磁盘文件
所以如果能使用SSD固态硬盘那么你的数据库每秒能抗的并发请求量就会更高一些。
## 生产经验:互联网公司的生产环境数据库是如何进行性能测试的?
### QPS和TPS到底有什么区别
- QPS他的英文全称是Query Per Second。
- QPS就是说你的这个数据库每秒可以处理多少个请求你大致可以理解为一次请求就是一条SQL语句也就是说这个数据库每秒可以处理多少个SQL语句
- TPS他的英文全称是Transaction Per Second
- 其实就是每秒可处理的事务量这个TPS往往是用在数据库中较多一些其实从字面意思就能看的出来他就是说数据库每秒会处理多少次事务提交或者回滚。
- PS: 不同的服务或者系统, 关注的是QPS还是TPS是不一样的。
### IO相关的压测性能指标
- IOPS
- 这个指的是机器的随机IO并发处理的能力比如机器可以达到200 IOPS意思就是说每秒可以执行200个随机 IO读写请求
- 这个指标是很关键的你在内存中更新的脏数据库最后都会由后台IO线程在不确定的时间刷回到磁盘里去这就是随机IO的过程。
- 如果说IOPS指标太低了那么会导致你内存里的脏数据刷回磁盘的效率就会不高。
- 吞吐量
- 这个指的是机器的磁盘存储每秒可以读写多少字节的数据量
- 这个指标也是很关键的我们平时在执行各种SQL语句的时候提交事务的时候其实都是大量的会写redo log之类的日志的这些日志都会直接写磁盘文件。
- 一台机器他的存储每秒可以读写多少字节的数据量就决定了他每秒可以把多少redo log之类的日志写入到磁盘里去。
- 一般来说我们写redo log之类的日志都是对磁盘文件进行顺序写入的也就是一行接着一行的写不会说进行随机的读写
- 一般普通磁盘的顺序写入的吞吐量每秒都可以达到200MB左右
- 所以通常而言,机器的磁盘吞吐量都是足够承载高并发请求的。
- latency
- 这个指标说的是往磁盘里写入一条数据的延迟
- 这个指标同样很重要因为我们执行SQL语句和提交事务的时候都需要顺序写redo log磁盘文件所以此时你写一条日志到磁盘文件里去
到底是延迟1ms还是延迟100us这就对你的数据库的SQL语句执行性能是有影响的。
- 一般来说当然是你的磁盘读写延迟越低那么你的数据库性能就越高你执行每个SQL语句和事务的时候速度就会越快。
### 压测的时候要关注的其他性能指标
- CPU负载
- CPU负载是一个很重要的性能指标因为假设你数据库压测到了每秒处理3000请求了可能其他的性能指标都还正常但是此时CPU负载特别高
那么也说明你的数据库不能继续往下压测更高的QPS了否则CPU是吃不消的
- 网络负载
- 这个主要是要看看你的机器带宽情况下在压测到一定的QPS和TPS的时候每秒钟机器的网卡会输入多少
MB数据会输出多少MB数据因为有可能你的网络带宽最多每秒传输100MB的数据那么可能你的QPS到1000的时候
卡就打满了已经每秒传输100MB的数据了此时即使其他指标都还算正常但是你也不能继续压测下去了
- 内存负载
- 这个就是看看在压测到一定情况下的时候,你的机器内存耗费了多少,如果说机器内存耗费过高了,说明也不能继续压测下去了
# 事物和锁
# 索引优化
## 64. 深入研究索引之前,先来看看磁盘数据页的存储结构
- 大量的数据页是按顺序一页一页存放的,然后两两相邻的数据页之间会采用双向链表的格式互相引用
- 上述表示在磁盘中的形态
- 其实一个数据页在磁盘文件里就是一段数据,可能是二进制或者别的特殊格式的数据,然后数据页里包含两个指针,一个指针指向自己上一个数据页的物理地址,
一个指针指向自己下一个数据页的物理地址,大概可以认为类似下面这样:
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=15367, linked_list_next_pointer=34126 ||
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=23789, linked_list_next_pointer=46589 ||
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=33198, linked_list_next_pointer=55681
- MySQL实际存储大致也是类似这样的就是每个数据页在磁盘文件里都是连续的一段数据。
然后每个数据页里可以认为就是DataPage打头一直到 || 符号的一段磁盘里的连续的数据,你可以认为每一个数据页就是磁盘文件里这么一段连续的东西。
每个数据页,都有一个指针指向自己上一个数据页在磁盘文件里的起始物理位置,比如 linked_list_pre_pointer=15367 就是指向了上一个数据页在磁盘文件里的起始物理位置
那个15367可以认为就是在磁盘文件里的position或者offset同理也有一个指针指向自己下一个数据页的物理位置。
- 然后一个数据页内部会存储一行一行的数据,也就是平时我们在一个表里插入的一行一行的数据就会存储在数据页里,然后数据页里的每一行数据都会按照主键大小进行排序存储,同时每一行数据都有指针
指向下一行数据的位置,组成单向链表
![磁盘数据页的存储结构](pic/磁盘数据页的存储结构.png)
- 总结: 数据页之间是组成双向链表的,然后数据页内部的数据行是组成单向链表的,而且数据行是根据主键从小到大排序的。
## 65. 假设没有任何索引,数据库是如何根据查询语句搜索数据的?
- 每个数据页里都会有一个页目录,里面根据数据行的主键存放了一个目录,同时数据行是被分散存储到不同的槽位里去的,所以实际上每个数据页的目录里,就是这个页里每个主键跟所在槽位的映射关系
![页目录1](pic/页目录1.png)
- 假设你要根据主键查找一条数据,而且假设此时你数据库里那个表就没几条数据,那个表总共就一个数据页,首先就会先到数据页的页目录里根据主键进行二分查找
- 然后通过二分查找在目录里迅速定位到主键对应的数据是在哪个槽位里,然后到那个槽位里去,遍历槽位里每一行数据,就能快速找到那个主键对应的数据了。
- 每个槽位里都有一组数据行,你就是在里面遍 历查找就可以了。
- 但是假设你要是根据非主键的其他字段查找数据呢?
- 此时你是没办法使用主键的那种页目录来二分查找的,只能进入到数据页里,根据单向链表依次遍历查找数据了,这就性能很差了。
- 假如我们有很多数据页呢?
- 假设你要是没有建立任何索引,那么无论是根据主键查询,还是根据其他字段来条件查询,实际上都没有什么取巧的办法
- 一个表里所有数据页都是组成双向链表的吧?好,有链表就好办了,直接从第一个数据页开始遍历所有数据页,从第一个数据页开始,
你得先把第一个数据页从磁盘上读取到内存buffer pool的缓存页里来。
- 然后你就在第一个数据页对应的缓存页里,按照上述办法查找
- 假设是根据主键查找的,你可以在数据页的页目录里二分查找,假设你要是根据其他字段查找的,只能是根据数据页内部的单向链表来遍历查找
![页目录2](pic/页目录2.png)
- 假设第一个数据页没找到你要的那条数据
- 只能根据数据页的双向链表去找下一个数据页然后读取到buffer pool的缓存页里去然后按一样的方法在一个缓存页内部查找那条数据。
- 如果依然还是查找不到呢?
- 那只能根据双向链表继续加载下一个数据页到缓存页里来了,以此类推,循环往复。
- 你似乎是在做一个数据库里很尴尬的操作:全表扫描?
## 66. 不断在表中插入数据时,物理存储是如何进行页分裂的?
- 我们在一个表里不停的插入数据的时候,会涉及到一个页分裂的过程,也就是说,这个表里是如何出现一个又一个的数据页的。
- 正常情况下我们在一个表里插入一些数据后,他们都会进入到一个数据页里去,在数据页内部,他们会组成一个单向链表,这个数据页内部的单向链表大致如下所示
![页分裂1](pic/页分裂1.png)
- 里面就是一行一行的数据刚开始第一行是个起始行他的行类型是2就是最小的一行
- 他有一个指针指向了下一行数据,每一行数据都有自己每个字段的值,然后每一行通过一个指针不停的指向下一行数据
- 普通的数据行的类型都是0最后一行是一个类型为3的就是代表最大的一行。
- 假设你不停的在表里插入数据,那么刚开始是不是就是不停的在一个数据页插入数据?接着数据越来越多,越来越多,此时就要再搞一个数据页了
![页分裂2](pic/页分裂2.png)
- 索引运作的一个核心基础就是要求你后一个数据页的主键值都大于前面一个数据页的主键值
- 但是如果你的主键是自增的,那还可以保证这一点,因为你新插入后一个数据页的主键值一定都大于前一个数据页的主键值
- 但是有时候你的主键并不是自增长的,所以可能会出现你后一个数据页的主键值里,有的主键是小于前一个数据页的主键值的
- 比如在第一个数据页里有一条数据的主键是10第二个数据页里居然有一条数据的主键值是8那此时肯定有问题了。
- 所以此时就会出现一个过程,叫做页分裂
- 就是万一你的主键值都是你自己设置的,那么在增加一个新的数据页的时候,实际上会把前一个数据页里主键值较大的,挪动到新的数据页里来,然后把你新插入
的主键值较小的数据挪动到上一个数据页里去,保证新数据页里的主键值一定都比上一个数据页里的主键值大。
- 假设新数据页里,有两条数据的主键值明显是小于上一个数据页的主键值的
![页分裂3](pic/页分裂3.png)
- 第一个数据页里有1、5、6三条数据第二个数据页里有2、3、4三条数据明显第二个数据页里的数据的主键值比第一个数据页里的5和6两个主键都小所以这个是不行的
- 此时就会出现页分裂的行为,把新数据页里的两条数据挪动到上一个数据页,上一个数据页里挪两条数据到新数据页里去
![页分裂4](pic/页分裂4.png)
- 这就是一个页分裂的过程,核心目标就是保证下一个数据页里的主键值都比上一个数据页里的主键值要大。
- 保证了每个数据页的主键值,就能为后续的 索引打下基础
## 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里也有一些索引条目的分别都是下层各个索引页202859和他们里面最小的主键值
此时在索引页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字段值肯定是插入到主键值较大的那个数据页里去的。
## 72. 一个表里是不是索引搞的越多越好?那你就大错特错了!
- 正常我们在一个表里灌入数据的时候,都会基于主键给我们自动建立聚簇索引
- 随着我们不停的在表里插入数据,他就会不停的在数据页里插入数据,然后一个数据页放满了就会分裂成多个数据页,这个时候就需要索引页去指向各个数据页
- 如果数据页太多了,那么索引页里里的数据页指针也就会太多了,索引页也必然会放满的,此时索引页也会分裂成多个,再形成更上层的索引页
- 默认情况下MySQL给我们建立的聚簇索引都是基于主键的值来组织索引的聚簇索引的叶子节点都是数据页里面放的就是我们插入的一行一行的完整的数据了
- 在一个索引B+树中,他有一些特性
- 数据页/索引页里面的记录都是组成一个单向链表的而且是按照数据大小有序排列的
- 数据页/索引页互相之间都是组成双向链表的,而且也都是按照数据大小有序排列的
- 其实B+树索引是一个完全有序的数据结构,无论是页内还是页之间
- 因为这个有序的B+树索引结构,才能让我们查找数据的时候,直接从根节点开始按照数据值大小一层一层往下找,这个效率是非常高的。
- 如果是针对主键之外的字段建立索引的话实际上本质就是为那个字段的值重新建立另外一颗B+树索引
- 那个索引B+树的叶子节点,存放的都是数据页,里面放的都是你字段的值和主键值,然后每一层索引页里存放的都是下层页的引用
- 包括页内的排序规则页之间的排序规则B+树索引的搜索规则,都是一样的
- 假设我们要根据其他字段的索引来搜索那么只能基于其他字段的索引B+树快速查找到那个值所对应的主键,接着再次做回表查询
- 基于主键在聚簇索引的B+树里,重新从根节点开始查找那个主键值,找到主键值对应的完整数据
- 你在MySQL的表里建立一些字段对应的索引好处是什么
- 你在MySQL的表里建立一些字段对应的索引好处是什么
- 但是坏处呢?索引当然有缺点了,主要是两个缺点,一个是空间上的,一个是时间上的。
- 空间上而言你要是给很多字段创建很多的索引那你必须会有很多棵索引B+树每一棵B+树都要占用很多的磁盘空间啊!所以你要是搞的索引太多了,是很耗费磁盘空间的。
- 你要是搞了很多索引那么你在进行增删改查的时候每次都需要维护各个索引的数据有序性因为每个索引B+树都要求页内是按照值大小排序的,页之间也是有序的,下一个页的所有值必须大于上一个页的所有值!
- 所以你不停的增删改查,必然会导致各个数据页之间的值大小可能会没有顺序,比如下一个数据页里插入了一个比较小的值,居然比上一个数据页的值要小!此时就没办法了,只能进行数据页的挪动,维护
页之间的顺序。
- 或者是你不停的插入数据,各个索引的数据页就要不停的分裂,不停的增加新的索引页,这个过程都是耗费时间的。
- 所以你要是一个表里搞的索引太多了,很可能就会导致你的增删改的速度就比较差了,也许查询速度确实是可以提高,但是增删改就会受到影响,因此通常来说,我们是不建议一个表里搞的索引太多的!
- 那么怎么才能尽量用最少的索引满足最多的查询请求还不至于让索引占用太多磁盘空间影响增删改性能呢这就需要我们深入理解索引的使用规则了我们的SQL语句要怎么写才能用上索引B+树来查询!
## 73. 通过一步一图来深入理解联合索引查询原理以及全值匹配规则
- 之所以讲解联合索引,那是因为平时我们设计系统的时候一般都是设计联合索引,很少用单个字段做索引,原因之前讲过,我们还是要尽可能的让索引数量少一些,避免磁盘占用太多,增删改性能太差。
- 然后呢,就是包含了学生班级、学生姓名、科目名称、成绩分数四个字段,平时查询,可能比较多的就是查找某个班的某个学生的某个科目的成绩。
- 所以,我们可以针对学生班级、学生姓名和科目名称建立一个联合索引。
- 接着我们画了下面的一个图,这个图就展示了这个三个字段组成的联合索引的部分内容
![联合索引](pic/联合索引.png)
- 下面有两个数据页,第一个数据页里有三条数据,每条数据都包含了联合索引的三个字段的值和主键值,数据页内部是按照顺序排序的。
- 首先按照班级字段的值来排序,如果一样则按照学生姓名字段来排序,如果一样,则按照科目名称来排序,所以数据页内部都是按照三个字段的值来排序的,而且还组成了单向链表。
- 然后数据页之间也是有顺序的,第二个数据页里的三个字段的值一定都大于上一个数据页里三个字段的值,比较方法也是按照班级名称、学生姓名、科目名称依次来比较的,数据页之间组成双向链表。
- 索引页里就是两条数据,分别指向两个数据页,索引存放的是每个数据页里最小的那个数据的值,大家看到,索引页里指向两个数据页的索引项里都是存放了那个数据页里最小的值!
- 索引页内部的数据页是组成单向链表有序的,如果你有多个索引页,那么索引页之间也是有序的,组成了双向链表。
- 假设我们想要搜索1班+张小强+数学的成绩此时你可能会写一个类似下面的SQL语句
- select * from student_score where class_name='1班' and student_name='张小强' and subject_name='数学'。
- 此时就涉及到了一个索引使用的规则那就是你发起的SQL语句里where条件里的几个字段都是基于等值来查询都是用的等于号
- 而且where条件里的几个字段的名称和顺序也跟你的联合索引一模一样此时就是等值匹配规则
- 上面的SQL语句是百分百可以用联合索引来查询的
- 那么查询的过程也很简单了首先到索引页里去找索引页里有多个数据页的最小值记录此时直接在索引页里基于二分查找法来找就可以了先是根据班级名称来找1班这个值对应的数据页直接可以定
位到他所在的数据页
- 然后你就直接找到索引指向的那个数据页就可以了在数据页内部本身也是一个单向链表你也是直接就做二分查找就可以了先按1班这个值来找
你会发现几条数据都是1班此时就可以按照张小强这个姓名来二分查找此时会发现多条数据都是张小强接着就按照科目名称数学来二分查找。
很快就可以定位到下图中的一条数据1班的张小强的数学科目他对应的数据的id是127
- 然后就根据主键id=127到聚簇索引里按照一样的思路从索引根节点开始二分查找迅速定位下个层级的页再不停的找很快就可以找到id=127的那条数据然后从里面提取所有字段包括分数就可以了。
- 对于联合索引而言,就是依次按照各个字段来进行二分查找,先定位到第一个字段对应的值在哪个页里,然后如果第一个字段有多条数据值都一样,就根据第二个字段来找,以此类推,一定可以定位到某
条或者某几条数据!
## 74. 再来看看几个最常见和最基本的索引使用规则
- 当我们建立好一个联合索引之后我们的SQL语句要怎么写才能让他的查询使用到我们建立好的索引呢
- 等值匹配规则就是你where语句中的几个字段名称和联合索引的字段完全一样而且都是基于等号的等值匹配那百分百会用上我们的索引这个大家是没有问题的即使你where语句里
写的字段的顺序和联合索引里的字段顺序不一致也没关系MySQL会自动优化为按联合索引的字段顺序去找。
- 现在看第二个规则就是最左侧列匹配这个意思就是假设我们联合索引是KEY(class_name, student_name, subject_name)那么不一定必须要在where语句里根据三个字段来查其实只要根据
最左侧的部分字段来查,也是可以的。
- 比如你可以写select * from student_score where class_name='' and student_name='',就查某个学生所有科目的成绩,这都是没有问题的。
- 但是假设你写一个select * from student_score where subject_name=''那就不行了因为联合索引的B+树里是必须先按class_name查再按student_name查不能跳过前面两个字段直接按最后一
个subject_name查的。
- 另外假设你写一个select * from student_score where class_name='' and subject_name=''那么只有class_name的值可以在索引里搜索剩下的subject_name是没法在索引里找的道理同上
- 所以在建立索引的过程中,你必须考虑好**联合索引字段的顺序**以及你平时写SQL的时候要按哪几个字段来查。
- 第三个规则是最左前缀匹配原则即如果你要用like语法来查比如select * from student_score where class_name like '1%'查找所有1打头的班级的分数那么也是可以用到索引的。
- 因为你的联合索引的B+树里都是按照class_name排序的所以你要是给出class_name的确定的最左前缀就是1然后后面的给一个模糊匹配符号那也是可以基于索引来查找的这是没问题的。
- 但是你如果写class_name like '%班',在左侧用一个模糊匹配符,那他就没法用索引了,因为不知道你最左前缀是什么,怎么去索引里找啊?
- 第四个规则就是范围查找规则这个意思就是说我们可以用select * from student_score where class_name>'1班' and class_name<'5班'这样的语句来范围查找某几个班级的分数
- 这个时候也是会用到索引的,因为我们的索引的最下层的数据页都是按顺序组成双向链表的,所以完全可以先找到'1班'对应的数据页,再找到'5班'对应的数据页,两个数据页中间的那些数据页,就全都是在你范围内的数据了!
- 但是如果你要是写select * from student_score where class_name>'1班' and class_name<'5班' and student_name>''
这里只有class_name是可以基于索引来找的student_name的范围查询是没法用到索引的
- 就是你的where语句里如果有范围查询那只有对联合索引里最左侧的列进行范围查询才能用到索引
- 第五个规则,就是等值匹配+范围匹配的规则如果你要是用select * from student_score where class_name='1班' and student_name>'' and subject_name<''
- 那么此时你首先可以用class_name在 索引里精准定位到一波数据接着这波数据里的student_name都是按照顺序排列的所以 student_name>''也会基于索引来查找但是接下来的subject_name<''是不能用索引的
- 总结:
- 一般我们如果写SQL语句都是用联合索引的最左侧的多个字段来进行等值匹配+范围搜索
- 或者是基于最左侧的部分字段来进行最左前缀模糊匹配
- 或者基于最左侧字段来进行范围搜索
### 75. 当我们在SQL里进行排序的时候如何才能使用索引
- 当我们的SQL语句里使用order by语句进行排序的时候如何才能用上索引呢
- 假设你有一个select * from table where xxx=xxx order by xxx这样的一个SQL语句似乎应该是基于where语句通过索引快速筛选出来一波数据接着放到内存里或者
放在一个临时磁盘文件里,然后通过排序算法按照某个字段走一个排序,最后把排序好的数据返回。
- 假设你有一个select * from table where xxx=xxx order by xxx这样的一个SQL语句似乎应该是基于where语句通过索引快速筛选出来一波数据接着放到内存里或者
放在一个临时磁盘文件里,然后通过排序算法按照某个字段走一个排序,最后把排序好的数据返回。
- 通常而言咱们尽量是最好别这么搞尤其是类似于select * from table order by xx1,xx2,xx3 limit 100这样的SQL语句按照多个字段进行排序然后返回排名前100条数据类似的语句其实常常见于分页
SQL语句里可能需要对表里的数据进行一定的排序然后走一个limit拿出来指定部分的数据
- 你要是纯粹把一坨数据放到一个临时磁盘文件里然后直接硬上各种排序算法在磁盘文件里搞一通排序接着按照你指定的要求走limit语句拿到指定分页的数据这简直会让SQL的速度慢到家了
- 所以通常而言在这种情况下假设我们建立了一个INDEX(xx1,xx2,xx3)这样的一个联合索引这个时候默认情况下在索引树里本身就是依次按照xx1,xx2,xx3三个字段的值去排序的那么此时你再运行
select * from table order by xx1,xx2,xx3 limit 100这样的SQL语句你觉得还需要在什么临时磁盘文件里排序吗
- 显然是不用了啊因为他要求也不过就是按照xx1,xx2,xx3三个字段来进行排序罢了在联合索引的索引树里都排序好了
直接就按照索引树里的顺序把xx1,xx2,xx3三个字段按照从小到大的值获取前面 100条就可以了
- 然后拿到100条数据的主键再去聚簇索引里回表查询剩余所有的字段。
- 所以说在你的SQL语句里应该尽量最好是按照联合索引的字段顺序去进行order by排序这样就可以直接利用联合索引树里的数据有序性到索引树里直接按照字段值的顺序去获取你需要的数据了。
- 但是这里有一些限定规则,因为联合索引里的字段值在索引树里都是从小到大依次排列的 ,所以你在 order by里要不然就是每个字段后面什么都不加直接就是order by xx1,xx2,xx3要不然就都加DESC
降序排列就是order by xx1 DESC,xx2 DESC,xx3 DESC
- 如果都是升序排列直接就从索引树里最小的开始读取一定条数就可以了要是都是降序排列就是从索引树里最大的数据开始读取一定的条数就可以了但是你不能order by语句里有的字段升序有的字段
降序,那是不能用索引的
- 另外要是你order by语句里有的字段不在联合索引里或者是你对order by语句里的字段用了复杂的函数这些也不能使用索引去进行排序了。
### 76. 当我们在SQL里进行分组的时候如何才能使用索引
- 有时候我们会想要做一个group by把数据分组接着用count sum之类的聚合函数做一个聚合统计。
- 假设你要是走一个类似select count(*) from table group by xx的SQL语句似乎看起来必须把你所有的数据放到一个临时磁盘文件里还有加上部分内存去搞一个分组按照指定字段的值分成一组一组
的,接着对每一组都执行一个聚合函数,这个性能也是极差的,因为毕竟涉及大量的磁盘交互。
- 因为在我们的索引树里默认都是按照指定的一些字段都排序好的,其实字段值相同的数据都是在一起的,假设要是走索引去执行分组后再聚合,那性能一定是比临时磁盘文件去执行好多了
- 所以通常而言对于group by后的字段最好也是按照联合索引里的最左侧的字段开始按顺序排列开来这样的话其实就可以完美的运用上索引来直接提取一组一组的数据然后针对每一组的数据执行聚合函数就可以了
- 其实大家会发现这个group by和order by用上索引的原理和条件都是差不多的本质都是在group by和order by之后的字段顺序和联合索引中的从最左侧开始的字段顺序一致然后就可以充分利用索引树
里已经完成排序的特性,快速的根据排序好的数据执行后续操作了。
- 这样就不再需要针对杂乱无章的数据利用临时磁盘文件加上部分内存数据结构进行耗时耗力的现场排序和分组,那真是速度极慢,性能极差的。
- 我们平时设计表里的索引的时候必须充分考虑到后续你的SQL语句要怎么写大概会根据哪些字段来进行where语句里的筛选和过滤大概会根据
哪些字段来进行排序和分组?
- 在考虑好之后就可以为表设计两三个常用的索引覆盖常见的where筛选、order by排序和 group by分组的需求保证常见的SQL语句都可以用上索引这样你真正系统跑起来起码是不会有太
大的查询性能问题了
- 毕竟只要你所有的查询语句都可以利用索引来执行那么速度和性能通常都不会太慢。如果查询还是有问题那就要深度理解查询的执行计划和执行原理了然后基于执行计划来进行深度SQL调优。
- 然后对于更新语句而言,其实最核心的就是三大问题
- 一个是你索引别太多,索引太多了,更新的时候维护很多索引树肯定是不行的;
- 一个是可能会涉及到一些锁等待和死锁的问题;
- 一个就是可能会涉及到 MySQL连接池、写redo log文件之类的问题
### 77. 回表查询对性能的损害以及覆盖索引是什么?
- 有的时候MySQL的执行引擎甚至可能会认为你要是类似select * from table order by xx1,xx2,xx3的语句相当于是得把联合索引和聚簇索引两个索引的所有数据都扫描一遍了
,那还不如就不走联合索引了,直接全表扫描得了,这样还就扫描一个索引而已。
- 但是你如果要是select * from table order by xx1,xx2,xx3 limit 10这样的语句那执行引擎就知道了你先扫描联合索引的索引树拿到10条数据接着对10条数据在聚簇索引里查找10次就可以了那么就
还是会走联合索引的。
- 覆盖索引的概念,其实覆盖索引不是一种索引,他就是一种基于索引查询的方式罢了。
- 针对类似select xx1,xx2,xx3 from table order by xx1,xx2,xx3这样的 语句,这种情况下,你仅仅需要联合索引里的几个字段的值,那么其实就只要扫描联合索引的索引树就可以了,不需要
回表去聚簇索引里找其他字段了。
- 这个时候,需要的字段值直接在索引树里就能提取出来,不需要回表到聚簇索引,这种查询方式就是覆盖索引。
- 在写SQL语句的时候
- 一方面是你要注意一下也许你会用到联合索引,但是是否可能会导致大量的回表到聚簇索引,如果需要回表到聚簇索引的次数太多了,可能就直接给你做成全表扫描 不走联合索引了;
- 一方面是尽可能还是在SQL里指定你仅仅需要的几个字段不要搞一个select *把所有字段都拿出来,甚至最好是直接走覆盖索引的方式,不要去回表到聚簇索引
- 即使真的要回表到聚簇索引那你也尽可能用limit、where之类的语句限定一下回表到聚簇索引的次数就从联合索引里筛选少数数据然后再回表到聚簇索引里去这样性能也会好一些
### 78-80. 设计索引的时候,我们一般要考虑哪些因素呢?
- 首先,我们在针对业务需求建立好一张表的结构之后,就知道这个表有哪些字段,每个字段是什么类型的,会包含哪些数据
- 接着设计好表结构之后,接下来要做的,就是要设计表的索引,这个设计索引的时候,我们要考虑第一点,就是未来我们对表进行查询的时候,大概会如何来进行查询?
- PS读索引的设计可以放在流程设计之后
- 规则
- 索引设计原则1: 针对你的SQL语句里的where条件、order by条件以及 group by条件去设计索引,可以设计一个或者两三个联合索引每一个联合索引都尽量去包含上你的where、order by、
group by里的字段接着你就要仔细审查每个SQL语句是不是每个where、order by、group by后面跟的字段顺序都是某个联合索引的最左侧字段开始的部分字段
- 索引设计原则2: 字段基数问题原则,一般建立索引尽量使用那些基数比较大的字段就是值比较多的字段那么才能发挥出B+树快速二分查找的优势来。一般建立索引尽量使用那些基数比较大的字段就是值比较多的字段那么才能发挥出B+树快速二分
查找的优势来。不过当然了这个所谓的字段类型小一点的列也不是绝对的很多时候你就是要针对varchar(255)这种字段建立索引,哪怕多占用一些磁盘空间,那你也得去设计这样的索引,比较关键的其实还是尽量别
把基数太低的字段包含在索引里因为意义不是太大。万一要是你真的有那种varchar(255)的字段,可能里面的值太大了,你觉得都放索引树里太
占据磁盘空间了此时你仔细考虑了一下发现完全可以换一种策略也就是仅仅针对这个varchar(255)字段的前20个字符建立索引就是说对这个字段里的每个值的前20个字符放在索引树里而已。
此时你建立出来的索引其实类似于KEY my_index(name(20),age,course)就这样的一个形式假设name是varchar(255)类型的但是在索引树里你对name的值仅仅提取前20个字符而已。
但是假如你要是order by name那么此时你的name因为在索引树里仅仅包含了前20个字符所以这个排序是没法用上索引了group by也是同理的。
- 索引设计规则3: 不要让你的查询语句里的字段搞什么函数,或者是搞个计算。
- 总结:
- 因为你插入的数据值可能根本不是按照顺序来的,很可能会导致索引树里的某个页就会自动分裂,这个页分裂的过程就很耗费时间,因此一般让大家设计索引别太多,建议两三个联合索引就应该覆盖掉
你这个表的全部查询了。
- 建议大家主键一定是自增的别用UUID之类的因为主键自增那么起码你的聚簇索引不会频繁的分裂主键值都是有序的就会自然的新增一个页而已但是如果你用的是UUID
么也会导致聚簇索引频繁的页分裂
# 查询语句的执行原理
# 多表join语句的执行原理
# MySQL执行计划
# SQL语句调优
# 数据库的备份和恢复
# 主从架构和读写分离,高可用架构,分库分表架构