java八股文-MySql面试题-参考回答
一. 面试官:MySQL中,如何定位慢查询?
- 有无运维监控系统?例如:Skywalking 、Prometheus
- 有监控系统:
直接利用监控系统进行分析
例如:公司 Prometheus + Grafana 看板里,有 “InnoDB Row Lock Time”、“Innodb_rows_read”、“QPS/RT” 曲线。实时监控慢查询数量及趋势。
无监控系统:使用mysql自带的慢日志查询的功能
- 启用慢查询日志
- 分析慢查询日志
- 实时监控正在执行的查询
- 使用
EXPLAIN
分析执行计划(开始分析)- 线上实时抓包(应急阶段)
- 总结:最终确认线上环境慢查询问题,可以通过 监控系统+mysql慢查询日志 快速定位问题。
- 参考回答:
嗯~,我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统Skywalking ,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体的执行时间,所以可以定位是哪个sql出了问题
如果,项目中没有这种运维的监控系统,其实在MySQL中也提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置的是2秒,只要SQL执行的时间超过了2秒就会记录到日志文件中,我们就可以在日志文件找到执行比较慢的SQL了。
1. 启用慢查询日志
慢查询日志是记录执行时间超过阈值的 SQL 语句的核心工具。超过了某个时间就会记录到日志文件中
步骤:
- 动态开启(无需重启):
SET GLOBAL slow_query_log = 'ON'; -- 启用慢查询日志 SET GLOBAL long_query_time = 1; -- 设置慢查询阈值(单位秒) SET GLOBAL slow_query_log_file = '/path/to/slow.log'; -- 指定日志文件路径 SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录未使用索引的查询
- 配置文件永久生效(需重启): 修改
my.cnf
或my.ini
:[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1
2. 分析慢查询日志(可选)
工具推荐:
mysqldumpslow
(官方工具):- 按执行时间排序前 10 条:
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
- 搜索特定关键词(如
LEFT JOIN
):mysqldumpslow -g "LEFT JOIN" slow.log
- 按执行时间排序前 10 条:
pt-query-digest
(Percona Toolkit):- 生成详细报告:
pt-query-digest /var/log/mysql/slow.log > report.txt
- 生成详细报告:
手动查看日志: 日志格式示例:
# Time: 2025-08-10T14:34:31.000000Z # User@Host: root[root] @ localhost [] # Query_time: 3.123456 Lock_time: 0.000000 Rows_sent: 10 Rows_examined: 1000 SET timestamp=1723298071; SELECT * FROM users WHERE id = 1;
关键指标:
Query_time
:查询耗时,超过阈值则为慢查询。Rows_examined
:扫描的行数,高值可能表示索引缺失。Rows_sent
:返回的行数,与扫描行数差异过大可能需要优化。
3. 实时监控正在执行的查询
SHOW PROCESSLIST
:
- 查看当前正在执行的线程:
SHOW FULL PROCESSLIST;
- 重点关注
Time
(执行时间)和State
(执行状态)列。 - 若某查询长时间处于
Sending data
或Copying to tmp table
等状态,可能是慢查询。
- 重点关注
4. 使用 EXPLAIN
分析执行计划
对定位到的慢查询,使用 EXPLAIN
查看执行计划:
EXPLAIN SELECT * FROM users WHERE phone = '13800138000';
- 关键字段解释:
type
:访问类型(ALL
表示全表扫描,需优化;ref
或range
更优)。key
:使用的索引,若为NULL
表示未命中索引。rows
:预估扫描的行数,越少越好。rows
估算值与真实差两个数量级(统计信息过期)Extra
:额外信息(如Using filesort
或Using temporary
表示需优化)。
如果怀疑索引选择错误,再跑
EXPLAIN ANALYZE
(MySQL 8.0)或optimizer trace
,看优化器到底怎么选。
5. 使用性能分析工具
SHOW PROFILE
:
- 查看查询的详细资源消耗:
SET profiling = 1; -- 开启性能分析 -- 执行待分析的查询 SHOW PROFILES; -- 查看所有查询的耗时 SHOW PROFILE FOR QUERY 1; -- 查看具体查询的详细耗时
- 关注
CPU
、Context switches
、Memory used
等指标。
- 关注
Performance Schema(MySQL 5.6+):
- 启用后可获取更细粒度的性能数据:
SELECT * FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;
6. 第三方工具辅助
- MySQL Workbench:图形化界面分析慢查询日志。
- Percona Toolkit:
pt-online-schema-change
可在线优化表结构,减少锁表时间。 - 慢查询监控系统:如 Prometheus + Grafana,实时监控慢查询数量及趋势。
二. 面试官:那这个SQL语句执行很慢, 如何分析呢?
- 参考回答:
- 如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,
- 第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描
- 第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
- 使用
EXPLAIN
分析执行计划- 使用
SHOW PROFILE
分析SQL耗时- 优化索引与查询:添加合适索引、减少全表扫描、避免复杂查询。
- 持续监控:通过工具实时监控数据库性能,及时调整策略
1. 使用 EXPLAIN
分析执行计划
- 作用:查看SQL语句的执行计划,判断是否命中索引、是否存在全表扫描等问题。
- 操作步骤:
- 在SQL语句前添加
EXPLAIN
:EXPLAIN SELECT name, age FROM users WHERE name = 'Alice';+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+ | 1 | SIMPLE | users | NULL | ref | idx_name_age | idx_name_age| 50 | const | 1 | 100.00 | Using index | +----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
- 关键字段解读:
- type:连接类型(从好到差):
const
/eq_ref
/ref
/range
(使用索引)。index
(全索引扫描)。ALL
(全表扫描,需优化)。
- key:实际使用的索引。
- rows:预估扫描的行数(越小越好)。
- Extra:额外信息(如
Using filesort
、Using temporary
等低效操作)。Using filesort
表示 MySQL 需要对结果集进行额外的排序操作,且无法通过索引直接获取有序数据。这种排序可能发生在内存中(小数据量)或磁盘上(大数据量),因此称为“文件排序”Using temporary
表示 MySQL 在执行查询时,需要创建一个临时表(Memory 或磁盘)来存储中间结果。通常出现在涉及GROUP BY
、DISTINCT
、UNION
、ORDER BY
等操作时。
- type:连接类型(从好到差):
- 优化方向:
- 若
type
为ALL
,需添加合适的索引。 - 若
key
为NULL
,说明未使用索引。 - 若出现
Using filesort
,需优化ORDER BY
或添加索引。 - 若出现
Using temporary
,通过添加覆盖索引或联合索引、减少查询字段、优化GROUP BY
/ORDER BY
逻辑、调整临时表参数(如tmp_table_size
),以及拆分复杂查询
- 若
- 在SQL语句前添加
2. 使用 SHOW PROFILE
分析SQL耗时
- 作用:查看SQL执行的各个阶段耗时(如解析、优化、执行等)。
- 操作步骤:
- 开启 profiling:
SET profiling = 1;
- 执行慢SQL:
SELECT * FROM orders WHERE user_id = 123;
- 查看执行耗时:
SHOW PROFILES; -- 查看所有SQL的耗时 SHOW PROFILE FOR QUERY 1; -- 查看具体SQL的详细耗时
- 分析结果:
- 找出耗时最长的阶段(如
Copying to tmp table
、Sending data
),针对性优化。
- 找出耗时最长的阶段(如
- 开启 profiling:
3 优化索引
- 常见问题:
- 缺少索引:导致全表扫描。
- 索引冗余:增加写入开销。
- 索引未覆盖查询字段(未使用覆盖索引)。
- 优化策略:
- 添加合适索引:
- 对
WHERE
、JOIN
、ORDER BY
、GROUP BY
的字段添加索引。 - 使用复合索引(多列索引),注意字段顺序。
- 对
- 避免冗余索引:
- 删除重复或不必要的索引。
- 使用覆盖索引:
- 查询字段全部包含在索引中,避免回表查询。
CREATE INDEX idx_user_age ON users (age, name); SELECT name FROM users WHERE age > 30; -- 覆盖索引
- 添加合适索引:
4. 优化SQL语句
- 常见问题:
- 使用
SELECT *
(获取多余字段)。 - 使用
OR
连接条件(可能失效索引)。 - 复杂的子查询或嵌套视图。
- 使用
- 优化策略:
- 避免
SELECT *
:- 只选择需要的字段,减少数据传输。
SELECT id, name FROM users WHERE age > 30;
- 替换
OR
为UNION ALL
:- 避免索引失效。
SELECT * FROM users WHERE id = 1 UNION ALL SELECT * FROM users WHERE age > 30;
- 减少复杂查询:
- 拆分大查询为多个小查询。
- 避免过多表连接(
JOIN
表数量建议不超过3张)。
- 分页优化:
- 大数据量分页时,避免使用
LIMIT offset, size
,改用游标分页。
SELECT * FROM users WHERE id > 1000 ORDER BY id LIMIT 10;
- 大数据量分页时,避免使用
- 避免
三. 面试官:了解过索引吗?(什么是索引)
- 参考回答:
- 索引在项目中还是比较常见的,它是帮助MySQL高效获取数据的数据结构,
- 主要是用来提高数据检索的效率,
- 降低数据库的IO成本,
- 同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了CPU的消耗
- 简单来说就是:加速 查找(WHERE / JOIN)
- 加速 排序(ORDER BY 走索引即免排序)
- 加速 分组/去重(GROUP BY / DISTINCT)
- 额外福利:唯一索引天然约束;覆盖索引可避免回表。
- 总结:
- “排好序、可复制、可持久化的数据结构,让 MySQL 用 O(log n) 甚至更小的代价找到需要的行,而不是 O(n) 去扫全表。”
- 索引就是拿空间换时间,让查询从“翻整本书”变成“直接翻目录”。
四. 面试官:索引的底层数据结构了解过嘛 ?
- 参考回答:
- MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,
- 选择B+树的主要的原因是:
1. 更高的查询效
B+树的阶数更高 → 每个节点存储更多键值 → 树高更低(层级更少,路径更短) → 减少磁盘I/O次数。
非叶子节点存储键值和指向子节点的指针,叶子节点存储数据或指向数据的指针。
- 2. 支持范围查询与排序
B+树的叶子节点通过 双向链表 连接,便于范围查询和排序操作。
- 3. 叶子节点—聚簇索引(主键索引)
叶子节点存储 完整的行数据(主键 + 所有字段),按主键顺序组织。
主键查询无需回表,效率最高
- 4. 叶子节点—非聚簇索引(二级索引)
叶子节点存储 索引列值 + 对应的主键值。
查询时需通过主键值 回表 获取完整数据(若查询字段包含在索引中,可避免回表,称为 覆盖索引)。
- 示例: 对 `users(name)` 创建非聚簇索引,查询 `WHERE name = 'Alice'`:
1. 通过 `name` 索引找到主键值(如 `id=1`)。
2. 通过主键值回表到聚簇索引中获取完整数据。
3. 若查询字段仅包含 `name` 和 `age`,且索引为 `(name, age)`,则可直接通过索引返回数据(覆盖索引)。
1. 为什么选择 B + 树?
索引的核心目标是 加速查询效率,需要平衡 “查询速度” 和 “维护成本”(如插入、删除时的结构调整)。常见的数据结构中:
- 哈希表:适合等值查询(O (1)),但无法支持范围查询(如
WHERE age > 30
),且哈希冲突会增加复杂度。 - 二叉搜索树(BST):极端情况下会退化为链表(如有序插入时),查询效率降至 O (n)。
- 平衡二叉树(AVL / 红黑树):虽能保持平衡,但树高较高(假设 100 万数据,树高约 20),磁盘 IO 次数多(每次访问节点对应一次 IO)。
- B 树:多路平衡查找树,降低树高,但每个节点存储数据,叶子节点不连续,范围查询需回溯,效率低。
- B + 树 是 B 树的优化版本,专为磁盘存储设计,完美适配 MySQL 的查询场景,成为主流选择。
2. B + 树的结构与核心特性
B + 树是一种 多路平衡查找树,其结构和特性专为索引设计优化:
树结构分层:
- 非叶子节点:仅存储 索引键 和 子节点指针,不存储具体数据(减少节点大小,提高扇出率)。
- 叶子节点:存储 完整索引键 和 数据地址(InnoDB 中叶子节点直接存储数据行,即 “聚簇索引”)。
- 所有叶子节点通过 双向链表 连接,便于范围查询(如
BETWEEN ... AND ...
)。
多路性(高扇出率):
- 每个节点可存储多个索引键(取决于键的大小和页大小)。MySQL 中数据页默认大小为 16KB,假设每个索引键(如 int 类型)占 4 字节,指针占 8 字节,则一个非叶子节点可存储约 16KB/(4+8) ≈ 1365 个键,即每个节点可指向 1366 个子节点。
- 高扇出率使树高极低(例如:1000 万数据,树高仅 3 层),查询时只需 3 次磁盘 IO,效率极高。
平衡性:
- 插入 / 删除时通过 分裂 或 合并 节点保持树的平衡,确保查询效率稳定在 O (log n)。
3. InnoDB 中的索引实现(基于 B + 树)
InnoDB 是 MySQL 最常用的存储引擎,其索引实现与 B + 树深度结合,核心分为两类:
聚簇索引(Clustered Index):
- 定义:以主键(PRIMARY KEY)为索引键的 B + 树,叶子节点直接存储 完整数据行。
- 特性:
- 每张表只有一个聚簇索引(由主键决定,若无主键则选唯一索引,否则隐式生成行 ID)。
- 查询时若能通过聚簇索引定位,可直接获取数据,无需回表。
- 示例:查询
SELECT * FROM user WHERE id = 100
,通过聚簇索引的 B + 树找到叶子节点,直接读取数据行。
二级索引(Secondary Index,非聚簇索引):
- 定义:以非主键字段为索引键的 B + 树,叶子节点存储的是 主键值(而非数据行)。
- 特性:
- 一张表可有多个二级索引(如
INDEX (name)
、INDEX (age)
)。 - 查询时需先通过二级索引找到主键值,再通过聚簇索引查询数据(称为 “回表”)。
- 一张表可有多个二级索引(如
- 示例:查询
SELECT * FROM user WHERE name = '张三'
,流程为:- 通过
name
二级索引的 B + 树找到对应的主键值(如 id=200)。 - 再通过聚簇索引(id=200)找到完整数据行(回表操作)。
- 通过
4. B + 树索引的优势总结
- 高效查询:低树高 + 高扇出率,减少磁盘 IO 次数,支持等值查询和范围查询。
- 范围查询友好:叶子节点双向链表,可快速遍历连续数据(如
ORDER BY
、GROUP BY
)。 - 稳定性强:平衡结构确保插入 / 删除后查询效率稳定,无极端退化情况。
- 适配磁盘存储:节点大小与磁盘页(16KB)匹配,减少碎片化,提高 IO 效率。
五. 面试官:B树和B+树的区别是什么呢?
B+ 树是对 B 树的优化,尤其适合磁盘存储和范围查询,这也是现代数据库索引普遍采用 B+ 树的原因。”
特性 | B树 | B+树 |
---|---|---|
数据存储位置 | 所有节点存储数据 | 只有叶子节点存储数据 |
叶子节点链接 | 无链接 | 双向链表链接 |
查询效率 | 可能提前命中(路径短) | 路径固定(稳定) |
范围查询 | 效率低 | 高效(利用链表) |
树高 | 较高(存储数据少) | 较低(存储键值多) |
磁盘I/O | 较多 | 较少 |
应用场景 | 单条记录查询 | 范围查询、排序、数据库索引 |
1. 数据存储位置
B树:
- 所有节点(包括内部节点和叶子节点)都存储数据(键值和实际记录)。
- 示例:B树的非叶子节点可能同时存储键和数据,查找时可能在非叶子节点直接命中。
B+树:
- 只有叶子节点存储数据,非叶子节点仅存储键值(用于索引)。
- 示例:B+树的非叶子节点仅存储键值和子节点指针,数据全部集中在叶子节点。
2. 叶子节点结构
B树:
- 叶子节点之间无链接,无法高效支持范围查询。
B+树:
- 叶子节点通过双向链表连接,形成有序链表,支持高效的范围查询和顺序遍历。
- 示例:查找
WHERE id BETWEEN 10 AND 100
时,B+树可以直接沿着叶子链表顺序扫描,而B树需要递归访问父节点。
3. 查询效率
B树:
- 查找可能在非叶子节点直接命中数据,减少I/O次数。
- 但查询路径长度不固定(可能提前结束)。
B+树:
- 所有查询必须遍历到叶子节点,路径长度固定(等于树高)。
- 优势:查询性能更稳定,适合高并发场景。
4. 范围查询与排序
B树:
- 范围查询效率较低,需多次回溯父节点。
B+树:
- 范围查询高效,利用叶子节点链表顺序访问。
- 排序操作天然支持,无需额外处理。
5. 树的高度与磁盘I/O
B树:
- 内部节点存储数据,导致每个节点能存储的键值较少,树高可能较高。
- 磁盘I/O次数较多(树高较高)。
B+树:
- 非叶子节点仅存储键值,可容纳更多键值,树高更低。
- 磁盘I/O次数更少(树高更低),性能更优。
6. 插入与删除操作
B树:
- 插入和删除可能影响非叶子节点的数据,调整复杂度较高。
B+树:
- 插入和删除仅影响叶子节点和内部节点的键值,操作更简单且稳定。
7. 应用场景
B树:
- 适用于单条记录的快速查找(如
WHERE id = 10
)。
- 适用于单条记录的快速查找(如
B+树:
- 更适合数据库索引(如 MySQL 的 InnoDB 存储引擎),尤其是范围查询(
BETWEEN
)、排序(ORDER BY
)和全表扫描。
- 更适合数据库索引(如 MySQL 的 InnoDB 存储引擎),尤其是范围查询(
六. 面试官:什么是聚簇索引什么是非聚簇索引 ?
1. 一句话定义
聚簇索引(主键索引):数据行和索引放在同一棵 B+ 树里,叶子节点就是整行数据。
非聚簇索引(自定义索引、二级索引):索引树和数据是分离的,叶子节点只存“索引键 + 指向数据行的指针(主键的key)”。
2. 对比维度(面试表格答法)
维度 | 聚簇索引 | 非聚簇索引(二级索引) |
---|---|---|
索引即数据 | ✅ 是 | ❌ 否,需回表 |
叶子节点内容 | 完整的行记录 | 索引列值 + 主键值(InnoDB) |
一张表个数 | 最多 1 个(通常是主键) | 可以有多个 |
物理顺序 | 表数据按主键顺序物理存储 | 与数据物理顺序无关 |
回表 | 不需要 | 需要(通过主键再查聚簇索引) |
范围/排序 | 主键范围查询很快 | 需要回表,额外 IO |
典型实现 | InnoDB 的主键 | InnoDB 的普通索引、唯一索引 |
3. 举例(把抽象变具体)
“假设有表
user(id PK, name, age)
聚簇索引:主键
id
这棵树,叶子节点就是id=1
、id=2
… 的整行数据。非聚簇索引:在
name
上建索引,叶子节点存name→id
的映射;查到name='Tom'
后,还要用主键id
回表拿其余列。”
4. 性能提示(加分项)
聚簇索引
插入顺序最好按主键递增,否则会导致页分裂(UUID 主键性能差)。非聚簇索引
覆盖索引(SELECT
的列全在索引里)可避免回表,性能接近聚簇索引。
5. 一句话总结
- “聚簇索引可以叫他主键索引,就是‘索引即数据’,一张表只能有一个;
- 非聚簇索引可以叫他二级索引或者自定义索引,是‘另建目录’,查到后常需回表,但可建多个,配合覆盖索引可提速。”
七. 面试官:知道什么是回表查询嘛?
参考回答:
- 回表查询是指在使用非聚簇索引(如普通索引)查询时,由于索引中不包含所有查询字段,需要通过主键值再次访问聚簇索引(主键索引)获取完整数据的过程。
- 例如,在
SELECT name, email FROM users WHERE name = 'Alice'
中,若name
的索引中,则会触发回表。- 如何避免? 可以通过覆盖索引(如创建
(name, email)
复合索引),使查询字段全部包含在索引中,从而避免回表。此外,减少查询字段、合理设计索引也能优化性能。
1. 一句话定义
回表查询就是:通过非聚簇索引拿到主键后,再回到聚簇索引里把整行数据捞出来的那一次额外 IO。
2. 举个例子
-- 表结构
user(id PK, name, age)
-- 索引
idx_name(name) -- 非聚簇索引-- SQL
SELECT age FROM user WHERE name = 'Tom';
先走
idx_name
B+ 树 → 找到name='Tom'
的叶子节点,拿到主键id = 7
;再用
id = 7
回到 聚簇索引 → 找到整行数据 → 取出age
;
第 2 步就是“回表”。
3. 面试加分点
如何避免回表?
建立“覆盖索引”:把查询列全放到联合索引里,例如(name, age)
,InnoDB 直接在二级索引叶子节点拿到age
,无需回表。性能影响?
回表 ≈ 一次随机 IO,高并发场景下覆盖索引可把 RT(响应时间)从毫秒级降到百微秒级。
4. 一句话总结(收尾)
- “回表就是非聚簇索引查到主键后,再回聚簇索引取整行的二次查询;
- 覆盖索引可以干掉回表,提升查询性能。”
八. 面试官:知道什么叫覆盖索引嘛?
参考回答:
- 覆盖索引:是指查询所需的所有字段都包含在某个索引中,使得数据库可以直接从索引中提取数据,无需回表获取完整数据行。
- 例如,在
SELECT name, age FROM users WHERE name = 'Alice'
中,若创建了复合索引(name, age)
,则可直接从索引中获取name
和age
,无需访问聚簇索引进行回表查询。- 优势
- 减少 I/O 开销、
- 提高查询速度,
- 并支持范围查询和排序。
- 验证方法:是通过
EXPLAIN
查看Extra
字段是否为Using index
。- 设计原则:是覆盖查询字段和条件字段,并遵循最左前缀原则。实际开发中,合理设计覆盖索引可显著优化高频查询性能。尽量避免使用select *,尽量在返回的列中都包含添加索引的字段。
1. 覆盖索引的定义
- 覆盖索引是指:索引本身包含了查询语句中所有需要的字段。
- 执行查询时,数据库可以直接从索引中提取所需数据,无需访问聚簇索引(主键索引)或数据行。
- 典型场景:查询字段被索引覆盖,且查询条件字段也在索引中。
示例
假设表 users
的结构如下:
CREATE TABLE users (id INT PRIMARY KEY, -- 主键(聚簇索引)name VARCHAR(50), -- 普通字段age INT -- 普通字段
);
- 非覆盖索引:
CREATE INDEX idx_name ON users (name); SELECT name, age FROM users WHERE name = 'Alice';
- 索引
idx_name
仅包含name
字段,需通过主键回表获取age
字段(低效)。
- 索引
- 覆盖索引:
CREATE INDEX idx_name_age ON users (name, age); SELECT name, age FROM users WHERE name = 'Alice';
- 索引
idx_name_age
同时包含name
和age
字段,无需回表(高效)。
- 索引
2. 覆盖索引的工作原理
2.1 回表查询 vs 覆盖索引
- 传统查询路径(需回表):
- 通过非聚簇索引(如
idx_name
)找到主键值id
。 - 通过主键值
id
回表到聚簇索引(主键索引)获取完整数据行。
- 通过非聚簇索引(如
- 覆盖索引路径(无需回表):
- 通过复合索引(如
idx_name_age
)直接获取所有查询字段(name
,age
),无需访问聚簇索引。
- 通过复合索引(如
2.2 InnoDB 的索引结构
- 聚簇索引(主键索引):
- 叶子节点存储完整数据行(
id
,name
,age
)。
- 叶子节点存储完整数据行(
- 非聚簇索引(二级索引):
- 叶子节点存储索引字段值 + 主键值(如
name
+id
)。 - 若查询字段未包含在索引中,需通过主键回表到聚簇索引获取数据。
- 叶子节点存储索引字段值 + 主键值(如
3. 覆盖索引的优势
优势 | 说明 |
---|---|
减少 I/O 开销 | 直接从索引读取数据,避免回表操作(减少 50%-90% 的磁盘 I/O)。 |
提高查询速度 | 索引通常比数据行小,遍历更快;且无需解析数据行字段。 |
减少内存和 CPU 使用 | 减少数据页加载量,降低解析和映射字段的开销。 |
支持排序和范围查询 | 复合索引的字段顺序可优化 ORDER BY 和 WHERE 条件(如范围查询)。 |
4. 覆盖索引的适用场景
典型场景
场景 | 示例 |
---|---|
高频查询字段 | SELECT name, age FROM users WHERE name = 'Alice'; (name 和 age 在索引中)。 |
分页查询优化 | SELECT id, name FROM users ORDER BY name LIMIT 10000, 10; (name 和 id 在索引中)。 |
聚合查询 | SELECT COUNT(*) FROM users WHERE status = 'active'; (status 在索引中)。 |
避免回表的联合查询 | SELECT user_id, order_time FROM orders WHERE user_id = 1001; (user_id 和 order_time 在索引中)。 |
实现方式
- 创建复合索引:将查询条件字段和结果字段组合到一个索引中。
CREATE INDEX idx_user_status_time ON orders (user_id, status, order_time); SELECT user_id, order_time FROM orders WHERE user_id = 1001 AND status = 'paid';
5. 如何验证覆盖索引是否生效?
使用 EXPLAIN
分析
- 关键字段:
Extra
列:若显示Using index
,表示使用了覆盖索引。key
列:显示使用的索引名称。type
列:显示索引访问类型(如ref
、range
)。
示例
EXPLAIN SELECT name, age FROM users WHERE name = 'Alice';
- 输出示例:
+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+ | 1 | SIMPLE | users | NULL | ref | idx_name_age | idx_name_age| 50 | const | 1 | 100.00 | Using index | +----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
- 关键点:
Extra
列为Using index
,表示覆盖索引生效。
6. 覆盖索引的设计原则
核心策略
- 字段覆盖:
- 查询字段 + 查询条件字段需全部包含在索引中。
- 示例:
SELECT a, b FROM t WHERE a = 1
→ 索引(a, b)
。
- 最左前缀原则:
- 复合索引需按字段顺序使用前缀(如
(a, b, c)
可用于WHERE a=1
或WHERE a=1 AND b=2
,但不能跳过中间字段)。
- 复合索引需按字段顺序使用前缀(如
- 避免过度设计:
- 索引字段过多会增加存储开销和写入成本,需权衡读写性能。
反例
-- 错误:查询字段未覆盖索引,需回表
CREATE INDEX idx_name ON users (name);
SELECT name, age FROM users WHERE name = 'Alice';-- 正确:查询字段覆盖索引,无需回表
CREATE INDEX idx_name_age ON users (name, age);
SELECT name, age FROM users WHERE name = 'Alice';
7. 覆盖索引的局限性
限制 | 说明 |
---|---|
仅适用于 B+ 树索引 | 哈希索引、全文索引不支持覆盖索引。 |
索引字段过长 | 多个长字符串字段可能导致索引体积过大,影响效率。 |
需结合业务查询设计 | 单一查询的专用索引可能冗余,需根据高频查询优化。 |
九. 面试官:MYSQL超大分页怎么处理 ?
参考回答:
MySQL超大分页的核心问题是
LIMIT offset, size
在大偏移量时需扫描并丢弃大量数据,导致性能下降。优化方法包括:
- 游标分页:通过记录上一页最后一条的排序字段值,直接定位下一页起点(如
WHERE id > last_id
),避免扫描丢弃数据。- 覆盖索引:查询字段被索引覆盖,减少回表操作(如
SELECT name FROM users
使用(name, age)
索引)。- 延迟关联:先查主键ID,再通过主键回表(如
SELECT * FROM table WHERE id IN (子查询)
)。- 缓存:将高频分页结果缓存到Redis,减少数据库压力。
数据库分片:将数据分散到多个节点,降低单表数据量。
例如,在订单查询场景中,使用游标分页可将
LIMIT 1000000, 10
优化为WHERE order_id > 1000000 LIMIT 10
,性能提升显著。实际开发中,需结合索引设计和业务需求选择合适方案。
1. 问题背景
MySQL的分页查询通常使用 LIMIT offset, size
实现。例如:
SELECT * FROM table ORDER BY id LIMIT 1000000, 10;
- 问题:当
offset
很大时(如LIMIT 1000000, 10
),MySQL需要扫描并丢弃前1000000
条数据,仅返回最后10
条。这会导致 大量I/O开销 和 性能急剧下降。
2. 核心优化策略
2.1 覆盖索引 + 子查询(延迟关联)
- 原理:通过覆盖索引减少回表操作,降低I/O开销。
- 实现:
-- 先通过覆盖索引获取主键ID SELECT id FROM table ORDER BY id LIMIT 1000000, 10; -- 再通过主键ID回表查询完整数据 SELECT * FROM table WHERE id IN (SELECT id FROM table ORDER BY id LIMIT 1000000, 10);
- 优点:避免全表扫描,仅扫描索引树。
- 适用场景:查询字段较多,需兼容复杂条件。
2.2 游标分页(键值分页)
- 原理:利用排序字段的有序性,通过记录上一页的最后一条记录的值,直接定位下一页的起点。
- 实现:
-- 第一页 SELECT * FROM table ORDER BY id LIMIT 10; -- 下一页(假设上一页最后一条的 id 是 1000000) SELECT * FROM table WHERE id > 1000000 ORDER BY id LIMIT 10;
- 优点:无需扫描丢弃数据,性能稳定。
- 适用场景:连续分页(如“下一页”),不支持跳页。
2.3 覆盖索引优化
- 原理:查询字段被索引覆盖,直接从索引获取数据。
- 实现:
-- 创建覆盖索引 CREATE INDEX idx_name_age ON users (name, age); -- 查询仅需索引字段 SELECT name, age FROM users ORDER BY name LIMIT 1000000, 10;
- 优点:避免回表,减少I/O。
- 适用场景:查询字段少且可被索引覆盖。
2.4 缓存与预加载
- 原理:将高频访问的分页结果缓存到内存(如Redis)。
- 实现:
// 伪代码:从缓存获取分页数据 String cacheKey = "page_" + pageNum; List<User> users = redis.get(cacheKey); if (users == null) {users = db.query("SELECT * FROM users LIMIT ?, ?", (pageNum-1)*size, size);redis.set(cacheKey, users, 5, TimeUnit.MINUTES); }
- 优点:减少数据库压力,提升响应速度。
- 适用场景:数据变动不频繁或可容忍一定延迟。
2.5 数据库分片
- 原理:将数据分散到多个节点,降低单表数据量。
- 实现:
水平拆分(Horizontal Sharding)
水平拆分是将同一张表的数据按 行 的维度拆分到多个数据库或表中。每个分片(Shard)存储的是原表的一部分数据,但表的结构保持一致。
垂直拆分(Vertical Sharding)
垂直拆分是将同一张表的 列 按业务或功能拆分到不同的数据库或表中。每个分片存储的是原表的一部分字段,但数据行保持一致。
混合分片(Hybrid Sharding)结合水平与垂直拆分
先垂直拆分:按业务模块拆分(如用户库、订单库)。
再水平拆分:对高并发模块(如订单库)按用户ID分片。
- 优点:横向扩展,适合超大规模数据。
- 适用场景:分布式系统,数据量极大。
推荐场景
- 水平拆分:
✅ 推荐:当单表数据量极大(如上亿条),且分页查询依赖分片键(如user_id
)。例如,电商订单按用户ID分片,用户翻页时直接定位到目标分片。 - 垂直拆分:
⚠️ 谨慎推荐:仅当表字段过多(如包含大字段),且分页查询字段可完全包含在某个分片时有效。否则需跨分片关联,反而增加复杂度。 - 混合拆分:
✅ 推荐:复杂业务系统中,先垂直拆分解耦模块,再水平拆分扩展容量。
- 水平拆分:
3. 优化对比表
方法 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
覆盖索引 | 索引覆盖查询字段 | 避免回表 | 仅适用于字段较少的查询 | 查询字段简单 |
延迟关联 | 先查主键,再回表 | 减少扫描数据量 | 需两次查询 | 兼容复杂查询条件 |
游标分页 | 通过排序字段定位下一页起点 | 无偏移扫描,性能稳定 | 不支持跳页 | 连续分页(如“下一页”) |
缓存 | 缓存分页结果 | 减少数据库压力 | 数据更新时需更新缓存 | 数据变动不频繁 |
数据库分片 | 数据分散到多个节点 | 横向扩展 | 架构复杂,需维护分片逻辑 | 超大规模数据 |
4. 实际案例
案例1:电商订单分页
- 问题:用户查看“最近一年的订单”,翻到第1000页(偏移量100000)。
- 优化方案:
- 使用 游标分页:
-- 上一页最后一条的订单ID为 1000000 SELECT * FROM orders WHERE order_id > 1000000 ORDER BY order_id LIMIT 10;
- 缓存高频页(如第1-10页)到Redis。
- 使用 游标分页:
案例2:用户列表分页
- 问题:用户按姓名排序,翻到第100000页。
- 优化方案:
- 创建 覆盖索引
(name, id)
。 - 查询仅需索引字段:
SELECT name, id FROM users ORDER BY name LIMIT 1000000, 10;
- 创建 覆盖索引
5. 总结
- 优先使用游标分页(简单高效),适用于连续分页场景。
- 复杂查询用延迟关联,依赖索引设计。
- 覆盖索引是减少回表的关键,需合理设计复合索引。
- 缓存适合数据变动少的场景,结合业务需求灵活使用。
- 数据库分片是终极解决方案,但需权衡架构复杂度。
十. 面试官:索引创建(优化)原则有哪些?
给 MySQL 建索引就像给图书馆做目录:要高频访问、过滤性强、基数高、长度短、更新少的列先做,联合索引满足最左前缀,覆盖查询能省 IO,宁缺勿滥防止写放大。”
1. 面试背诵版 10 条原则(顺口溜:高基短更联覆控写)
“索引创建口诀:高基短更联覆控写——高选择性、短字段、更新少、联合最左、覆盖查询、控制数量、注意写放大。”
# | 原则 | 口诀 | 一句话解释 | 反面案例 |
---|---|---|---|---|
1 | 高选择性 | 高基 | 列的基数/总行数 > 20 % 才有意义 | sex 只有 0/1 建索引浪费 |
2 | 短长度 | 短 | 控制索引长度,字符串用前缀索引 | varchar(255) 全列索引 1 KB+ |
3 | 高频查询列 | 频 | WHERE , JOIN , ORDER BY , GROUP BY 高频出现的列优先 | 日志表的 create_time条件频繁,却没加索引 |
4 | 最左前缀 | 联 | 联合索引 (a,b,c) 必须按顺序用,不能跳过 a | WHERE b=1 不走索引 |
5 | 覆盖索引 | 覆 | 把查询列一起放进联合索引,避免回表 | SELECT age FROM t WHERE name=? → (name, age) |
6 | 控制数量 | 控 | 单表索引 ≤ 5 个,写多读少的表再减 | 插入 1 行更新 7 棵树 |
7 | 更新少列 | 更 | 频繁 UPDATE 的列别建索引,维护成本高 | last_login_time 每秒刷新 |
8 | 区分大小写 | 区 | utf8mb4_0900_as_cs 区分大小写时索引才生效 | 模糊 LIKE '%abc' 失效 |
9 | 排序方向 | 序 | 联合索引 ASC/DESC 与 SQL 一致,避免 filesort | ORDER BY a ASC, b DESC → 建 (a ASC, b DESC) |
10 | 前缀/函数 | 函 | 只对列前缀或函数结果建索引 | idx_left(email, 20) |
1️⃣ 高选择性(高基)
① 定义:列里不重复值越多,选择性越高。
② 为什么:选择性低时,MySQL 回表比例高,不如全表扫描。
③ 怎么做:SELECT COUNT(DISTINCT col) / COUNT(*) >= 0.2
再建索引。
④ 翻车现场:sex
只有 0/1,100 万行里筛 50 万行,索引形同虚设。
2️⃣ 短长度(短)
① 定义:索引键越短,16 KB 页里能存越多 key,树更矮。
② 为什么:页装得多 → IO 次数少 → 缓存命中高。
③ 怎么做:
字符串用前缀索引:假设 email 字段 varchar(255),值大部分在前 20 个字符就能区分
ALTER TABLE t ADD INDEX idx_email_pre(email(20));
长文本存哈希列再索引:
UNHEX(SHA1(text))
。
④ 翻车现场:varchar(255)
全列索引,每个节点 255 B,只能放 63 个 key,树高 4 层,随机 IO 爆炸。
3️⃣ 高频查询列(频)
① 定义:经常出现在 WHERE / JOIN / ORDER BY / GROUP BY
的列优先建索引。
② 为什么:索引是为了加速高频场景,冷字段建了没人用。
③ 怎么做:慢 SQL 日志抓 WHERE
出现次数 top-N,先给它建。
④ 翻车现场:日志表 create_time
每 10 秒查一次,却没索引,全表扫 3 秒。
4️⃣ 最左前缀(联)
① 定义:联合索引 (a,b,c)
必须按顺序用,不能跳过 a 只用 b。
② 为什么:B+ 树先按 a 排序再按 b、c,断档后无法二分。
③ 怎么做:把最常过滤的列放最左。
④ 翻车现场:建了 (a,b,c)
,SQL 却只写 WHERE b=1
,索引直接失效。
5️⃣ 覆盖索引(覆)
① 定义:查询列全部包含在索引里,无需回表。
② 为什么:少一次随机 IO,RT 从毫秒级降到百微秒级。
③ 怎么做:把 SELECT
的列加到联合索引尾部,如 (name, age)
覆盖 SELECT age FROM t WHERE name=?
。
④ 翻车现场:SELECT *
必回表,联合索引再宽也救不了。
6️⃣ 控制数量(控)
① 定义:单表索引别超过 5 个,写多读少的表再砍。
② 为什么:每多一棵 B+ 树,插入/更新/删除都要维护,写放大。
③ 怎么做:定期 SHOW INDEX
看未使用索引,直接 DROP
。
④ 翻车现场:一张小表 7 个索引,插入 1 行写 7 棵树,TPS 从 2 k 掉到 200。
7️⃣ 更新少列(更)
① 定义:频繁 UPDATE
的列别建索引。
② 为什么:更新值 → 索引节点分裂/合并 → 随机 IO + 锁竞争。
③ 怎么做:把易变列踢出联合索引,或只放在最右。
④ 翻车现场:last_login_time
每秒更新一次,建了索引后 CPU 飙 30 %。
8️⃣ 区分大小写(区)
① 定义:排序规则 utf8mb4_0900_as_cs
区分大小写,索引才能精确匹配。
② 为什么:不区分大小写时,LIKE '%abc'
无法使用 B+ 树范围搜索。
③ 怎么做:
列级别指定:
ALTER TABLE t MODIFY name VARCHAR(100) COLLATE utf8mb4_0900_as_cs;
或查询里写
WHERE BINARY name='Abc'
。
④ 翻车现场:utf8mb4_general_ci
下LIKE '%abc'
全表扫。
9️⃣ 排序方向(序)
① 定义:联合索引的 ASC/DESC
与 SQL 的 ORDER BY
方向一致,避免 filesort。
② 为什么:方向不一致时,MySQL 得在内存/磁盘再排一次序。
③ 怎么做:
-- SQL
ORDER BY create_time DESC, id ASC
-- 索引
INDEX idx_ctime_id (create_time DESC, id ASC)
④ 翻车现场:建了 (create_time ASC, id ASC)
,SQL 写 ORDER BY create_time DESC, id ASC
,依旧 filesort。
🔟 前缀/函数(函)
① 定义:对列前缀或函数结果建索引,减少键长度。
② 为什么:直接对长列索引体积大;函数结果列短又稳定。
③ 怎么做:
前缀:
ALTER TABLE t ADD INDEX idx_email(email(20));
函数:
ALTER TABLE t ADD INDEX idx_hash(MD5(email));
④ 翻车现场:varchar(255)
全列索引,磁盘 1 GB;改前缀 20 B 后 80 MB,查询一样快。
2. 现场举个例子
-- 高频慢 SQL
SELECT id, amount
FROM orders
WHERE user_id = 123AND create_time BETWEEN '2024-01-01' AND '2024-02-01'
ORDER BY create_time DESC
LIMIT 20;
优化索引
CREATE INDEX idx_user_ctime_amount ON orders(user_id, create_time DESC, amount);
满足最左前缀
(user_id)
覆盖
(amount)
避免回表与
ORDER BY create_time DESC
同序,避免 filesort二级索引自带主键,SELECT 主键列也能走覆盖索引,不必再冗余将
id加到联合索引中
十一. 面试官:什么情况下索引会失效 ?
参考回答:MySQL 索引失效的常见场景包括:
联合索引未遵循最左前缀原则:例如 (A,B,C) 联合索引,若查询只用 B 或 C 则失效。
对索引列使用函数或计算:如 YEAR(create_time) 会导致索引失效。
OR 连接非索引列:若 OR 条件中存在非索引列,可能放弃索引。
LIKE 通配符以 % 开头:无法利用索引前缀匹配。
隐式类型转换:字段类型与查询值类型不一致。
!= 或 NOT IN:排除操作需扫描大量数据。
范围查询后列失效:联合索引中范围查询后的列无法使用索引。
数据量小或区分度低:优化器选择全表扫描。
IS NULL/IS NOT NULL:B+Tree 索引通常不存储 NULL。
优化器主动放弃索引:基于统计信息判断全表扫描更快。
排查方法:使用 EXPLAIN 分析执行计划,关注 key、type 和 Extra 字段。
索引失效的核心场景
场景 | 原因 | 解决方案 |
---|---|---|
联合索引未遵循最左前缀 | 缺少最左列 | 调整查询条件或重建索引 |
函数/计算操作 | 破坏索引有序性 | 改写查询条件或使用函数索引 |
OR 连接非索引列 | 全表扫描风险 | 拆分为多个查询或添加索引 |
LIKE 通配符开头 | 无法定位前缀 | 改用右模糊或全文索引 |
隐式类型转换 | 类型不一致导致转换 | 确保查询值与字段类型一致 |
!= /NOT IN | 排除操作扫描数据多 | 分页或限制查询范围 |
范围查询后列失效 | 联合索引顺序问题 | 调整索引顺序 |
数据量小/区分度低 | 全表扫描更快 | 避免在低区分度字段建索引 |
IS NULL /IS NOT NULL | B+Tree 不存储 NULL 值 | 使用默认值替代 NULL |
优化器放弃索引 | 统计信息估算误差 | 更新统计信息或强制使用索引 |
如何排查索引失效?
- 使用
EXPLAIN
分析执行计划:key
: 实际使用的索引(NULL
表示未使用)。type
: 访问类型(ALL
表示全表扫描)。Extra
: 是否出现Using filesort
、Using temporary
。
- 示例:
EXPLAIN SELECT * FROM users WHERE name LIKE '%Tom';
1. 联合索引未遵循最左前缀原则
失效原因
联合索引(复合索引)必须按定义顺序使用最左列,否则索引失效。
示例
-- 联合索引 (user_id, create_time)
SELECT * FROM orders WHERE create_time > '2024-01-01'; -- 未使用 user_id,索引失效
解决方案
- 调整查询条件:确保包含最左列。
SELECT * FROM orders WHERE user_id = 1001 AND create_time > '2024-01-01';
- 重建索引:若查询常跳过最左列,可调整联合索引顺序(如
(create_time, user_id)
)。
2. 对索引列进行函数或计算
失效原因
索引存储的是原始值,函数或计算会破坏索引的有序性。
示例
-- 索引列 create_time 上使用函数
SELECT * FROM orders WHERE YEAR(create_time) = 2024; -- 索引失效
解决方案
- 改写查询条件:避免函数操作。
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
- 使用函数索引(MySQL 8.0+):
CREATE INDEX idx_year ON orders((YEAR(create_time)));
3. 使用 OR
连接非索引列
失效原因
OR
条件中若存在非索引列,MySQL 可能放弃索引直接全表扫描。
示例
-- user_id 有索引,age 无索引
SELECT * FROM users WHERE user_id = 1001 OR age = 20; -- 索引失效
解决方案
- 拆分为多个查询并用
UNION
合并:SELECT * FROM users WHERE user_id = 1001 UNION SELECT * FROM users WHERE age = 20;
- 为
age
添加索引(若业务允许)。
4. LIKE
通配符以 %
开头
失效原因
B+Tree 索引按前缀排序,%
开头无法定位起始点。
示例
-- 模糊查询以通配符开头
SELECT * FROM users WHERE name LIKE '%Tom'; -- 索引失效
解决方案
- 使用右模糊查询:
SELECT * FROM users WHERE name LIKE 'Tom%';
- 使用全文索引(针对复杂文本搜索):
ALTER TABLE users ADD FULLTEXT(name); SELECT * FROM users WHERE MATCH(name) AGAINST('Tom' IN BOOLEAN MODE);
先在文本列上建 FULLTEXT
倒排索引,再用 MATCH ... AGAINST
进行高效的全文搜索;示例 BOOLEAN MODE
支持关键词匹配、排除、通配符等高级语法。”
5. 隐式类型转换
失效原因
字段类型与查询值类型不一致时,MySQL 会自动转换,导致索引失效。
示例
-- phone 是 VARCHAR 类型,但传入数字
SELECT * FROM users WHERE phone = 13812345678; -- 索引失效
解决方案
- 确保类型一致:
SELECT * FROM users WHERE phone = '13812345678';
6. 使用 !=
或 NOT IN
失效原因
排除操作需要扫描大量数据,优化器可能选择全表扫描。
示例
-- 排除特定值
SELECT * FROM orders WHERE status != 'completed'; -- 索引失效
解决方案
- 分页或限制查询范围:
SELECT * FROM orders WHERE status != 'completed' AND create_time > '2024-01-01';
7. 范围查询后列失效
失效原因
联合索引中,范围查询后的列无法使用索引。
示例
-- 联合索引 (user_id, create_time, status)
SELECT * FROM orders
WHERE user_id = 1001 AND create_time > '2024-01-01' AND status = 'paid'; -- status 无法使用索引
解决方案
- 调整索引顺序:将
status
放在create_time
前。CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);
8. 数据量小或索引区分度低
失效原因
- 小表:全表扫描比索引+回表更快。
- 低区分度字段:如
gender
,值重复率高。
解决方案
- 监控小表:定期检查数据量,必要时合并或拆分表。
- 避免在低区分度字段建索引。
9. 使用 IS NULL
或 IS NOT NULL
失效原因
B+Tree 索引通常不存储 NULL
值,查询 IS NULL
时可能失效。
示例
-- 查询 email 为空
SELECT * FROM users WHERE email IS NULL; -- 索引失效
解决方案
- 单独维护空值表(业务允许时)。
- 使用默认值替代 NULL(如
''
或0
)。
10. 优化器主动放弃索引
失效原因
MySQL 根据统计信息估算,发现全表扫描更快。
示例
-- 大表中大部分行满足条件
SELECT * FROM orders WHERE status = 'pending'; -- 索引失效
解决方案
- 强制使用索引(谨慎):
SELECT * FROM orders USE INDEX(idx_status) WHERE status = 'pending';
- 更新统计信息:
ANALYZE TABLE orders;
十二:面试官:sql的优化的经验
参考回答:
“先抓慢 SQL,再 EXPLAIN;能改 SQL 就不加索引,能加联合索引就不拆表;分页用游标,大事务拆小;最后别忘了 Java 端批处理和连接池。”
MySQL SQL 优化的核心经验包括:
- 索引优化:合理创建覆盖索引,避免函数操作和隐式类型转换。
- 查询语句优化:避免 SELECT *,用 UNION ALL 替代 UNION,优化分页查询。
- 表结构设计:垂直拆分减少字段,水平拆分分区大表。
- 执行计划分析:通过 EXPLAIN 分析索引使用和扫描行数。
- 高级技巧:读写分离、缓存、JOIN 替代子查询。
- 典型问题解决:大数据量分页用范围查询,多表关联确保索引和顺序优化。
- 那你平时对sql做了哪些优化:
- 这个也有很多,比如SELECT语句务必指明字段名称,不要直接使用select * ,
- 还有就是要注意SQL语句避免造成索引失效的写法;
- 如果是聚合查询,尽量用union all代替union ,union会多一次过滤,效率比较低;
- 如果是表关联的话,尽量使用innerjoin ,不要使用用left join right join,如必须使用 一定要以小表为驱动
十三. 面试官:创建表的时候,你们是如何优化的呢?
1. 一句话先给结论
“建表阶段就把 90% 的性能隐患埋掉:选对引擎+字符集、主键自增短整型、字段长度够用即可、NOT NULL + 默认值、预留扩展字段、合理分区/分表策略、一次性规划好索引。”比上线后改 SQL 加索引便宜一百倍。”
2. 7 步落地清单(面试直接背)
步骤 | 动作 | 一句话理由 | 示例 |
---|---|---|---|
1 | 引擎 & 字符集 | InnoDB + utf8mb4,行锁+多语言 | ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin |
2 | 主键设计 | 自增 BIGINT 或雪花 ID;短、有序、非业务 | id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY |
3 | 字段瘦身 | 长度够用即可,节省磁盘 & 内存 | varchar(32) 存手机号,拒绝 varchar(255) |
4 | NOT NULL + 默认值 | 减少 NULL 位图,查询更快 | status TINYINT NOT NULL DEFAULT 0 |
5 | 预留 JSON 扩展 | 避免后期 DDL 锁表 | ext JSON COMMENT '预留扩展字段' |
6 | 索引预规划 | 高频条件、排序、覆盖索引一次性建好 | UNIQUE(phone), INDEX(user_id, create_time DESC) |
7 | 分区/分表预案 | 按时间/哈希预留,后期平滑切 | 按年月分区:PARTITION BY RANGE (to_days(create_time)) |
3. Java 项目实操脚本模板(直接贴)
CREATE TABLE `orders` (`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,`user_id` BIGINT UNSIGNED NOT NULL,`product_id` BIGINT UNSIGNED NOT NULL,`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,`status` TINYINT NOT NULL DEFAULT 0,`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,`ext` JSON COMMENT '预留扩展',UNIQUE KEY uk_order_no (`order_no`),INDEX idx_user_ctime (`user_id`, `create_time DESC`)
) ENGINE=InnoDBDEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_binPARTITION BY RANGE (to_days(create_time)) (PARTITION p202401 VALUES LESS THAN (to_days('2024-02-01')),PARTITION p202402 VALUES LESS THAN (to_days('2024-03-01'))
);
- “表设计阶段就敲定:InnoDB + utf8mb4 保基础;BIGINT 自增主键保容量;NOT NULL + TINYINT/JSON 省空间;唯一索引防重复,联合索引覆盖业务;RANGE 分区让千万级订单也能秒级查一个月数据,后期平滑扩容。”
- RANGE 分区把不同月份的数据拆到不同文件;查询时只要
WHERE create_time
条件存在,MySQL 会自动做 分区剪枝,只扫必要分区,我们写 SQL 与普通表完全一致。”
十四. 面试官:事务的特性是什么?可以详细说一下吗?
参考回答:嗯,这个比较清楚,ACID,分别指的是:原子性、一致性、隔离性、持久性;
我举个例子:
- 原子性:A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败
- 一致性:在转账的过程中,数据要一致,A扣除了500,B必须增加500
- 隔离性:在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰
- 持久性:在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)
1️⃣ Atomicity – 原子性
定义:事务中的操作要么全部成功,要么全部失败回滚。
原理:InnoDB 使用 undo log 记录反向操作,提交时刷盘,失败时按 undo log 回滚。
反例:转账扣了 A 账户,还没来得及加 B 账户就宕机,原子性保证钱不会凭空消失。
2️⃣ Consistency – 一致性
定义:事务执行前后,数据库从一个合法状态变为另一个合法状态;业务约束(主键、外键、触发器、CHECK)必须始终成立。
原理:原子性 + 隔离性 + 持久性共同保证;InnoDB 在事务提交前做约束检查,失败即回滚。
反例:订单表总金额必须等于明细金额之和,若事务只插入明细未更新订单总额,一致性被破坏。
3️⃣ Isolation – 隔离性
定义:并发事务之间互不干扰,读写结果如同串行执行。
实现:InnoDB 通过 MVCC(多版本并发控制) + 锁(行锁 / 间隙锁) 提供 4 种隔离级别:
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交)
REPEATABLE READ(可重复读)
SERIALIZABLE(串行化)
反例:A 事务读到 B 事务未提交的余额(脏读),导致重复扣款。
4️⃣ Durability – 持久性
定义:一旦事务提交,结果永久生效,即使系统崩溃。
实现:提交时先写 redo log(顺序写磁盘,双写缓冲),崩溃重启后按 redo log 重放,保证已提交事务不丢。
反例:提交成功但未落盘时断电,持久性保证重启后数据仍在。
十五. 面试官:并发事务带来哪些问题?
MySQL 并发事务带来的问题要分两层说:
数据一致性异常(脏读、不可重复读、幻读、更新丢失)。
- 脏读:读取其他事务未提交的数据,导致数据无效。
- 不可重复读:同一事务内多次读取结果不一致。
- 幻读:查询结果集的行数因其他事务的插入 / 删除而变化。
- 丢失更新:后提交的事务覆盖前者的修改。
调度异常(死锁):事务循环等待锁资源,导致系统阻塞。
解决方案包括:
- 设置合适的事务隔离级别(如
REPEATABLE READ
或SERIALIZABLE
)。- 使用行级锁(如
SELECT ... FOR UPDATE
)或乐观锁(如版本号控制)。- 通过 MVCC(多版本并发控制)实现读写不阻塞。
问题对比与解决方案
问题类型 | 触发条件 | 隔离级别解决方式 | 典型场景 |
---|---|---|---|
脏读 | 读取未提交的事务数据 | READ COMMITTED 及以上级别 | 临时数据查询(如未审核的订单) |
不可重复读 | 同一行数据被其他事务修改 | REPEATABLE READ 及以上级别 | 金融交易(如账户余额查询) |
幻读 | 新增/删除符合条件的行,两次范围查询(同一查询条件) 返回的行数不同 | REPEATABLE READ (需 Next-Key 锁)或 SERIALIZABLE | 库存扣减(防止超卖) |
丢失更新 | 并发修改同一数据 | 使用 SELECT ... FOR UPDATE 或乐观锁 | 库存管理、秒杀场景 |
死锁 | 循环锁依赖 | 数据库自动检测并回滚其中一个事务 | 多表关联操作(如订单+用户表) |
1. 脏读(Dirty Read)
现象:事务 A 读到事务 B 尚未提交 的修改。
后果:B 回滚 → A 读到“幽灵”数据。
示例:A 查账户余额 1000,B 扣 200 未提交,A 看到 800;B 回滚 → 实际仍是 1000,但 A 已按 800 做后续逻辑。
2. 不可重复读(Non-Repeatable Read)
现象:同一事务内,两次读取 同一行 结果不同(被别的事务 已提交 的 UPDATE 影响)。
- 影响:破坏事务的一致性,导致业务逻辑混乱。
示例:A 第一次读余额 1000,B 提交 扣款后,A 再读变成 800,导致对账不一致。
3. 幻读(Phantom Read)
现象:同一事务内,两次范围查询(同一查询条件) 返回的行数不同(被别的事务 已提交 的 INSERT/DELETE 影响)。
- 影响:破坏事务对数据完整性的预期,尤其在范围查询中尤为明显。
示例:A 统计 2024-06 订单共 100 条,B 插入 1 条并提交,A 再统计变成 101 条,出现“幻影”行。
4. 更新丢失 / 写倾斜(Lost Update)
现象:两个事务同时读同一行,各自基于旧值做修改,后提交的事务 覆盖 了先提交的更新。
- 分类:
- 第一类丢失更新(脏写):事务 A 修改数据后,事务 B 在 A 提交前修改同一数据,A 回滚导致 B 的修改被覆盖。
- 第二类丢失更新:事务 A 和 B 依次提交,B 的提交覆盖 A 的修改。
- 示例:
- 事务 A 和 B 同时读取库存
100
。 - 事务 A 扣减
20
(库存80
),事务 B 扣减30
(库存70
),最终提交时 B 的操作覆盖 A,库存实际减少30
而非50
。
- 事务 A 和 B 同时读取库存
5. 死锁(Deadlock)
- 定义:多个事务互相等待对方释放锁,形成循环依赖,导致事务无法继续执行。
- 影响:系统资源被占用,需通过超时或手动干预解除死锁。
- 示例:
- 事务 A 锁定订单表,事务 B 锁定用户表。
- A 尝试锁定用户表,B 尝试锁定订单表,双方互相等待,形成死锁。
十六. 面试官:怎么解决这些问题呢?MySQL的默认隔离级别是?
- “用 隔离级别 + 锁 + MVCC 三板斧解决:MySQL 默认 REPEATABLE READ(可重复读),靠 MVCC + 行锁 + Next-Key Lock 把脏读、不可重复读、幻读全部挡掉,更新丢失则用乐观/悲观锁或业务重试。”
- mysql默认的隔离级别是:REPEATABLE READ(可重复读)
隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现要点 | 典型场景 |
---|---|---|---|---|---|
READ UNCOMMITTED(读未提交) | ✅ | ✅ | ✅ | 不加锁,直接读取最新数据。 | 无一致性要求的临时数据查询 |
READ COMMITTED(读已提交) | ❌ | ✅ | ✅ | 每次读取生成新 Read View ,仅读取已提交数据。 | 高并发 OLTP 场景(如秒杀系统) |
REPEATABLE READ(可重复读)(默认) | ❌ | ❌ | ❌ | MVCC(多版本并发控制) + 间隙锁(Gap Lock) + Next-Key Lock(行锁+间隙锁)。 | 默认级别,适合金融交易、账单生成 |
SERIALIZABLE(串行化) | ❌ | ❌ | ❌ | 所有读加共享锁,串行执行 | 强一致性需求(如核心账务系统) |
行级锁(Row-Level Lock):
通过SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
锁定特定行,避免其他事务修改。
示例:START TRANSACTION; SELECT * FROM accounts WHERE user_id = 'A' FOR UPDATE; -- 锁定账户A UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A'; COMMIT;
间隙锁(Gap Lock):
在 RR 级别下,InnoDB 通过间隙锁防止幻读(如范围查询时锁定索引区间)。乐观锁(Optimistic Locking):
通过版本号(version
字段)或时间戳(timestamp
)检测冲突,适用于读多写少的场景。
示例// Java 中使用乐观锁更新数据 String updateSql = "UPDATE products SET stock = stock - 1, version = version + 1 "+ "WHERE product_id = ? AND version = ?";
十七. 面试官:undo log和redo log的区别
undo log 负责 “把数据恢复到过去”(事务回滚、MVCC,保证事务的原子性)。
redo log 负责 “把数据带到未来”(崩溃恢复、保证已提交事务不丢,保证事务的持久性)。
类型 | 核心功能 | 操作性质 | 存储内容 | 作用 |
---|---|---|---|---|
undo | 回滚、支持 MVCC | 逻辑反向操作 | 数据旧值 | 保证事务的原子性(A) |
redo | 恢复数据、保证持久化 | 物理页修改 | 数据新值 | 保证事务的持久性(D) |
维度 | undo log | redo log |
---|---|---|
作用 | 回滚事务、构造旧版本快照(MVCC) | 崩溃恢复时重放已提交修改 |
记录内容 | 逻辑日志:反向操作(delete→insert,update→反向update) | 物理日志:页号、偏移、新值 |
生命周期 | 事务提交后不会立即删除,需等 MVCC 无快照引用 | 事务提交后持久化即可丢弃(Checkpoint) |
存储位置 | 共享表空间 ibdata1 或独立 undo tablespace | redo log 文件组(ib_logfile0/1) |
刷盘策略 | 写缓存 + 后台 purge | 顺序追加,先写 log 再写数据页(WAL) |
是否影响性能 | 写操作同时写 undo,purge 线程异步清理 | 顺序写盘,I/O 延迟极低 |
典型场景 | 回滚 ROLLBACK 、一致性读快照 | 宕机重启 → 自动重放 redo → 数据页恢复 |
十八. 面试官:事务中的隔离性是如何保证的呢?(你解释一下MVCC)
- 在 MySQL 中,事务的隔离性 是通过 锁机制 和 MVCC(多版本并发控制) 共同实现的。
- MVCC 是 InnoDB 存储引擎解决读-写冲突的核心机制,
- 而锁机制则用于处理写-写冲突。
- MVCC详解:
- 其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突
- 它的底层实现主要是分为了三个部分(隐藏字段 + undo log日志 + readView读视图)
- 隐藏字段:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址
- undo log:主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
- readView:解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id 判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用
以下是详细解释:
1. 写-写冲突(锁机制)
当多个事务同时修改同一数据时,锁机制 是隔离性的核心保障:
- 行级锁:InnoDB 通过行级锁(如
X Lock
)确保同一时刻只有一个事务能修改某一行数据。 - 间隙锁(Gap Lock):在可重复读(RR)隔离级别下,通过锁定索引区间防止幻读。
- Next-Key Lock:行锁 + 间隙锁的组合,彻底解决幻读问题。
示例:
-- 事务 A 更新 user_id=1 的数据
START TRANSACTION;
SELECT * FROM users WHERE user_id = 1 FOR UPDATE; -- 加行锁
UPDATE users SET name = 'Alice' WHERE user_id = 1;
COMMIT;-- 事务 B 在事务 A 提交前无法修改或读取 user_id=1 的数据
2. 读-写冲突(MVCC)
当读操作与写操作并发时,MVCC 通过 多版本数据 和 一致性视图(Read View) 实现隔离性,避免阻塞:
- 核心思想:为每个事务提供数据的“快照”,使读操作不加锁,同时保证一致性。
- 适用场景:在 读已提交(RC) 和 可重复读(RR) 隔离级别下,MVCC 解决脏读、不可重复读和幻读问题(在 RR 下通过间隙锁辅助)。
十九. 面试官:MySQL主从同步原理
- “MySQL主从同步的核心原理是基于二进制日志(Binary Log)的复制机制,通过三个线程:主库写 binlog → 从库 I/O 线程拉 binlog → 从库 SQL 线程重放 的三步流水线,本质是 逻辑复制。”
- 主库记录二进制日志(Binary Log)
- 从库复制主库的 binlog 到中继日志(Relay Log)
- 从库执行中继日志中的操作
主库记录二进制日志(Binary Log)
主库执行 SQL 操作后,会将操作按顺序记录到二进制日志(binlog)中,包括数据修改(增删改)和表结构变更等。binlog 是主从同步的基础,记录了操作的逻辑或物理变更细节。从库复制主库的 binlog 到中继日志(Relay Log)
从库启动一个 I/O 线程,连接主库并请求读取 binlog。主库会启动一个 binlog dump 线程,将新产生的 binlog 事件发送给从库。从库的 I/O 线程接收后,将这些事件写入本地的中继日志(relay log)。从库执行中继日志中的操作
从库的 SQL 线程读取中继日志,解析并执行其中的 SQL 操作,确保从库数据与主库保持一致。SQL 线程与 I/O 线程独立工作,可并行执行,提高同步效率。
复制模式 | 原理 | 优点 | 缺点 |
---|---|---|---|
异步复制(Async) | 主库提交事务后,不等待从库的响应,直接返回客户端结果。 | 主库写性能高,无延迟。 | 存在数据不一致风险(主库宕机时,从库可能未同步最新数据)。 |
半同步复制(Semi-Sync) | 主库提交事务后,需至少一个从库确认已接收并写入 Relay Log,才返回客户端结果。 | 提升数据一致性(至少一个从库同步成功)。 | 增加网络延迟,降低主库写性能。 |
全同步复制(Full Sync) | 主库和从库都执行完事务并确认后,才返回客户端结果。 | 数据一致性最高。 | 性能最低,适用于对一致性要求极高的场景(如金融系统)。 |
二十. 面试官:你们项目用过MySQL的分库分表吗(介绍一下方案)?
拆分类型 | 核心原理 | 常见拆分维度 | 优势 | 注意事项 |
垂直拆分 | 按业务或字段属性拆分,降低单库 / 表复杂度 | 1. 垂直分库:按业务模块(如用户库、订单库、商品库)2. 垂直分表:按字段访问频率(核心字段表 + 大字段 / 低频字段表) | 1. 业务隔离,便于独立扩容2. 减少单表数据量,提升查询效率3. 符合微服务架构设计 | 1. 跨库 JOIN 需通过应用层处理(如多次查询拼接)2. 拆分后表结构更复杂,需维护多表关系 |
水平拆分 | 按规则拆分同结构数据,分散单表数据量 | 1. 范围拆分:按时间(如订单表按月份拆分)、ID 区间2. 哈希拆分:按 ID 哈希取模(如用户 ID%10 分 10 个子表) | 1. 数据分布均匀,避免单表压力集中2. 支持大规模数据存储,可横向扩容 | 1. 范围拆分可能导致数据倾斜(如近期表数据过多)2. 哈希拆分扩容时需迁移数据(可通过一致性哈希优化) |
实现方案 | 借助中间件拦截 SQL,自动路由到目标库 / 表 | - Sharding-JDBC:轻量级框架,嵌入应用层,支持读写分离、分布式事务- MyCat:独立服务,支持更多协议 | 1. 透明化分库分表细节,应用层无需修改代码2. 支持灵活的拆分规则配置 | 1. 需提前规划拆分策略(预留扩容空间)2. 全局索引维护复杂,跨库事务需额外处理(如 Seata) |