Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL面试准备

MySQL存粹问题面试准备总结大全

作者:Nontee

这篇文章主要介绍了MySQL存粹问题面试准备总结的相关资料,MySQL面试中常见问题,包括但不限于事务,索引,引擎,场景优化等常见问题,有助于面试前的准备,需要的朋友可以参考下

一、MySQL基础篇

面试官:请你介绍一下MySQL的架构,它主要由哪些部分组成?

回答:

好的,MySQL的架构可以分为三层,我从上到下给您介绍一下:

第一层是连接层,也叫客户端连接层。当我们的应用程序连接MySQL时,首先会经过这一层。它主要负责:

第二层是服务层,这是MySQL的核心层,包含了很多重要组件:

第三层是存储引擎层,MySQL支持插件式的存储引擎,常见的有InnoDB、MyISAM等。存储引擎负责数据的存储和读取。

一条SQL语句的执行流程大概是这样的:客户端发送SQL → 连接器验证 → 解析器解析 → 优化器优化 → 执行器执行 → 存储引擎读写数据。

面试官:你刚才提到了InnoDB和MyISAM,能详细说说它们的区别吗?

回答:

好的,这两个是MySQL最常用的存储引擎,它们的区别我从几个维度来说:

首先是事务支持方面:

其次是锁的粒度:

第三是外键约束:

第四是索引结构:

第五是COUNT(*)性能:

第六是崩溃恢复:

总结一下,现在基本都推荐使用InnoDB,它从MySQL 5.5版本开始就是默认引擎了。除非是一些只读的、不需要事务的场景,否则都应该选InnoDB。

面试官:你提到InnoDB使用聚簇索引,能详细解释一下什么是聚簇索引和非聚簇索引吗?

回答:

好的,这个问题很重要,我来详细说一下。

聚簇索引,英文叫Clustered Index,它的特点是:索引和数据是存储在一起的。在InnoDB中,主键索引就是聚簇索引,它的B+Tree叶子节点直接存储的是完整的数据行。

举个形象的例子,聚簇索引就像一本按拼音顺序排列的字典,字的解释就直接跟在拼音后面,找到拼音就找到了内容。

非聚簇索引,也叫二级索引或辅助索引,它的叶子节点存储的不是数据行,而是主键的值。

比如我们在username字段上建了一个普通索引,当我们通过username查询时:

这个过程就叫做回表

关于回表,我再补充一下: 回表是有性能损耗的,因为需要两次B+Tree查找。所以在实际开发中,我们会尽量避免回表,方法就是使用覆盖索引

什么是覆盖索引呢?就是我们查询的字段刚好都在索引中,不需要回表就能拿到数据。比如:

-- 假设有联合索引 idx_name_age(name, age)
SELECT name, age FROM user WHERE name = '张三';
-- 这个查询就用到了覆盖索引,不需要回表

还有一点要补充:InnoDB要求表必须有主键。如果我们没有显式定义主键,InnoDB会这样处理:

所以建表时一定要显式定义主键,而且推荐使用自增主键,因为顺序插入效率更高,不会造成页分裂。

二、索引篇

面试官:为什么MySQL选择B+Tree作为索引的数据结构,而不是B-Tree或者Hash?

回答:

这是个好问题,我从几个角度来分析:

首先说说为什么不用Hash:

Hash索引的等值查询确实很快,时间复杂度是O(1)。但它有几个致命缺点:

然后说说B+Tree相比B-Tree的优势:

B-Tree的每个节点都存储数据,而B+Tree只在叶子节点存储数据,非叶子节点只存储索引键。这带来几个好处:

实际举个例子:

假设一个节点能存1000个key,3层的B+Tree能存储1000×1000×1000 = 10亿条数据,而3次磁盘IO就能定位到任意数据。这就是B+Tree的威力。

面试官:什么是最左前缀原则?能举例说明吗?

回答:

好的,最左前缀原则是使用联合索引时必须遵循的规则。

简单来说,对于联合索引,查询条件必须从索引的最左边开始匹配,并且不能跳过中间的列。

我举个具体例子:

假设我们有一个联合索引idx_a_b_c(a, b, c),包含a、b、c三个字段。

-- 能用到索引的情况:
WHERE a = 1                    -- 用到索引(a)
WHERE a = 1 AND b = 2          -- 用到索引(a, b)
WHERE a = 1 AND b = 2 AND c = 3  -- 用到索引(a, b, c),完全匹配

-- 能部分用到索引的情况:
WHERE a = 1 AND c = 3          -- 只用到(a),c用不上,因为跳过了b

-- 用不到索引的情况:
WHERE b = 2                    -- 用不到,没有最左边的a
WHERE b = 2 AND c = 3          -- 用不到,没有最左边的a
WHERE c = 3                    -- 用不到

有一个特殊情况要注意:查询条件的顺序不影响索引使用,因为MySQL优化器会自动调整顺序。

WHERE b = 2 AND a = 1          -- 优化器会调整为 a = 1 AND b = 2,能用到索引

还有一个重要的点:范围查询会导致后面的列无法使用索引。

WHERE a = 1 AND b > 5 AND c = 3  
-- a走索引,b走索引(范围),但c就用不到索引了

实际工作中的建议:

面试官:哪些情况会导致索引失效?

回答:

索引失效是面试高频题,我总结了几种常见的情况:

1. 违反最左前缀原则

-- 联合索引 idx_a_b_c(a, b, c)
SELECT * FROM t WHERE b = 1;  -- 没有a,索引失效

2. 在索引列上使用函数或运算

-- 索引失效
SELECT * FROM user WHERE LEFT(name, 3) = '张';
SELECT * FROM user WHERE age + 1 = 25;
SELECT * FROM user WHERE YEAR(create_time) = 2024;
-- 正确写法
SELECT * FROM user WHERE name LIKE '张%';
SELECT * FROM user WHERE age = 24;
SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

3. 隐式类型转换

-- 假设phone是varchar类型
SELECT * FROM user WHERE phone = 13800138000;  -- 数字,索引失效
SELECT * FROM user WHERE phone = '13800138000';  -- 字符串,索引有效

这里的原理是:当类型不匹配时,MySQL会对索引列进行类型转换,相当于加了函数,就失效了。

4. LIKE以%开头

SELECT * FROM user WHERE name LIKE '%张';   -- 索引失效
SELECT * FROM user WHERE name LIKE '%张%';  -- 索引失效
SELECT * FROM user WHERE name LIKE '张%';   -- 索引有效

5. OR条件中有非索引列

-- 假设name有索引,status没有索引
SELECT * FROM user WHERE name = '张三' OR status = 1;  -- 整体不走索引

要解决这个问题,要么给status也加索引,要么改写成UNION:

SELECT * FROM user WHERE name = '张三'
UNION
SELECT * FROM user WHERE status = 1;

6. 使用NOT IN、NOT EXISTS、!=、<>

SELECT * FROM user WHERE id NOT IN (1, 2, 3);
SELECT * FROM user WHERE name != '张三';

这些操作可能导致优化器认为全表扫描更快,从而放弃索引。

7. IS NULL / IS NOT NULL(看情况)

如果表中NULL值很多,IS NOT NULL可能走索引;如果NULL值很少,IS NULL可能走索引。这个取决于数据分布和优化器的判断。

总结一句话:索引列要保持"干净",不要对它做任何加工,让它直接参与比较。

三、事务篇

面试官:说说事务的四大特性ACID,以及MySQL是如何实现的?

回答:

ACID是事务的四大特性,我来逐一说明:

A - 原子性(Atomicity)

原子性是指事务是一个不可分割的整体,要么全部成功,要么全部失败。

比如转账操作,A给B转100块:

这两步必须同时成功或同时失败,不能出现A扣了钱但B没收到的情况。

MySQL的实现方式:通过undo log(回滚日志) 来实现。在执行事务时,MySQL会把修改前的数据保存到undo log中。如果事务需要回滚,就用undo log中的数据恢复。

C - 一致性(Consistency)

一致性是指事务执行前后,数据库从一个一致状态变到另一个一致状态。比如转账前后,A和B的总金额应该不变。

MySQL的实现方式:一致性是事务的最终目标,是由其他三个特性(原子性、隔离性、持久性)共同保证的。

I - 隔离性(Isolation)

隔离性是指多个事务并发执行时,相互之间不能干扰。一个事务内部的操作对其他并发事务是隔离的。

MySQL的实现方式:通过MVCC(多版本并发控制) 来实现。

D - 持久性(Durability)

持久性是指事务一旦提交,对数据的修改就是永久的,即使系统崩溃也不会丢失。

MySQL的实现方式:通过redo log(重做日志) 来实现。事务提交时,先把修改写入redo log并刷盘,这样即使系统崩溃,重启后也能通过redo log恢复数据。

补充一下redo log和undo log的区别:

面试官:MySQL的事务隔离级别有哪些?分别能解决什么问题?

回答:

MySQL有四种事务隔离级别,从低到高分别是:

1. 读未提交(READ UNCOMMITTED)

这是最低的隔离级别,一个事务可以读取到另一个事务未提交的数据。

问题:会产生脏读。比如事务A读到了事务B修改但未提交的数据,如果B回滚了,A读到的就是无效的脏数据。

实际开发中基本不用这个级别。

2. 读已提交(READ COMMITTED)

一个事务只能读取到其他事务已提交的数据。

解决了:脏读问题
仍存在:不可重复读。就是在同一个事务中,两次读取同一条数据可能得到不同的结果,因为期间可能有其他事务修改并提交了。

Oracle数据库的默认隔离级别就是这个。

3. 可重复读(REPEATABLE READ)

在同一个事务中多次读取同一数据,结果是一致的。

解决了:脏读、不可重复读
仍存在:幻读。就是在同一事务中,两次查询的结果集行数不同,因为有其他事务插入或删除了数据。

但是,MySQL的InnoDB在这个级别通过Next-Key Lock(临键锁) 在很大程度上解决了幻读问题。

MySQL的默认隔离级别就是REPEATABLE READ。

4. 串行化(SERIALIZABLE)

最高的隔离级别,事务串行执行,完全隔离。

解决了:脏读、不可重复读、幻读
代价:性能最差,并发度最低

我用表格总结一下:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不会可能可能
REPEATABLE READ不会不会可能(InnoDB基本解决)
SERIALIZABLE不会不会不会

实际工作中的选择:

面试官:什么是MVCC?它是怎么实现的?

回答:

MVCC,全称Multi-Version Concurrency Control,多版本并发控制。它的核心思想是:通过保存数据的多个版本,让读操作和写操作不冲突,从而提高并发性能。

简单来说,当你读数据的时候,读的是某一个历史版本;当其他事务在写数据的时候,并不影响你的读取。这就是所谓的"快照读"。

MVCC的实现依赖三个核心组件:

1. 隐藏字段

InnoDB会为每行数据添加几个隐藏字段:

2. Undo Log(版本链)

每次修改数据时,旧版本会被保存到undo log中。多次修改就形成了一个版本链:

当前数据 → undo log版本1 → undo log版本2 → undo log版本3 → ...

通过DB_ROLL_PTR指针可以找到所有历史版本。

3. Read View(读视图)

当事务执行快照读(普通SELECT)时,会生成一个Read View,它包含:

Read View的可见性判断规则:

对于版本链中的某个版本,它的trx_id与Read View比较:

不同隔离级别下Read View的生成时机不同:

这就是为什么RC级别会有不可重复读,而RR级别可以保证可重复读的原因。

四、锁篇

面试官:MySQL中有哪些类型的锁?

回答:

MySQL的锁可以从多个维度来分类,我来详细说一下:

一、按锁的粒度分

1. 全局锁锁住整个数据库实例,使其处于只读状态。主要用于全库逻辑备份。

FLUSH TABLES WITH READ LOCK;  -- 加锁
UNLOCK TABLES;                -- 解锁

2. 表级锁锁住整张表,开销小、加锁快,但并发度低。包括:

3. 行级锁只锁住需要的行,开销大、加锁慢,但并发度高。InnoDB支持行级锁。

二、按锁的模式分

1. 共享锁(S锁/读锁)

SELECT ... LOCK IN SHARE MODE;  -- MySQL 5.x
SELECT ... FOR SHARE;           -- MySQL 8.0+

多个事务可以同时持有S锁,用于读取数据。

2. 排他锁(X锁/写锁)

SELECT ... FOR UPDATE;

只有一个事务能持有X锁,用于修改数据。INSERT/UPDATE/DELETE会自动加X锁。

三、InnoDB的行锁类型(重点)

1. Record Lock(记录锁)锁住索引中的一条记录。

2. Gap Lock(间隙锁)锁住索引记录之间的间隙,防止其他事务在间隙中插入数据。这是为了解决幻读问题。

比如表中有id为1、5、10的记录,间隙锁可以锁住(1,5)、(5,10)这些区间。

3. Next-Key Lock(临键锁)Record Lock + Gap Lock的组合,锁住一条记录以及它前面的间隙。

这是InnoDB在REPEATABLE READ级别下默认的行锁算法。

举个例子:假设表中有id为1、5、10的记录,执行:

SELECT * FROM t WHERE id = 5 FOR UPDATE;

在RR级别下,会加Next-Key Lock,锁住(1,5]这个范围。

面试官:什么是死锁?怎么避免和解决?

回答:

什么是死锁?

死锁是指两个或多个事务在执行过程中,因互相持有对方需要的锁而造成的一种阻塞现象。如果没有外力介入,这些事务都无法继续执行。

举个经典的例子:

-- 事务A
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 获得id=1的X锁
-- 此时事务A持有id=1的锁,等待id=2的锁

-- 事务B
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 2;  -- 获得id=2的X锁
UPDATE account SET balance = balance + 100 WHERE id = 1;  -- 等待id=1的锁

-- 事务A继续
UPDATE account SET balance = balance + 100 WHERE id = 2;  -- 等待id=2的锁
-- 死锁产生!A等B释放id=2,B等A释放id=1

如何检测和处理死锁?

InnoDB有两种策略:

1. 等待超时参数innodb_lock_wait_timeout,默认50秒。超时后事务会回滚。
缺点:等待时间长,业务响应慢。

2. 死锁检测(推荐)参数innodb_deadlock_detect=ON(默认开启)。
InnoDB会主动检测死锁,发现后立即回滚其中一个代价较小的事务。

如何避免死锁?

1. 按固定顺序访问资源所有业务代码都按照相同的顺序获取锁。比如都先锁id小的,再锁id大的。

2. 减小锁的粒度和持有时间

3. 使用合理的索引如果查询没有走索引,InnoDB会进行全表扫描并锁住所有行,更容易死锁。

4. 降低隔离级别如果业务允许,使用READ COMMITTED级别,没有Gap Lock,死锁概率降低。

5. 使用乐观锁代替悲观锁

-- 乐观锁方式
UPDATE product SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 10;

如何排查死锁?

-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS;

-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

面试官:说说乐观锁和悲观锁的区别,以及各自的使用场景

回答:

悲观锁

悲观锁的思想是:每次操作数据时都认为会有并发冲突,所以先加锁再操作。

实现方式:数据库的锁机制,如SELECT FOR UPDATE

START TRANSACTION;
-- 先加排他锁
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
-- 业务逻辑处理
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;

特点:

乐观锁

乐观锁的思想是:假设冲突很少发生,不加锁,而是在更新时检查数据是否被修改过。

实现方式:通常用版本号或时间戳

-- 1. 先查询数据和版本号
SELECT id, stock, version FROM product WHERE id = 1;
-- 假设返回 stock=100, version=1

-- 2. 更新时检查版本号
UPDATE product 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 1;

-- 3. 检查受影响行数
-- 如果是0,说明数据被其他事务修改过,需要重试

特点:

使用场景对比:

场景推荐使用原因
电商秒杀/库存扣减乐观锁读多写少,用户量大
银行转账悲观锁资金安全优先,写操作频繁
文章点赞/阅读数乐观锁允许少量误差,并发量大
订单状态流转悲观锁/分布式锁状态一致性要求高

CAS(Compare And Swap):

乐观锁的本质其实就是CAS思想:比较并交换。在Java中,Atomic类就是用CAS实现的。在数据库中,我们用version字段来模拟CAS操作。

五、SQL优化篇

面试官:如何定位和优化慢SQL?

回答:

这是一个很实际的问题,我按照实际工作中的流程来说:

第一步:开启慢查询日志,定位慢SQL

-- 查看慢查询日志是否开启
SHOW VARIABLES LIKE 'slow_query_log';

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;

-- 设置慢查询阈值,比如2秒
SET GLOBAL long_query_time = 2;

-- 查看慢查询日志文件位置
SHOW VARIABLES LIKE 'slow_query_log_file';

在生产环境中,也可以通过监控系统(如Prometheus+Grafana)来发现慢SQL。

第二步:使用EXPLAIN分析执行计划

EXPLAIN SELECT * FROM user WHERE username = 'zhangsan';

重点关注这几个字段:

第三步:针对性优化

1. 添加合适的索引

-- 为WHERE条件字段添加索引
CREATE INDEX idx_username ON user(username);

-- 为排序字段添加索引
CREATE INDEX idx_create_time ON user(create_time);

-- 联合索引
CREATE INDEX idx_status_create_time ON order(status, create_time);

2. 优化SQL写法

-- 避免SELECT *
SELECT id, username, email FROM user WHERE id = 1;

-- 避免函数操作索引列
-- 不好
SELECT * FROM user WHERE YEAR(create_time) = 2024;
-- 好
SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

-- 避免隐式类型转换
-- phone是varchar类型
-- 不好
SELECT * FROM user WHERE phone = 13800138000;
-- 好
SELECT * FROM user WHERE phone = '13800138000';

3. 分页优化

-- 深分页问题
SELECT * FROM order LIMIT 1000000, 10;  -- 需要扫描100万+10行

-- 优化方案:延迟关联
SELECT * FROM order 
WHERE id >= (SELECT id FROM order ORDER BY id LIMIT 1000000, 1) 
LIMIT 10;

-- 或者:记录上次查询的最后一个ID
SELECT * FROM order WHERE id > 1000000 ORDER BY id LIMIT 10;

4. 批量操作

-- 不好:循环单条插入
INSERT INTO user VALUES (1, 'a');
INSERT INTO user VALUES (2, 'b');

-- 好:批量插入
INSERT INTO user VALUES (1, 'a'), (2, 'b'), (3, 'c');

-- 大批量数据建议分批,每批500-1000条

5. 合理使用JOIN

-- 小表驱动大表
-- 如果department表小,用IN
SELECT * FROM employee WHERE dept_id IN (SELECT id FROM department);

-- 如果employee表小,用EXISTS
SELECT * FROM employee e 
WHERE EXISTS (SELECT 1 FROM department d WHERE d.id = e.dept_id);

-- JOIN时确保关联字段有索引
-- 控制JOIN的表数量,一般不超过3张

面试官:你在实际项目中做过哪些SQL优化?能举个具体的例子吗?

回答:

好的,我举一个之前处理过的真实案例。

背景:有一个订单列表查询接口,在数据量达到500万后,查询变得很慢,平均响应时间超过5秒。

原始SQL:

SELECT * FROM order 
WHERE user_id = 12345 
  AND status IN (1, 2, 3) 
  AND create_time >= '2024-01-01' 
ORDER BY create_time DESC 
LIMIT 0, 20;

排查过程:

优化方案:

第一步:添加联合索引

CREATE INDEX idx_user_status_time ON order(user_id, status, create_time);

添加后,type变成了range,rows从500万降到了几千。

**第二步:优化SELECT ***

-- 改成只查需要的字段
SELECT id, order_no, status, amount, create_time 
FROM order WHERE ...

第三步:考虑覆盖索引

因为查询的字段比较多,创建覆盖索引不太现实。但对于一些只需要少量字段的查询,可以考虑。

第四步:分页优化

原来的分页到后面页数时会很慢:

-- 原来的,第1000页时要扫描20000行
SELECT ... LIMIT 19980, 20;

-- 优化后,使用游标分页
SELECT ... WHERE id < 上一页最后一条的id ORDER BY id DESC LIMIT 20;

优化效果:

总结几个要点:

六、其他高频问题

面试官:MySQL主从复制的原理是什么?

回答:

MySQL主从复制是通过binlog来实现的,整个过程可以分为三个步骤:

第一步:Master记录binlog主库执行写操作时(INSERT/UPDATE/DELETE),会把变更记录到binlog(二进制日志)中。

第二步:从库IO线程读取binlog从库有一个IO线程,它会连接到主库,读取主库的binlog,然后写入到从库本地的relay log(中继日志)中。

第三步:从库SQL线程执行relay log从库还有一个SQL线程,它会读取relay log中的事件,在从库上重新执行一遍,从而实现数据同步。

整个流程:

Master写数据 → 写入binlog → 从库IO线程读取 → 写入relay log → 从库SQL线程执行 → 数据同步

binlog的三种格式:

推荐使用ROW格式,虽然日志量大,但数据一致性有保证。

主从延迟的原因和解决方案:

原因:

解决方案:

面试官:如何保证MySQL和Redis缓存的数据一致性?

回答:

这是一个经典问题,我来说说常见的几种策略:

策略一:Cache Aside Pattern(旁路缓存模式)

这是最常用的策略:

// 读取
public User getUser(Long id) {
    User user = redis.get("user:" + id);
    if (user == null) {
        user = db.getUser(id);
        redis.set("user:" + id, user);
    }
    return user;
}

// 写入
public void updateUser(User user) {
    db.update(user);
    redis.delete("user:" + user.getId());
}

为什么是删除缓存而不是更新缓存?

为什么是先更新DB再删缓存,而不是先删缓存再更新DB?

策略一的问题:即使先更新DB再删缓存,极端情况下仍可能不一致:

不过这种情况概率很低,因为写操作通常比读操作慢。

策略二:延迟双删

为了解决上面的极端情况:

public void updateUser(User user) {
    redis.delete("user:" + user.getId());  // 第一次删除
    db.update(user);
    Thread.sleep(500);  // 等待一段时间
    redis.delete("user:" + user.getId());  // 第二次删除
}

延迟时间要大于一次读操作的时间。

策略三:异步更新缓存(基于消息队列或binlog)

写操作 → 更新DB → 发送消息/监听binlog → 异步更新/删除缓存

使用Canal监听binlog的方式更可靠,缺点是有一定延迟。

总结:

面试官:最后一个问题,你觉得一个合格的索引应该怎么设计?

回答:

这个问题很好,我总结一下索引设计的核心原则:

1. 选择合适的字段建索引

2. 联合索引的设计原则

-- 假设查询条件是:WHERE status = 1 AND type = 2 AND create_time > '2024-01-01'
-- 好的设计:idx_status_type_time(status, type, create_time)
-- 等值查询在前,范围查询在后

3. 避免过多索引

4. 主键索引的设计

5. 避免冗余索引

-- 冗余
INDEX idx_a (a)
INDEX idx_a_b (a, b)  -- idx_a是冗余的,idx_a_b可以覆盖它

-- 不冗余
INDEX idx_a_b (a, b)
INDEX idx_b_a (b, a)  -- 顺序不同,不冗余

6. 使用前缀索引压缩长字段

-- 对于很长的字符串字段
CREATE INDEX idx_email ON user(email(10));  -- 只索引前10个字符

7. 实践建议

总结 

到此这篇关于MySQL存粹问题面试准备总结的文章就介绍到这了,更多相关MySQL面试准备内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文