《MySQL实战45讲》学习笔记 31~45讲

July 2nd, 2019 by JasonLe's Tech Leave a reply »

31 | 误删数据后除了跑路,还能怎么办?

千万不能误删

误删行

binlog_format=row 和 binlog_row_image=FULL 可以使用Flashback回放。
不建议直接在主库使用,应该在备库执行,然后再将确认过的临时库的数据,恢复回主库。

误删库 / 表

取全量备份,和全量备份时间点之后的binlog恢复。但mysqlbinlog不够快。
一个加速的方法,将全量备份恢复的临时实例,设置为线上备库的从库。

延迟复制备库

MySQL 5.6 版本引入,通过 CHANGE MASTER TO MASTER_DELAY = N 命令,可以指定这个备库持续保持跟主库有N 秒的延迟。

32 | 为什么还有kill不掉的语句?

kill query + 线程 id:表示终止这个线程中正在执行的语句;

kill connection + 线程 id,这里 connection 可缺省,表示断开这个线程的连接,如果这个线程有语句正在执行,也是要先停止正在执行的语句的。

mysql kill命令不是直接终止线程。

  1. 把 session 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY);
  2. 给 session 的执行线程发一个信号。有些session 由于锁在等待,信号让session 退出等待来处理THD::KILL_QUERY 状态。

mysql处理过程中有许多埋点,这些“埋点”的地方判断线程状态,如果发现线程状态是 THD::KILL_QUERY,才开始进入语句终止逻辑。

如果碰到一个被 killed 的事务一直处于回滚状态,尽量不要重启,因为重启之后该做的回滚动作还是不能少的,所以从恢复速度的角度来说,应该让它自己结束。如果这个语句可能会占用别的锁,或者由于占用 IO 资源过多,从而影响到了别的语句执行的话,就需要先做主备切换,切到新主库提供服务。避免大事务。

33 | 我查这么多数据,会不会把数据库内存打爆?

全表扫描对 server 层的影响

net_buffer由参数 net_buffer_length 定义的,默认是 16k。
mysql是遍读遍发的,所以当net_buffer写满的时候就需要等待。使用show processlist可以看到state=“Sending to client”。
mysql还要一个state=“Sending data”,它的意思只是“正在执行”。

全表扫描对 InnoDB 的影响

介绍 WAL 机制时,分析了Buffer Pool 加速更新的作用。Buffer Pool 还有一个更重要的作用,就是加速查询。执行 show engine innodb status可以查看一个系统当前的 BP 命中率。
InnoDB Buffer Pool 的大小是由参数 innodb_buffer_pool_size 确定的,一般建议设置成可用物理内存的 60%~80%。

InnoDB 内存管理用的是最近最少使用 (LRU) 算法,这个算法的核心就是淘汰最久未使用的数据。如果在查询历史数据使用这个算法,会导致很多请求会从磁盘读取数据。所以mysql对LRU算法进行了改进。

在 InnoDB 实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。

  1. 访问数据页P3,在young区所以把它移到链表同步。
  2. 如果访问不存在的数据,则把链表尾部数据淘汰,但把新数据页Px放在LRU_old处。
  3. 处于 old 区域的数据页,每次被访问的时候都要做下面这个判断:
    若这个数据页在 LRU 链表中存在的时间超过了 1 秒,就把它移动到链表头部;
    如果这个数据页在 LRU 链表中存在的时间短于 1 秒,位置保持不变。

1s由参数 innodb_old_blocks_time 控制的。其默认值是 1000,单位毫秒。

34 | 到底可不可以使用join?

表结构

CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;

drop procedure idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();

create table t1 like t2;
insert into t1 (select * from t2 where id<=100)

Index Nested-Loop Join

select * from t1 straight_join t2 on (t1.a=t2.a);

t1 是驱动表,t2 是被驱动表。

  1. 从表 t1 中读入一行数据 R;
  2. 从数据行 R 中,取出 a 字段到表 t2 里去查找;
  3. 取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;
  4. 重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。

t1只有100行,所有一共扫描200行。
如果执行select * from t1,再执行select * from t2 where a=$R.a。虽然都可以走索引,也只扫描200行。但需要执行101行sql。
如果可以走索引:

  1. 使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;
  2. 如果使用 join 语句的话,需要让小表做驱动表。

Simple Nested-Loop Join

如果驱动表用不上索引。select * from t1 straight_join t2 on (t1.a=t2.b);

因为t2.b没有索引,所以需要全表扫描。总共需扫描100*1000行。

MySQL 没有使用 Simple Nested-Loop Join 算法,而是使用了“Block Nested-Loop Join”算法,简称BNL。

Block Nested-Loop Join

  1. 把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;
  2. 扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。

虽然都会扫描100*1000行,但BNL是内存判断,所以会快一点。

小结

  1. 如果可以使用被驱动表的索引,join 语句还是有其优势的;
  2. 不能使用被驱动表的索引,只能使用 BNL 算法,这样的语句就尽量不要使用;
  3. 在使用 join 的时候,应该让小表做驱动表。

如果被驱动表是个大表,会把冷数据的page加入到buffer pool,并且BNL要扫描多次,两次扫描的时间可能会超过1秒,使上节提到的分代LRU优化失效,把热点数据从buffer pool中淘汰掉,影响正常业务的查询效率。

35 | join语句怎么优化?

表结构

create table t1(id int primary key, a int, b int, index(a));
create table t2 like t1;
drop procedure idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t1 values(i, 1001-i, i);
set i=i+1;
end while;

set i=1;
while(i<=1000000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;

end;;
delimiter ;
call idata();

Multi-Range Read 优化

回表是指,InnoDB 在普通索引 a 上查到主键 id 的值后,再根据一个个主键 id 的值到主键 id 的值到主键索引上去查整行数据的过程。主键索引是一棵 B+ 树,在这棵树上,每次只能根据一个主键 id 查到一行数据。因此,回表肯定是一行行搜索主键索引的。

如果随着 a 的值递增顺序查询的话,id 的值就变成随机的,那么就会出现随机访问,性能相对较差。虽然“按行查”这个机制不能改,但是调整查询的顺序,还是能够加速的。

因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
MRR 优化的设计思路:

  1. 根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 ;
  2. 将 read_rnd_buffer 中的 id 进行递增排序;
  3. 排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回。

read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制。如果想要稳定地使用 MRR 优化的话,需要设置set optimizer_switch=“mrr_cost_based=off”,如果不设置,优化器会判断消耗,倾向于不使用MRR。

Batched Key Access

MySQL 在 5.6 版本后开始引入的 Batched Key Acess(BKA) 算法了。其实就是对 NLJ 算法的优化。
NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时MRR 的优势就用不上了。
BKA 算法就是缓存多行传给其他表,流程如下:

启动BKA:set optimizer_switch=‘mrr=on,mrr_cost_based=off,batched_key_access=on’;

BNL 算法的性能问题

上篇文章末尾说了,如果一个使用 BNL 算法的 join 语句,多次扫描一个冷表,而且这个语句执行时间超过 1 秒,就会在再次扫描冷表的时候,把冷表的数据页移到LRU 链表头部。
为了减少这种影响,可以考虑增大join_buffer_size 的值,减少对被驱动表的扫描次数。

优化的常见做法是,给被驱动表的 join 字段加上索引,把 BNL 算法转成 BKA 算法。
还可以考虑使用临时表。使用临时表的大致思路是:

  1. 把表 t2 中满足条件的数据放在临时表 tmp_t 中;
  2. 为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引;
  3. 让表 t1 和 tmp_t 做 join 操作。

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);

扩展 -hash join

mysql目前还没有hash索引,MariaDB支持。
所以可以自己实现在业务端。实现流程大致如下:

  1. select * from t1;取得表 t1 的全部 1000 行数据,在业务端存入一个 hash 结构;
  2. select * from t2 where b>=1 and b<=2000; 获取表 t2 中满足条件的 2000 行数据。
  3. 把这 2000 行数据,一行一行地取到业务端,到 hash 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行。

这个过程会比临时表方案的执行速度还要快一些。

36 | 为什么临时表可以重名?

上节提到了临时表。
如果是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用 Memory 引擎。
临时表的特点:

  1. 临时表只能被创建它的 session 访问,对其他线程不可见。所以在这个 session 结束的时候,会自动删除临时表。
  2. 临时表可以与普通表同名(还是不要这么做)。
  3. session A 内有同名的临时表和普通表的时候,show create 语句,以及增删改查语句访问的是临时表。
  4. show tables 命令不显示临时表。

临时表的应用

分表分库跨库查询
分库分表系统都有一个中间层 proxy,如果 sql 能够直接确定某个分表,这种情况是最理想的。
但如果涉及到跨库,一般有两种方式

  1. 在 proxy 层的进程代码中实现排序,但对 proxy 的功能和性能要求较高。
  2. 把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在这个汇总实例上做逻辑操作。如果每个分库的计算量都不饱和,那么直接可以在把临时表放到某个分库上。

为什么临时表可以重名?

MySQL 要给临时 InnoDB 表创建一个 frm 文件保存表结构定义,还要有地方保存表数据。
这个 frm 文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程 id}_{线程 id}_ 序列号”。可以使用 select @@tmpdir 命令,来显示实例的临时文件目录。

表中数据存放:

  1. MySQL5.6 会在临时文件目录下创建一个相同前缀、以.ibd 为后缀的文件,用来存放数据文件;
  2. MySQL5.7版本开始引入了一个临时文件表空间,专门用来存放临时文件的数据。因此,我们就不需要再创建 ibd 文件了。

MySQL 维护数据表,除了物理上要有文件外,内存里面也有一套机制区别不同的表,每个表都对应一个table_def_key。

  1. 一个普通表的 table_def_key 的值是由“库名+ 表名”得到。
  2. 临时表,table_def_key 在“库名 + 表名”基础上,又加入了“server_id+thread_id”。

临时表和主备复制

如果当前的 binlog_format=row,那么跟临时表有关的语句,就不会记录到 binlog 里。
binlog_format=statment/mixed 的时候,binlog 中才会记录临时表的操作。
这种情况下,创建临时表的语句会传到备库执行,因此备库的同步线程就会创建这个临时表。主库在线程退出的时候,会自动删除临时表,但是备库同步线程是持续在运行的。所以,这时候我们就需要在主库上再写一个 DROP TEMPORARY TABLE 传给执行。

37 | 什么时候会使用内部临时表?

create table t1(id int primary key, a int, b int, index(a));
delimiter ;;
create procedure idata()
begin
declare i int;

set i=1;
while(i<=1000)do
insert into t1 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();

执行这条语句(select 1000 as f) union (select id from t1 order by id desc limit 2);

  • 第二行key=PRIMARY,说明第二个子句用到了索引 id
  • 第三行 Extra 字段,说明 UNION 时使用了临时表 (Using temporary)

执行流程:

  1. 创建一个内存临时表,这个临时表只有一个整型字段 f,并且 f 是主键字段。
  2. 执行第一个子查询,得到 1000 这个值,并存入临时表中。
  3. 执行第二个子查询:拿到第一个1000,但已存在存入失败,拿到第二个999,存入成功。
  4. 从临时表中按行取出数据(1000和999两行),返回结果,并删除临时表。

如果把上面这个语句中的 union 改成 union all的话,就不需要“去重”。这样执行的时候,就依次执行子查询,得到的结果直接作为结果集的一部分,发给客户端。因此也就不需要临时表了。

group by 执行流程

select id%10 as m, count(*) as c from t1 group by m;

  1. Using index,表示这个语句使用了覆盖索引,选择了索引 a,不需要回表;
  2. Using temporary,表示使用了临时表;
  3. Using filesort,表示需要排序。

执行流程:

  1. 创建内存临时表,表里有两个字段 m 和 c,主键是 m;
  2. 扫描表 t1 的索引 a,依次取出叶子节点上的 id 值,计算 id%10 的结果,记为 x;如果临时表中没有主键为 x 的行,就插入一个记录 (x,1),如果表中有主键为 x 的行,就将 x 这一行的 c 值加 1;
  3. 遍历完成后,再根据字段 m 做排序(内存临时表的排序 17 篇文章有),得到结果集返回给客户端。

如果不需要排序则直接取内存临时表的数据。

但内存临时表的大小是有限制的,参数 tmp_table_size 就是控制这个内存大小的,默认是 16M。如果内存不够则使用磁盘临时表。

group by 优化方法

索引

如果可以确保输入的数据是有序的,那么计算 group by 的时候,就只需要从左到右,顺序扫描,依次累加。

  1. 当碰到第一个 1 的时候,已经知道累积了 X 个 0,结果集里的第一行就是 (0,X);
  2. 当碰到第一个 2 的时候,已经知道累积了 Y 个 1,结果集里的第二行就是 (1,Y);

InnoDB 的索引,就可以满足这个输入有序的条件。
直接排序
如果临时表数据量特别大,可让 MySQL 直接走磁盘临时表,在 group by 语句中加入 SQL_BIG_RESULT 这个提示(hint)。
MySQL 的优化器会直接用数组来存,而不是B+ 树存储。这样 select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;

执行流程:

  1. 初始化 sort_buffer,确定放入一个整型字段,记为 m;
  2. 扫描表 t1 的索引 a,依次取出里面的 id 值, 将 id%100 的值存入 sort_buffer 中;
  3. 扫描完成后,对 sort_buffer 的字段 m 做排序(如果 sort_buffer 内存不够用,,就会利用磁盘临时文件辅助排序);
  4. 排序完成后,就得到了一个有序数组。

总结:

MySQL 什么时候会使用内部临时表?

  1. 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果;
  2. join_buffer 是无序数组,sort_buffer 是有序数组,临时表是二维表结构;
  3. 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如,union 需要用到唯一索引约束, group by 还需要用到另外一个字段来存累积计数。

group by使用的指导原则:

  1. 如果对 group by 语句的结果没有排序要求,要在语句后面加 order by null;
  2. 尽量让 group by 过程用上表的索引,确认方法是 explain 结果里没有 Using temporary 和 Using filesort;
  3. 如果 group by 需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大 tmp_table_size 参数,来避免用到磁盘临时表;
  4. 如果数据量实在太大,使用 SQL_BIG_RESULT 这个提示,来告诉优化器直接使用排序算法得到 group by 的结果。

38 | 都说InnoDB好,那还要不要使用Memory引擎?

内存表的数据组织结构

表 t1 使用 Memory 引擎, 表 t2 使用InnoDB 引擎。

create table t1(id int primary key, c int) engine=Memory;
create table t2(id int primary key, c int) engine=innodb;
insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);

可以看到两个引擎顺序不一致。
InnoDB 表的数据就放在主键索引树上,主键索引是 B+ 树。
与 InnoDB 引擎不同,Memory 引擎的数据和索引是分开的。

内存表的数据部分以数组的方式单独存放,而主键 id 索引里,存的是每个数据的位置。主键 id 是 hash 索引,可以看到索引上的 key 并不是有序的。
在内存表 t1 中,执行 select * 按数组顺序全表扫描。因此,0 就是最后一个被读到。

所以InnoDB 和 Memory 引擎的数据组织方式是不同的:

  • InnoDB 引擎把数据放在主键索引上,其他索引上保存的是主键 id。这种方式,我们称之为索引组织表(Index Organizied Table)。
  • Memory 引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表(Heap Organizied Table)。

两个引擎的一些典型不同:

  1. InnoDB 表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的;
  2. 当数据文件有空洞的时候,InnoDB 表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
  3. 数据位置发生变化的时候,InnoDB 表只需要修改主键索引,而内存表需要修改所有索引;
  4. InnoDB 表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的。
  5. InnoDB 支持变长数据类型,不同记录的长度可能不同;内存表不支持 Blob 和 Text 字段,并且即使定义了 varchar(N),实际也当作 char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。

由于内存表的这些特性,每个数据行被删除以后,空出的这个位置都可以被接下来要插入的数据复用。内存表 t1 的这个主键索引是哈希索引,因此如果执行范围查询是用不上主键索引的,需要走全表扫描。

hash 索引和 B-Tree 索引

内存表也是支持 B-Tree 索引的alter table t1 add index a_btree_index using btree (id);

不建议你在生产环境上使用内存表,这里的原因主要包括两个方面:
内存表的锁
内存表不支持行锁,只支持表锁。
数据持久性问题
数据库重启的时候,所有的内存表都会被清空。
主从模式,从库掉电重启收到主库请求会找不到行。双主模式下,一台掉电重启会发送delete到另一台清空数据。

create temporary table temp_t(id int primary key, a int, b int, index (b))engine=memory;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);

39 | 自增主键为什么不是连续的?

不同的引擎对于自增值的保存策略不同。

  • MyISAM 引擎的自增值保存在数据文件中。
  • InnoDB 引擎保存在内存里,MySQL 8.0 版本后才有自增值持久化能力,记录在redo log中。MySQL 5.7 及之前的版本,每次重启后,第一次打开表的时候,都会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。

自增值修改机制

如果字段 id 被定义为 AUTO_INCREMENT

  1. 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
  2. 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。

假设,某次要插入的值是 X,当前的自增值是 Y。

  1. 如果 X<Y,那么这个表的自增值不变;
  2. 如果>=Y,就需要把当前自增值修改为新的自增值。

新的自增值生成算法是:从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 X 的值,作为新的自增值。(双主架构可以设置一个库的自增id都是奇数,另一个都是偶数)。

自增值的修改时机

自增值会在插入数据之前自增。
所以唯一键冲突是导致自增主键 id 不连续的第一种原因。类似,事务回滚也会产生类似的现象。

自增锁的优化

MySQL 5.1.22 版本引入了一个新策略,新增参数 innodb_autoinc_lock_mode,默认值是 1。

  1. 语句执行结束后才释放锁;
  2. 普通 insert 语句,自增锁在申请之后就马上释放;类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
  3. 这个参数的值被设置为 2 时,所有的申请自增主键的动作都是申请自增主键的动作都是申请后就释放锁。

生产上,如果有insert … select、replace … select 和 load data 语句,这种批量插入数据的场景时,建议设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row。
对于批量插入数据的语句,MySQL 有一个批量申请自增 id 的策略:同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。所以如果多申请了id也会导致自增主键 id 不连续。

普通insert语句,即使 innodb_autoinc_lock_mode 设置为 1,也不会等语句执行完成才释放锁。因为在申请自增 id 的时候,是可以精确计算出需要多少个 id 的,然后一次性申请,申请完成后锁就可以释放了。

40 | insert语句的锁为什么这么多?

insert … select 语句

可重复读隔离级别下,binlog_format=statement。
表结构

CREATE TABLE `t` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);

create table t2 like t

session B执行时需要对表 t 的所有行和间隙加锁。如果没有锁,就可能出现 session B 的 insert 语句先执行,但是后写入 binlog 的情况。所以会引起主备不一致。

insert 循环写入

执行 insert … select 的时候,对目标表也不是锁全表,而是只锁住需要访问的资源。现在有这么一个需求:要往表 t2 中插入一行数据,这一行的 c 值是表 t 中 c 值的最大值加 1。insert into t2(c,d) (select c+1, d from t force index(c) order by c desc limit 1);

这个语句的加锁范围,就是表 t 索引 c 上的 (3,4]和 (4,supremum] 这两个 next-key lock,以及主键索引上 id=4 这一行。它的执行流程也比较简单,从表 t 中按照索引 c 倒序,扫描第一行,拿到结果写入到表 t2 中。insert into t(c,d) (select c+1, d from t force index(c) order by c desc limit 1);

如果执行这句sql,可以看到,这时候的 Rows_examined 的值是 5。并且使用了临时表。

Explain 结果 rows=1 是因为受到了 limit 1 的影响。可能不准确。
使用执行Innodb_rows_read 语句查看查看sql执行前后扫描行数。

可以看到,这个语句执行前后,Innodb_rows_read 的值增加了 4。因为默认临时表是使用 Memory 引擎的,所以这 4 行查的都是表 t,也就是说对表 t 做了全表扫描。

  1. 创建临时表,表里有两个字段 c 和 d。
  2. 按照索引 c 扫描表 t,依次取 c=4、3、2、1,然后回表,读到 c 和 d 的值写入临时表。这时,Rows_examined=4。
  3. 由于语义里面有 limit 1,所以只取了临时表的第一行,再插入到表 t 中。这时,Rows_examined 的值加 1,变成了 5。

这个语句会导致在表 t 上做全表扫描,并且会给索引 c 上的所有间隙都加上共享的 next-key lock。
这个语句的执行为什么需要临时表,原因是这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符。
由于实现上这个语句没有在子查询中就直接使用 limit 1,从而导致了这个语句的执行需要遍历整个表 t。

create temporary table temp_t(c int,d int) engine=memory;
insert into temp_t (select c+1, d from t force index(c) order by c desc limit 1);
insert into t select * from temp_t;
drop table temp_t;

insert 唯一键冲突

insert into … on duplicate key update

语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。insert into t values(11,10,10) on duplicate key update d=100;

41 | 怎么最快地复制一张表?

如果可以控制对源表的扫描行数和加锁范围很小的话,我们简单地使用 insert … select 语句即可实现。

create database db1;
use db1;

create table t(id int primary key, a int, b int, index(a))engine=innodb;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t values(i,i,i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();

create database db2;
create table db2.t like db1.t

mysqldump 方法

mysqldump -h$host -P$port -u$user –add-locks=0 –no-create-info –single-transaction –set-gtid-purged=OFF db1 t –where=”a>900″ –result-file=/client_tmp/t.sql

导出 CSV 文件

//导出 select * from db1.t where a>900 into outfile ‘/server_tmp/t.csv’; //导入 load data infile ‘/server_tmp/t.csv’ into table db2.t;

物理拷贝方法

在 MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能。
假设我们现在的目标是在 db1 库下,复制一个跟表 t 相同的表 r:

  1. 执行 create table r like t,创建一个相同表结构的空表;
  2. 执行 alter table r discard tablespace,这时候 r.ibd 文件会被删除;
  3. 执行 flush table t for export,这时候 db1 目录下会生成一个 t.cfg 文件;
  4. 在 db1 目录下执行 cp t.cfg r.cfg; cp t.ibd r.ibd;这两个命令;
  5. 执行 unlock tables,这时候 t.cfg 文件会被删除;
  6. 执行 alter table r import tablespace,将这个 r.ibd 文件作为表 r 的新的表空间,由于这个文件的数据内容和 t.ibd 是相同的,所以表 r 中就有了和表 t 相同的数据。

42 | grant之后要跟着flush privileges吗?

先创建一个用户:create user ‘ua’@’%‘ identified by ‘pa’;

这条命令做了两个动作:

  1. 磁盘上,往 mysql.user 表里插入一行,由于没有指定权限,所以这行数据上所有表示权限的字段的值都是 N;
  2. 内存里,往数组 acl_users 里插入一个 acl_user 对象,这个对象的 access 字段值为 0。

全局权限

// 增加权限 grant all privileges on *.* to ‘ua’@’%‘ with grant option; // 取消权限 revoke all privileges on *.* from ‘ua‘@’%;

将上述第1步权限字段的值 N 全改为 Y;把上述第2步内存数组 acl_users 全改为1。

db 权限

grant all privileges on db1.* to ‘ua’@’%’ with grant option;

grant 操作对于已经存在的连接的影响,在全局权限和基于 db 的权限效果是不同的。如果当前会话已经处于某一个 db 里面, use 这个库的时候拿到的库权限会保存在会话变量中,所以 revoke 会不生效。

表权限和列权限

表权限定义存放在表 mysql.tables_priv 中,列权限定义存放在表 mysql.columns_priv 中。这两类权限,组合起来存放在内存的 hash 结构 column_priv_hash 中。

create table db1.t1(id int, a int);

grant all privileges on db1.t1 to ‘ua’@’%’ with grant option;
GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO ‘ua’@’%’ with grant option;

flush privileges 使用场景

正常情况下,grant 命令之后,没有必要跟着执行 flush privileges 命令,因为会同时刷新内存数据。
但当数据表中的权限数据跟内存中的权限数据不一致的时候,flush privileges 语句可以用来重建内存数据,达到一致状态。这种不一致往往是由不规范的操作导致的,比如直接用 DML 语句操作系统权限表。

44 | 答疑文章(三):说一说这些好问题

join 的写法

create table a(f1 int, f2 int, index(f1))engine=innodb;
create table b(f1 int, f2 int)engine=innodb;
insert into a values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
insert into b values(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);

  1. 如果用 left join 的话,左边的表一定是驱动表吗?
  2. 如果两个表的 join 包含多个条件的等值匹配,是都要写到 on 里面呢,还是只把一个条件写到 on 里面,其他条件写到 where 部分?

其实就是下面这两种写法的区别:

select * from a left join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q1*/ select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q2*/

Q1的explain:

Q1使用BNL算法,第 35 篇文章《join 语句怎么优化?》中讲过。
Q2的explain:

Q1使用NLJ算法,执行流程是这样的:顺序扫描表 b,每一行用 b.f1 到表 a 中去查,匹配到记录后判断 a.f2=b.f2 是否满足,满足条件的话就作为结果集的一部分返回。
差别
在 MySQL 里,NULL 跟任何值执行等值判断和不等值判断的结果,都是 NULL。所以 Q2 没有1和2。

Q2这条语句虽然用的是 left join,但是语义跟 join 是一致的。优化器会把Q2优化成join。因为表 a 的 f1 上有索引,就把表 b 作为驱动表,这样就可以用上 NLJ 算法。使用show warning;可以看到优化后的语句。

所以使用 left join 时,左边的表不一定是驱动表
如果需要 left join 的语义,就不能把被驱动表的字段放在 where 条件里面做等值判断或不等值判断,必须都写在 on 里面。

select * from a join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q3*/ select * from a join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q4*/

在这种情况下,join 将判断条件是否全部放在 on 部分就没有区别了。

Simple Nested Loop Join 的性能问题

Simple Nested Loop Join 算法,其实也是把数据读到内存里,然后按照匹配条件进行判断,为什么性能差距会这么大呢?
解释这个问题,需要用到 MySQL 中索引结构和 Buffer Pool 的相关知识点:

  1. 在对被驱动表做全表扫描的时候,如果数据没有在 Buffer Pool 中,就需要等待这部分数据从磁盘读入;多次访问容易将这些数据页放到 Buffer Pool 的头部,影响 Buffer Pool 的命中率。
  2. 即使被驱动表数据都在内存中,每次查找“下一个记录的操作”,都是类似指针操作。而 join_buffer 中是数组,遍历的成本更低。

distinct 和 group by 的性能

select a from t group by a order by null;

select distinct a from t;

group by 没有聚合函数,这两句sql的效率相同。

  1. 创建一个临时表,临时表有一个字段 a,并且在这个字段 a 上创建一个唯一索引;
  2. 遍历表 t,依次取数据插入临时表中:如果发现唯一键冲突,就跳过;否则插入成功;
  3. 遍历完成后,将临时表作为结果集返回给客户端。

备库自增主键问题

第 39 篇文章《自增主键为什么不是连续的?》评论区,@帽子掉了 同学问到:在 binlog_format=statement 时,语句 A 先获取 id=1,然后语句 B 获取 id=2;接着语句 B 提交,写 binlog,然后语句 A 再写 binlog。这时候,如果 binlog 重放,是不是会发生语句 B 的 id 为 1,而语句 A 的 id 为 2 的不一致情况呢?
不会,虽然 statement 格式下“自增 id 的生成顺序,和 binlog 的写入顺序可能是不同的”。

45 | 自增id用完怎么办?

表定义自增值 id

表定义的自增值达到上限后的逻辑是:再申请下一个 id 时,得到的值保持不变。

create table t(id int unsigned auto_increment primary key) auto_increment=4294967295;
insert into t values(null);
// 成功插入一行 4294967295
show create table t;
/* CREATE TABLE `t` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4294967295;
*/

insert into t values(null);
//Duplicate entry ‘4294967295’ for key ‘PRIMARY’

主键冲突,如果 4 个字节无符号整型 (unsigned int) 不够用的情况下,可以使用 8 个字节的 bigint unsigned。

InnoDB 系统自增 row_id

如果你创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id。InnoDB 维护了一个全局的 dict_sys.row_id 值,所有无主键的 InnoDB 表,每插入一行数据,都将当前的 dict_sys.row_id 值作为要插入数据的 row_id,然后把 dict_sys.row_id 的值加 1。
如果到达上限后,再有插入数据的行为要来申请 row_id,拿到以后再取最后 6 个字节的话就是 0,然后继续循环。所以会导致覆盖数据。

Xid

redo log 和 binlog 相配合的时候,它们有一个共同的字段叫作 Xid。它在 MySQL 中是用来对应事务的。
MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,然后给这个变量加 1。如果当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。
而 global_query_id 是一个纯内存变量,重启之后就清零了。所以你就知道了,在同一个数据库实例中,不同事务的 Xid 也是有可能相同的。但是 MySQL 重启之后会重新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 一定是唯一的。
不过 global_query_id 达到上限后,会继续从 0 开始计数,由于 global_query_id 为8个字节,所以一般不会出现到达上限的情况。

Innodb trx_id

Xid 是由 server 层维护的。InnoDB 内部使用 Xid ,就是为了能够在 InnoDB 事务和 server 之间做关联。但是,InnoDB 自己的 trx_id,是另外维护的。
InnoDB 内部维护了一个 max_trx_id 全局变量,每次需要申请一个新的 trx_id 时,就获得 max_trx_id 的当前值,然后并将 max_trx_id 加 1。
InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是通过事务的一致性视图与这行数据的 trx_id 做对比。
对于正在执行的事务,你可以从 information_schema.innodb_trx 表中看到事务的 trx_id。
但是对于只读事务,InnoDB 并不会分配 trx_id。

max_trx_id 会持久化存储,重启也不会重置为 0,那么从理论上讲,只要一个 MySQL 服务跑得足够久,就可能到达上限,然后从 0 开始的情况。然后就会导致脏读。但只存在理论上,如果一个 MySQL 实例的 TPS 是每秒 50 万,持续这个压力的话,在 17.8 年后,就会出现这个情况。

thread_id

show processlist 里面的第一列,就是 thread_id。
系统保存了一个全局变量 thread_id_counter,每新建一个连接,就将 thread_id_counter 赋值给这个新连接的线程变量。
thread_id_counter 定义的大小是 4 个字节,到达上限则从0开始。