1.3 java面试题:索引优化(以 MySQL InnoDB 为例)
明白了我理解错上下文了。你问的“索引优化”和“慢SQL优化”是纯 Java 面试中关于数据库典型是 MySQL的那部分内容跟 ES 没关系。我重新用 Java 技术面的老练视角把这两个核心点讲透。一、索引优化以 MySQL InnoDB 为例1. 索引的本质空间换时间 最左前缀 有序结构BTree索引范围查询、排序、分组利器。Hash索引只等值查询不支持范围。全文索引模糊匹配场景性能仍难控通常让 ES 去干。Java 开发要记住你建的每个索引都要能推动查询走 最左前缀 并尽可能覆盖。2. 建索引的通用原则面试必背-- 联合索引 (a,b,c)相当于创建了-- (a), (a,b), (a,b,c) 三个索引-- 单独查 b 或 c 不走索引CREATEINDEXidx_a_b_cONt(a,b,c);Java 工程师在写代码时就要反向推算 SQLwhere 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 性能的银弹// MyBatis 示例不要 select *Select(SELECT id, status FROM orders WHERE user_id #{userId})ListOrderlistOrders(Param(userId)LonguserId);建索引INDEX idx_uid_status (user_id, status)SQL 全程只读索引不回表QPS 能高几倍。5. 索引失效的经典场景Java 写 SQL 时务必避开WHERE function(col) ?或col 1 ?函数/运算破坏索引。LIKE %keyword%左模糊不走索引除非用全文搜索。隐式类型转换varchar列用数字比较导致索引失效。OR 连接非索引列WHERE a1 OR b2如果 b 没索引全表扫描。NOT IN、!、 大部分情况不走索引。二、慢 SQL 优化Java 项目实战术1. 定位慢 SQL 三板斧慢查询日志mysqldumpslow。Performance Schema开标准备监控。线上实时用SHOW FULL PROCESSLIST或SELECT * FROM information_schema.processlist抓慢语句。Java 端可用 Druid 连接池内置的监控DruidStatFilter直接打印慢 SQLslowSqlMillis配置。2. 拿到慢 SQL 后第一件事EXPLAINEXPLAINSELECT*FROMtWHEREa1ANDb2ORDERBYc;关注字段type从优到差system const eq_ref ref range index ALL。至少要到range。key实际用的索引为空则全表。rows预估扫描行数过大就是信号。Extra出现Using filesort、Using temporary必须干掉。3. 常见 Java 层性能杀手与解决方案循环查库N1MyBatis 用select标签的collection做嵌套映射或手动拼接 ID 列表一次IN查询。深分页LIMIT 1000000,20改写为游标式查询利用覆盖索引子查询SELECT*FROMtJOIN(SELECTidFROMtWHERE...ORDERBYidLIMIT1000000,20)AStmpONt.idtmp.id;或使用 ES 做分页MySQL 只当存储。大数据量统计离线走数仓在线用预估或Redis 计数别在事务表里实时COUNT(*)。复杂连表拆解成多次简单查询在 Java 里Map组装数据库只做数据的存取。4. Java 代码级防患于未然强制索引提示DBA 一般不同意但有时救急SELECT*FROMtFORCEINDEX(idx_a)WHEREa1;小结果集驱动大结果集JOIN 时小表写左边MySQL 优化器通常会选但代码里保证更好。批量操作批量插入用foreach拼接单次 500 条左右批量更新用 CASE WHEN。insertidbatchInsertINSERT INTO t (id, name) VALUESforeachcollectionlistitemitemseparator,(#{item.id}, #{item.name})/foreach/insert分库分表后SQL 必须带分片键否则全库全表扫描。5. 架构层面的优化简单一提读多写少加缓存Redis/本地缓存。写多读少异步削峰MQ 批量写入。大表历史数据归档。读写分离主库写、从库读。面试时如果问“你做过哪些慢 SQL 优化”老练的回答是“定位上通过 Druid 慢日志EXPLAIN 定位到一条深分页 SQLtype 是 ALLExtra 有 filesort。优化方案是先用覆盖索引减少回表再改成分页游标模式利用 JOIN 最小化扫描行数。最终扫描行数从 200 万降到 2 万耗时从 5s 降到 50ms 以内。”这样既有工具链又有代码级方案还有量化结果面试官会觉得你是真操盘过。你抓住的这个点特别好很多开发者写代码时都不太理解这条 MyBatis 注解背后真正的性能含义。我用最简单的表结构把覆盖索引拆解清楚。一、先看一个真实表格假设有一张orders表CREATETABLEorders(idBIGINTPRIMARYKEY,user_idINT,statusTINYINT,amountDECIMAL(10,2),created_atDATETIME,INDEXidx_user_status(user_id,status)-- 联合索引);索引idx_user_status就像一本只有 user_id 和 status 以及主键 id的小册子。二、什么是“回表”你的 MyBatis 代码如果这样写Select(SELECT * FROM orders WHERE user_id #{userId})ListOrderlistOrders(Param(userId)LonguserId);MySQL 的执行过程是先在idx_user_status里找到所有user_id 100的索引记录取出对应主键 id。因为SELECT *需要amount、created_at等字段这些不在索引里所以 MySQL 必须拿着每个主键 id再回到主键索引聚簇索引里读完整行数据。→ 这就是回表一次查询可能产生大量随机 I/O性能下降。三、覆盖索引怎么避开回表改为Select(SELECT id, status FROM orders WHERE user_id #{userId})ListOrderlistOrders(Param(userId)LonguserId);此时查询只需要id、status两个字段。巧了idx_user_status这棵 BTree 里已经包含了user_id、status和id主键被隐含携带。也就是说索引叶子节点已经提供了查询所需全部数据MySQL 根本不需要再回表直接扫描索引就返回结果。这就是覆盖索引Covering Index查询数据全部由索引“覆盖”。在 EXPLAIN 的 Extra 列你会看到Using index而不是NULL或Using where这是直接信号。四、性能差距到底有多大举一个极端的类比未覆盖索引找到 100 万行再回表 100 万次大量磁盘随机读。覆盖只顺序扫描索引叶子节点可能全在内存里。实际测试中覆盖索引的 QPS 可以是原来的数倍到数十倍特别是表宽、查询结果行数多时。五、Java 开发如何刻意使用覆盖索引拒绝SELECT *哪怕开始设计时就需要多字段也要时常审视是否某些场景可以只查小部分列。建联合索引时把查询条件列和结果列组合在一起。比如经常有查询SELECT id, status FROM orders WHERE user_id ? ORDER BY created_at可以尝试建INDEX (user_id, created_at, status)来实现覆盖与排序都走索引。用 MyBatis 的结果映射只映射需要的字段不要在实体里映射大字段。面试时只要你说出“覆盖索引就是查询列被索引完全包含避免回表执行计划 Extra 显示Using index”再加上一个实际的代码对比就非常有说服力了。还有什么细节想深挖直接问。