《Mysql实战45讲》学习笔记 1-22

Mysql

《Mysql实战45讲》

1、一条sql查询语句是如何执行的

《Mysql实战45讲》学习笔记 1-22_第1张图片

Server层: 连接器,查询缓存,分析器,优化器,执行器
存储引擎层: 负责数据的存储和提取 (Innodb, MyISAM,Memory)

连接器 :
TCP握手之后,连接器就要开始认真你的身份,这时候用的就是你输入的用户名和密码。
可以通过 showprocesslist 查看连接

查询缓存 :
大多数情况下不要去使用查询缓存,为什么? 因为查询缓存往往弊大于利。

查询缓存的失效特别频繁,只要有对一个表的更新,这个表上的所有的查询缓存都会被清空,对于更新压力大的数据库来说,查询缓存的命中率会非常的低。除非是一张静态的表,比如说系统配置表,那么在这张表上的查询才适合用查询缓存。

mysql 8.0版本直接将查询缓存的整块功能删除掉了

分析器 :
词法分析: select 分析出是一条查询语句
语法分析: 语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法

优化器 :
优化器是在表里面有很多索引的时候,决定使用哪个索引,或者在一个语句有多表关联的时候,决定各个表的连接顺序

执行器 :
执行器操作引擎返回SQL执行结果

2、一条sql更新语句是如何执行的

Mysql 可以恢复到半个月内任意一秒的状态,这是如何做到的?

更新流程还设计两个重要的日志模块。

  • redolog 重做日志
  • binlog 归档日志

如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本,查找成本都很高。

MYSQL 采用write ahead logging ,它的关键点就是先写日志,再写磁盘

具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了,同时InnodDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

innoDB的redo log 是固定大小的,比如可以配置为一组4个文件,每个文件大小是1GB,那么这块粉板总共就可以记录4GB的操作。
从头开始写,写到末尾就又回到开头循环写

《Mysql实战45讲》学习笔记 1-22_第2张图片
write pos 是当前记录的位置,一边写一遍后移,写到第3皓文件末尾后就回到0号文件开头,checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos 和 checkpoint之间的粉板上还空着的部分,可以用来记录新的操作

有了redo log , InnoDB就可以保证技术数据库发生异常重启,之前提交的记录都不会丢失,这就是crash-safe

binlog :

redolog 是InnoDB引擎特有的日志
而Server层也有自己的日志,称之为binlog

只依靠binlog是没有crash-safe能力的,所以innoDB使用林伟一套日志系统,也就是redo-log来实现crash-safe能力

这两个日志有以下三点不同

1. redo log 是innoDB特有的,binlog 是Mysql的server层实现的,所有的引擎都可以使用
2. redo log是物理日志,记录的是某个数据页上发生了什么修改,binlong是逻辑日志,记录的这个语句的原始逻辑。比如 给ID =2 这一行的c字段加1
3. redo log 是循环写的,空间固定会用完,binlog是可以追加写入的,追加写是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志

update 语句执行的流程

《Mysql实战45讲》学习笔记 1-22_第3张图片

redo log 的写入拆成了两个步骤 : prepare 和 commit,这就是两阶段提交

两阶段提交

为什么要有两阶段提交,主要是让两份日志的逻辑一致。

binlog数据恢复

如果不采用两阶段提交,会出现什么问题?

  1. 先写redolog 再写binlog 假设在redolog 写完,binlog 还没有写完的时候,Mysql进程异常重启,redolog写完之后,系统及时崩溃,任然能够吧数据恢复回来,所以恢复后这一行c的值是1
    但是遇有binlog没写完就crash了,这个时候binlog里面就没有记录这个语句,因此之后备份日志的时候,存起来binlog里面就没有这条语句,然后你就发现,如果需要用这个binlog 来回复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了一次更新,回复出来的这一行c的值就是0,与原库的值不同

  2. 先写binlog后写redolog。 如果在binlog写完之后crash,由于redo log 还没写,崩溃回复以后这个事务无效,所以这一行c的值是0,但是binlog里面已经记录了把c从0改成1这个日志,所以在只用用binlog来回复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。

    当你需要扩容过得时候,也就是需要再多搭建一些备库来增加系统的读能力的时候,现在常见的做法也是用全量备份加上引用binlog来实现的,这个不一致就会导致你的线上出现主从数据库不一致的情况。

    简单来说 : redolog 和 binlog 都可以用于表示事物的提交状态,而两阶段提交就是让这个状态保持逻辑上的一致。

3、事物隔离:为什么你改了我看不见?

在实现上,数据库里面会创建一个视同,访问的时候以试图的逻辑结果为准,在可重复度的隔离界别下,这个视图是在事物启动的时候创建的,整个事物存在期间都用这个试图,在读提交的隔离几倍下,这个试图是在每个SQL开始执行的时候创建的,这里需要注意的是,读未提交隔离级别下直接返回记录上的最新的值,没有试图的概念,而串行化,隔离级别下直接使用加锁的方式俩避免并行访问。

同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制MVCC

为什么不建议使用长事务?

长事务意味着系统里面会存在很多老的事务试图,由于这些事务随时可能访问数据库俩面的任何数据,所以在这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

索引

Hash表这种结构适用于等值查询的场景

自增主键的插入数据的模式,正好符合了递增插入的场景,每次插入一条新的记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。

主键的长度越小,普通索引的叶子节点也就越小,普通索引占用的空间也就越小。

KV场景,直接用业务字段做主键,由于没有其他索引所以也就不用考虑其他索引的叶子节点大小的问题

回表: 回到主键索引树搜索的过程,称为回表
覆盖索引 : 索引k 已经覆盖了查询需求,称之为覆盖索引,由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。

最左前缀

索引下推

在MYSQL5.6里面引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录。减少回表的次数。

全局锁 表锁 行锁

全局锁 : 让整个库处于只读状态,全局锁的典型使用场景是,做全库逻辑备份。

行锁 : 在Innodb事务中,行锁是在需要的时候才加上的,单并不是不要了就立刻释放,而是等到事务结束的时候才释放

如果你的事务中需要锁多个行,要把最可能造成所冲突的,最可能影响并发度的锁尽可能往后放。

怎么解决由这种行更新导致的性能问题?
问题的症结在于,死锁检测需要耗费大量的CPU资源

  1. 一种头痛医头的方法,就是如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉
  2. 控制并发度 比如同一行勇士最多只有10个线程在更新

8.事务到底是隔离的还是不隔离的?

视图 : InnoDB在实现MVCC时用到的一致性视图,即consistent read view 用于支持RC(Read committed,读提交)和 RR(Repeated Read 可重复读)隔离级别的实现。

InnoDB里面每个事务有一个唯一的事务ID,每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id 复制给这个数据版本的事务ID,即为row trx_id,同时,旧的数据版本也要保留,并且在新的数据版本中,能够有信息可以直接拿到它。

也就是说,数据表中的一行记录,其实可能有多个版本,每个版本有自己的row——trx_id

一个数据版本,对于一个事务视图来说,除了自己的更新总是可见意外,有三种情况:

  1. 版本未提交,不可见
  2. 版本已提交,但是是在视图创建之后提交的,不可见
  3. 版本已提交,而且是在视图创建前提交的,可见。

更新都是先读后写,而这个读,只能读当前的值,称为当前读
事务更新数据的时候,只能用当前读,如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

select 语句如果加锁也可以是当前读

  1. lock in share mode
  2. for update

9. 唯一索引和普通索引,应该怎么选择?

这个在我的面试视频里面其实问了好几次了,核心是需要回答到change buffer,那change buffer又是个什么东西呢?

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。

在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作,通过这种方式就能保证这个数据逻辑的正确性。

需要说明的是,虽然名字叫作change buffer,实际上它是可以持久化的数据。也就是说,change buffer在内存中有拷贝,也会被写入到磁盘上。

将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。

除了访问这个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)的过程中,也会执行merge操作。

《Mysql实战45讲》学习笔记 1-22_第4张图片

显然,如果能够将更新操作先记录在change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用buffer pool的,所以这种方式还能够避免占用内存,提高内存利用率

那么,什么条件下可以使用change buffer呢?

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。

要判断表中是否存在这个数据,而这必须要将数据页读入内存才能判断,如果都已经读入到内存了,那直接更新内存会更快,就没必要使用change buffer了。

因此,唯一索引的更新就不能使用change buffer,实际上也只有普通索引可以使用。

change buffer用的是buffer pool里的内存,因此不能无限增大,change buffer的大小,可以通过参数innodb_change_buffer_max_size来动态设置,这个参数设置为50的时候,表示change buffer的大小最多只能占用buffer pool的50%。

将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一,change buffer因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

change buffer的使用场景
因为merge的时候是真正进行数据更新的时刻,而change buffer的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做merge之前,change buffer记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。

因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时change buffer的使用效果最好,这种业务模型常见的就是账单类、日志类的系统。

反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在change buffer,但之后由于马上要访问这个数据页,会立即触发merge过程。这样随机访问IO的次数不会减少,反而增加了change buffer的维护代价,所以,对于这种业务模式来说,change buffer反而起到了副作用。

change buffer 与 redolog

redolog 主要节省的是随机写磁盘的IO消耗(转成顺序写),而changebuffer主要节省的则是随机读擦盘的IO消耗

10. Mysql 为什么有时候会选错索引?

选择索引是优化器的工作,而优化器选择索引的目的,是找到一个最优的执行方案,并且最小的代价去执行语句。

扫描夯实是影响执行代价的因数之一,扫描的行数越少,一位置访问磁盘数据的次数越少, 消耗的CPU资源越少。

当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表,是否排序等因素进行综合判断。

基数 : 一个索引上不同的值的个数,称为基数

Mysql 是怎样得到索引的基数的呢?

采样统计 : 采样统计的时候,InnoDB默认会选择N个数据也,统计这些页面上的不同的值,得到一个平均的值,然后乘以这个索引的页面数,就得到了这个索引的基数。

统计信息不对 : 修正统计信息 analyze table t

优化器误判的情况 :

  1. force index 来强行指定索引
  2. 也可以通过修改语句来引导优化器
  3. 增加或删除索引来绕过这个问题

11. 怎么给字符串字段添加索引

我们存在邮箱作为用户名的情况,每个人的邮箱都是不一样的,那我们是不是可以在邮箱上建立索引,但是邮箱这么长,我们怎么去建立索引呢?

MySQL是支持前缀索引的,也就是说,你可以定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。

我们是否可以建立一个区分度很高的前缀索引,达到优化和节约空间的目的呢?

使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。

上面说过覆盖索引了,覆盖索引是不需要回表的,但是前缀索引,即使你的联合索引已经包涵了相关信息,他还是会回表,因为他不确定你到底是不是一个完整的信息,就算你是[email protected]一个完整的邮箱去查询,他还是不知道你是否是完整的,所以他需要回表去判断一下。

下面这个也是我在阿里面试面试官问过我的,很长的字段,想做索引我们怎么去优化他呢?

因为存在一个磁盘占用的问题,索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。

我当时就回答了一个hash,把字段hash为另外一个字段存起来,每次校验hash就好了,hash的索引也不大。

我们都知道只要区分度过高,都可以,那我们可以采用倒序,或者删减字符串这样的情况去建立我们自己的区分度,不过大家需要注意的是,调用函数也是一次开销哟,这点当时没注意。

就比如本来是www.aobing@qq,com 其实前面的www.基本上是没任何区分度的,所有人的邮箱都是这么开头的,你一搜一大堆出来,放在索引还浪费内存,你可以substring()函数截取掉前面的,然后建立索引。

我们所有人的身份证都是区域开头的,同区域的人很多,那怎么做良好的区分呢?REVERSE()函数翻转一下,区分度可能就高了。

这些操作都用到了函数,我就说一下函数的坑。

1. 直接创建完整索引,这样可能比较占用空间
2. 创建前缀索引,节省空间,但是会增加查询扫描次数,并且不能使用覆盖索引
3. 倒序存储,在创建前缀索引,用于绕过字符串本身前缀的区分度不够高的问题
4. 创建hash索引,查询性能稳定,有额外的存储和计算消耗,和第三种方式一样,都不支持范围扫描

12. 为什么Mysql为突然抖一下

当内存数据数据页更磁盘数据页内容不一致的时候,我们称这个内存页为脏爷,内存数据写入到磁盘之后,内存和磁盘上的数据也的内容就一致了,称为干净页

触发flush的四种场景

  1. innodb的redolog写满了,这个时候系统会挺迟所有的更新操作,把checkpoint往前推进,redolog留出空间可以继续写
    出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住

  2. 系统内存不足,当需要新的内存也,而内存不够用的时候,就要淘汰一些数据也,空出内存给别的数据页使用,如果淘汰的是脏页,就要先将脏页写入到磁盘

    innoDB采用buffer pool缓冲池管理内存,缓冲池的内存也有三种状态
    第一种,还没有使用的
    第二种,使用了并且是干净页
    第三种, 使用了并且是脏页
    innoDB的策略是尽量使用内存,因此对一个长时间运行的库来说,未被使用的页面很少
    而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页
    这时候只能把最久不能使用的数据页从内存中淘汰掉,如果淘汰的是一个干净页,就直接释放出来复用,但如果是脏页呢,就必须将脏页先刷到磁盘,编程干净页后才能复用。
    所以刷脏页虽然是常态,但是出现以下这两种情况,都会会明显影响性能的:

    1. 一个查询要淘汰的脏页的个数太多,会导致查询的响应时间明显变长
    2. 日志写满,更新全部堵住,写新能跌为0
  3. mysql 认为系统空闲的时候

  4. mysql正常关闭的情况,这个时候,mysql会把内存的脏页都flush到磁盘,这样下次mysql启动的时候,就可以直接从磁盘上读数据,启动速度会更快

13. 为什么表数据删掉一半,表文件大小不变

14. count(*)这么慢,我该怎么办?

在不同mysql引擎中,count(*)有不同的实现方式

  1. MYISAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高
  2. InnoDB引擎就麻烦了,它执行count(*)的时候,需要把数据一行一行的从引擎里面读出来,然后累积计数

为什么InnoDB不跟MyISAM一样,也把数字存储起来呢?

这是因为即使在同一个时刻的多个查询,由于多版本并发控制MVCC的原因,InnoDB表应该返回多少行也是不确定的。

这和InnoDB的事务设计有关系, 可重复读是他的默认的隔离级别,在嗲马上就是通过多版本并发控制,也就是MVCC来实现的,每一行记录都要判断自己是否对这个会话可见,因此对于count(*)请求来说,InnoDB只好把数据一行一行的读取出来依次判断,可见的行才能够用于计算”基于这个查询“的表的总行数

InnoDB对count(*)所作的优化

Innodb是索引组织表,主机索引树的叶子节点是数据,而普通索引的叶子节点是主键值,所以,普通索引树比主键索引树小很多,对于count(*)这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的,因此,Mysql优化器会找到最小的那棵树来遍历。
在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。

show table status命令的话,就会发现这个命令的输出结果里面也有一个Table ROWS用于显示这个表当前有多少行, Table ROWS能带地count(*)嘛?

不能,索引的值是通过采样来估算的,实际上,Table ROWS就是从这个采样估算得到的,因此它也很不准。

  • 用缓存系统redis 来保存计数
    但是讲技术保存在redis的方式,还不只是丢失更新的问题,即使Redis正常工作,这个值还是逻辑上不精确的
  • 在数据库保存计数

不同count 的用法

count()是一个聚合函数,对于返回的结果集,一行行的判断,如果count函数的参数不是NULL,累计值就加1,否则不加,最后返回累加值

count(主键id) : InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层,server层拿到id的时候,判断不可能为空,就按行累加

count(1) : InnoDB引擎会遍历整张表,但是不取值,server层对于返回的每一行,放一个数字1进去,判断是不可能为空的,按行累加。

但看这两个用法的差别的话,你能对比出来,count(1)执行的要比count(id)块,因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作

count(字段)

  1. 如果这个字段时定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加
  2. 如果这个字段定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加

count(*)
count()并不会吧全部字段取出来,而是专门做了优化,不取值,count()肯定不是null,按行累加

按照效率排序

count(字段) < count(id) < count(1) < count(*)

15. 日志和索引相关问题

16. orderby是怎样工作的?

select city,name,age from t where city = ‘杭州’ order by name limit 1000

全字段排序

explain
Extra这个字段 using filesort 表示的就是需要排序,Mysql会给每个线程分配一块内存用于排序 称为sort_buffer

  1. 初始化sort_buffer,确定放入name age city 这三个字段
  2. 从索引city找到第一个满足city = 杭州 条件的主键id,也就是图中的ID_x
  3. 到主键id 索引取出整行,取name, city, age三个字段的值,存入sort_buffer
  4. 从索引city取下一个记录的主键id
  5. 重复步骤3、4直到city的值不满足查询条件为止,对应的主键id也就是图中的ID_Y
  6. 对sort_buffer中的数据按照字段name做快速排序
  7. 按照排序结果取前10000行返回给客户端

rowid排序

如果查询要返回的字段很多的话,那么sort_buffer里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能很差

如果Mysql认为排序的单行长度太大会怎么样

比之前多了一步:

遍历排序结果,取前1000行,并按照id的值回到原表中取出city、name、和age三个字段返回给客户端

rowid 排序多访问了一次表t的主键索引

全字段排序 vs rowid排序

  • 如果Mysql是在是担心排序内存太小,会影响排序效率,才会采用rowid排序算法,这样排序过程一次可以排序更多行,但是需要再回到原表去取数据
  • 如果Mysql认为内存足够大,会优先选择全字段排序,把需要的字段都放到sort——buffer中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据

如果内存够,就更多利用内存, 尽量减少磁盘访问

建立city name 联合索引

这个查询过程不需要临时表,也不需要排序
Extra 字段中没有Using filesort

再优化 : 覆盖索引 cover掉要查询的字段
extra : using index

17. 如何正确的显示随机消息?

18. 为什么这些SQL逻辑相同,性能却差异巨大?

条件字段函数操作

对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能

隐式类型转换

字符串索引 + “”

隐式字符编码转换

一个表是utf-8,一个表是utf8mb4

19. 为什么我只查一行的语句,也执行这么慢?

查询长时间没返回

show processlist : 查看当前语句处于什么状态

等MDL锁

wating for table metadata lock

除了这个状态表示的是,现在有一个线程正在表t上请求或持有MDL写锁,把select语句堵住了

等flush

等行锁

lock in share mode

如果在事务中使用的是当前读,则直接返回

select * from t where id = 1这个语句,是一致性读,因此需要从1000001开始,一次执行undo log,执行了100万次之后,才将1这个结果返回。

20. 幻读是什么,幻读有什么问题?

幻读有什么问题

  • 语义上的问题 : 我要把id = 5的行锁住,不准别的事务进行读写操作
  • 数据一致性问题

即使把所有的记录都加上锁,还是阻止不了新插入的记录

如何解决幻读 ?

间隙锁 : gap lock

**next_key Lock **: 间隙锁和行锁合称next - key lock 每个next - key lock是前开后闭区间的

如果用 select * from t for update 要把整个表所有记录锁起来,就形成了7个 next - key lock分别是
(-%, 0], (0,5],(5,10],(10,15],(15, +supremum]

间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

间隙所 是在可重复读隔离级别下才会生效的

21. 为什么我只改一行的语句,锁这么多?

  1. 加锁的基本单位是next-key lock
  2. 查找过程中访问到的对象才会加锁
  3. 优化1 : 索引上的等值查询,给唯一索引加锁的时候,next - ke lock会退化成行锁
  4. 优化2 : 索引上的等值查询,向右遍历且最后一个值不满足等值条件的时候,next - key lock 退化成间隙锁
  5. 一个bug : 唯一索引上的范围查询会访问到不满足条件的第一个值为止

22. Mysql有哪些饮鸩止渴提高性能的办法

短连接 :

  1. 先处理掉那些站着连接但是不工作的线程
  2. 减少连接过程中的消耗

慢查询性能问题 :

  1. 索引没有设计好
  2. SQL语句没有写好
  3. Mysql选错了索引

参考 :
《Mysql实战45讲》
被敖丙用烂的「数据库调优」连招?真香,淦

你可能感兴趣的:(Mysql)