当前位置: 首页 > news >正文

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
  • 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;  -- 查看具体查询的详细耗时
    • 关注 CPUContext switchesMemory 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 Toolkitpt-online-schema-change 可在线优化表结构,减少锁表时间。
  • 慢查询监控系统:如 Prometheus + Grafana,实时监控慢查询数量及趋势。

二. 面试官:那这个SQL语句执行很慢, 如何分析呢?

  • 参考回答:
    • 如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,
    • 第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描
    • 第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
  • 使用 EXPLAIN 分析执行计划
  • 使用 SHOW PROFILE 分析SQL耗时
  • 优化索引与查询:添加合适索引、减少全表扫描、避免复杂查询。
  • 持续监控:通过工具实时监控数据库性能,及时调整策略

1. 使用 EXPLAIN 分析执行计划

  • 作用:查看SQL语句的执行计划,判断是否命中索引、是否存在全表扫描等问题。
  • 操作步骤
    1. 在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 |
      +----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
    2. 关键字段解读
      • type:连接类型(从好到差):
        • const/eq_ref/ref/range(使用索引)。
        • index(全索引扫描)。
        • ALL(全表扫描,需优化)。
      • key:实际使用的索引。
      • rows:预估扫描的行数(越小越好)。
      • Extra:额外信息(如 Using filesortUsing temporary 等低效操作)。
        • Using filesort 表示 MySQL 需要对结果集进行额外的排序操作,且无法通过索引直接获取有序数据。这种排序可能发生在内存中(小数据量)或磁盘上(大数据量),因此称为“文件排序”
        • Using temporary 表示 MySQL 在执行查询时,需要创建一个临时表(Memory 或磁盘)来存储中间结果。通常出现在涉及 GROUP BYDISTINCTUNIONORDER BY 等操作时。
    3. 优化方向
      • 若 type 为 ALL,需添加合适的索引。
      • 若 key 为 NULL,说明未使用索引。
      • 若出现 Using filesort,需优化 ORDER BY 或添加索引。
      • 若出现 Using temporary,通过添加覆盖索引或联合索引、减少查询字段、优化GROUP BY/ORDER BY逻辑、调整临时表参数(如tmp_table_size),以及拆分复杂查询

2. 使用 SHOW PROFILE 分析SQL耗时

  • 作用:查看SQL执行的各个阶段耗时(如解析、优化、执行等)。
  • 操作步骤
    1. 开启 profiling
      SET profiling = 1;
    2. 执行慢SQL
      SELECT * FROM orders WHERE user_id = 123;
    3. 查看执行耗时
      SHOW PROFILES; -- 查看所有SQL的耗时
      SHOW PROFILE FOR QUERY 1; -- 查看具体SQL的详细耗时
    4. 分析结果
      • 找出耗时最长的阶段(如 Copying to tmp tableSending data),针对性优化。

3 优化索引

  • 常见问题
    • 缺少索引:导致全表扫描。
    • 索引冗余:增加写入开销。
    • 索引未覆盖查询字段(未使用覆盖索引)。
  • 优化策略
    1. 添加合适索引
      • 对 WHEREJOINORDER BYGROUP BY 的字段添加索引。
      • 使用复合索引(多列索引),注意字段顺序。
    2. 避免冗余索引
      • 删除重复或不必要的索引。
    3. 使用覆盖索引
      • 查询字段全部包含在索引中,避免回表查询。
      CREATE INDEX idx_user_age ON users (age, name);
      SELECT name FROM users WHERE age > 30; -- 覆盖索引

4. 优化SQL语句

  • 常见问题
    • 使用 SELECT *(获取多余字段)。
    • 使用 OR 连接条件(可能失效索引)。
    • 复杂的子查询或嵌套视图。
  • 优化策略
    1. 避免 SELECT *
      • 只选择需要的字段,减少数据传输。
      SELECT id, name FROM users WHERE age > 30;
    2. 替换 OR 为 UNION ALL
      • 避免索引失效。
      SELECT * FROM users WHERE id = 1
      UNION ALL
      SELECT * FROM users WHERE age > 30;
    3. 减少复杂查询
      • 拆分大查询为多个小查询。
      • 避免过多表连接(JOIN 表数量建议不超过3张)。
    4. 分页优化
      • 大数据量分页时,避免使用 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 + 树是一种 多路平衡查找树,其结构和特性专为索引设计优化:

  1. 树结构分层

    • 非叶子节点:仅存储 索引键 和 子节点指针,不存储具体数据(减少节点大小,提高扇出率)。
    • 叶子节点:存储 完整索引键 和 数据地址(InnoDB 中叶子节点直接存储数据行,即 “聚簇索引”)。
    • 所有叶子节点通过 双向链表 连接,便于范围查询(如 BETWEEN ... AND ...)。
  2. 多路性(高扇出率)

    • 每个节点可存储多个索引键(取决于键的大小和页大小)。MySQL 中数据页默认大小为 16KB,假设每个索引键(如 int 类型)占 4 字节,指针占 8 字节,则一个非叶子节点可存储约 16KB/(4+8) ≈ 1365 个键,即每个节点可指向 1366 个子节点。
    • 高扇出率使树高极低(例如:1000 万数据,树高仅 3 层),查询时只需 3 次磁盘 IO,效率极高。
  3. 平衡性

    • 插入 / 删除时通过 分裂 或 合并 节点保持树的平衡,确保查询效率稳定在 O (log n)。

3. InnoDB 中的索引实现(基于 B + 树)

InnoDB 是 MySQL 最常用的存储引擎,其索引实现与 B + 树深度结合,核心分为两类:

  1. 聚簇索引(Clustered Index)

    • 定义:以主键(PRIMARY KEY)为索引键的 B + 树,叶子节点直接存储 完整数据行
    • 特性
      • 每张表只有一个聚簇索引(由主键决定,若无主键则选唯一索引,否则隐式生成行 ID)。
      • 查询时若能通过聚簇索引定位,可直接获取数据,无需回表。
    • 示例:查询 SELECT * FROM user WHERE id = 100,通过聚簇索引的 B + 树找到叶子节点,直接读取数据行。
  2. 二级索引(Secondary Index,非聚簇索引)

    • 定义:以非主键字段为索引键的 B + 树,叶子节点存储的是 主键值(而非数据行)。
    • 特性
      • 一张表可有多个二级索引(如 INDEX (name)INDEX (age))。
      • 查询时需先通过二级索引找到主键值,再通过聚簇索引查询数据(称为 “回表”)。
    • 示例:查询 SELECT * FROM user WHERE name = '张三',流程为:
      1. 通过 name 二级索引的 B + 树找到对应的主键值(如 id=200)。
      2. 再通过聚簇索引(id=200)找到完整数据行(回表操作)。

4. B + 树索引的优势总结

  1. 高效查询:低树高 + 高扇出率,减少磁盘 IO 次数,支持等值查询和范围查询。
  2. 范围查询友好:叶子节点双向链表,可快速遍历连续数据(如 ORDER BYGROUP BY)。
  3. 稳定性强:平衡结构确保插入 / 删除后查询效率稳定,无极端退化情况。
  4. 适配磁盘存储:节点大小与磁盘页(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)和全表扫描。

六. 面试官:什么是聚簇索引什么是非聚簇索引 ?

1. 一句话定义

  • 聚簇索引(主键索引)数据行和索引放在同一棵 B+ 树里,叶子节点就是整行数据。

  • 非聚簇索引(自定义索引、二级索引)索引树和数据是分离的,叶子节点只存“索引键 + 指向数据行的指针(主键的key)”。


2. 对比维度(面试表格答法)

维度聚簇索引非聚簇索引(二级索引)
索引即数据✅ 是❌ 否,需回表
叶子节点内容完整的行记录索引列值 + 主键值(InnoDB)
一张表个数最多 1 个(通常是主键)可以有多个
物理顺序表数据按主键顺序物理存储与数据物理顺序无关
回表不需要需要(通过主键再查聚簇索引)
范围/排序主键范围查询很快需要回表,额外 IO
典型实现InnoDB 的主键InnoDB 的普通索引、唯一索引

3. 举例(把抽象变具体)

“假设有表 user(id PK, name, age)

  • 聚簇索引:主键 id 这棵树,叶子节点就是 id=1id=2 … 的整行数据。

  • 非聚簇索引:在 name 上建索引,叶子节点存 name→id 的映射;查到 name='Tom' 后,还要用主键 id 回表拿其余列。”


4. 性能提示(加分项)

  • 聚簇索引
    插入顺序最好按主键递增,否则会导致页分裂(UUID 主键性能差)。

  • 非聚簇索引
    覆盖索引SELECT 的列全在索引里)可避免回表,性能接近聚簇索引。


5. 一句话总结

  • 聚簇索引可以叫他主键索引,就是‘索引即数据’,一张表只能有一个
  • 非聚簇索引可以叫他二级索引或者自定义索引,是‘另建目录’,查到后常需回表,但可建多个,配合覆盖索引可提速。”

七. 面试官:知道什么是回表查询嘛?

参考回答:

  • 回表查询是指在使用非聚簇索引(如普通索引)查询时,由于索引中不包含所有查询字段,需要通过主键值再次访问聚簇索引(主键索引)获取完整数据的过程。
  • 例如,在 SELECT name, email FROM users WHERE name = 'Alice' 中,若 email 未包含在 name 的索引中,则会触发回表。
  • 如何避免? 可以通过覆盖索引(如创建 (name, email) 复合索引),使查询字段全部包含在索引中,从而避免回表。此外,减少查询字段、合理设计索引也能优化性能

1. 一句话定义

回表查询就是:通过非聚簇索引拿到主键后,再回到聚簇索引里把整行数据捞出来的那一次额外 IO。


2. 举个例子

-- 表结构
user(id PK, name, age)
-- 索引
idx_name(name)   -- 非聚簇索引-- SQL
SELECT age FROM user WHERE name = 'Tom';
  1. 先走 idx_name B+ 树 → 找到 name='Tom' 的叶子节点,拿到主键 id = 7

  2. 再用 id = 7 回到 聚簇索引 → 找到整行数据 → 取出 age
    第 2 步就是“回表”。


3. 面试加分点

  • 如何避免回表?
    建立“覆盖索引”:把查询列全放到联合索引里,例如 (name, age),InnoDB 直接在二级索引叶子节点拿到 age,无需回表。

  • 性能影响?
    回表 ≈ 一次随机 IO,高并发场景下覆盖索引可把 RT(响应时间)从毫秒级降到百微秒级。


4. 一句话总结(收尾)

  • “回表就是非聚簇索引查到主键后,再回聚簇索引取整行的二次查询
  • 覆盖索引可以干掉回表,提升查询性能。”

八. 面试官:知道什么叫覆盖索引嘛?

参考回答:

  • 覆盖索引:是指查询所需的所有字段都包含在某个索引中,使得数据库可以直接从索引中提取数据,无需回表获取完整数据行。
  • 例如,在 SELECT name, age FROM users WHERE name = 'Alice' 中,若创建了复合索引 (name, age),则可直接从索引中获取 nameage,无需访问聚簇索引进行回表查询。
  • 优势
    • 减少 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 覆盖索引

  • 传统查询路径(需回表):
    1. 通过非聚簇索引(如 idx_name)找到主键值 id
    2. 通过主键值 id 回表到聚簇索引(主键索引)获取完整数据行。
  • 覆盖索引路径(无需回表):
    1. 通过复合索引(如 idx_name_age)直接获取所有查询字段(nameage),无需访问聚簇索引。

2.2 InnoDB 的索引结构

  • 聚簇索引(主键索引):
    • 叶子节点存储完整数据行(idnameage)。
  • 非聚簇索引(二级索引):
    • 叶子节点存储索引字段值 + 主键值(如 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 列:显示索引访问类型(如 refrange)。

示例

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. 覆盖索引的设计原则

核心策略

  1. 字段覆盖
    • 查询字段 + 查询条件字段需全部包含在索引中。
    • 示例:SELECT a, b FROM t WHERE a = 1 → 索引 (a, b)
  2. 最左前缀原则
    • 复合索引需按字段顺序使用前缀(如 (a, b, c) 可用于 WHERE a=1 或 WHERE a=1 AND b=2,但不能跳过中间字段)。
  3. 避免过度设计
    • 索引字段过多会增加存储开销和写入成本,需权衡读写性能。

反例

-- 错误:查询字段未覆盖索引,需回表
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 在大偏移量时需扫描并丢弃大量数据,导致性能下降。优化方法包括:

  1. 游标分页:通过记录上一页最后一条的排序字段值,直接定位下一页起点(如 WHERE id > last_id),避免扫描丢弃数据。
  2. 覆盖索引:查询字段被索引覆盖,减少回表操作(如 SELECT name FROM users 使用 (name, age) 索引)。
  3. 延迟关联:先查主键ID,再通过主键回表(如 SELECT * FROM table WHERE id IN (子查询))。
  4. 缓存:将高频分页结果缓存到Redis,减少数据库压力。
  5.  数据库分片:将数据分散到多个节点,降低单表数据量。

例如,在订单查询场景中,使用游标分页可将 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)。
  • 优化方案
    1. 使用 游标分页
      -- 上一页最后一条的订单ID为 1000000
      SELECT * FROM orders WHERE order_id > 1000000 ORDER BY order_id LIMIT 10;
    2. 缓存高频页(如第1-10页)到Redis。

案例2:用户列表分页

  • 问题:用户按姓名排序,翻到第100000页。
  • 优化方案
    1. 创建 覆盖索引 (name, id)
    2. 查询仅需索引字段:
      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高频查询列WHEREJOINORDER BYGROUP BY 高频出现的列优先日志表的 create_time条件频繁,却没加索引
4最左前缀联合索引 (a,b,c) 必须按顺序用,不能跳过 aWHERE 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 一致,避免 filesortORDER 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_ciLIKE '%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 索引失效的常见场景包括:

  1. 联合索引未遵循最左前缀原则:例如 (A,B,C) 联合索引,若查询只用 B 或 C 则失效。

  2. 对索引列使用函数或计算:如 YEAR(create_time) 会导致索引失效。

  3. OR 连接非索引列:若 OR 条件中存在非索引列,可能放弃索引。

  4. LIKE 通配符以 % 开头:无法利用索引前缀匹配。

  5. 隐式类型转换:字段类型与查询值类型不一致。

  6. != 或 NOT IN:排除操作需扫描大量数据。

  7. 范围查询后列失效:联合索引中范围查询后的列无法使用索引。

  8. 数据量小或区分度低:优化器选择全表扫描。

  9. IS NULL/IS NOT NULL:B+Tree 索引通常不存储 NULL。

  10. 优化器主动放弃索引:基于统计信息判断全表扫描更快。

排查方法:使用 EXPLAIN 分析执行计划,关注 key、type 和 Extra 字段。

索引失效的核心场景

场景原因解决方案
联合索引未遵循最左前缀缺少最左列调整查询条件或重建索引
函数/计算操作破坏索引有序性改写查询条件或使用函数索引
OR 连接非索引列全表扫描风险拆分为多个查询或添加索引
LIKE 通配符开头无法定位前缀改用右模糊或全文索引
隐式类型转换类型不一致导致转换确保查询值与字段类型一致
!=/NOT IN排除操作扫描数据多分页或限制查询范围
范围查询后列失效联合索引顺序问题调整索引顺序
数据量小/区分度低全表扫描更快避免在低区分度字段建索引
IS NULL/IS NOT NULLB+Tree 不存储 NULL 值使用默认值替代 NULL
优化器放弃索引统计信息估算误差更新统计信息或强制使用索引

 如何排查索引失效?

  1. 使用 EXPLAIN 分析执行计划
    • key: 实际使用的索引(NULL 表示未使用)。
    • type: 访问类型(ALL 表示全表扫描)。
    • Extra: 是否出现 Using filesortUsing temporary
  2. 示例
    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 优化的核心经验包括: 

    1. 索引优化:合理创建覆盖索引,避免函数操作和隐式类型转换。
    2. 查询语句优化:避免 SELECT *,用 UNION ALL 替代 UNION,优化分页查询。
    3. 表结构设计:垂直拆分减少字段,水平拆分分区大表。
    4. 执行计划分析:通过 EXPLAIN 分析索引使用和扫描行数。
    5. 高级技巧:读写分离、缓存、JOIN 替代子查询。
    6. 典型问题解决:大数据量分页用范围查询,多表关联确保索引和顺序优化。

    • 那你平时对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)
    4NOT 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 并发事务带来的问题要分两层说:

    1. 数据一致性异常(脏读、不可重复读、幻读、更新丢失)。

      1. 脏读:读取其他事务未提交的数据,导致数据无效。
      2. 不可重复读:同一事务内多次读取结果不一致。
      3. 幻读:查询结果集的行数因其他事务的插入 / 删除而变化。
      4. 丢失更新:后提交的事务覆盖前者的修改。
    2. 调度异常(死锁):事务循环等待锁资源,导致系统阻塞。

    解决方案包括:

    • 设置合适的事务隔离级别(如 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

    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 UPDATESELECT ... 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 logredo log
    作用回滚事务、构造旧版本快照(MVCC)崩溃恢复时重放已提交修改
    记录内容逻辑日志:反向操作(delete→insert,update→反向update)物理日志:页号、偏移、新值
    生命周期事务提交后不会立即删除,需等 MVCC 无快照引用事务提交后持久化即可丢弃(Checkpoint)
    存储位置共享表空间 ibdata1 或独立 undo tablespaceredo 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)
      • 从库执行中继日志中的操作
    1. 主库记录二进制日志(Binary Log)
      主库执行 SQL 操作后,会将操作按顺序记录到二进制日志(binlog)中,包括数据修改(增删改)和表结构变更等。binlog 是主从同步的基础,记录了操作的逻辑或物理变更细节。

    2. 从库复制主库的 binlog 到中继日志(Relay Log)
      从库启动一个 I/O 线程,连接主库并请求读取 binlog。主库会启动一个 binlog dump 线程,将新产生的 binlog 事件发送给从库。从库的 I/O 线程接收后,将这些事件写入本地的中继日志(relay log)。

    3. 从库执行中继日志中的操作
      从库的 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)

      http://www.dtcms.com/a/328253.html

      相关文章:

    • GPFS api
    • 在 C语言 中构建安全泛型容器:使用 maybe 实现安全除法
    • 【PCB设计经验】去耦电容如何布局?
    • 力扣top100(day01-04)
    • 企业级的即时通讯平台怎么保护敏感行业通讯安全?
    • 电竞 体育数据 API 应用场景全解析
    • Day50--图论--98. 所有可达路径(卡码网),797. 所有可能的路径
    • Quartz
    • Mybatis源码解读-SqlSession 会话源码和Executor SQL操作执行器源码
    • 谷歌云代理商:用 AI 启航,Gemini 重塑旅游酒店行业新体验
    • 【SpringBoot】07 容器功能 - SpringBoot底层注解的应用与实战 - @ConfigurationProperties配置绑定
    • 从0入门LangGraph,手搓高质量Agent
    • 【自动化运维神器Ansible】playbook文件内变量定义全流程解析
    • 谷歌ADK接入文件操作MCP
    • Linux中Https配置与私有CA部署指南
    • Java 工厂方法模式
    • C++单继承虚函数表探索
    • 京东方 DV133FHM-NN1 FHD13.3寸 工业液晶模组技术档案
    • 玩转Docker | 使用Docker部署Radicale日历和联系人工具
    • [激光原理与应用-250]:理论 - 几何光学 - 透镜成像的优缺点,以及如克服缺点
    • 万物平台模型导入样例大全(实时更新中~)
    • SM4对称加密算法的加密模式介绍
    • JavaEE 初阶第十八期:叩开网络世界的大门(上)
    • ffmpeg-AVFilter 和 Filter Graph 使用指南
    • ffmpeg,ffplay, vlc,rtsp-simple-server,推拉流命令使用方法,及测试(二)
    • Stereolabs ZED相机 选型指南:双目 / 单目、短距 / 长距,如何为机器人视觉系统匹配最优方案?
    • 力扣-394.字符串解码
    • 【模型剪枝2】不同剪枝方法实现对 yolov5n 剪枝测试及对比
    • Homebrew 入门教程(2025 年最新版)
    • 获取虚谷数据库所有表名、表注释、字段名、字段类型、字段注释到word中