Java进阶必修课:为什么你建了索引,SQL 还是很慢?
很多 Java 开发在工作几年后,都会遇到一个很真实的问题:
明明给表建了索引,为什么接口还是慢?
明明 EXPLAIN 里显示走了索引,为什么 QPS 还是上不去?
明明只是查个订单列表,为什么数据库 CPU 会突然飙高?
如果你也有类似困惑,那么这篇文章想讲的不是“索引是什么”,而是更重要的一件事:
作为 Java 后端工程师,怎么从“会建索引”进阶到“真正会用索引”。
1. 索引的目的:帮你缩小查找范围
很多人第一次接触索引,会把它理解成“加了索引就一定快”。
这个理解不算错,但太粗糙。
索引的本质是更快地缩小数据范围。
对于 InnoDB 来说,大部分索引底层依赖 B+Tree,它擅长的是:
- 精确查找
- 范围查找
- 有序访问
这也就决定了:你的 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};
很多系统慢就慢在这里。
问题有两个:
- SELECT * 容易导致大量回表
- 深分页 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 走向工程能力的关键一步。
更多推荐


所有评论(0)