👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Java后端面试:Redis面试篇(原理+场景题)
📚订阅专栏:Java后端面试
希望文章对你们有所帮助
MySQL可能是面试最常问的了,经常喜欢问MySQL的优化,包括定位慢查询、SQL执行计划、索引、优化经验等等,相关的详细原理也可以看之前的专栏:
MySQL进阶
在这里更多是对常见面试题做个总结。
MySQL面试篇(底层事务、SQL调优)
- SQL优化
- MySQL事务
-
- 事务特性
- 并发事务问题、隔离级别
- [undo log与redo log的区别](#undo log与redo log的区别)
- 解释MVCC
- MySQL主从同步原理
- MySQL分库分表
SQL优化
定位慢查询
一般的慢查询可能是这些情况:
1、聚合查询
2、多表查询
3、表数据量过大查询
4、深度分页查询
表象:页面加载过慢、接口压测的响应时间过长(超过1s)。
定位慢查询可以通过开源工具,但是我没有用过,就不介绍了,还有一种慢查询的定位方式是通过MySQL自带的慢查询日志
。
慢查询日志记录了所有执行时间超过指定参数(单位:秒)的所有SQL语句的日志
若要开启慢查询日志,需要修改配置文件
powershell
# 开启慢查询开关
slow_query_log = 1
# 设置慢查询时间为2s
long_query_time = 2
SQL语句执行慢,如何分析(SQL执行计划)
若这个SQL语句执行的很慢,如何分析呢?
通过
SQL执行计划
,可以找到SQL执行慢的原因
可以采用EXPLAIN
或者DESC
命令获取MySQL如何执行SELECT语句的信息,只需要在select之前加上这两种关键词。
查询出来后需要关注的字段:
1、possible_key:当前SQL可能会用到的索引
2、key:当前SQL实际命中的索引
3、key_len:索引占用的大小
4、Extra:额外的优化建议
(1)Using where;Using Index:查找过程用了索引,需要的数据在索引列中能找到,无需回表查询
(2)Using index condition:查找过程用了索引,但需要回表查询
5、type:SQL的连接类型,性能由好到差为NULL、system、const、eq_ref、ref、range、index、all
(1)system:查询系统中的表
(2)const:根据主键查询
(3)eq_ref:主键索引查询或唯一索引查询
(4)ref:索引查询
(5)range:范围查询
(6)index:索引树扫描
(7)all:全盘索引
其中,我们可以通过key和key_len检查是否命中索引(索引是否有失效的情况)
,通过type查看SQL是否有进一步的优化空间
,通过extra查询是否有回表的情况,如果出现则可以通过添加索引或者修改返回的字段来修复
索引及其底层
1、索引的作用:
索引是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库还维护了一个B+树,可以在B+树上实现高级查找算法。
若没有索引,查找数据可能要遍历整张表,效率过于低。
2、为什么不用二叉搜索树、红黑树、或者B树?
(1)二叉搜索树:最坏的二叉树(按顺序插入),相当于退化成了一个链表,查询效率与全盘扫描没区别(复杂度不稳定)
(2)红黑树:虽然复杂度更稳定了,但是如果数据特别大的话,由于红黑树每个结点只有两个子结点,还是会导致树太高,查询效率低
(3)B树:多叉路衡查找树,有很多分支(若最大度数为5,则每个节点最多存储4个key)。
(4)B+树:比起B树更适合实现外存储索引结构,相比较B树,B+树只有叶子节点才会存储数据,非叶子节点只是一个导航定位的作用,除此之外,叶子节点还加上了双向链表。InnoDB引擎就是用B+树来实现的索引结构。
B树与B+树的对比:
①磁盘读写代价B+树更低;②查询效率B+树更稳定;③B+树更便于扫库和区间查询(由于双向链表,只需要根据区间的左边界查,然后直接扫链表就好了)
聚簇索引、非聚簇索引、回表查询
1、什么是聚簇索引和非聚簇索引?(其实还有一种称呼是聚集索引和二级索引)
1、聚簇索引(聚集索引):将存储数据域索引放在一起,索引的叶子节点保存了行数据。必须要有聚集索引而且只能有一个
2、非聚簇索引(二级索引):将数据与索引分开存储,索引的叶子节点关联的是对应的主键。可以存在多个。
2、聚集索引的选取规则?
1、存在主键,则主键索引就是聚集索引
2、不存在主键,则第一个唯一索引作为聚集索引
3、没有主键和唯一索引,则InnoDB引擎会自动生成rowid作为隐藏的聚集索引
3、什么是回表查询
先通过二级索引找到对应的主键值,到聚集索引中查找整行数据并返回,这个过程就是回表查询
覆盖索引、超大分页查询
覆盖索引:查询使用了索引,并且需要返回的列在该索引树中已经能够全部找到。
例如,id为主键索引,name为普通索引,则下列指令符合覆盖索引
:
sql
select id, name from tb_user where name = 'wxj';
select * from tb_user where id = 2; # 查询聚集索引,叶子节点就是一整行的数据,也算覆盖索引
如果不符合覆盖索引,例如select id,name,gender from tb_user where name='Arm'
,首先在二级索引中查询name字段,查询完成之后由于叶子节点只包含了id和name,所以还需要进行回表查询,这时候的效率就会很低。这也就是为啥要尽量避免select *,除非查询的条件是根据主键的。
MySQL超大分页处理:当数据量很大的时候用limit做分页查询,越往后,分页查询的效率就越低。这是因为limit底层会给前面的记录都先排序,再去查询,这种排序代价可能会很大。
优化思路
:一般分页查询时,通过创建覆盖索引
能够比较好的提高性能,可以通过覆盖索引+子查询
的形式进行优化。
sql
select *
from tb_sku t,
(select id from tb_sku order by id limit 9000000,10) a
where t.id = a.id;
索引创建原则
1、针对数据量较大,且查询频繁的表建立索引(增加用户体验)
2、针对常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引
3、尽量选择区分度高的列作为索引,尽量建议唯一索引,区分度越高,使用索引的效率就越高
4、若是字符串类型的字段,可以建立前缀索引
5、尽量使用联合索引,减少单列索引,因为查询的时候联合索引很多时候可以覆盖索引,避免回表,提高查询效率
6、控制索引的数量,索引越多,维护的代价就越大
重点是1、2、5、6条,这在面试的时候肯定要讲到。
索引失效的情况
1、违背最左前缀法则
若索引了多列,要遵守最左前缀法则。也就是查询从索引的最左前列开始,否则索引会完全失效。并且最好不跳过索引中的列,否则会部分失效
2、范围查询右边的列,不能使用索引
sql
# address是范围查询status > '1'的列,address失效
explain select * from tb_seller where name = '小米科技' and status > '1' and address = '北京市';
3、在索引列上进行运算操作,索引会失效
sql
select * from tb_seller where substring(name,3,2) = '科技';
4、字符串不加单引号,可能会索引失效(因为底层可能会出现类型转换)
5、以%开头的Like模糊查询,索引失效
若仅仅是尾部模糊查询,索引不会失效。如果是头部模糊查询,索引失效
SQL优化经验
从五个方面回答这个问题:
1、表的设计优化(参考了阿里的开发手册)
(1)设置合适的数值(tinyint、int、bigint)
(2)设置合适的字符串类型(char和varchar,其中char定长效率高,varchar长度可变但效率偏低)
2、索引优化(优化的原则、索引失效的情况)
3、SQL语句优化
(1)避免使用select *
(2)避免索引失效
(3)尽量用union all代替union,因为union会增加一次过滤
(4)避免在where子句中对表达式进行表达式操作(因为也会出现索引失效,如substring)
4、主从复制、读写分离:若读的操作很多,为了避免写操作造成的性能影响,可以使用读写分离,其解决的
5、分库分表
MySQL事务
事务特性
ACID特性:
(1)原子性
(2)一致性
(3)隔离性
(4)持久性
并发事务问题、隔离级别
并发事务问题有哪些?解决的方法是什么?
并发事务问题:脏读、不可重复读、幻读
隔离级别:读未提交、读已提交、
可重复读(默认)
、串行化
并行事务问题:
问题 | 描述 |
---|---|
脏读 | 一个事务读到另外一个事务还没有提交的数据 |
不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同 |
幻读 | 一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现了这行数据已经存在,好像出现了"幻影"(插入的时候显示存在,再次查询还是查不到,因为此时已经解决了不可重复读) |
解决方案:对事务进行隔离
1、读未提交:三种问题都存在
2、读已提交:解决脏读
3、可重复读:解决脏读和不可重复读(默认)
4、串行化:三种问题都解决
级别越高,越安全,但是性能越低。一般情况下是不会用串行化的,如果一定要在默认(可重复读)的隔离级别下解决幻读,需要使用MVCC。
undo log与redo log的区别
需要记住两个概念:
缓冲池 :主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查时,先操作缓冲池数据(若没有就从磁盘加载并缓存),并以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
数据页:InnoDB存储引擎磁盘管理的最小单元,每个页的默认大小为16KB,页中存储的是行数据
redo log
:重做日志,记录的是事务提交时数据页的物理修改,用来实现事务的持久性
。
该日志由重做日志缓冲
和重做日志文件
组成,前者在内存中,后者在磁盘中。当事务提交后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘发生错误时,进行数据恢复。
如果业务出现错误导致数据不同步,当然也可以直接把缓冲池的数据加载回磁盘,但是这样的方式将会很影响性能。
而由于日志文件都是追加的,所以使用日志文件的这种方式的磁盘IO都是顺序的,就会提高效率。这种方式称为WAL(先写日志)
undo log
:回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚
和MVCC(多版本并发控制)
。undo log与redo log记录物理日志不同,它是逻辑日志
。
当delete一条记录时,undo log会记录一条insert记录,反之亦然。
当update时,会记录一条相反的update记录。
当执行rollback时,可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
也就是说,undo log用于实现事务的一致性和原子性
。
总结:
1、redo log记录的是数据页的物理变化,服务宕机可以用来同步数据
2、undo log记录的是逻辑日志,当事务回滚时,通过逆操作恢复数据
3、
redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
解释MVCC
undo log和redo log保证了事务的原子性、持久性、一致性,而隔离性是如何保证的呢?
隔离性由两个因素决定:
1、锁:排他锁(若一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁)
2、MVCC:多版本并发控制
解释一下MVCC?(事务中的隔离性是如何保证的呢?)
1、MVCC是
多版本并发控制
。指维护一个版本的多个数据,使得读写操作没有冲突。2、MVCC的具体实现,主要依赖于数据库记录中的
隐藏字段
、undo log日志
、readView
:(1)隐藏字段:
①DB_TRX_ID:最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID
②DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本
③DB_ROW_ID:隐藏主键,若表结构没有指定主键,将会生成该隐藏字段
(2)undo log:回滚日志,在增删改的时候产生的便于数据回滚的日志。
需要重点理解
undo log版本链
,可以看我之前的MySQL进阶的专栏,里面有详细解析。(3)readView(读视图):是
快照读
(记得区分一下什么是当前读,什么是快照读)SQL执行时MVCC提取数据的一句,记录并维护系统当前活跃的事务(未提交的)id。readView包含四个核心字段:①m_ids:当前活跃的事务ID集合
②min_trx_id:最小活跃事务ID
③max_trx_id:预分配事务ID,即当前最大事务ID+1
④creator_trx_id:readView创建者的事务ID
而具体的工作方式,需要依赖
undo log版本链
,具体依旧看我之前的文章。需要记住,
不同的隔离级别下,生成ReadView的时机不同
:(1)READ COMMITED:每次执行快照读时生成readView
(2)Repeatable Read:仅在第一次快照读生成readView,后面复用(就是因为这样,才解决了不可重复读)
MySQL主从同步原理
主从同步无非就是要实现主结点到从结点的复制操作,即主从复制。
MySQL主从复制的核心是二进制日志(BINLOG),记录了所有的DDL和DML语句。
主从复制的步骤分为三步:
1、主库在事务提交时,会把数据变更记录在二进制文件BINLOG中
2、从库读取主库的二进制日志文件BINLOG,写入到从库的中继日志
Relay log
3、从库重做中继日志中的事件,将改变反映它自己的数据
MySQL分库分表
我自己做的项目还没有用到海量数据,所以我根本没有用过分库分表,既然没有,面试被问到就说自己了解,能懂一些概念就好了。
为什么要用分库分表?
当使用主从分离、读写分离(主库写数据,从库读数据),可以分担访问的压力,但是主库和从库的表都是一样的,海量数据下不合适。
单表的数据量达到了1000W,优化就已经解决不了性能的问题了,而且还会出现IO瓶颈、CPU瓶颈
分库分表的分类?
1、垂直拆分
(1)垂直分库:以表为依据,根据业务把不同表拆到不同库中。
(2)垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中。(例如把大字段、不常用的字段拆分出来单独放在一张表)
2、水平拆分
(1)水平分库:将一个库的数据拆分到多个库中
(2)水平分表:将一个表的数据拆分到多个表中(可以在同一个库内)。