1.3 java面试题:索引优化(以 MySQL InnoDB 为例)

明白了,我理解错上下文了。你问的"索引优化"和"慢SQL优化"是纯 Java 面试中关于数据库(典型是 MySQL)的那部分内容,跟 ES 没关系。我重新用 Java 技术面的老练视角,把这两个核心点讲透。


一、索引优化(以 MySQL InnoDB 为例)

1. 索引的本质:空间换时间 + 最左前缀 + 有序结构

  • B+Tree 索引:范围查询、排序、分组利器。
  • Hash 索引:只等值查询,不支持范围。
  • 全文索引:模糊匹配场景,性能仍难控,通常让 ES 去干。

Java 开发要记住:你建的每个索引都要能推动查询走 最左前缀 并尽可能覆盖

2. 建索引的通用原则(面试必背)

sql 复制代码
-- 联合索引 (a,b,c),相当于创建了
-- (a), (a,b), (a,b,c) 三个索引
-- 单独查 b 或 c 不走索引
CREATE INDEX idx_a_b_c ON t (a,b,c);

Java 工程师在写代码时就要反向推算 SQL

  • where a=? and b>? and c=?
    → a 用到索引,b 走范围后 c 不能再走索引(范围断掉)。
  • where a=? order by b
    → a 过滤后利用索引本身有序,不用 filesort
  • where a=? and c=? order by b
    → a 和 c 条件,但 b 缺位,排序会 filesort。

3. 哪些列不适合单独建索引

  • 区分度低的:性别(男/女)只有一半。
  • 频繁更新的:导致页分裂和重建。
  • 长字符串:可考虑前缀索引或哈希列。

4. 覆盖索引是 Java 性能的银弹

java 复制代码
// MyBatis 示例:不要 select *
@Select("SELECT id, status FROM orders WHERE user_id = #{userId}")
List<Order> listOrders(@Param("userId") Long userId);

建索引 INDEX idx_uid_status (user_id, status),SQL 全程只读索引不回表,QPS 能高几倍。

5. 索引失效的经典场景(Java 写 SQL 时务必避开)

  • WHERE function(col) = ?col + 1 = ?:函数/运算破坏索引。
  • LIKE '%keyword%':左模糊不走索引,除非用全文搜索。
  • 隐式类型转换:varchar 列用数字比较,导致索引失效。
  • OR 连接非索引列:WHERE a=1 OR b=2,如果 b 没索引,全表扫描。
  • NOT IN、!=、<> 大部分情况不走索引。

二、慢 SQL 优化(Java 项目实战术)

1. 定位慢 SQL 三板斧

  • 慢查询日志 + mysqldumpslow
  • Performance Schema 开标准备监控。
  • 线上实时SHOW FULL PROCESSLISTSELECT * FROM information_schema.processlist 抓慢语句。

Java 端可用 Druid 连接池内置的监控:DruidStatFilter,直接打印慢 SQL(slowSqlMillis 配置)。

2. 拿到慢 SQL 后第一件事:EXPLAIN

sql 复制代码
EXPLAIN SELECT * FROM t WHERE a=1 AND b>2 ORDER BY c;

关注字段:

  • type :从优到差 system > const > eq_ref > ref > range > index > ALL。至少要到 range
  • key:实际用的索引,为空则全表。
  • rows:预估扫描行数,过大就是信号。
  • Extra :出现 Using filesortUsing temporary 必须干掉。

3. 常见 Java 层性能杀手与解决方案

  • 循环查库(N+1) :MyBatis 用 select 标签的 collection 做嵌套映射,或手动拼接 ID 列表一次 IN 查询。

  • 深分页 LIMIT 1000000,20 :改写为游标式查询,利用 覆盖索引+子查询

    sql 复制代码
    SELECT * FROM t
    JOIN (SELECT id FROM t WHERE ... ORDER BY id LIMIT 1000000,20) AS tmp
    ON t.id = tmp.id;

    或使用 ES 做分页,MySQL 只当存储。

  • 大数据量统计 :离线走数仓,在线用 预估Redis 计数 ,别在事务表里实时 COUNT(*)

  • 复杂连表 :拆解成多次简单查询,在 Java 里 Map 组装,数据库只做数据的存取。

4. Java 代码级防患于未然

  • 强制索引提示 (DBA 一般不同意,但有时救急):

    sql 复制代码
    SELECT * FROM t FORCE INDEX(idx_a) WHERE a=1;
  • 小结果集驱动大结果集:JOIN 时小表写左边(MySQL 优化器通常会选,但代码里保证更好)。

  • 批量操作 :批量插入用 foreach 拼接,单次 500 条左右;批量更新用 CASE WHEN。

    xml 复制代码
    <insert id="batchInsert">
        INSERT INTO t (id, name) VALUES
        <foreach collection="list" item="item" separator=",">
            (#{item.id}, #{item.name})
        </foreach>
    </insert>
  • 分库分表后,SQL 必须带分片键,否则全库全表扫描。

5. 架构层面的优化(简单一提)

  • 读多写少加 缓存(Redis/本地缓存)。
  • 写多读少异步削峰(MQ + 批量写入)。
  • 大表历史数据归档。
  • 读写分离,主库写、从库读。

面试时,如果问"你做过哪些慢 SQL 优化",老练的回答是:

"定位上,通过 Druid 慢日志+EXPLAIN 定位到一条深分页 SQL,type 是 ALL,Extra 有 filesort。优化方案是:先用覆盖索引减少回表,再改成分页游标模式,利用 JOIN 最小化扫描行数。最终扫描行数从 200 万降到 2 万,耗时从 5s 降到 50ms 以内。"

这样既有工具链,又有代码级方案,还有量化结果,面试官会觉得你是真操盘过。

你抓住的这个点特别好,很多开发者写代码时都不太理解这条 MyBatis 注解背后真正的性能含义。我用最简单的表结构把 覆盖索引 拆解清楚。


一、先看一个真实表格

假设有一张 orders 表:

sql 复制代码
CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  user_id INT,
  status TINYINT,
  amount DECIMAL(10,2),
  created_at DATETIME,
  INDEX idx_user_status (user_id, status)  -- 联合索引
);

索引 idx_user_status 就像一本只有 user_id 和 status 以及主键 id 的小册子。


二、什么是"回表"

你的 MyBatis 代码如果这样写:

java 复制代码
@Select("SELECT * FROM orders WHERE user_id = #{userId}")
List<Order> listOrders(@Param("userId") Long userId);

MySQL 的执行过程是:

  1. 先在 idx_user_status 里找到所有 user_id = 100 的索引记录,取出对应 主键 id
  2. 因为 SELECT * 需要 amountcreated_at 等字段,这些不在索引里,所以 MySQL 必须拿着每个主键 id 再回到主键索引(聚簇索引) 里读完整行数据。
    → 这就是回表,一次查询可能产生大量随机 I/O,性能下降。

三、覆盖索引怎么避开回表

改为:

java 复制代码
@Select("SELECT id, status FROM orders WHERE user_id = #{userId}")
List<Order> listOrders(@Param("userId") Long userId);

此时查询只需要 idstatus 两个字段。

巧了,idx_user_status 这棵 B+Tree 里已经包含了 user_idstatusid(主键被隐含携带)。

也就是说,索引叶子节点已经提供了查询所需全部数据 ,MySQL 根本不需要再回表,直接扫描索引就返回结果。

这就是 覆盖索引(Covering Index),查询数据全部由索引"覆盖"。

在 EXPLAIN 的 Extra 列你会看到 Using index,而不是 NULLUsing where,这是直接信号。


四、性能差距到底有多大

举一个极端的类比:

  • 未覆盖:索引找到 100 万行,再回表 100 万次,大量磁盘随机读。
  • 覆盖:只顺序扫描索引叶子节点,可能全在内存里。

实际测试中,覆盖索引的 QPS 可以是原来的 数倍到数十倍,特别是表宽、查询结果行数多时。


五、Java 开发如何刻意使用覆盖索引

  1. 拒绝 SELECT *,哪怕开始设计时就需要多字段,也要时常审视是否某些场景可以只查小部分列。
  2. 建联合索引时把查询条件列和结果列组合在一起
    比如经常有查询 SELECT id, status FROM orders WHERE user_id = ? ORDER BY created_at,可以尝试建 INDEX (user_id, created_at, status) 来实现覆盖与排序都走索引。
  3. 用 MyBatis 的结果映射只映射需要的字段,不要在实体里映射大字段。

面试时,只要你说出"覆盖索引就是查询列被索引完全包含,避免回表,执行计划 Extra 显示 Using index",再加上一个实际的代码对比,就非常有说服力了。

还有什么细节想深挖,直接问。