数据库入门到初级 · 第 20/22 篇。实验口径:MySQL 8.4、InnoDB、utf8mb4。案例:小型商城。
一条 SQL 慢,是数据库慢、没索引,还是一次取了太多数据?不要先猜。执行计划能告诉我们数据库准备怎样访问数据,是定位问题的第一步。它提供线索,但不是一张“看到某个词就判错”的评分表。
1. 小表不适合证明性能规律
系列订单表只有 4 行,全表扫描很可能足够快。为了观察不同访问方式,本篇单独创建演示表,不修改商城结构。以下建表和造数只执行一次。
CREATE TABLE perf_digits_demo (n INT PRIMARY KEY) ENGINE=InnoDB;
INSERT INTO perf_digits_demo VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
CREATE TABLE perf_orders_demo (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL,
total_amount DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB;
INSERT INTO perf_orders_demo (user_id, created_at, total_amount)
SELECT MOD(seq.n, 100) + 1,
DATE_ADD('2026-01-01 00:00:00', INTERVAL seq.n SECOND),
100.00
FROM (
SELECT a.n + 10*b.n + 100*c.n + 1000*d.n AS n
FROM perf_digits_demo AS a
CROSS JOIN perf_digits_demo AS b
CROSS JOIN perf_digits_demo AS c
CROSS JOIN perf_digits_demo AS d
) AS seq;
SELECT COUNT(*) FROM perf_orders_demo;
10 个数字做四次组合,产生 10000 行,分布在 100 个用户下。这个分布是教学构造,与真实订单不一定相同。
2. 先查看没有目标索引的计划
EXPLAIN FORMAT=TRADITIONAL
SELECT id, created_at
FROM perf_orders_demo
WHERE user_id = 10
ORDER BY created_at DESC, id DESC
LIMIT 20;
显式指定传统格式,方便对照下面的字段。输出会因版本、统计信息和环境不同而变化,本文不提供虚构的实测截图或固定耗时。
| 字段 | 初步理解 |
|---|---|
type |
访问方式,例如全表扫描 ALL、索引匹配 ref、范围访问 range |
possible_keys |
优化器认为可能相关的索引 |
key |
实际选择使用的索引 |
rows |
预计需要检查的行数,不是最终返回条数 |
filtered |
估计经过条件筛选后保留的比例 |
Extra |
排序、覆盖读取等补充信息 |

3. 创建索引,再观察变化
CREATE INDEX idx_perf_user_created_id
ON perf_orders_demo (user_id, created_at, id);
ANALYZE TABLE perf_orders_demo;
EXPLAIN FORMAT=TRADITIONAL
SELECT id, created_at
FROM perf_orders_demo
WHERE user_id = 10
ORDER BY created_at DESC, id DESC
LIMIT 20;
观察是否选择了新索引、预计扫描量是否下降、是否仍需要额外排序。理论上这个索引与查询匹配,但最终以实际计划为准。如果没有变化,先检查索引存在、查询一致、统计信息有效,不要马上强制使用索引。
4. 三个容易误读的地方
Using filesort 表示需要额外排序,不保证排序一定落盘,也不自动说明查询不可接受。Using index 常说明能用索引覆盖获取所需列,不等于“这条查询已经最优”。rows=100 是估计值,不意味着执行一定恰好读取 100 行。
扫描全表也不一定是坏事。查找极少数记录与导出全表,对应的合理访问策略不同。评价计划前,先看任务究竟需要多少数据。
5. EXPLAIN ANALYZE 会实际执行查询
EXPLAIN ANALYZE
SELECT id, created_at
FROM perf_orders_demo
WHERE user_id = 10
ORDER BY created_at DESC, id DESC
LIMIT 20;
它能展示实际执行相关信息,但会运行查询,并带来实际负载。先在这个独立练习表使用;不要把普通 EXPLAIN 与实际执行分析混为一谈。MySQL 官方 EXPLAIN 说明 介绍了不同格式与执行行为。
比较性能时还要控制缓存、并发、返回列和参数。一次偶然快几毫秒,不足以证明方案稳定更好。也不要把数据库内部耗时直接当作接口端到端响应时间。
练习与答案
练习:SQL 只返回 20 行,为什么 rows 可能很大? 因为数据库可能先扫描大量候选记录,再过滤和排序,最后只返回 20 行。返回少不代表处理少。
练习:把用户过滤条件移除后,原索引一定适合按时间列出全部订单吗? 不一定。索引先按用户排列,跨用户的全局时间排序需要重新评估。
延伸阅读
上一篇:第 19 篇 · 系列目录 · 下一篇:第 21 篇