文章列表只返回 20 条,MySQL 仍可能扫描大量记录。LIMIT 20 描述返回数量,执行计划中的索引名称也只说明访问路径;两者都没有回答数据库读了多少行、过滤了多少行、在哪里完成排序。
下面用一张假设的文章表分析查询。表包含 tenant_id、status、published_at、id 和 title,发布时间非空,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_at 和 id 则对应查询的排序方向。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 毫秒”需要对应的测量记录。
