很多 Java 开发在工作几年后,都会遇到一个很真实的问题:

明明给表建了索引,为什么接口还是慢?
明明 EXPLAIN 里显示走了索引,为什么 QPS 还是上不去?
明明只是查个订单列表,为什么数据库 CPU 会突然飙高?

如果你也有类似困惑,那么这篇文章想讲的不是“索引是什么”,而是更重要的一件事:

作为 Java 后端工程师,怎么从“会建索引”进阶到“真正会用索引”。


1. 索引的目的:帮你缩小查找范围

很多人第一次接触索引,会把它理解成“加了索引就一定快”。

这个理解不算错,但太粗糙。

索引的本质是更快地缩小数据范围
对于 InnoDB 来说,大部分索引底层依赖 B+Tree,它擅长的是:

  1. 精确查找
  2. 范围查找
  3. 有序访问

这也就决定了:你的 SQL 是否“符合索引的使用习惯”,比“有没有索引”更重要。


2. Java 开发必须理解的 3 个索引核心概念

2.1 聚簇索引和二级索引

InnoDB 的主键索引是聚簇索引,数据行本身就存储在主键索引叶子节点上。

普通索引则是二级索引,叶子节点存的是:

  • 索引列值
  • 主键值

这意味着一个常见性能问题:

通过普通索引查到主键后,还要再回主键索引取整行数据,这个过程就叫“回表”。

如果回表次数太多,性能就会明显下降。


2.2 覆盖索引为什么这么重要

如果一个查询需要的列,刚好都在索引里,那么数据库就不需要回表了,这就是覆盖索引。

例如:

SELECT id, status FROM orders WHERE user_id = 1001;

如果索引是:

CREATE INDEX idx_user_id_status ON orders(user_id, status);

那么这条 SQL 在很多场景下就可能形成覆盖索引,性能通常会比 SELECT * 更好。

这也是为什么在 Java 项目里,我一直建议:

能明确字段就不要写 SELECT *。

这不只是代码规范问题,更是性能问题。


2.3 最左前缀原则不是面试题,是生产规则

假设你有一个联合索引:

CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);

那么下面这些查询,索引利用情况完全不同:

WHERE user_id = ?

WHERE user_id = ? AND status = ?

WHERE user_id = ? AND status = ? AND create_time > ?

WHERE status = ?

前三种通常能较好利用索引,最后一种大概率不行。

原因就是联合索引遵循最左前缀原则

联合索引列顺序,必须围绕真实查询条件来设计。


3. 为什么你建了索引,SQL 还是不走?

这是 Java 项目里最常见的误区区。

3.1 在索引列上做函数运算

SELECT * FROM orders WHERE DATE(create_time) = '2026-06-04';

看起来很自然,但这样会让索引失效。

更好的写法是:

SELECT * FROM orders WHERE create_time >= '2026-06-04 00:00:00' AND create_time < '2026-06-05 00:00:00';

3.2 隐式类型转换

比如数据库字段是 varchar,Java 代码里却传了数字参数;或者数据库字段是数字,SQL 却按字符串去比。

这类问题在 MyBatis、JPA 项目里非常常见,而且很隐蔽。
有时候你以为是索引设计有问题,实际上是参数类型不一致导致优化器放弃了理想执行计划。


3.3 LIKE '%xxx' 前置模糊匹配

WHERE name LIKE '%java%'

这种写法很难有效利用 B+Tree 索引,因为树是从左到右有序的。

如果业务允许,尽量改成:

WHERE name LIKE 'java%'

如果必须做全文检索,就该考虑搜索引擎方案,而不是强压 MySQL 普通索引。


3.4 范围条件后面的列,利用率会下降

还是这个联合索引:

(user_id, status, create_time)

当 SQL 变成:

WHERE user_id = ? AND status > ? AND create_time = ?

一旦中间列出现范围查询,后续列的索引利用通常就会受影响。

所以联合索引的设计原则通常是:

高频等值条件在前,范围条件靠后。


4. 一个真实的 Java 接口优化案例

假设我们有一个订单列表接口:

public List<Order> queryOrders(Long userId, Integer status, int pageNo, int pageSize) 
{ 
   return orderMapper.queryOrders(userId, status, pageNo * pageSize, pageSize); 
}

对应 SQL:

SELECT * FROM orders WHERE user_id = #{userId} 
AND status = #{status} ORDER BY create_time DESC 
LIMIT #{offset}, #{pageSize};

很多系统慢就慢在这里。

问题有两个:

  1. SELECT * 容易导致大量回表
  2. 深分页 LIMIT 100000, 20 会扫描大量无效数据

5. 正确的优化思路,不只是“补一个索引”

先建联合索引:

CREATE INDEX idx_user_status_ctime ON orders(user_id, status, create_time);

然后改 SQL,只取必要字段:

SELECT o.* FROM orders o JOIN ( 
SELECT id FROM orders WHERE user_id = #{userId} AND status = #{status} 
ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} ) 
t ON o.id = t.id;

如果分页很深,再进一步优化成“延迟关联”:

SELECT o.* FROM orders o JOIN ( 
SELECT id FROM orders WHERE user_id = #{userId} AND status = #{status} 
ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} ) 
t ON o.id = t.id;

这种写法的核心思想是:

先利用索引快速定位主键,再回表取完整数据,把大范围扫描的成本压缩到最小。

这类优化,在订单、评论、消息、日志等大表场景里非常常见。


6. Java 工程师做索引设计时,最容易犯的 4 个错误

6.1 按字段名建索引,不按查询场景建索引

索引是为 SQL 服务的,不是为表结构服务的。
你要先看接口怎么查,再决定索引怎么建。

6.2 一个字段一个索引,以为数据库会自动完美组合

MySQL 并不会总是把多个单列索引组合成你想要的效果。
很多时候,一个设计合理的联合索引,远比几个单列索引更有效。

6.3 区分度太低的字段也强行建索引

像性别、布尔状态、删除标记这类字段,区分度很低。
单独建索引意义通常不大,更适合放到联合索引中。

6.4 只看“走没走索引”,不看扫描行数

真正该关注的不是 type = ref 还是 range 这么简单,
而是:

  • rows 扫描了多少
  • 是否回表
  • 是否出现 Using filesort
  • 是否出现 Using temporary

会看 EXPLAIN,才算真正进入索引优化的门。


7. 我总结的一套索引设计口诀

送你一套非常适合 Java 开发落地的经验口诀:

等值在前,范围在后,排序跟上,字段别多,能覆盖就覆盖,能不回表就不回表。

翻译成工程语言就是:

  • 优先围绕高频查询设计联合索引
  • 等值过滤字段优先放前面
  • 范围查询字段尽量放后面
  • ORDER BY 尽量和索引顺序一致
  • 查询字段尽量精简,争取覆盖索引
  • 不要为了“保险”滥建索引,因为索引也会拖慢写入

8. 最后说一句:索引能力,是 Java 工程师的分水岭

初级开发写功能,关注的是“能不能查出来”。
中级开发做优化,关注的是“为什么查得慢”。
高级开发做系统,关注的是“这个查询未来数据量涨 10 倍后还能不能扛住”。

而索引,恰恰就是这条成长路径上绕不过去的一关。

你会发现,真正优秀的 Java 工程师,未必天天手写复杂 SQL,
但一定知道:

什么时候该建索引,为什么这么建,以及 SQL 为什么没有按预期工作。

这才是从 CRUD 走向工程能力的关键一步。

更多推荐