Py学习  »  DATABASE

MySQL 中使用索引一定有效吗?如何排查索引效果?

鸭哥聊Java • 4 周前 • 68 次点击  

idx_user_id 明明建了,接口还是 2 秒多。

这种问题我一般不先改业务代码,也不先怪 MySQL。先把 SQL 拎出来,看执行计划。索引这东西,不是你建了它就一定用;用了,也不代表快。

线上常见的慢 SQL 大概长这样:

select id, order_no, user_id, pay_status, create_time
from t_order
where user_id = ?
  and date(create_time) = ?
order by create_time desc
limit 20;

表上有索引:

create index idx_user_time on t_order(user_id, create_time);

第一眼看,好像挺对。user_id 有,create_time 也有。

但这个 SQL 我第一眼就不太信,因为它对 create_time 做了函数处理。

跑一下:

explain
select id, order_no, user_id, pay_status, create_time
from t_order
where user_id = 10086
  and date(create_time) = '2026-05-14'
order by create_time desc
limit 20;

如果看到类似这种结果,就别再纠结“我明明加了索引”了。

type: ref
key: idx_user_time
rows: 43892
Extra: Using where; Using filesort

这里容易误判。

key 显示用了 idx_user_time,但 rows 还要扫几万行,Extra 里还有 Using filesort。这就说明索引只是帮你缩小了一点范围,并没有把查询变成真正舒服的查询。

我一般会把 SQL 改成范围查询:




    
select id, order_no, user_id, pay_status, create_time
from t_order
where user_id = ?
  and create_time >= ?
  and create_time < ?
order by create_time desc
limit 20;

Java 里不要偷懒拼 date(),时间边界在代码里算清楚:

public List queryUserOrders(long userId, LocalDate day) {
    LocalDateTime start = day.atStartOfDay();
    LocalDateTime end = day.plusDays(1).atStartOfDay();

    String sql = """
        select id, order_no, user_id, pay_status, create_time
        from t_order
        where user_id = ?
          and create_time >= ?
          and create_time < ?
        order by create_time desc
        limit 20
        "
"";

    return jdbcTemplate.query(
        sql,
        ps -> {
            ps.setLong(1, userId);
            ps.setTimestamp(2, Timestamp.valueOf(start));
            ps.setTimestamp(3, Timestamp.valueOf(end));
        },
        (rs, i) -> new OrderRow(
            rs.getLong("id"),
            rs.getString("order_no"),
            rs.getLong("user_id"),
            rs.getInt("pay_status"),
            rs.getTimestamp("create_time").toLocalDateTime()
        )
    );
}

再看执行计划,比较理想的是:

type: range
key: idx_user_time
rows: 126
Extra: Using index condition

这才像个正常索引该有的样子。

但索引无效,不只函数这一种。

还有一种特别隐蔽:字段类型不一致。

表字段是  varchar

phone varchar(20)

代码里却这么写:

String sql = "select id, phone, name from t_customer where phone = ?";
jdbcTemplate.query(sql, ps -> ps.setLong(1, phoneNumber), mapper);

MySQL 可能会发生隐式类型转换。字段被转换了,索引就可能废掉。这里我一般要求:数据库字段是什么类型,Java 参数就按什么类型传。

ps.setString(1, phoneNumber);

别觉得这点小事没影响,线上很多慢 SQL 就是这种“不起眼”的地方拖出来的。

排查索引效果,我通常按这个顺序来,不绕。

先看慢日志,别凭感觉。

set global slow_query_log = on;
set global long_query_time = 0.5;

捞到 SQL 后,不要只看耗时,要看它到底扫了多少行:

Query_time: 2.318
Rows_examined: 386421
Rows_sent: 20

Rows_sent 才 20,Rows_examined 三十多万,这种 SQL 基本就是索引没吃好。

然后看执行计划。

重点盯这几个字段就够了:

type
key
rows
filtered
Extra

key 为空,说明没用索引。

rows 很大,说明即使用了索引,过滤效果也差。

Extra 里出现 Using filesortUsing temporary,就要看看排序、分组有没有和索引顺序配上。

还有一个坑,单列索引堆太多,不一定有用。

比如:

create index idx_user_id on t_order(user_id);
create index idx_status on t_order(pay_status);
create index idx_time on t_order(create_time);

SQL 是:

where user_id = ?
  and pay_status = ?
order by create_time desc

这种场景我更愿意建联合索引:

create index idx_user_status_time
on t_order(user_id, pay_status, create_time);

不是所有字段都往索引里塞。顺序要按查询条件来。

等值条件放前面,范围和排序字段放后面。这个不是口诀,是因为 B+Tree 就按这个顺序组织数据。你顺序乱了,MySQL 也救不了你。

有时候索引也不是越命中越好。

比如一个状态字段:

pay_status tinyint

全表 90% 都是 pay_status = 1。你给它单独建索引,查询 pay_status = 1 大概率没什么意义。扫索引再回表,还不如直接扫表。

这时候可以查一下区分度:

select pay_status, count(*)
from t_order
group by pay_status;

区分度太低的字段,单独建索引通常很鸡肋。它适合放进联合索引里,配合用户、租户、时间这类字段一起用。

我在线上还会加一段很粗暴的 SQL 耗时日志,不复杂,但很好用:

public  watchSql(String tag, Supplier action) {
    long begin = System.nanoTime();
    try {
        return action.get();
    } finally {
        long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin);
        if (costMs > 300) {
            log.warn("slow-sql-tag={}, costMs={}", tag, costMs);
        }
    }
}

调用时别写一堆虚的 tag,要能反查业务入口:

return watchSql("order.queryUserOrders.userId=" + userId,
        () -> orderDao.queryUserOrders(userId, day));

日志、SQL、执行计划三件东西对上,问题基本就跑不掉。

最后还有个判断。

索引有没有效果,不看“有没有建”,也不只看“有没有用”。要看它有没有减少扫描行数,有没有避免排序,有没有减少回表,有没有让接口耗时真的下来。

索引不是护身符。

慢 SQL 也不是加个索引就完事。

它更像一张现场照片:字段类型、查询写法、数据分布、排序方式、分页方式,哪个地方别扭,执行计划都会露出来。看它就行。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/197306