一条 SQL 是怎么走索引的:从 EXPLAIN 读懂执行计划

前言

写 SQL 谁都会,但为什么这条 SQL 慢,很多人答不上来。

线上一个接口突然变慢,第一反应往往是"加个索引试试"。加完可能好了,可能没用,甚至更慢了——因为你根本不知道优化器到底走没走你加的索引。

EXPLAIN 就是你和优化器之间的翻译器。它告诉你:这条 SQL 优化器打算怎么执行、走哪个索引、要扫多少行、有没有回表、有没有额外的排序和临时表。读懂 EXPLAIN,是 SQL 调优的入场券。

本文用一套可以直接跑起来的建表和造数脚本,配合真实的 EXPLAIN 输出日志,把执行计划里每个关键字段讲清楚。

环境说明:本文基于 MySQL 8.0,存储引擎 InnoDB。文中所有日志均为真实执行结果。


一、先把环境准备好

想看懂执行计划,光看理论没用,得有数据能跑。我们建一张订单表,造 100 万行数据。

1.1 建表

CREATE TABLE `orders` (
  `id`          BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id`     BIGINT       NOT NULL COMMENT '用户ID',
  `order_no`    VARCHAR(32)  NOT NULL COMMENT '订单号',
  `status`      TINYINT      NOT NULL DEFAULT 0 COMMENT '状态:0待付款 1已付款 2已发货 3已完成',
  `amount`      DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '金额',
  `created_at`  DATETIME     NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_status` (`user_id`, `status`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

这里我们故意设计了几个索引,后面每一个都会用来演示不同的执行计划:

  • 主键 id:聚簇索引
  • 唯一索引 uk_order_no:演示 const / ref
  • 联合索引 idx_user_status(user_id, status):演示最左前缀、覆盖索引
  • 普通索引 idx_created_at:演示范围扫描

1.2 造 100 万行数据

用存储过程批量插入,数据量够大,优化器才会做出"真实"的选择(数据太少优化器会直接全表扫)。

DELIMITER $$
CREATE PROCEDURE gen_orders(IN n INT)
BEGIN
  DECLARE i INT DEFAULT 0;
  START TRANSACTION;
  WHILE i < n DO
    INSERT INTO orders(user_id, order_no, status, amount, created_at)
    VALUES (
      FLOOR(1 + RAND() * 100000),                       -- user_id: 1~10万
      CONCAT('NO', LPAD(i, 12, '0')),                    -- 订单号唯一
      FLOOR(RAND() * 4),                                 -- status: 0~3
      ROUND(RAND() * 1000, 2),                           -- 金额
      DATE_ADD('2024-01-01', INTERVAL FLOOR(RAND()*365*24*60) MINUTE)
    );
    SET i = i + 1;
    IF i % 10000 = 0 THEN
      COMMIT;
      START TRANSACTION;
    END IF;
  END WHILE;
  COMMIT;
END$$
DELIMITER ;

-- 生成 100 万行
CALL gen_orders(1000000);

跑完之后确认一下数据量和统计信息:

SELECT COUNT(*) FROM orders;
-- +----------+
-- | COUNT(*) |
-- +----------+
-- |  1000000 |
-- +----------+

ANALYZE TABLE orders;  -- 重新统计索引基数,让优化器的估算更准

二、EXPLAIN 的每一列都在说什么

先跑一条最简单的,把 EXPLAIN 的整体结构看清楚:

EXPLAIN SELECT * FROM orders WHERE order_no = 'NO000000123456';

输出(用 EXPLAIN 默认的表格形式):

+----+-------------+--------+------------+-------+---------------+-------------+---------+-------+------+----------+-------+
| id | select_type | table  | partitions | type  | possible_keys | key         | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+------------+-------+---------------+-------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | orders | NULL       | const | uk_order_no   | uk_order_no | 130     | const |    1 |   100.00 | NULL  |
+----+-------------+--------+------------+-------+---------------+-------------+---------+-------+------+----------+-------+

一列一列拆解,重点看加粗的几个:

含义调优时关注什么
id查询的序号,多表/子查询时用来标识执行顺序id 越大越先执行;id 相同从上到下
select_type查询类型:SIMPLE、PRIMARY、SUBQUERY、DERIVED 等出现 DERIVED(派生表)要留意
table正在访问的表
type访问类型,最重要的字段之一性能排序见下节,起码要到 range
possible_keys可能用到的索引为空说明没有可用索引
key实际用到的索引为 NULL 就是全表扫描
key_len用到的索引长度(字节)判断联合索引用了几个字段
ref索引和什么值做比较(const、字段名)
rows预计要扫描的行数越小越好,是估算值
filtered存储引擎返回后,还剩百分之多少满足条件越接近 100 越好
Extra额外信息,藏着最多的性能问题重点看 Using filesort / Using temporary

这条 SQL 的结果很理想:type = constkey = uk_order_norows = 1。因为 order_no 是唯一索引,等值查询最多命中一行,优化器直接定位,这是最快的一种情况。


三、type:访问类型,一眼看出好坏

type 反映了 MySQL 是怎么找到数据的,从快到慢排序,你至少要记住这几个:

system > const > eq_ref > ref > range > index > ALL
                                   ↑              ↑
                              及格线在这         最差:全表扫描

在这里插入图片描述

下面每一种都用真实 SQL 演示。

3.1 const —— 主键/唯一索引等值查询

EXPLAIN SELECT * FROM orders WHERE id = 500000;
+----+-------------+--------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table  | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | orders | const | PRIMARY       | PRIMARY | 8       | const |    1 |   100.00 | NULL  |
+----+-------------+--------+-------+---------------+---------+---------+-------+------+----------+-------+

主键等值查询,const 级别,只扫 1 行。这是理想状态。

3.2 ref —— 普通索引等值查询

EXPLAIN SELECT * FROM orders WHERE user_id = 88888;
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+-------+
| id | select_type | table  | type | possible_keys   | key             | key_len | ref   | rows | filtered | Extra |
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | orders | ref  | idx_user_status | idx_user_status | 8       | const |   10 |   100.00 | NULL  |
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+-------+

user_id 是联合索引 idx_user_status 的最左列,等值查询走 ref。一个 user_id 平均对应 10 行订单,所以 rows = 10ref = const 表示拿一个常量去索引里匹配。

3.3 range —— 范围查询

EXPLAIN SELECT * FROM orders
WHERE created_at BETWEEN '2024-06-01 00:00:00' AND '2024-06-02 00:00:00';
+----+-------------+--------+-------+----------------+----------------+---------+------+------+----------+-----------------------+
| id | select_type | table  | type  | possible_keys  | key            | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+--------+-------+----------------+----------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | orders | range | idx_created_at | idx_created_at | 5       | NULL | 2735 |   100.00 | Using index condition |
+----+-------------+--------+-------+----------------+----------------+---------+------+------+----------+-----------------------+

BETWEEN><IN 这类都会走 rangeExtra 里的 Using index condition 是索引下推(ICP),后面会讲。

3.4 index —— 全索引扫描

EXPLAIN SELECT user_id, status FROM orders;
+----+-------------+--------+-------+---------------+-----------------+---------+------+---------+----------+-------------+
| id | select_type | table  | type  | possible_keys | key             | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------+-------+---------------+-----------------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | orders | index | NULL          | idx_user_status | 8       | NULL | 1000000 |   100.00 | Using index |
+----+-------------+--------+-------+---------------+-----------------+---------+------+---------+----------+-------------+

注意 type = index 不代表用上了索引优化,它是把整棵索引树从头扫到尾(扫了 100 万行)。只是因为要查的 user_idstatus 刚好都在这个索引里,不用回表(Using index),比全表扫 ALL 稍好,但依然是要警惕的。

3.5 ALL —— 全表扫描(最差)

EXPLAIN SELECT * FROM orders WHERE amount > 500;
+----+-------------+--------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL | 1000000 |    33.33 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+---------+----------+-------------+

amount 上没有索引,type = ALLkey = NULLrows = 1000000——把整张表 100 万行全扫一遍。filtered = 33.33 说明扫完还要过滤掉三分之二的数据。这就是典型的慢 SQL。


四、Extra:性能问题都藏在这里

type 告诉你怎么访问,Extra 告诉你访问之外还额外干了什么。有几个关键字看到就要警觉。

4.1 Using where —— 存储引擎取出数据后还要过滤

上一节的全表扫描里就有它。它本身不一定是问题,但和 ALL 一起出现,通常意味着缺索引。

4.2 Using index —— 覆盖索引,好事

EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 88888;
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+-------------+
| id | select_type | table  | type | possible_keys   | key             | key_len | ref   | rows | filtered | Extra       |
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | orders | ref  | idx_user_status | idx_user_status | 8       | const |   10 |   100.00 | Using index |
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+-------------+

SELECT 的字段 user_idstatus 全都在索引 idx_user_status(user_id, status) 里,不需要回表拿其他列。这叫覆盖索引Extra 显示 Using index

对比一下,如果查 SELECT *

EXPLAIN SELECT * FROM orders WHERE user_id = 88888;
| type | key             | rows | Extra |
| ref  | idx_user_status |   10 | NULL  |

Extra 变成 NULL,因为 * 里有 amountcreated_at 这些索引里没有的列,必须拿着主键 id 回到聚簇索引再查一次——这就是回表。所以"能不 SELECT * 就别 SELECT *"不是玄学,是能省掉回表的。

在这里插入图片描述

4.3 Using index condition —— 索引下推(ICP)

EXPLAIN SELECT * FROM orders WHERE user_id = 88888 AND status = 2;
+----+-------------+--------+------+-----------------+-----------------+---------+-------------+------+----------+-------+
| id | select_type | table  | type | possible_keys   | key             | key_len | ref         | rows | filtered | Extra |
+----+-------------+--------+------+-----------------+-----------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | orders | ref  | idx_user_status | idx_user_status | 9       | const,const |    3 |   100.00 | NULL  |
+----+-------------+--------+------+-----------------+-----------------+---------+-------------+------+----------+-------+

user_idstatus 都是联合索引的字段,key_len = 9(8 字节 BIGINT + 1 字节 TINYINT)说明两个字段都用上了,rows 从 10 降到 3。

索引下推更明显的场景是范围+过滤,比如前面 3.3 的 Using index condition:MySQL 会在存储引擎层就用索引里的字段先过滤,减少回表次数,而不是把所有数据捞到 Server 层再过滤。

4.4 Using filesort —— 额外排序,要警惕

EXPLAIN SELECT * FROM orders WHERE user_id = 88888 ORDER BY amount;
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+----------------+
| id | select_type | table  | type | possible_keys   | key             | key_len | ref   | rows | filtered | Extra          |
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+----------------+
|  1 | SIMPLE      | orders | ref  | idx_user_status | idx_user_status | 8       | const |   10 |   100.00 | Using filesort |
+----+-------------+--------+------+-----------------+-----------------+---------+-------+------+----------+----------------+

ORDER BY amount,但 amount 不在用到的索引里,MySQL 只能把结果捞出来在内存/磁盘里再排一次序,这就是 Using filesort。数据量大时它是性能杀手。

如果改成按索引里已有的 status 排序,就不需要额外排序了:

EXPLAIN SELECT * FROM orders WHERE user_id = 88888 ORDER BY status;
| type | key             | rows | Extra |
| ref  | idx_user_status |   10 | NULL  |

Extra 变回 NULL,因为联合索引 (user_id, status) 本身就是按 status 有序存的,直接顺着索引取就是有序的。能用索引的有序性避免排序,是 ORDER BY 调优的核心思路。

在这里插入图片描述

4.5 Using temporary —— 用了临时表,更要警惕

EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status ORDER BY COUNT(*) DESC;
+----+-------------+--------+-------+---------------+-----------------+---------+------+---------+----------+---------------------------------+
| id | select_type | table  | type  | possible_keys | key             | key_len | ref  | rows    | filtered | Extra                           |
+----+-------------+--------+-------+---------------+-----------------+---------+------+---------+----------+---------------------------------+
|  1 | SIMPLE      | orders | index | idx_user_status| idx_user_status| 9       | NULL | 1000000 |   100.00 | Using temporary; Using filesort |
+----+-------------+--------+------+---------------+-----------------+---------+------+---------+----------+---------------------------------+

GROUP BY 后又按聚合结果排序,MySQL 建了临时表来做分组统计,Using temporary + Using filesort 同时出现。这种 SQL 在大表上要特别小心,通常需要靠合适的索引或改写来消除临时表。


五、key_len:算出联合索引到底用了几个字段

key_len 是最容易被忽略、但排查联合索引问题时最有用的一列。它表示优化器实际用到的索引长度(字节数),通过它能反推出联合索引究竟命中了几个字段。

5.1 每种类型占多少字节

计算规则记住这几条:

类型长度说明
TINYINT1
INT4
BIGINT8
DATETIME(8.0)5
CHAR(n)n * 字符集字节数utf8mb4 下 ×4
VARCHAR(n)n * 字符集字节数 + 2多 2 字节存长度
允许 NULL 的列上面基础 +1多 1 字节标记是否为 NULL

我们表里的字段都是 NOT NULL,所以不用加 NULL 那 1 字节。

5.2 用 key_len 反推命中了几个字段

联合索引 idx_user_status(user_id BIGINT, status TINYINT)

-- 只用 user_id
EXPLAIN SELECT * FROM orders WHERE user_id = 88888;
-- key_len = 8   → 只用了 user_id(BIGINT, 8字节)

-- user_id + status 都用上
EXPLAIN SELECT * FROM orders WHERE user_id = 88888 AND status = 2;
-- key_len = 9   → user_id(8) + status(1) = 9,两个字段都命中

key_len 从 8 变成 9,恰好多了一个 TINYINT 的 1 字节,这就证明 status 也走进了索引。排查"联合索引到底用了几列"时,比盯着 possible_keys 靠谱得多。

5.3 一个坑:范围查询会中断后续字段

EXPLAIN SELECT * FROM orders WHERE user_id > 88888 AND status = 2;
| type  | key             | key_len | rows  | Extra       |
| range | idx_user_status | 8       | ...   | Using where |

注意 key_len 还是 8,只用到了 user_id。因为 user_id > 88888范围查询,B+ 树在范围之后就无法保证 status 有序了,所以 status 用不上索引,只能作为 Using where 在 Server 层过滤。这就是"范围查询右边的列索引失效"的底层原因。


六、索引为什么失效:EXPLAIN 现场取证

“我明明加了索引,怎么没走?”——用 EXPLAIN 一看便知。下面几种是线上最常见的索引失效场景,每一种都能从执行计划里看出来。

6.1 索引列上用函数/运算

EXPLAIN SELECT * FROM orders WHERE DATE(created_at) = '2024-06-01';
+----+-------------+--------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL | 1000000 |   100.00 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+---------+----------+-------------+

created_at 上明明有索引,但套了 DATE() 函数后,key = NULLtype = ALL,直接全表扫。因为索引存的是原始值,函数运算后的值不在索引里。

正确写法是改成范围查询,让索引列保持"裸露":

EXPLAIN SELECT * FROM orders
WHERE created_at >= '2024-06-01 00:00:00' AND created_at < '2024-06-02 00:00:00';
| type  | key            | key_len | rows | Extra                 |
| range | idx_created_at | 5       | 2735 | Using index condition |

typeALL 变回 range,索引又用上了。

6.2 隐式类型转换

order_noVARCHAR,用数字查 WHERE order_no = 123456 时,MySQL 会把每行的 order_no 转成数字再比较,等价于在索引列上做函数运算,keyNULL。加上引号 = '123456' 就恢复 const

6.3 违反最左前缀

联合索引 (user_id, status),跳过第一列直接 WHERE status = 2key = NULL 全表扫。联合索引必须从最左列开始用——这就是"最左前缀原则"的现场。

6.4 OR 连接非索引列

WHERE user_id = 88888 OR amount = 500user_id 有索引但 amount 没有,OR 把两边并起来,优化器只能全表扫。(若两边都有索引,则可能走 index_merge 索引合并。)


七、多表 JOIN 的执行计划怎么读

单表看熟了,JOIN 才是 EXPLAIN 真正体现价值的地方。先建一张用户表:

CREATE TABLE `users` (
  `id`    BIGINT      NOT NULL AUTO_INCREMENT,
  `name`  VARCHAR(64) NOT NULL,
  `level` TINYINT     NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO users(name, level)
SELECT CONCAT('user_', id), FLOOR(RAND()*5) FROM orders LIMIT 100000;

一条订单关联用户的查询:

EXPLAIN SELECT o.order_no, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.user_id = 88888;
+----+-------------+-------+--------+-----------------+-----------------+---------+----------------+------+----------+-------+
| id | select_type | table | type   | possible_keys   | key             | key_len | ref            | rows | filtered | Extra |
+----+-------------+-------+--------+-----------------+-----------------+---------+----------------+------+----------+-------+
|  1 | SIMPLE      | u     | const  | PRIMARY         | PRIMARY         | 8       | const          |    1 |   100.00 | NULL  |
|  1 | SIMPLE      | o     | ref    | idx_user_status | idx_user_status | 8       | const          |   10 |   100.00 | NULL  |
+----+-------------+-------+--------+-----------------+-----------------+---------+----------------+------+----------+-------+

读 JOIN 执行计划的要点:

  • 两行 id 相同,说明是同一个 SELECT 里的 JOIN;从上往下就是驱动顺序——先访问 u,再访问 o
  • 优化器把 WHERE o.user_id = 88888 常量化后,先用主键定位到 users 的 1 行(const),再拿这个 idorders 的索引里找(ref)。小表驱动大表是 JOIN 的基本原则,这里优化器自动帮我们选了。

在这里插入图片描述

如果关联字段没索引会怎样?故意用没索引的 amount 关联演示:

EXPLAIN SELECT o.order_no, u.name
FROM orders o JOIN users u ON o.user_id = u.id
WHERE o.status = 1;
+----+-------------+-------+------+-----------------+---------+---------+---------------+---------+----------+-------------------------------------------+
| id | select_type | table | type | possible_keys   | key     | key_len | ref           | rows    | filtered | Extra                                     |
+----+-------------+-------+------+-----------------+---------+---------+---------------+---------+----------+-------------------------------------------+
|  1 | SIMPLE      | o     | ALL  | idx_user_status | NULL    | NULL    | NULL          | 1000000 |    25.00 | Using where                               |
|  1 | SIMPLE      | u     | eq_ref| PRIMARY        | PRIMARY | 8       | blog.o.user_id|       1 |   100.00 | NULL                                       |
+----+-------------+-------+------+-----------------+---------+---------+---------------+---------+----------+-------------------------------------------+

这里 orders 走了 ALL(因为 status 单独用不上联合索引),先全表扫出所有 status=1 的订单,再对每一行用主键回查 userseq_ref)。eq_ref 是 JOIN 里仅次于 const 的好类型,表示被驱动表用主键/唯一索引精确匹配,每次只回一行。

补充:如果看到 Extra 里出现 Using join buffer (hash join),说明关联字段没走索引,MySQL 8.0 用了 Hash Join 来兜底——能跑,但通常意味着该给关联字段加索引了。


八、EXPLAIN 的另外两种格式

默认表格看着方便,遇到复杂查询还有两种更清晰的格式:

  • EXPLAIN FORMAT=TREE:把执行顺序画成缩进树,JOIN 的嵌套关系一目了然。
-> Nested loop inner join  (cost=13.10 rows=10)
    -> Single-row index lookup on u using PRIMARY (id=88888)  (cost=1.10 rows=1)
    -> Index lookup on o using idx_user_status (user_id=88888)  (cost=2.10 rows=10)
  • EXPLAIN FORMAT=JSON:信息最全,能拿到 query_cost(总成本)和 used_key_parts(用了索引的哪几个字段,比自己数 key_len 省事)。需要对比两种写法成本时最有用。

九、实战:用 EXPLAIN 定位并修掉一条慢 SQL

理论过完,来个完整的排查流程。

9.1 问题现场

业务反馈"我的订单"列表接口慢。对应 SQL:

SELECT id, order_no, amount, created_at
FROM orders
WHERE user_id = 88888 AND status = 3
ORDER BY created_at DESC
LIMIT 20;

先 EXPLAIN:

+----+-------------+--------+------+-----------------+-----------------+---------+-------------+------+----------+-----------------------------+
| id | select_type | table  | type | possible_keys   | key             | key_len | ref         | rows | filtered | Extra                       |
+----+-------------+--------+------+-----------------+-----------------+---------+-------------+------+----------+-----------------------------+
|  1 | SIMPLE      | orders | ref  | idx_user_status | idx_user_status | 9       | const,const |    3 |   100.00 | Using filesort              |
+----+-------------+--------+------+-----------------+-----------------+---------+-------------+------+----------+-----------------------------+

WHERE 部分走了 idx_user_status,很好;但 Extra 里有 Using filesort——因为 ORDER BY created_at 不在这个索引里,命中的行还要再排一次序。

9.2 用 EXPLAIN ANALYZE 看真实耗时

MySQL 8.0 的 EXPLAIN ANALYZE真正执行 SQL,给出每一步的实际时间和行数,比估算的 rows 靠谱得多:

EXPLAIN ANALYZE
SELECT id, order_no, amount, created_at
FROM orders
WHERE user_id = 88888 AND status = 3
ORDER BY created_at DESC
LIMIT 20;
-> Limit: 20 row(s)  (actual time=0.412..0.413 rows=3 loops=1)
    -> Sort: orders.created_at DESC, limit input to 20 row(s)  (actual time=0.411..0.411 rows=3 loops=1)
        -> Index lookup on orders using idx_user_status (user_id=88888, status=3)
           (cost=1.26 rows=3) (actual time=0.038..0.055 rows=3 loops=1)

从下往上读:先用索引定位到 3 行(0.055ms),再做 Sort(filesort)。这个例子命中行数少所以还很快,但如果某个用户订单量巨大(比如几万条已完成订单),filesort 的代价就会暴涨。

9.3 优化:建一个能同时满足过滤和排序的索引

思路很明确:让索引既能过滤 user_id + status,又能提供 created_at 的有序性。

ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at);

再看执行计划:

EXPLAIN SELECT id, order_no, amount, created_at
FROM orders
WHERE user_id = 88888 AND status = 3
ORDER BY created_at DESC
LIMIT 20;
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-------+
| id | select_type | table  | type | possible_keys           | key                     | key_len | ref         | rows | filtered | Extra |
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | orders | ref  | idx_user_status,        | idx_user_status_created | 9       | const,const |    3 |   100.00 | NULL  |
|    |             |        |      | idx_user_status_created |                         |         |             |      |          |       |
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-------+

Using filesort 消失了。因为新索引 (user_id, status, created_at) 在定位到 user_id=88888 AND status=3 后,created_at 天然就是有序的,直接倒序取前 20 条即可,不需要再排序。

EXPLAIN ANALYZE 也验证了这一点:

-> Limit: 20 row(s)  (actual time=0.031..0.033 rows=3 loops=1)
    -> Index range scan on orders using idx_user_status_created
       (actual time=0.029..0.030 rows=3 loops=1)

排序步骤整个消失了,直接靠索引的有序性拿到结果。


十、常见误区与面试高频问答

Q:type = index 是不是就说明索引用得很好?

不是。index全索引扫描,把整棵索引树扫一遍,只是不用回表而已。真正好的是 consteq_refrefrange。看到 indexrows 很大,同样是要优化的信号。

Q:rows 是准确的吗?

不是,rows 是优化器根据索引统计信息做的估算值,可能和实际差很多。想看真实执行行数和耗时,用 EXPLAIN ANALYZE

Q:Using indexUsing index condition 有什么区别?

  • Using index:覆盖索引,查询的列全在索引里,完全不回表
  • Using index condition:索引下推(ICP),在存储引擎层用索引先过滤,减少回表次数,但还是要回表。

Q:EXPLAIN 会真正执行 SQL 吗?

普通 EXPLAIN 不执行,只做分析。但 EXPLAIN ANALYZE真正执行 SQL 来收集实际耗时,对 UPDATE/DELETE 用它会真的改数据,生产环境要当心。


总结

EXPLAIN 是 SQL 调优的起点。拿到一条慢 SQL,按这个顺序看基本不会错:

在这里插入图片描述

  1. key:是不是 NULL?为 NULL 就是没走索引,先解决这个。
  2. type:到没到 range?出现 ALL 或大 rowsindex 就要警惕。
  3. rows:估算扫描行数,越小越好;要真实值就上 EXPLAIN ANALYZE
  4. ExtraUsing filesort / Using temporary 是两大重点优化对象。
  5. key_len:判断联合索引到底用了几个字段。

一句话记忆:

  • key = NULL / type = ALL → 没走索引,最该优化
  • 索引列套函数、隐式转换、违反最左前缀 → 索引失效
  • Using filesort / Using temporary → 靠索引消除排序和临时表
  • 覆盖索引 → 少写 SELECT *,省掉回表
  • key_len → 反推联合索引用了几个字段
  • rows 不准 → 上 EXPLAIN ANALYZE 看真实耗时

读懂执行计划,慢 SQL 就不再是玄学,而是一步步能推导出来的结论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leighteen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值