十道MySQL必问面试题

  1. 你是如何理解最左前缀原则的?
  2. 你是如何理解行锁、GAP锁、临键锁的?
  3. 你是如何理解MVCC的?
  4. 你是如何理解count(*)和count(1)的?
  5. 你是如何理解Online DDL的?
  6. 你知道哪些情况下会导致索引失效?
  7. 你是如何理解filesort的?
  8. 你知道哪些情况下会锁表吗?
  9. 你是如何理解MySQL里面的死锁机制的?
  10. 你是如何优化慢查询的?

更多面试题在公众号:IT周瑜

你是如何理解最左前缀原则的?

创建一个表:

CREATE TABLE t1 (
  id int primary key auto_increment,
  a int,
  b int,
  c int,
  d int
);

创建了一个abc联合索引:

alter table t1 add index idx_abc(a, b, c);

执行:

explain select * from t1 where b=1 and c=1 and a=1;

以上SQL能走abc联合索引吗?注意a=1在where条件的最右边哦。

答案是可以的:
image.png

把a=1放到中间:

explain select * from t1 where b=1 and a=1 and c=1;

一样是可以的:
image.png

那把a=1放到最左边就肯定更没问题了

explain select * from t1 where a=1 and b=1 and c=1;

image.png

从这可以看出,不管a=1放在哪个位置,以上SQL都能走索引,那如果把a=1去掉呢?

explain select * from t1 where b=1 and c=1;

此时就不会走abc联合索引了,而是全表扫描:
image.png

从这可以看出,所谓的最左前缀原则中的“最左”,并不是指where条件中a=1一定要在最左边,而是指where条件中一定要给出定义联合索引时的最左边字段,比如我们定义abc联合索引的SQL为:

alter table t1 add index idx_abc(a, b, c);

其中a字段是最左边的字段,因此如果想要走idx_abc索引,那么SQL中就一定要给出a字段的条件,这才是“最左”的意思。

你是如何理解行锁、GAP锁、临键锁的?

假如有以下表:

CREATE TABLE t1 (
  id int primary key,
  name varchar(10)
);

假如表中有两行记录:
image.png
如果我现在执行以下SQL,想要更新id=3的这一行记录:

update t1 set name='dadudu' where id = 3;

此时当前事务会给id=3这一行加一个行锁,其他事务如果也想要更新这一行也需要给这一行加锁,而且两个事务加的都是X锁,也就是排他锁,两个排他锁是不兼容的,所以其他事务只能等待持有锁的事务提交后将锁释放掉才能获取到锁,从而更新数据,这也可以理解为悲观锁。

所谓行锁其实在InnoDB源码中是通过一个lock_t和一个hash表来实现的,这里就不展开分析了,想在面试的时候装B的同学可以联系我,我单独给你分析分析。

行锁锁的是某一行,而GAP锁锁的是行前面的间隙,注意只是行前面的间隙,你可能会问那表的最后一行前后都有间隙啊,最后一行后面的间隙不锁吗?

当然会所了,只不过是交给了一个叫做PAGE_NEW_SUPREMUM的记录来说的,你可以理解为PAGE_NEW_SUPREMUM记录是InnoDB默认的,它固定作为最后一条记录,因此只要锁住PAGE_NEW_SUPREMUM前面的间隙,就相当了锁住了我们所理解的最后一行后面的间隙

而临键锁,也就是next-key锁,它既锁行又锁行前面的间隙,而且我们经常遇到的是行锁或临键锁。

比如上面的SQL:

update t1 set name='dadudu' where id = 3;

由于是根据id进行的更新,id是主键,是唯一的,这种情况就只需要对id=3这条记录加行锁就可以了,并不需要加next-key锁,也就是并不需要锁id=3前面的间隙,因为id=3前面的间隙对应的记录一定不是id=3,所以不用锁间隙。

而如果是下面这个SQL:

update t1 set name='dadudu' where name = 'zhouyu';

就需要锁全部的间隙了,因为全部的间隙都有可能插入name='zhouyu’的记录,或者可以理解为会给所有记录都加临键锁,这样就锁住了所有的间隙。

你是如何理解MVCC的?

所谓MVCC就是多版本并发控制,MySQL为了实现可重复读这个隔离级别,而且为了不采用锁机制来实现可重复读,所以采用MVCC机制来实现。

当一个事务修改一条记录时,会将记录的历史版本也记录下来,从而一条记录会对应一条版本链,而且每次修改时都会将当前的事务ID保存下来,所以一条记录的多个版本可能对应了不同的事务ID,而且在InnoDB中,事务ID是自增的,是有顺序的。

因此,当一个事务在查询某一条记录时,会根据自己的事务ID以及这条记录的版本链,来判断当前事务可见的是哪个版本,从而读出这个版本对应的记录,从而实现了可重复读。

MVCC中还有一个概念就是ReadView,在一个事务首次执行查询时,会创建一个ReadView,而ReadView中会记录当前整个InnoDB中有哪些活跃的事务ID,最大的事务ID,最小的事务ID,然后根据这些信息来判断某条记录的某个版本是否可见,比如:

  1. 如果某个版本的事务ID,比ReadView中最小的事务ID,还要小,那么此版本可见,因为表示该版本的事务是在本事务之前就已经提交了
  2. 如果某个版本的事务ID,属于ReadView中活跃事务,那么此版本不可见,因为表示该版本的事务并不是本事务之前就已经提交了
  3. 如果某个版本的事务ID,比ReadView中最大的事务ID,还要大,那么此版本不可见,因为表示该版本的事务是本事务之后开启的新事务

如果某个版本对当前ReadView不可见,那就继续取上一个版本,直到对当前ReadView可见。

你是如何理解count(*)和count(1)的?

这两个并没有区别,不要觉得count()会查出全部字段,而count(1)不会,所以count()会更慢,你觉得MySQL作者会这么做吗?

我看了MySQL源码,也写了对应了文章麻烦不要再问我count(*)、count(1)、count(id)、count(name)之间的区别了

可以很明确的告诉你们count()和count(1)是一样的,而正确有区别的是count(字段),如果你count()的是具体的字段,那么MySQL会判断某行记录中对应字段是否为null,如果为null就不会进行统计了,因此count(字段)的结果可能会小于count()和count(1)。

另外,直接执行select (*) from t1;也是可以利用到索引的,并不一定是全表扫描,也可以扫描某个索引B+树的叶子节点,从而得到总条数,因为不管是什么索引,主键索引还是辅助索引,实际上它们对应的B+树中的叶子节点中的数据条数是一样的,只不过字段数不一样,主键索引存了全部字段,而辅助索引只存了定义的索引字段+主键字段,所以通常辅助索引是要比主键索引小的,因此遍历起来也会更快,但是记录条数是一样的。

你是如何理解Online DDL的?

一般DDL操作,比如新增字段,会有三个步骤:

  • 第一,新建一个临时表,表的字段是原始表字段和新增字段
  • 第二,将原始表中的数据复制到临时表中
  • 第三,删除原始表,并将临时表重命名为原始表的表名

Online DDL表示在复制数据的过程中允许其他数据库连接读取数据、删除数据、修改数据、新增数据,但并不是所有的DDL操作都支持Online,那些需要修改现有表数据的DDL就不支持,比如新增带有默认值的字段,所谓Online,其实对应的是Inplace算法,在执行DDL时,MySQL会自动判断应该用Copy算法还是Inplace算法,能用Inplace算法的DDL就是Online DDL,MySQL8中,新增了一种效率更高的DDL算法,叫做Instant,算法效率更高,但支持的DDL场景更加有限。

你知道哪些情况下会导致索引失效?

最常见的就是对索引列做了运算、函数操作、类型转换了,比如:

CREATE TABLE t1 (
  id int primary key,
  name varchar(10),
  age varchar(2)
);
insert into t1(id, name, age) values(3, 'zhouyu', '18');
insert into t1(id, name, age) values(6, 'zhangfei', '19');
alter table t1 add index idx_name(name);
alter table t1 add index idx_age(age);

以下SQL是能走索引的:

explain select * from t1 where name = 'zhou';

image.png

但是如果改成:

explain select * from t1 where substring(name, 1, 4) = 'zhou';

image.png
就变成了全表扫描。

还有就是一种常见的索引失效就是对索引列进行类型转换,在MySQL中会将字符串转成数字,所以如果是以下SQL能走索引:

explain select * from t1 where age = '18';

image.png

但是如果是:

explain select * from t1 where age = 18;

image.png
就变成了全表扫描,因为age字段的类型是varchar,而给的值是数字,MySQL需要把age字段的所有内容都转成数字之后再进行where条件的过滤。

你是如何理解filesort的?

首先,order by、group by都有可能触发filesort,因为它们都需要对数据按指定字段进行排序,当然group by也可以不排序,只不过它默认会进行排序。

如果order by指定的字段能走索引,那就不会触发filesort,因为索引本身已经有顺序了,按索引上的顺序直接返回结果就可以了。

如果order by指定的字段不能走索引,此时就会出现filesort,但是不一定代表一定会用到文件排序,实际上MySQL中的order by的大体实现思路是这样的:

具体可参考我写的另外一篇文章假如你是MySQL作者,你会如何实现order by?

你知道哪些情况下会锁表吗?

  1. 表的元数据锁,比如在执行一些DDL操作时,比如给表增加字段,这其实就是在修改表的元数据,因此会给表加元数据锁,如果此时有其他DDL操作也来加元数据锁,那么就会阻塞,此时就出现了锁表
  2. 自增锁,如果一个表的主键是自增id,那么每次新增一条记录时,就需要先获取自增锁,然后得到最新的主键id,然后释放自增锁,然后插入新记录,在这个过程中也可能会短暂的锁表,因为自增锁也是表级别的锁
  3. 还有一种就是表中的所有行和间隙都被锁了,就像前面提到的update语句,也相当于出现了锁表现象

你是如何理解MySQL里面的死锁机制的?

所谓死锁就是:

  1. 事务A对记录1加锁
  2. 事务B对记录2加锁
  3. 事务A想对记录2加锁,阻塞
  4. 事务B想对记录1加锁,阻塞

两个事务都需要等待对方释放锁才能往下执行,此时就出现了死锁,而出现死锁的原因就是两个事务的加锁顺序不一样,如果都是先对记录1加锁,再对记录2加锁,就不会出现死锁现象了。

不过在MySQL中有死锁检查机制,在加锁时会自动判断当前是否出现了死锁,比如上面的第4步,MySQL一旦发现了死锁就会直接报错,从而导致事务B回滚并释放所有锁,从而导致事务A能正常执行下去,最终不会形成死锁。

image.png

另外需要注意,如果加锁请求比较多,死锁检查机制可能会比较消耗性能,此时可以选择关闭死锁检查机制,参数为innodb_deadlock_detect

你是如何优化慢查询的?

  1. 首先肯定是检查走索引的情况,特别是join查询、字查询、排序字段是否能走索引,如果没有走加上索引基本能解决问题
  2. 如果走了索引,则看是否能减少查询,让SQL不会表走索引覆盖或索引下推的逻辑
  3. 如果是join,或者某些子查询会自动优化为join的,如果MySQL所在机器内存够,可以增加join buffer的大小
  4. 如果是order by慢,那么也可以调整sort buffer的大小
  5. 如果还是慢,那就看是否是返回的数据确实太多了,是不是可以改成分页,这就需要修改应用层的代码了
  6. 如果还是慢,那肯定就是表中的数据确实太多了,那就看是不是能把一些数据进行归档,减少当前表的数据量
  7. 如果还是慢,那就要考虑分库分表,或者一些分布式数据库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值