老板问我为什么查询慢,我甩给他一个 EXPLAIN,结果他闭嘴了

2026-04-16 10 0

凌晨两点,你被报警电话吵醒。线上数据库CPU飙升,接口响应时间从200ms涨到了8秒。老板在群里疯狂@你,你打开数据库一顿操作,敲了一行命令:

SELECT * FROM orders WHERE user_id = 12345 AND status = "paid" ORDER BY created_at DESC LIMIT 20;

Explain一下?好,你explain了。然后你看着输出的10行数据陷入了沉思——type: ALLrows: 500000Using filesort。你知道这不妙,但你跟老板说不清楚。

今天我们来彻底搞定EXPLAIN,看完这篇,你也能甩回去一个眼神让他闭嘴。

EXPLAIN不是只看type的

很多人看EXPLAIN只看两样东西:typerows。type是ALL就说要加索引,rows太多就说数据量大。这没错,但太浅了。就像看体检报告只盯着白细胞看,其他指标全略过——你可能会错过真正的问题。

先建一张测试表:

CREATE TABLE order_items (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_id BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  quantity INT DEFAULT 1,
  price DECIMAL(10,2),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_order_id (order_id),
  INDEX idx_product_id (product_id),
  INDEX idx_created (created_at)
) ENGINE=InnoDB;

然后插点数据,跑个查询:

EXPLAIN SELECT * FROM order_items
WHERE product_id = 100
ORDER BY created_at DESC
LIMIT 10;

典型输出:

id: 1
select_type: SIMPLE
table: order_items
type: ref
possible_keys: idx_product_id
key: idx_product_id
key_len: 8
rows: 1200
filtered: 10.00
Extra: Using where; Using index; Backward index scan

光看type=ref感觉还行,但rows=1200,filtered=10%——这意味着要扫1200行才拿到10条结果。过滤性很差,索引选得不够精准。

key_len:你以为选对了索引,其实没有

这是最容易忽略的字段。key_len告诉你实际上这个查询用了多少字节的索引。

看这个例子:

ALTER TABLE order_items ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT "";

假设有一个联合索引:

INDEX idx_name_product (name, product_id)

然后查:

EXPLAIN SELECT * FROM order_items
WHERE name = "张三" AND product_id = 100;

如果key_len=257,说明只用了name那一列(VARCHAR(255)变长,1~2字节头 + 实际长度),product_id根本没进索引。

为什么?因为MySQL优化器认为255长度的VARCHAR已经足够特殊了,没必要再拼上product_id。

key_len的正确看法:数值越小,说明索引被用得越少。如果你的查询明明有两个条件,但key_len只有一个字段的长度,那第二个条件根本没进索引。

Using filesort不一定是坏事——但你要知道什么时候是

一说filesort,全公司都要给你加索引。这太片面了。

MySQL的filesort有两种模式:

模式一:索引覆盖的filesort

当ORDER BY的列正好在索引里,且SELECT的列也在同一索引中,MySQL可以按索引顺序直接返回,连filesort都懒得用了。

EXPLAIN SELECT id, created_at FROM order_items
WHERE product_id = 100
ORDER BY created_at DESC;

Extra里写的是Using index——这叫索引覆盖扫描(Covering Index),性能极好,不需要回表,filesort?不存在。

模式二:回表后的filesort

EXPLAIN SELECT * FROM order_items
WHERE product_id = 100
ORDER BY created_at DESC;

Extra:Using where; Using index; Using filesort

这里虽然用了索引(Using index),但因为SELECT * 需要所有列,MySQL先按索引找到主键,再回表拿全量数据,最后filesort排。这个filesort就要注意了——数据量大的话,灾难现场。

模式三:真正的filesort(双路排序)

Extra:Using filesort,没有Using index

这时MySQL分成两步:先按条件拿数据到sort buffer,用快排排好,再返回。这是最慢的情况,特别是sort buffer装不下的时候,会临时写磁盘。

怎么判断?

EXPLAIN FORMAT=JSON
SELECT * FROM order_items
WHERE product_id = 100
ORDER BY created_at DESC
LIMIT 10;

sort_buffer_size和实际数据量。

Backward index scan:PostgreSQL玩剩下的,MySQL 8.0也有了

上面的输出里有个Backward index scan,很多人没见过。这是MySQL 8.0引入的优化。

当你ORDER BY created_at DESC的时候,如果created_at是升序索引,MySQL以前要全扫一遍再倒序。现在它可以倒着扫索引,性能提升明显。

但如果你同时有Using filesort,那这个优化就废了——filesort不支持预排序倒扫。

子查询的EXPLAIN:别被id骗了

EXPLAIN SELECT * FROM (
  SELECT order_id, MAX(created_at) as last_time
  FROM order_items
  WHERE product_id > 0
  GROUP BY order_id
) t
JOIN orders o ON t.order_id = o.id;

EXPLAIN输出里有id列,表示执行顺序。id越大越先执行。但这里有个坑:derived表(子查询)的id可能比外层大,不代表它真的先跑

MySQL会对子查询进行物化(materialization),意思是把子查询结果存成临时表。物化之前,优化器会估算临时表的行数,这个估算值就藏在rows里。如果rows虚高,说明选错了执行计划。

解决方案:把子查询改成JOIN,或者加hint强制走索引

SELECT * FROM order_items oi
INNER JOIN (
  SELECT order_id, MAX(created_at) as last_time
  FROM order_items
  WHERE product_id > 0
  GROUP BY order_id
) t ON t.order_id = oi.order_id;

压箱底工具:EXPLAIN ANALYZE

MySQL 8.0之后有个大杀器:EXPLAIN ANALYZE。它不仅显示计划,还会告诉你实际执行的时间和行数。

EXPLAIN ANALYZE
SELECT * FROM order_items
WHERE product_id = 100
ORDER BY created_at DESC
LIMIT 10;

输出类似:

-> Limit: 10 row(s)  (actual time=0.021..0.035 rows=10 loops=1)
  -> Backward index scan on order_items using idx_product_id
      (actual time=0.018..0.030 rows=10 loops=1)
      Filter: (product_id = 100)
      Rows removed by filter: 1190

这里清清楚楚:扫了1200行,过滤掉1190行,才拿到10条。真正的性能瓶颈。

这就是EXPLAIN和EXPLAIN ANALYZE的区别——一个是预测,一个是实测。

实战优化流程

说一千道一万,给个可以直接抄的流程:

  1. 跑EXPLAIN ANALYZE,找actual time最大的节点
  2. 看rows和实际返回的比例,过滤性差就加条件或改索引
  3. 检查key_len,确认所有条件列都被索引覆盖
  4. 如果有filesort,看能不能改成覆盖索引
  5. 多表JOIN时,按id从小到大读,id相同从上往下读,检查驱动表是不是小表

最后

EXPLAIN是门手艺,不是玄学。那些能把慢查询快速定位出来的人,不是因为他们天赋异禀,而是因为他们真的把EXPLAIN的每个字段都读懂了。

下次老板问你为什么慢,你不需要急着加索引。给他看看EXPLAIN ANALYZE的输出,告诉他:实际扫描了1200行,实际耗时30毫秒,过滤掉了99%。这个数字,比任何口头解释都有说服力。

当然,如果老板继续追问,那就不是技术问题了。

相关文章

别再把API设计成一坨屎了:RESTful设计避坑指南
为什么你的API总是被人骂?一位老油条的掏心窝子经验
你的HTTP客户端正在悄悄偷走你的性能:那些连接池不会告诉你的事
OpenClaw 使用经验分享:一只小龙虾的AI调教记录
当别人还在纠结服务器配置,我已经在用AI工具搞钱了
为什么你的API总是被人吐槽?一次把REST设计说清楚

发布评论