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

相关推荐
tatasix5 分钟前
MySQL UPDATE语句执行链路解析
数据库·mysql
ifanatic12 分钟前
[面试]-golang基础面试题总结
面试·职场和发展·golang
南城花随雪。17 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了18 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol20 分钟前
java基础概念37:正则表达式2-爬虫
java
懒是一种态度20 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮23 分钟前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
xmh-sxh-131436 分钟前
jdk各个版本介绍
java