MySQL 慢查询怎么查:把过滤、排序与回表放进同一份计划

用一个文章列表查询说明 MySQL 8.4 执行计划的读法、联合索引的取舍与游标分页边界,避免凭索引命中或单次耗时宣布优化成功。

·6 min
B 树索引沿高亮分支定位到表中的小段记录,其余扫描范围被淡化。

文章列表只返回 20 条,MySQL 仍可能扫描大量记录。LIMIT 20 描述返回数量,执行计划中的索引名称也只说明访问路径;两者都没有回答数据库读了多少行、过滤了多少行、在哪里完成排序。

下面用一张假设的文章表分析查询。表包含 tenant_idstatuspublished_atidtitle,发布时间非空,ID 唯一。示例采用 MySQL 8.4 语法,所有性能判断都需要目标数据验证。

把慢请求对应到具体 SQL

用户等待页面的时间包括网关处理、连接池排队、SQL 执行、数据传输和页面渲染。数据库里观察到的语句耗时只覆盖其中一部分。如果查询本身很快,时间花在连接池里,加索引未必能解决问题。

定位时要保留参数和环境。同一条模板 SQL,查询大租户和小租户可能表现不同;一个租户刚发布了大量文章,也会改变状态和时间的分布。只拿一组“容易命中”的参数比较,会遗漏真正的慢请求。

下面的例子读取某个租户已发布的文章,按发布时间倒序排列。同一时间的记录再按 ID 排序,保证排序有确定的次序:

EXPLAIN FORMAT=TREE
SELECT id, title, published_at
FROM posts
WHERE tenant_id = 42
  AND status = 'PUBLISHED'
ORDER BY published_at DESC, id DESC
LIMIT 20;

这是估计计划。需要实际执行信息时,可以在有资源预算的环境使用:

EXPLAIN ANALYZE
SELECT id, title, published_at
FROM posts
WHERE tenant_id = 42
  AND status = 'PUBLISHED'
ORDER BY published_at DESC, id DESC
LIMIT 20;

MySQL 的 EXPLAIN ANALYZE 会执行语句,并报告迭代器的实际信息。它不是一个没有负载的查看命令,慢查询也可能在分析时继续占用资源。PostgreSQL 的 EXPLAIN (ANALYZE, BUFFERS) 不能用于这个 MySQL 示例。具体语法与计时含义见 MySQL 8.4 EXPLAIN 文档

顺着数据流读执行计划

先看访问节点取得多少行,再看过滤后留下多少,最后看排序和限制发生在哪里。若扫描取得很多行、过滤只留少量结果,说明访问路径没能提前缩小范围。若过滤后仍有大量行需要排序,排序本身也可能成为主要工作。

估计行数与实际行数相差很大,是检查统计信息和数据倾斜的线索,但不能单凭这个差异宣布优化器选错。还要比较可用访问路径、排序要求和回表代价。

嵌套循环尤其需要注意 loops。一个节点单次返回很少记录,执行很多轮后仍可能做了大量工作。MySQL 报告的迭代器时间包含其子节点的工作,存在多轮时还涉及平均值;不要把整棵树上的时间直接相加,当成总查询耗时。

对于这个列表查询,可能存在两种不理想的计划:沿发布时间索引扫描大量其他租户的数据,再过滤出当前租户;或者先按租户和状态找到许多文章,再排序取前 20 条。前者怕目标租户稀疏,后者怕目标集合太大。

让联合索引同时服务过滤和排序

可以评估以下候选索引,DDL 仅供测试环境讨论,不应直接在生产执行:

CREATE INDEX idx_posts_tenant_status_time_id
ON posts (tenant_id, status, published_at DESC, id DESC);

前两个字段把同一租户、同一状态的记录聚在一段索引范围内,published_atid 则对应查询的排序方向。MySQL 的联合索引文档说明了左前缀访问规则。目标数据上的实际计划决定这个索引是否合适。

这个索引没有包含 title。读取标题通常还需要访问表记录,不能称它为覆盖本例查询的索引。如果访问路径最终只需要取少量记录,这部分代价可能可接受。把长标题加入索引会增加存储和写入成本,未必划算。

查询条件改变后,索引收益也会变化。例如不限制 status、一次读取多个状态,或者增加另一个范围条件,都可能改变排序与访问方式。不要把一条查询的优化结果推广到所有列表接口。

索引还有维护成本。发布或撤回文章会修改状态相关的索引项,新增索引也会增加写入工作。在发布前,应评估建索引期间的磁盘空间、锁等待、复制延迟和取消方案。在线 DDL 能力取决于实际表结构与版本,先在同版本测试环境验证。

用上一页末尾定位下一页

LIMIT 100000, 20 即使走了合适的索引,也可能需要越过许多记录。按上一页最后一条记录继续读取,可以避免用页码表达大偏移量。沿用前面的排序,游标条件示例如下:

SELECT id, title, published_at
FROM posts
WHERE tenant_id = 42
  AND status = 'PUBLISHED'
  AND (
    published_at < '2026-07-01 08:00:00.000000'
    OR (
      published_at = '2026-07-01 08:00:00.000000'
      AND id < 9001
    )
  )
ORDER BY published_at DESC, id DESC
LIMIT 20;

实际接口使用参数绑定,并把租户、筛选条件和排序版本纳入游标的约束,不能信任客户端自行指定的租户。时间精度必须与数据库一致。

游标分页不提供跨请求的数据库快照。如果记录在两页之间更改发布时间或状态,仍可能造成用户观察到的变化。需要稳定导出时,应使用额外的一致性方案;需要跳到任意页时,也要承认游标与页码有不同的产品取舍。

留下能复现实验的材料

比较前后方案时,保留相同的数据范围和参数样本,区分冷缓存、暖缓存与并发压力。执行计划负责说明数据库做了什么,接口观测负责说明用户等待是否改善。两者都需要,单次手动查询的耗时不能代表线上 P95。

记录脱敏 SQL、表和索引定义、实际计划、采样时间及负载条件,其他工程师才能复现实验。缺少这些材料时,结论只能描述索引可能减少哪类扫描;“10 秒降到 50 毫秒”需要对应的测量记录。