🛢 MySQL+MyBatis 优化面试10题

阅读量关键词: EXPLAIN执行计划 | 索引失效场景 | MyBatis N+1 | 慢SQL定位 | 批量优化 | 分库分表

Q1: EXPLAIN执行计划怎么看?type字段优先级是什么?

标准答案:
EXPLAIN是MySQL性能诊断的瑞士军刀。核心关注4个字段:

type(访问类型):system > const > eq_ref > ref > range > index > ALL
key:实际使用的索引名
rows:预估扫描行数
Extra:额外信息——Using filesort(需优化)、Using temporary(严重)、Using index(覆盖索引,好)

EXPLAIN SELECT * FROM orders WHERE user_id=100 AND status='paid';
-- type: ref  ✅
-- key: idx_user_status  ✅
-- rows: 10  ✅

Q2: MySQL索引失效的10种场景?

背这10条,面试够用了:
① LIKE '%keyword' — 左模糊
② 联合索引不满足最左前缀
③ WHERE子句中对索引列使用函数 WHERE YEAR(date)=2026
④ 隐式类型转换 WHERE phone=13800138000(phone是varchar)
⑤ OR条件中有非索引列
⑥ != 或 <> 操作符
⑦ IS NULL / IS NOT NULL(视数据分布)
⑧ 范围查询右边的列(联合索引 a_b_c,WHERE a=1 AND b>2 AND c=3 → c不走索引)
⑨ 不使用索引列做计算 WHERE age+1=20
⑩ 全表扫描更快时MySQL自动放弃索引(数据量小时)

Q3: 怎么定位和优化一条慢SQL?

四步法:
开启慢查询日志 SET GLOBAL slow_query_log=ON; SET long_query_time=0.5;
抓出慢SQL mysqldumpslow -s t -t 10 slow.log
EXPLAIN分析 看type/rows/Extra
针对性优化:加索引→改SQL→拆表→加缓存

举例:发现 SELECT * FROM order WHERE status='pending' ORDER BY create_time 走了 filesort→加联合索引 idx_status_time(status, create_time) → rows从50万降到200,耗时从3s降到50ms。

Q4: MyBatis怎么解决N+1查询问题?

N+1场景:查订单列表→循环查每个订单的明细→100条订单发101条SQL。

三种解决方案:
嵌套查询+延迟加载: <association fetchType="lazy"> — 需要时再查,减少首屏SQL
JOIN直接查: 一条SQL LEFT JOIN搞定,适合关联数据少的场景
批量查询(推荐): 先查订单→收集所有order_id→一次IN查所有明细→Java层组装


    

Q5: MyBatis #{}和${}区别?为什么SQL注入?

核心区别:
#{}:预编译占位符 → SELECT * FROM user WHERE id=? → 防注入 ✅
${}:字符串拼接 → SELECT * FROM user WHERE id=1 OR 1=1 → 危险 ❌

${}何时用? 动态表名、ORDER BY字段名——必须用且必须做白名单校验。
// ❌ 危险
SELECT * FROM ${tableName} WHERE id=#{id}
// 攻击者传 tableName="user; DROP TABLE user--"

// ✅ 安全:白名单校验
if (!ALLOWED_TABLES.contains(tableName)) throw...

Q6: MyBatis批量插入怎么优化?

三种方案,性能递增:
foreach拼接(推荐500条内)
INSERT INTO user(name,age) VALUES

    (#{u.name}, #{u.age})
ExecutorType.BATCH 批量提交 — 适合超大量,MyBatis内置
原生JDBC addBatch — 终极方案,绕过MyBatis一层开销

实测数据:10000条插入→foreach 8s → BATCH 2s → JDBC addBatch 1.2s

Q7: MySQL事务隔离级别有哪些?MVCC怎么实现的?

4个隔离级别: READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ(InnoDB默认) → SERIALIZABLE

MVCC核心原理:
• 每行数据隐藏列:DB_TRX_ID(最后修改事务ID) + DB_ROLL_PTR(回滚指针指向上一个版本)
• Undo Log版本链:每次更新产生旧版本,串联成链
• ReadView快照:事务开始时生成,根据事务ID判断可见性
• RC级别:每次SELECT重新生成ReadView
• RR级别:事务内第一次SELECT生成ReadView,之后复用

Q8: 分库分表怎么做?常用方案对比?

分片策略:
垂直拆分: 按业务模块(订单库/用户库)— 简单,但单表仍可能大
水平拆分: 按哈希(user_id%16)或范围(按时间)拆多张表

常用中间件:
ShardingSphere-JDBC: 客户端分片,无代理层,性能好
MyCat: 代理层,跨语言支持但多一跳
Vitess: Google开源,适合超大规模

核心问题: 分布式ID(雪花算法)、跨库事务(Seata)、跨库JOIN(应用层聚合)

Q9: 覆盖索引是什么?怎么利用它优化?

覆盖索引: SELECT的列都在索引中,不需要回表查聚簇索引。

-- ❌ 需要回表(name不在索引里)
CREATE INDEX idx_age ON user(age);
SELECT name FROM user WHERE age=20;  -- 还要去主键索引找name

-- ✅ 覆盖索引
CREATE INDEX idx_age_name ON user(age, name);
SELECT name FROM user WHERE age=20;  -- Extra: Using index  ✅
实战技巧:EXPLAIN 看 Extra 列是否出现 Using index。高频查询尽量设计覆盖索引。

Q10: 千万级大表怎么平滑迁移?

四步平滑迁移法:
双写阶段: 新老表同时写,读老表
历史数据搬迁: 分批次迁移(每次1000条),不锁表
校验数据: 逐条对比或抽样校验一致性
切读: 灰度切流(1%→10%→100%),有问题随时回滚

不会丢数据: 双写+定时对账+消息队列重试。面试说出这4步就已经是专家水平。

📊 面试模拟器 | 📝 简历生成器 | 🔍 简历诊断 | 📚 更多文章