
开场先看结果写法耗时一句话原因裸跑 LIMIT 400000,10741ms选择性 96%优化器正确拒绝索引延迟关联 旧索引464ms覆盖失败仍全表扫新复合索引 值写错364ms隐式转换废掉索引一级目录新复合索引 类型对齐197ms覆盖 索引有序免排序keyset 游标OR 写法0.13ms只扫需要的 10 行低区分度列单独索引比全表扫慢 30%成本模型算错账优化器被骗全文所有数字来自 MySQL 8 EXPLAIN ANALYZE 实测warm 值表为 50 万行订单表。一、实验条件与造数表order_info50 万行。48 万行 create_time 落在 2026 年内、2 万行 2025 年order_status1 占 90%条件命中 43.2 万行——保证 offset 40 万落在命中集内部深分页是真的深。列类型以 SHOW FULL COLUMNS 为准order_status varchar(20)、is_deleted tinyint、create_time timestamp。原有索引 idx_order_time(is_deleted,create_time,id) 等五个。〔图1造数验收 total500000 / cond_hits432000〕〔图2SHOW FULL COLUMNS 全列验伤报告〕造数先踩了一个坑值得单说我一开始想用 JMeter 打 50 万线程造订单1 分 48 秒 0/500000 卡死最终只出 1000 单。两个原因业务接口有库存封顶stock1000卖光后全部业务拒绝50 万线程远超单机 JMeter 能力。概念纠偏一句话造数模拟的是几个月业务累积态用 SQL 批量写压测模拟的是瞬时峰值才用 JMeter。改用递归 CTE 生成序列表 一条 INSERT...SELECT分钟级完成。二、进化 1裸跑 741ms——优化器拒绝索引是对的EXPLAIN ANALYZE ... WHERE is_deleted0 AND order_status1 AND create_time2026-01-01 ORDER BY create_time LIMIT 400000,10;计划Table scan 500000 行 Filter 432000 Sort 400010actual 741ms。〔图3t1 全表扫 EXPLAIN〕别骂优化器傻。时间范围命中 48 万/50 万96%走索引意味着 48 万次回表比顺序全表扫更贵。有索引≠该用索引选择性决定一切。三、进化 2延迟关联 旧索引 464ms——覆盖失败子查询只取 id本想吃覆盖索引的红利但 order_status 不在 idx_order_time 里每行仍要回表判断状态 → 覆盖失败 → 优化器继续选全表扫。延迟关联不是万能药子查询的过滤列不在索引里等于白写。〔图4t2 仍全表扫 EXPLAIN〕四、进化 3新复合索引但值写错 364ms——skip scan 与两次抢救失败新建 idx_del_status_ct(is_deleted, order_status, create_time)理想情况是三级目录顺翻等值等值范围且第三级天生有序免排序。实测却出现 Index skip scan Sort 40 万行364ms。〔图5skip scan Sort EXPLAIN〕两次抢救均失败ANALYZE TABLE 刷新统计后计划不变FORCE INDEX 按着头用仍 skip scan。这两次失败是重要证据——不是优化器选错路是路本身断了一级。断在哪order_status 是 varchar(20)SQL 里写 order_status 1数字每行要先隐式转成数字再比较转换加在列上该列无法做范围键复合索引第二级整级作废。skip scan 是 MySQL 的补救动作跳过坏掉的那一级把 0/1/2 三个取值各扫一遍时间范围拼出 48 万行再过滤、再排序。所有多余耗时都来自这套补救。〔图5t3 仍全表扫 EXPLAIN〕五、进化 4加一个引号 197ms——覆盖 有序order_status 1 后重跑Covering index range scanSort 节点消失197ms。〔图6covering range 无 Sort EXPLAIN〕三级目录通顺后顺着读就是排序结果40 万 offset 只是顺读 40 万索引项不回表不排序。一个引号的差距1.9 倍耗时 整个排序开销。六、进化 5keyset 游标 0.13ms——行构造器的语法卫生offset 型分页的本质浪费先捞前 40 万行再扔掉。游标型只问上一页最后一条之后是谁。坑一(create_time, id) (x, y) 行构造器在本表未被推进索引范围838~2382ms坑二游标条件接管后若保留 offset 时代的旧下界范围合并失败。改用 OR 展开并删除旧下界WHERE is_deleted0 AND order_status1 AND (create_time 2026-08-28 07:24:00 OR (create_time 2026-08-28 07:24:00 AND id 344604)) ORDER BY create_time, id LIMIT 10;Index range scanrows10actual 0.13ms。与页深无关这才是生产写法。〔图7OR 写法 range rows10 EXPLAIN〕七、隐式转换专案坑位在手写入口不在 DDLvarchar 存状态是合法的企业约定状态本质是字典码。本项目实体 OrderInfo.orderStatus 为 String与列类型天然对齐ORM 路径全程安全——踩坑的是手写测试脚本的数字字面量。所以治理结论不是改列类型而是类型对齐原则列类型服从团队字典约定所有入口ORM 实体、手写 SQL、报表脚本、DBA 临时查询传参类型必须与列类型对齐对手写 SQL 做评审与 EXPLAIN 抽查。识别隐式转换 (复合索引断路导致速度下降) 的三个签名skip scan 字样、Filter 里残留索引列条件、估算行数与实际差数倍FORCE 与 ANALYZE 无效可佐证路断了。〔图8information_schema 三行类型实锤〕八、低区分度列实验优化器被骗了一次新增 gender tinyint1男2女各 50%与单独索引 idx_gender。g1 默认计划优化器主动选 idx_gender25 万次回表786~1043msg2 IGNORE INDEX 全表扫基准570~609msg3 FORCE与 g1 同计划786ms。〔图9g1/g3 Index lookup 25 万回表 EXPLAIN〕〔图10g2 全表扫基准 EXPLAIN〕成本模型里索引路 cost26428 全表路 cost50330但回表单价被低估实测反转。结论低区分度列的单独索引是双层负资产——最好情况被无视白付写入成本最坏情况被误选本例比没索引还慢 30%。它的正确位置是复合索引的首列等值列对照 is_deleted 同样只有两个取值却在 idx_del_status_ct 中层层裁剪立功。实验后已 DROP INDEX idx_gender 清场。九、总结索引方法论三看 弯路清单三看看选择性命中占比、看位置首列等值/范围列、看覆盖子查询能否免回表。三者皆失索引即负资产。弯路清单① 96% 选择性弃索引它是对的② 覆盖失败延迟关联空转③ 隐式转换断索引一级skip scan 签名④ 行构造器未推范围 旧下界污染⑤ 低区分度单独索引骗过成本模型。防坑三件套建表评审定死列类型与参数对齐约定核心 SQL 的 EXPLAIN 断言进 CI批量导数后 ANALYZE TABLE本次虽非根因但它是排除嫌疑的第一步。十、复现清单造数递归 CTE 序列表 50 万 INSERT...SELECT48 万近期、status 9:0.5:0.5、分钟级时间戳索引原有 idx_order_time(is_deleted,create_time,id)新增 idx_del_status_ct(is_deleted,order_status,create_time)测量纪律EXPLAIN ANALYZE 连跑 3 次取第 3 次计划形态与耗时同录。同系列前篇《Redisson 看门狗勘误》分布式锁压测取证见文末链接。