首页
学习
活动
专区
圈层
工具
发布

MySQL 索引的最左前缀匹配原则,到底在匹配什么?

一条订单查询突然从几十毫秒拖到几秒,我一般不会先怀疑 MyBatis,更不会急着加缓存。先把 SQL 拿出来跑一遍EXPLAIN。

SELECT id, order_no, pay_amount

FROM trade_order

WHERE order_status = 2

AND created_at >= '2026-08-01 00:00:00';

表上明明有索引:

KEY idx_shop_status_time (shop_id, order_status, created_at)

但执行计划里,这条索引要么没用,要么只做了大范围扫描。

问题就在shop_id没传。

这个索引不是三个独立索引拼在一起,它更像一本先按店铺编号排序,再按订单状态排序,最后按创建时间排序的目录。

索引结构可以简单理解成这样:

shop_id -> order_status -> created_at

查找时必须尽量从左边开始。

这就是 MySQL 联合索引的最左前缀匹配原则。

假设有一个联合索引:

KEY idx_user_scene_time (user_id, biz_scene, created_at)

下面这条 SQL 能比较完整地使用索引:

SELECT id, biz_no

FROM user_record

WHERE user_id = 10086

AND biz_scene = 'PAY'

AND created_at >= '2026-08-01 00:00:00';

MySQL 可以先定位user_id=10086,再从这部分数据里定位biz_scene='PAY',最后根据时间做范围扫描。

查找路径是连续的:

user_id -> biz_scene -> created_at

下面这条也能用:

SELECT id, biz_no

FROM user_record

WHERE user_id = 10086

AND biz_scene = 'PAY';

它使用了索引的前两列。

只查user_id也没问题:

SELECT id, biz_no

FROM user_record

WHERE user_id = 10086;

它使用了联合索引最左边的一列。

但下面这种写法,我第一眼就不太信这个索引能用得舒服:

SELECT id, biz_no

FROM user_record

WHERE biz_scene = 'PAY'

AND created_at >= '2026-08-01 00:00:00';

因为最左边的user_id被跳过去了。

联合索引的数据先按照user_id排序。不同用户下面,才继续按照biz_scene排序。现在连用户是谁都不知道,MySQL 很难直接定位某一段连续的PAY数据。

这里有个容易混淆的地方:最左匹配看的是索引列顺序,不是 SQL 条件的书写顺序。

这两条 SQL 对索引的使用通常没有区别:

WHERE user_id = 10086

AND biz_scene = 'PAY'

WHERE biz_scene = 'PAY'

AND user_id = 10086

优化器会重新分析条件。把user_id写在 SQL 最后,不等于违反最左匹配。

真正需要盯住的,是建索引时列的排列:

(user_id, biz_scene, created_at)

还有一个经常把人绕进去的地方:遇到范围查询后,后面的列往往不能继续用于缩小索引扫描区间。

比如:

SELECT id, biz_no

FROM user_record

WHERE user_id = 10086

AND created_at >= '2026-08-01 00:00:00'

AND biz_scene = 'PAY';

索引还是:

(user_id, created_at, biz_scene)

MySQL 可以根据user_id定位,再根据created_at做范围扫描。但进入时间范围以后,后面的biz_scene通常不能继续参与索引范围定位。

它可能通过索引下推过滤,和完全没作用不是一回事,但扫描范围已经没有进一步缩小。

所以业务里如果常见查询是“用户 + 场景 + 时间范围”,索引通常应该这样排:

KEY idx_user_scene_time (user_id, biz_scene, created_at)

而不是:

KEY idx_user_time_scene (user_id, created_at, biz_scene)

等值条件一般放前面,范围条件往后放。当然不能只背这一句,列的区分度、排序需求、真实查询比例都要看。

Java 项目里更容易踩坑的是动态 SQL。

比如一个查询接口允许前端不传shopId:

public record OrderQuery(

      Long shopId,

      Integer status,

      LocalDateTime startTime

) {

}

代码直接按非空条件拼查询:

public List<OrderSummary> findOrders(OrderQuery query) {

  StringBuilder sql = new StringBuilder("""

          SELECT id, order_no, pay_amount

          FROM trade_order

          WHERE 1 = 1

          """);

  List<Object> args = new ArrayList<>();

  if (query.shopId() != null) {

      sql.append(" AND shop_id = ?");

      args.add(query.shopId());

  }

  if (query.status() != null) {

      sql.append(" AND order_status = ?");

      args.add(query.status());

  }

  if (query.startTime() != null) {

      sql.append(" AND created_at >= ?");

      args.add(Timestamp.valueOf(query.startTime()));

  }

  sql.append(" ORDER BY created_at DESC LIMIT 200");

  return jdbcTemplate.query(

          sql.toString(),

          orderSummaryMapper,

          args.toArray()

  );

}

代码没什么语法问题,麻烦在于查询组合变了。

当shopId为空,只剩下:

WHERE order_status = ?

AND created_at >= ?

原来的联合索引是:

(shop_id, order_status, created_at)

最左列断了。

这种情况不是改一下 Java 写法就能解决。要么业务上要求必须传shopId,要么为跨店铺查询单独设计索引,不能指望一个联合索引把所有查询组合都照顾到。

LIKE查询也遵循类似判断:

WHERE user_name LIKE 'dong%'

可以从确定的前缀开始定位,通常能够利用索引。

而下面这个:

WHERE user_name LIKE '%dong'

开头是不确定的,普通 B+Tree 索引很难直接找到扫描起点。

至于 MySQL 8 的索引跳跃扫描,确实可能在缺少最左列时利用联合索引,但它受数据分布、基数和成本估算影响。线上设计索引时,我不会把性能押在优化器“可能帮忙”上。

遇到联合索引问题,不要只看key字段里有没有索引名。我一般还会看:

EXPLAIN ANALYZE

SELECT ...

重点确认实际扫描了多少行、用了几个索引列、过滤发生在哪一层。

最左前缀不是一句“必须从左往右写 SQL”。它真正约束的是:联合索引只有左侧连续的列,才能稳定形成有效的查找路径。

中间断一列,后面的排序关系还在,但对当前查询未必有用了。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Od9IRARgiBADnvMEMTTIcfzQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券