news 2026/11/5 7:52:24

一条 MySQL 慢查询排查:我先后建了 4 个索引,只有一个真正生效

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一条 MySQL 慢查询排查:我先后建了 4 个索引,只有一个真正生效

本文摘要:给 500 万行订单表建索引时,常见误区是给每个 WHERE 列都建单列索引。MySQL 通常只用一个索引,需要的是等值列在前、范围列在后的联合索引。

一、问题与结论

merchant_order表约 500 万行,一条业务查询连续跑了 2 秒以上:

SELECT*FROMmerchant_orderWHEREcustomer_id=20034ANDprovince='Zhejiang'ANDorder_type='online'ANDorder_date>='2024-01-01 00:00:00'ORDERBYorder_dateDESCLIMIT50;

我先后建了 4 个索引——两次单列、一次列序错误的联合、一次列序正确的联合。逐个用EXPLAIN验证后,只有第四个把查询从秒级拉到了毫秒级。本文复现这四次实验,解释背后的优化器规则,并说明联合索引的适用边界。

二、排查与选择依据

四次尝试

尝试 1:idx_province(province)

province基数只有 10。单个省份命中约 50 万行,优化器估算回表成本接近全表扫描,key为NULL。低基数列建单列索引基本没有意义。

尝试 2:idx_order_date(order_date)

order_date基数高,优化器会选择(type: range),但order_date >= '2024-01-01'范围内仍有百万级行,回表次数远超LIMIT 50的需求。

尝试 3:idx_order_date_province(order_date, province)

联合索引但列序错误。order_date是范围条件,province虽然在索引中,却无法参与 B-Tree 导航。key_len只有 5——加了一个列,几乎等于没加。

尝试 4:idx_customer_province_orderdate(customer_id, province, order_date)

等值条件的列前置。customer_id和province通过等值匹配精确缩小 B-Tree 定位范围,order_date在剩余空间内做范围扫描。这是唯一一个让ORDER BY order_date DESC也由索引提供、消除了Using filesort的方案。

EXPLAIN 关键指标对比

以下为四次尝试的EXPLAIN输出对照(rows为优化器估算值;实际输出请在你自己的 MySQL 8.0 实例上执行验证,数据量和统计信息会影响估算结果):

索引typekeykey_lenrowsExtra
idx_provinceALLNULLNULL≈全表行数Using filesort
idx_order_daterangeidx_order_date5数十万Using filesort
idx_order_date_provincerangeidx_order_date_province5数十万Using filesort
idx_customer_province_orderdaterefidx_customer_province_orderdate90约百级无filesort

key_len是判断索引利用程度最直接的指标:尝试 1 的key为NULL,索引完全未被使用;尝试 3 的key_len与尝试 2 相同(均为 5),证明province被跳过;尝试 4 的key_len(customer_id4 字节 +province81 字节 +order_date5 字节)覆盖三列,说明全部参与导航。EXPLAIN FORMAT=JSON中的cost_info还能看到优化器估算的读取行数和总成本。

替代方案与取舍

当联合索引仍不满足需求时,以下方案各有适用边界:

方案选择条件代价不适用场景
覆盖索引查询列少、回表开销大索引变大,写入变慢,磁盘占用增加SELECT列多、写入频繁的表
改写 SQL 缩小范围业务允许缩小时间窗口或改用游标分页需修改业务逻辑需要跨大范围查询的报表场景
分区表以时间范围查询为主、历史数据极少访问分区管理复杂度增加查询条件不含分区键的场景

什么时候不该用本文方案:如果查询本身需要扫描大量行(如SELECT *且无有效过滤条件),联合索引无法解决——应改写查询或分批处理。FORCE INDEX 能强制优化器选某索引,但过度使用会掩盖问题,索引删除后查询会直接报错。

三、关键原理

最左前缀规则

联合索引(a, b, c)的 B-Tree 按a排序,a相同时按b排序。当a是范围条件时,b和c无法参与 B-Tree 导航,只能在回表后做过滤。这就是尝试 3 失效的直接原因——order_date是范围,后面的province白建了。

隐式类型转换

如果customer_id定义为VARCHAR(10),而查询传入WHERE customer_id = 20034(数字、无引号),MySQL 会将列值逐行转换为数字比较——而不是将常量转为字符串。这导致索引静默失效:EXPLAIN中key为NULL。修复:应用层改用setString()传参,或确保列类型与查询常量类型一致。

函数作用于索引列

WHERE DATE(order_date) = '2024-06-01'中,DATE()函数在 B-Tree 导航前被应用,优化器无法使用索引。MySQL 8.0.13+ 可建函数索引INDEX((DATE(order_date)))解决;更早版本只能改写为WHERE order_date >= '2024-06-01' AND order_date < '2024-06-02'。

四、可运行示例

以下脚本可在 MySQL 8.0 实例上直接执行,复现上述四次实验。数据生成约需 5–15 分钟,若想快速验证可将行数降为 50 万。

建表与造数据

CREATETABLEmerchant_order(idBIGINTNOTNULLAUTO_INCREMENTPRIMARYKEY,customer_idINTNOTNULL,provinceVARCHAR(20)NOTNULL,order_typeVARCHAR(10)NOTNULL,order_dateDATETIMENOTNULL,amountDECIMAL(10,2)NOTNULLDEFAULT0,statusTINYINTNOTNULLDEFAULT0)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;DELIMITER$$CREATEPROCEDUREgenerate_test_data()BEGINDECLAREiINTDEFAULT0;WHILEi<5000000DOINSERTINTOmerchant_order(customer_id,province,order_type,order_date,amount,status)VALUES(1+FLOOR(RAND()*100000),ELT(1+FLOOR(RAND()*10),'Zhejiang','Jiangsu','Guangdong','Shandong','Sichuan','Hubei','Henan','Fujian','Anhui','Hunan'),ELT(1+FLOOR(RAND()*3),'online','offline','cod'),DATE_ADD('2021-01-01 00:00:00',INTERVALFLOOR(RAND()*1095)DAY),ROUND(RAND()*5000,2),FLOOR(RAND()*4));SETi=i+1;IFi%1000=0THENCOMMIT;ENDIF;ENDWHILE;COMMIT;END$$DELIMITER;CALLgenerate_test_data();ANALYZETABLEmerchant_order;

ANALYZE TABLE是必要步骤——不刷新统计信息,优化器可能基于过时的rows估算做出错误决策。

四次实验

-- 尝试 1ALTERTABLEmerchant_orderADDINDEXidx_province(province);EXPLAINSELECT*FROMmerchant_orderWHEREcustomer_id=20034ANDprovince='Zhejiang'ANDorder_type='online'ANDorder_date>='2024-01-01 00:00:00'ORDERBYorder_dateDESCLIMIT50;-- 尝试 2ALTERTABLEmerchant_orderDROPINDEXidx_province;ALTERTABLEmerchant_orderADDINDEXidx_order_date(order_date);-- 执行相同 EXPLAIN 查询-- 尝试 3ALTERTABLEmerchant_orderDROPINDEXidx_order_date;ALTERTABLEmerchant_orderADDINDEXidx_order_date_province(order_date,province);-- 执行相同 EXPLAIN 查询-- 尝试 4(唯一生效)ALTERTABLEmerchant_orderDROPINDEXidx_order_date_province;ALTERTABLEmerchant_orderADDINDEXidx_customer_province_orderdate(customer_id,province,order_date);-- 执行相同 EXPLAIN 查询

五、验证结果与边界

预期输出

尝试 1–3 的EXPLAIN中rows均在数十万至百万级别,耗时为秒级;尝试 4 的rows约百级别,耗时为毫秒级,且Extra中无Using filesort。key_len从NULL/5/5 变为 90,是索引真正生效的标志。实际输出请在你自己的实例上执行上述脚本验证——不同数据分布下rows估算值会有所偏差,但key_len和type的变化模式一致。

翻车场景:数据分布破坏索引效果

即使idx_customer_province_orderdate设计正确,当customer_id = 1(系统"默认账户",累计约 120 万笔订单)时,索引在该customer_id下仍有百万级条目。120 万 / 500 万 ≈ 24%,优化器可能回退到全表扫描。

修复方向:缩小order_date时间范围;对极端账号数据做分表;或用覆盖索引减少回表。

隐式类型转换的常见失败

建表时若customer_id定义为VARCHAR(10),而 JDBC 使用setInt()传参(MyBatis 中参数不带引号),EXPLAIN中key为NULL——索引静默失效。改用setString()或统一列类型即可恢复。

函数调用导致索引失效

WHERE DATE(order_date) = '2024-06-01'使索引无法被使用。MySQL 8.0.13+ 可建函数索引;更早版本应改写为范围查询。函数索引会增加索引维护成本,仅在查询模式固定、无法改写时才值得引入。

思考

当业务确实需要跨大范围查询时,应该优先改写查询缩小扫描范围,还是投入资源做分区或分表?FORCE INDEX 在统计信息偏差时能救急,但长期来看是不是应该修根因而非压症状?

参考资料

  • MySQL 8.0 Reference Manual: Optimization
  • MySQL 8.0 Reference Manual: Multiple-Column Indexes
  • MySQL 8.0 Reference Manual: Type Conversion
  • MySQL 8.0 Reference Manual: EXPLAIN
  • MySQL 8.0 Reference Manual: CREATE INDEX
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 7:51:49

Buildroot 开机自启动:让你的 ARM64 程序随 Linux 启动运行

Buildroot 开机自启动&#xff1a;让你的 ARM64 程序随 Linux 启动运行 每次启动 QEMU&#xff0c;都要登录系统&#xff0c;再手动输入 /usr/bin/debug_demo。学习时这样做没问题&#xff0c;但如果这是一个采集数据、监测状态的设备&#xff0c;难道每次开机都要有人敲命令吗…

作者头像 李华
网站建设 2026/11/5 7:42:01

Horizon MCP集成教程:将新闻处理能力接入你的AI助手生态

Horizon MCP集成教程&#xff1a;将新闻处理能力接入你的AI助手生态 【免费下载链接】Horizon &#x1f4e1; Your own AI-powered news radar. Generates daily briefings in English & Chinese. | 用 AI 构建你专属的新闻雷达 项目地址: https://gitcode.com/GitHub_Tr…

作者头像 李华
网站建设 2026/11/5 7:38:57

零基础实现IEC61850(三)报告服务端

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 7:36:46

ADS122U04+dsPIC33EP实现PT100/PT1000高精度测温方案详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 7:36:23

基于ADS122U04与STM32F446ZE的RTD高精度温度测量方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华