Java后端面试:MySQL面试篇(底层事务、SQL调优)

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习

🌌上期文章:Java后端面试:Redis面试篇(原理+场景题)

📚订阅专栏:Java后端面试

希望文章对你们有所帮助

MySQL可能是面试最常问的了,经常喜欢问MySQL的优化,包括定位慢查询、SQL执行计划、索引、优化经验等等,相关的详细原理也可以看之前的专栏:
MySQL进阶

在这里更多是对常见面试题做个总结。

MySQL面试篇(底层事务、SQL调优)

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)水平分表:将一个表的数据拆分到多个表中(可以在同一个库内)。

相关推荐
LucianaiB31 分钟前
【金仓数据库征文】_AI 赋能数据库运维:金仓KES的智能化未来
运维·数据库·人工智能·金仓数据库 2025 征文·数据库平替用金仓
24k小善35 分钟前
Flink TaskManager详解
java·大数据·flink·云计算
想不明白的过度思考者41 分钟前
Java从入门到“放弃”(精通)之旅——JavaSE终篇(异常)
java·开发语言
天天扭码1 小时前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
时序数据说1 小时前
时序数据库IoTDB在航空航天领域的解决方案
大数据·数据库·时序数据库·iotdb
.生产的驴1 小时前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
猿周LV1 小时前
JMeter 安装及使用 [软件测试工具]
java·测试工具·jmeter·单元测试·压力测试
晨集1 小时前
Uni-App 多端电子合同开源项目介绍
java·spring boot·uni-app·电子合同
AnsenZhu1 小时前
2025年Redis分片存储性能优化指南
数据库·redis·性能优化·分片
时间之城1 小时前
笔记:记一次使用EasyExcel重写convertToExcelData方法无法读取@ExcelDictFormat注解的问题(已解决)
java·spring boot·笔记·spring·excel