《MySQL45讲》读书笔记

重建表

alter table t engine = InnoDB(也就是recreate),而optimize table t 等于recreate+analyze,让表大小变小

重建表的执行流程

  • 建立一个临时文件,扫描表 t 主键的所有数据页;
  • 用数据页中表 t 的记录生成 B+ 树,存储到临时文件中;
  • 生成临时文件的过程中,将所有对 t 的操作记录在一个日志文件(row log)中;
  • 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 t相同的数据文件;
  • 用临时文件替换表 t 的数据文件。

注意:在重建表的时候, InnoDB不会把整张表占满, 每个页留了1/16给后续的更新用。 也就是说, 其实重建表之后不是"最"紧凑的。

Online DDL

ALTER TABLE tbl_name ADD COLUMN col_name col_type, ALGORITHM=INPLACE, LOCK=NONE;

LOCK选项

  • SHARE:共享锁,执行DDL的表可以读,但是不可以写。

  • NONE:没有任何限制,执行DDL的表可读可写。

  • EXCLUSIVE:排它锁,执行DDL的表不可以读,也不可以写。

  • DEFAULT:默认值,也就是在DDL语句中不指定LOCK子句的时候使用的默认值。如果指定LOCK的值为DEFAULT,那就是交给MySQL子句去觉得锁还是不锁表。不建议使用,如果你确定你的DDL语句不会锁表,你可以不指定lock或者指定它的值为default,否则建议指定它的锁类型。

    准备:
    1、对表加元数据共享升级锁,并升级为排他锁;(此时DML不能并行)
    2、判断使用inplace算法
    3、判断语句是"rebuild table" 还是 "no-rebuild",rebuild 在原表所在的路径下创建.frm和.ibd临时中转文件;
    【在引擎层克隆,而不是像copy那样,在server层创建(create like)】

    注意:加列的操作是需要rebuild table的
    【rebuild table】---生成中转文件表

    ++++
    no-rebuild的情况:
    除创建二级索引外只创建.frm文件,其中添加二级索引操作最为特殊,该操作属于no-rebuild不会生成.ibd,
    但实际上对.ibd文件却做了修改,该操作会在参数tmpdir指定路径下生成临时文件,用于存储索引排序结果,然后再合并到.ibd文件中
    ++++
    4、申请row log空间,用于存放DDL执行阶段产生的DML操作。(no-rebuild不需要)【在innodb_sort_buffer块中】

    执行:(online)
    1、释放排他锁,保留元数据共享升级锁;(此时DML可以并行)
    2、扫描原表主键以及二级索引的所有数据页,生成 B+ 树,存储到临时文件中;【在引擎层扫描,最耗时】
    3、将所有对原表的DML操作记录在日志文件row log中,并回放部分row_log。

    提交阶段:
    1、升级元数据共享升级锁,产生排他锁锁表;(此时DML不能并行了)
    2、重做row log中的内容;(no-rebuild不需要)
    3、重命名原表文件,将临时文件改名为原表文件名,删除原表文件;
    4、提交事务,变更完成。

在 InnoDB 引擎中 count(*)、count(1)、count(主键)、count(字段) 哪个性能最高?

count(字段)<count(主键 id)<count(1)≈count(*) 题目解析:

  • 对于 count(主键 id) 来说,InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。
  • 对于 count(1) 来说,InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字"1"进去,判断是不可能为空的,按行累加。
  • 对于 count(字段) 来说,如果这个"字段"是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个"字段"定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。
  • 对于 count(*) 来说,并不会把全部字段取出来,而是专门做了优化,不取值,直接按行累加。

所以最后得出的结果是:count(字段)<count(主键 id)<count(1)≈count(*)。

--single-transaction逻辑备份

当mysqldump使用参数--single-transaction的时候, 导数据之前就会启动一个事务, 来确保拿到一致性视图。

single-transaction方法只适用于所有的表使用事务引擎的库。 如果有的表使用了不支持事务的引擎, 那么备份就只能通过FTWRL(全局锁)方法。

这往往是DBA要求业务开发人员使用InnoDB替代MyISAM的原因之一。

当备库用--single-transaction做逻辑备份的时候, 如果从主库的binlog传来一个DDL语句会怎么样?

Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT;
/* other tables */
Q3:SAVEPOINT sp;
/* 时刻 1 */
Q4:show create table `t1`;
/* 时刻 2 */
Q5:SELECT * FROM `t1`;
/* 时刻 3 */
Q6:ROLLBACK TO SAVEPOINT sp;
/* 时刻 4 */
/* other tables */
  1. 如果在Q4语句执行之前到达, 现象: 没有影响, 备份拿到的是DDL后的表结构。

  2. 如果在"时刻 2"到达, 则表结构被改过, Q5执行的时候, 报 Table definition has changed,please retrytransaction, 现象: mysqldump终止;

  3. 如果在"时刻2"和"时刻3"之间到达, mysqldump占着t1的MDL读锁, binlog被阻塞, 现象:主从延迟, 直到Q6执行完成。

  4. 从"时刻4"开始, mysqldump释放了MDL读锁, 现象: 没有影响, 备份拿到的是DDL前的表结构。

删除一个表里面的前10000行数据

要删除一个表里面的前10000行数据, 有以下三种方法可以做到:

第一种, 直接执行delete from Tlimit 10000;

第二种, 在一个连接中循环执行20次 delete from Tlimit 500;

第三种, 在20个连接中同时执行delete from Tlimit 500。

分析:

第一种方式(即: 直接执行delete from Tlimit 10000) 里面, 单个语句占用时间长, 锁的时间也比较长; 而且大事务还会导致主从延迟。

第二种方式是相对较好的。

第三种方式(即: 在20个连接中同时执行delete from Tlimit 500) , 会人为造成锁冲突。

change buffer

当需要更新一个数据页时, 如果数据页在内存中就直接更新, 而如果这个数据页还没有在内存中的话, 在不影响数据一致性的前提下, InooDB会将这些更新操作缓存在change buffer中, 这样就不需要从磁盘中读入这个数据页了。 在下次查询需要访问这个数据页的时候, 将数据页读入内存, 然后执行change buffer中与这个页有关的操作。 通过这种方式就能保证这个数据逻辑的正确性。将change buffer中的操作应用到原数据页, 得到最新结果的过程称为merge。 除了访问这个数据页会触发merge外, 系统有后台线程会定期merge。 在数据库正常关闭(shutdown) 的过程中,也会执行merge操作。对于唯一索引来说, 所有的更新操作都要先判断这个操作是否违反唯一性约束。 比如, 要插入(4,400)这个记录, 就要先判断现在表中是否已经存在k=4的记录, 而这必须要将数据页读入内存才能判断。 如果都已经读入到内存了, 那直接更新内存会更快, 就没必要使用change buffer了。

因此, 唯一索引的更新就不能使用change buffer, 实际上也只有普通索引可以使用。change buffer用的是buffer pool里的内存, 因此不能无限增大。 change buffer的大小, 可以通

过参数innodb_change_buffer_max_size来动态设置。 这个参数设置为50的时候, 表示changebuffer的大小最多只能占用buffer pool的50%。将数据从磁盘读入内存涉及随机IO的访问, 是数据库里面成本最高的操作之一。 change buffer因为减少了随机磁盘访问, 所以对更新性能的提升是会很明显的。

merge的执行流程是这样的:

  1. 从磁盘读入数据页到内存(老版本的数据页) ;

  2. 从change buffer里找出这个数据页的change buffer 记录(可能有多个) , 依次应用, 得到新版数据页;

  3. 写redo log。 这个redo log包含了数据的变更和change buffer的变更。

redo log 主要节省的是随机写磁盘的IO消耗( 转成顺序写) , 而change buffer主要节省的则是随机读磁盘的IO消耗。

字符集不同连表查询用不上索引

performance_schema库优化

MySQL 5.6版本以后提供的performance_schema库, 就在file_summary_by_event_name

表里统计了每次IO请求的时间

Buffer Pool

Buffer Pool对查询的加速效果, 依赖于一个重要的指标, 即: 内存命中率。

你可以在show engine innodb status结果中, 查看一个系统当前的BP命中率。 一般情况下, 一个稳定服务的线上系统, 要保证响应时间符合要求的话, 内存命中率要在99%以上。

执行show engine innodb status , 可以看到"Buffer pool hit rate"字样, 显示的就是当前的命中率。

InnoDB Buffer Pool的大小是由参数 innodb_buffer_pool_size确定的, 一般建议设置成可用物理内存的60%~80%。

所以, innodb_buffer_pool_size小于磁盘的数据量是很常见的。 如果一个 Buffer Pool满了, 而又要从磁盘读入一个数据页, 那肯定是要淘汰一个旧数据页的。

InnoDB管理Buffer Pool的LRU算法, 是用链表来实现的。

  1. 在图的状态1里, 链表头部是P1, 表示P1是最近刚刚被访问过的数据页; 假设内存里只能放下这么多数据页;

  2. 这时候有一个读请求访问P3, 因此变成状态2, P3被移到最前面;

  3. 状态3表示, 这次访问的数据页是不存在于链表中的, 所以需要在Buffer Pool中新申请一个

数据页Px, 加到链表头部。 但是由于内存已经满了, 不能申请新的内存。 于是, 会清空链表

末尾Pm这个数据页的内存, 存入Px的内容, 然后放到链表头部。

  1. 从效果上看, 就是最久没有被访问的数据页Pm, 被淘汰了。

实际上, InnoDB对LRU算法做了改进

在InnoDB实现上, 按照5:3的比例把整个LRU链表分成了young区域和old区域。 图中LRU_old指向的就是old区域的第一个位置, 是整个链表的5/8处。 也就是说, 靠近链表头部的5/8是young区域, 靠近链表尾部的3/8是old区域。改进后的LRU算法执行流程变成了下面这样。

  1. 图7中状态1, 要访问数据页P3, 由于P3在young区域, 因此和优化前的LRU算法一样, 将其移到链表头部, 变成状态2。

  2. 之后要访问一个新的不存在于当前链表的数据页, 这时候依然是淘汰掉数据页Pm, 但是新插入的数据页Px, 是放在LRU_old处。

  3. 处于old区域的数据页, 每次被访问的时候都要做下面这个判断:

若这个数据页在LRU链表中存在的时间超过了1秒, 就把它移动到链表头部;

如果这个数据页在LRU链表中存在的时间短于1秒, 位置保持不变。 1秒这个时间, 是由参数innodb_old_blocks_time控制的。 其默认值是1000, 单位毫秒。

MySQL采用的是边算边发的逻辑, 因此对于数据量很大的查询结果来说, 不会在server端保存完整的结果集。 所以, 如果客户端读结果不及时, 会堵住MySQL的查询过程, 但是不会把内

存打爆。而对于InnoDB引擎内部, 由于有淘汰策略, 大查询也不会导致内存暴涨。 并且, 由于InnoDB对LRU算法做了改进, 冷数据的全表扫描, 对Buffer Pool的影响也能做到可控。

InnoDB对Bufffer Pool的LRU算法做了优化,

即: 第一次从磁盘读入内存的数据页, 会先放在old区域。 如果1秒之后这个数据页不再被访问了, 就不会被移动到LRU链表头部, 这样对Buffer Pool的命中率影响就不大。

但是, 如果一个使用BNL算法的join语句(无索引join,参考https://www.cnblogs.com/booksea/p/17380941.html), 多次扫描一个冷表, 而且这个语句执行时间超过1秒,就会在再次扫描冷表的时候, 把冷表的数据页移到LRU链表头部。

这种情况对应的, 是冷表的数据量小于整个Buffer Pool的3/8, 能够完全放入old区域的情况。如果这个冷表很大, 就会出现另外一种情况: 业务正常访问的数据页, 没有机会进入young区

域。由于优化机制的存在, 一个正常访问的数据页, 要进入young区域, 需要隔1秒后再次被访问到。 但是, 由于我们的join语句在循环读磁盘和淘汰内存页, 进入old区域的数据页, 很可能在1秒之内就被淘汰了。 这样, 就会导致这个MySQL实例的Buffer Pool在这段时间内, young区域的数据页没有被合理地淘汰。也就是说, 这两种情况都会影响Buffer Pool的正常运作。

大表join操作虽然对IO有影响, 但是在语句执行结束后, 对IO的影响也就结束了。 但是,对Buffer Pool的影响就是持续性的, 需要依靠后续的查询请求慢慢恢复内存命中率。

判断要不要使用join语句时, 就是看explain结果里面, Extra字段里面有没有出现"BlockNested Loop"字样。

相关推荐
C吴新科39 分钟前
MySQL入门操作详解
mysql
Ai 编码助手3 小时前
MySQL中distinct与group by之间的性能进行比较
数据库·mysql
白云如幻3 小时前
MySQL排序查询
数据库·mysql
苹果醋34 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
stars_User5 小时前
MySQL数据库面试题(下)
数据库·mysql
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
yanwushu5 小时前
Xserver v1.4.2发布,支持自动重载 nginx 配置
mysql·nginx·php·个人开发·composer
蓝眸少年CY5 小时前
MySQL 【流程控制】函数
mysql
掘金-我是哪吒7 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
zmgst8 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql