ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LIMIT 深分页从 200 万行跳到 80 万行:MySQL 8.0 延迟关联改写前后对比

LIMIT 深分页从 200 万行跳到 80 万行:MySQL 8.0 延迟关联改写前后对比 本文摘要深分页翻到 80 万行耗时涨到秒级EXPLAIN显示命中索引。延迟关联内层只取主键、外层回表把 80 万次回表降到 20 次。仅当排序列被索引覆盖且带唯一决胜列时有效按主键排序反而更慢。一、问题与结论订单列表接口GET /orders?page40001size20最终生成SELECT ... FROM orders ORDER BY create_time DESC, id DESC LIMIT 800000, 20前几页几十毫秒跳到深页后进入秒级慢日志里rows_examined与offset N同量级而EXPLAIN写着keyidx_ct_id、Extra: Using index看上去索引全都用上了。标题数字口径先说清楚表orders约 200 万行“跳到 80 万行”指offset 800000、每页 20 行描述的是翻页位置不是任何“扫描行数下降”的口径。下文计数与耗时均标注为待实测。结论三条慢的主因不是索引没命中而是二级索引扫过offset N条条目后还要为每行回表取amount、note这类非索引列。延迟关联把回表次数从offset N降到N索引条目遍历量不变。只有排序列被窄索引覆盖、且ORDER BY带唯一决胜列时才有效排序键是主键时改写反而多一次 join。二、排查与选择依据先定度量口径再谈优化。可对比的数字有三个EXPLAIN ANALYZE的实际行数与耗时、Handler_read_next/Handler_read_key计数、慢日志的rows_examined。三者分别是计划树上的真实循环次数、索引顺序扫描与按键读取次数、执行器检查过的行。改写前后必须用同一口径比较只贴耗时无法区分“省了回表”与“buffer pool 变热”。“联合索引建成却只命中一列”常见三种成因最左前缀WHERE user_id ?用不上idx_ct_id (create_time, id)因为create_time不在条件里。范围列截断WHERE create_time ?是范围条件时id不再参与排序ORDER BY id DESC只能走Using filesort。覆盖列判断InnoDB 二级索引条目隐含主键列内层只取id时KEY (create_time)也够要取user_id就必须回表。判断依据是EXPLAIN的Extra: Using index与key_len不是索引名。替代方案与取舍方案选择条件代价边界延迟关联必须跳页、排序列有窄索引、行宽大或冷数据需要索引SQL 双份维护排序列为主键时无收益不解决COUNT(*)键集分页WHERE (create_time, id) (?, ?)只做上一页/下一页或无限滚动接口需改造、必须有唯一决胜列无法跳到任意页反向扫描LIMIT total-offset-N, N深页多为末尾且 UI 已拿到总数依赖总数准确性中段深页依旧慢产品侧无限滚动或虚拟列表交互可以改需产品与前端配合导出、“跳到第 N 页”仍需兜底不该用延迟关联的场景ORDER BY就是id且取列都在聚簇索引里offset恒小于几千表很小或结果集已被过滤到几十行列表页仍用SQL_CALC_FOUND_ROWS拿总数该特性在 8.0 较新小版本已标记弃用具体版本以本机手册为准。三、关键原理LIMIT offset, N的成本由两部分构成扫过offset N条索引条目再丢弃offset条。只有当ORDER BY顺序与索引一致、且取列被该索引覆盖时才不发生回表一旦要取note这类列被检查的行就要按主键回聚簇索引读一次。延迟关联是内层子查询只取id在窄索引上完成ORDER BY与LIMIT外层再按主键取回 20 行完整记录。省掉的是回表不是索引条目遍历pad CHAR(200)这类宽行会放大收益窄行收益有限。两个正确性要求ORDER BY必须带唯一决胜列id否则排序键重复会造成跨页重复或漏行这是正确性问题而非性能问题改写为 join 后外层必须重复ORDER BYjoin 不保证输出顺序。计划形态还会受optimizer_switch中derived_merge与派生表条件下推影响升级版本或数据分布变化后要重看EXPLAIN。四、可运行示例环境MySQL 8.0 InnoDB先用SELECT VERSION();与SHOW VARIABLES LIKE cte_max_recursion_depth;确认递归造数需临时调大会话变量EXPLAIN ANALYZE需较新 8.0.x以本机版本手册为准。DROPDATABASEIFEXISTSpagedemo;CREATEDATABASEpagedemoDEFAULTCHARACTERSETutf8mb4;USEpagedemo;CREATETABLEorders(idBIGINTUNSIGNEDNOTNULLAUTO_INCREMENT,create_timeDATETIME(3)NOTNULL,user_idBIGINTUNSIGNEDNOTNULL,statusTINYINTNOTNULL,amountDECIMAL(12,2)NOTNULL,noteVARCHAR(255)NOTNULLDEFAULT,padCHAR(200)NOTNULLDEFAULT,PRIMARYKEY(id),KEYidx_ct_id(create_time,id))ENGINEInnoDB;SETSESSIONcte_max_recursion_depth2100000;INSERTINTOorders(create_time,user_id,status,amount,note)WITHRECURSIVE seqAS(SELECT1ASnUNIONALLSELECTn1FROMseqWHEREn2000000)SELECTTIMESTAMPADD(MILLISECOND,n MOD86400000,2024-01-01 00:00:00),1000(n MOD100000),n MOD5,ROUND(1(n MOD9999)/100,2),CONCAT(order-,n)FROMseq;ANALYZETABLEorders;SELECTCOUNT(*)FROMorders;改写前EXPLAINANALYZESELECTid,create_time,user_id,status,amount,noteFROMordersORDERBYcreate_timeDESC,idDESCLIMIT800000,20\G改写后延迟关联EXPLAINANALYZESELECTo.id,o.create_time,o.user_id,o.status,o.amount,o.noteFROMorders oJOIN(SELECTidFROMordersORDERBYcreate_timeDESC,idDESCLIMIT800000,20)tONo.idt.idORDERBYo.create_timeDESC,o.idDESC\G度量口径专用连接执行FLUSH STATUS的作用域与副作用按本机版本与手册核对FLUSHSTATUS;-- 跑上面任一条 SQL随后SHOWSESSIONSTATUSWHEREVariable_nameIN(Handler_read_key,Handler_read_next,Handler_read_rnd_next);预期输出两条 SQL 都返回 20 行EXPLAIN ANALYZE中内层对orders的实际扫描行数应为 800020改写后外层 join 只取 20 行Handler_read_next两者同在 80 万量级索引条目遍历量不变Handler_read_key预期由 80 万量级降到 20 量级。以上均为语义推算、未实测不同版本的 handler 计数口径可能有差异。实际输出在测试机执行后把\G输出原样贴回逐项填入第五节表格。若出现Using filesort先确认排序列是否被范围条件截断、索引是否覆盖取列修完再测。常见失败与修复只写ORDER BY create_time数据里时间戳大量重复 → 相邻两页出现相同行或漏行。原因是排序键不唯一与性能无关修复方式是内外层都写ORDER BY create_time DESC, id DESC。改写后比原写法更慢。原因是排序键本身是id原写法在聚簇索引上一次扫描直接拿行改写多了派生表与 join修复方式是该场景保持原写法不要套延迟关联。五、验证结果与边界指标改写前改写后口径说明返回行数待实测应为 20待实测应为 20以结果集行数为准Handler_read_next待实测待实测二级索引条目遍历量Handler_read_key待实测待实测回表次数本次改写的主要收益来源wall time 中位数≥5 次待实测待实测冷缓存与热缓存分开测慢日志rows_examined待实测待实测需开扩展字段参数名按本机SHOW VARIABLES核对边界与代价线上新增(create_time, id)索引要评估在线 DDL 时长、磁盘占用、从库延迟与回滚预案改写不解决COUNT(*)总数可改为缓存、按需加载或用LIMIT N1只判断“是否有下一页”EXPLAIN计划会随统计信息和版本升级变化需把rows_examined与 P99 纳入监控才能证明改写长期有效。参考资料MySQL 8.0 Reference ManualLIMIT Query OptimizationMySQL 8.0 Reference ManualEXPLAIN Statement含 EXPLAIN ANALYZEMySQL 8.0 Reference ManualStatus VariablesHandler_read_*MySQL 8.0 Reference ManualSwitchable Optimizationsoptimizer_switch
返回列表