2026 慢 SQL 优化手册:EXPLAIN 深度解读 + 9 类索引失效场景(生产避坑)

张开发
2026/4/16 1:19:16 15 分钟阅读

分享文章

2026 慢 SQL 优化手册:EXPLAIN 深度解读 + 9 类索引失效场景(生产避坑)
2026 慢 SQL 优化手册EXPLAIN 深度解读 9 类索引失效场景生产避坑前言做开发/运维的同学几乎都踩过慢SQL的坑——线上接口突然卡顿、数据库CPU打满、日志里全是查询超时报警排查半天发现明明建了索引SQL却还是走全表扫描用EXPLAIN查看执行计划一堆参数看得眼花缭乱根本不知道问题出在哪。更头疼的是面试时慢SQL优化、EXPLAIN解读、索引失效场景几乎是必考题很多人背了理论却不会落地实操。2026年MySQL 8.4版本普及索引机制有了新优化但索引失效的核心坑点没变反而多了一些版本专属的避坑点。本文全程不堆理论、不玩概念只讲可直接复制的实操命令、能快速上手的EXPLAIN解读方法、9类生产高频索引失效场景含优化方案不管是线上排查慢SQL还是备战面试看完就能用小白也能轻松上手。核心重点慢SQL优化的核心不是盲目建索引而是用EXPLAIN找到“索引失效”的根因针对性优化——90%的慢SQL都是索引用错了一、前置准备3步定位慢SQL生产必做不做优化等于瞎忙活优化慢SQL前先找到“拖慢系统”的那条SQL这一步不能省否则只会白费功夫。以下操作适配MySQL 8.0含8.4可直接复制命令执行。开启慢查询日志临时永久捕获执行时间超标的SQL# 1. 临时开启重启MySQL失效适合快速排查show variables like ‘%slow_query%’; – 查看当前慢查询配置set global slow_query_log ON; – 开启慢查询日志set global long_query_time 1; – 阈值执行时间超过1秒的SQL记录生产建议0.5秒set global log_queries_not_using_indexes ON; – 记录未使用索引的SQL重点排查set global slow_query_log_file ‘/var/lib/mysql/slow_query.log’; – 日志存储路径需授权2. 永久开启修改配置文件my.cnf重启生效[mysqld]slow_query_log ONslow_query_log_file /var/lib/mysql/slow_query.loglong_query_time 1log_queries_not_using_indexes ON快速筛选慢SQLMySQL自带工具不用手动翻日志# 查看执行时间最长的10条慢SQL按时间排序mysqldumpslow -s t -t 10 /var/lib/mysql/slow_query.log查看最频繁的5条慢SQL按执行次数排序mysqldumpslow -s c -t 5 /var/lib/mysql/slow_query.log核心判断标准找到慢SQL后先看2个关键指标日志中可直接看到Query_time执行时间超过1秒就需要优化Rows_examined扫描行数扫描行数远大于返回行数大概率是索引失效。实操提示生产环境中慢查询日志建议长期开启搭配监控工具如Prometheus可实时告警避免慢SQL引发生产事故。二、EXPLAIN 深度解读3个核心字段秒判索引是否失效不用记所有参数很多人觉得EXPLAIN复杂其实不用记所有字段只要盯紧type、key、Extra这3个核心字段就能快速判断SQL是否走索引、哪里出了问题——这是排查慢SQL的“万能钥匙”生产和面试都高频用到。先看基础用法在慢SQL前加EXPLAIN执行后查看结果示例EXPLAINSELECTid,order_noFROMtb_orderWHEREuser_id10086ANDcreate_time2026-01-01;核心字段1type索引使用类型最关键表示MySQL查询数据的方式取值从好到坏排序重点记前5个生产中只要不是ALL基本合格通俗解读实操判断一看就懂type取值通俗解读是否走索引生产判断const通过主键/唯一索引查询只匹配1行数据最快是最优无需优化eq_ref唯一索引扫描一行一行精准匹配速度快是优秀正常情况ref普通索引扫描匹配多行数据常用是合格可优化至eq_refrange索引范围扫描如、、between、in是合格注意范围不要过大ALL全表扫描遍历所有数据最慢否必须优化大概率索引失效实操提示生产中type出现ALL直接定位索引失效问题出现range时注意优化范围查询如避免用in(1,2,…1000)。核心字段2key实际使用的索引最直观的判断依据不用复杂分析key取值≠NULL表示SQL走了索引取值就是实际使用的索引名称key取值NULL表示未使用任何索引大概率是索引失效走全表扫描对应typeALL。避坑点很多人建了索引但key还是NULL说明索引建错了或SQL写法导致索引失效后面会详细讲。核心字段3Extra额外信息优化的关键线索不用记所有取值重点记3个高频情况直接对应优化方向Using index最优情况走了覆盖索引无需回表查询查询的字段都在索引里不用优化Using filesort排序未走索引需优化比如order by的字段不在索引里Using temporary创建临时表性能极差必须优化比如group by的字段未建索引。实操总结用EXPLAIN排查时按这个顺序判断——先看type是否为ALL→ 再看key是否为NULL→ 最后看Extra是否有filesort/temporary3步就能定位核心问题。三、9类生产高频索引失效场景含优化方案可直接复制使用这是本文核心也是生产和面试的重点。每类场景都遵循“失效SQL示例→通俗原理→优化SQL→实操提示”的逻辑避免理论堆砌看完就能落地。所有示例适配MySQL 8.0MySQL 8.4用户注意专属避坑提示。场景1联合索引未遵循“最左匹配原则”最高频80%的人踩过失效SQL示例建了联合索引idx_userid_createtime(user_id, create_time)-- 失效跳过最左列user_id直接用create_time查询EXPLAINSELECT*FROMtb_orderWHEREcreate_time2026-01-01;-- 失效跳过中间列仅用最左列和第三列MySQL 8.0不支持跳过扫描8.4仅特定场景支持EXPLAINSELECT*FROMtb_orderWHEREuser_id10086ANDpay_status1;通俗原理联合索引的查询顺序必须从最左列开始连续匹配跳过中间列或最左列索引会直接失效MySQL会走全表扫描。MySQL 8.4虽支持“跳过扫描”但仅限等值查询且字段选择性高生产环境切勿依赖。优化SQL两种方案按需选择-- 方案1遵循最左匹配包含最左列user_idEXPLAINSELECT*FROMtb_orderWHEREuser_id10086ANDcreate_time2026-01-01;-- 方案2若需单独用create_time查询给create_time单独建索引CREATEINDEXidx_createtimeONtb_order(create_time);实操提示创建联合索引时把高频查询字段、选择性高的字段放在最左边选择性不重复值数量/总数据量选择性越高索引效果越好。场景2索引字段使用函数/算术运算高危容易被忽略失效SQL示例create_time、age为索引字段-- 失效对索引列使用函数EXPLAINSELECT*FROMtb_orderWHEREYEAR(create_time)2026;-- 失效对索引列做算术运算EXPLAINSELECT*FROMtb_userWHEREage125;通俗原理MySQL索引基于字段原始值构建B树对索引列做函数操作或算术运算会改变字段的原始有序性优化器无法利用索引快速匹配只能走全表扫描。优化SQL核心让索引字段“裸奔”把函数/运算移到等号右边-- 优化1函数移到右边用范围查询替代EXPLAINSELECT*FROMtb_orderWHEREcreate_time2026-01-01ANDcreate_time2027-01-01;-- 优化2算术运算移到右边EXPLAINSELECT*FROMtb_userWHEREage24;实操提示若必须对索引列使用函数如复杂日期计算可建函数索引如CREATE INDEX idx_create_time_year ON tb_order(YEAR(create_time))但函数索引维护成本高尽量不用。场景3字符串字段不加引号隐式类型转换高频雷区失效SQL示例phone为varchar类型已建索引-- 失效字符串字段用数字匹配触发隐式类型转换EXPLAINSELECT*FROMtb_userWHEREphone13800138000;通俗原理MySQL会自动将字符串字段转为数字相当于对索引列使用函数CAST(phone AS UNSIGNED)导致索引失效。注意若索引列是int类型用字符串匹配如user_id‘10086’MySQL会将字符串转为数字索引仍有效但不建议依赖该行为。优化SQL核心字符串字段必须加单引号保持类型一致EXPLAINSELECT*FROMtb_userWHEREphone13800138000;实操提示用ORM框架如MyBatis时需配置类型映射校验避免前端传参类型错误导致隐式转换。场景4模糊查询以“%”开头like ‘%xxx%’高频场景失效SQL示例product_name为索引字段-- 失效前缀用%无法确定B树搜索起点EXPLAINSELECT*FROMtb_productWHEREproduct_nameLIKE%手机%;通俗原理MySQL的B树索引是有序的前缀模糊匹配%xxx无法定位索引的起始位置只能全表扫描后缀模糊匹配xxx%可正常走索引。优化SQL3种方案按优先级排序-- 方案1业务允许的话用后缀模糊匹配优先EXPLAINSELECT*FROMtb_productWHEREproduct_nameLIKE苹果手机%;-- 方案2用覆盖索引仅查询索引包含的字段避免回表CREATEINDEXidx_product_nameONtb_product(product_name,id);EXPLAINSELECTid,product_nameFROMtb_productWHEREproduct_nameLIKE%手机%;-- 方案3复杂模糊查询如全文检索用Elasticsearch替代MySQL实操提示生产中模糊查询优先用ESMySQL仅处理简单后缀模糊查询避免前缀%导致索引失效。场景5OR条件导致索引失效非主键/非同一索引失效SQL示例a有索引b无索引-- 失效OR两边字段只有a有索引b无索引导致全表扫描EXPLAINSELECT*FROMtb_userWHEREa1ORb2;通俗原理MySQL优化器认为OR条件中只要有一个字段无索引走全表扫描比分别查索引再合并结果更高效因此放弃索引。优化SQL2种方案按需选择-- 方案1给OR两边的字段都建索引索引合并CREATEINDEXidx_bONtb_user(b);EXPLAINSELECT*FROMtb_userWHEREa1ORb2;-- 方案2用UNION替代OR性能更稳定优先选择EXPLAINSELECT*FROMtb_userWHEREa1UNIONALLSELECT*FROMtb_userWHEREb2;-- 无需去重用UNION ALL效率更高实操提示索引合并的性能不如UNION稳定生产中优先用UNION替代OR。场景6NULL值相关查询误区NULL不会导致索引失效分情况失效/有效SQL示例phone为索引字段允许NULL-- 有效普通索引/唯一索引查询IS NULL会走索引EXPLAINSELECT*FROMtb_userWHEREphoneISNULL;-- 可能失效查询IS NOT NULL取决于数据分布NULL值少则失效EXPLAINSELECT*FROMtb_userWHEREphoneISNOTNULL;通俗原理很多人误以为“NULL会导致索引失效”其实是错误的。普通索引和唯一索引都允许NULL值唯一索引仅允许1个NULL查询IS NULL会走索引IS NOT NULL可能失效因为MySQL优化器会判断数据分布若NULL值极少走全表扫描更高效。优化SQL针对IS NOT NULL失效场景-- 方案1用默认值替代NULL如用代替NULL推荐ALTERTABLEtb_userMODIFYCOLUMNphoneVARCHAR(20)DEFAULT;EXPLAINSELECT*FROMtb_userWHEREphone!;-- 方案2给字段建索引强制走索引慎用需结合数据分布EXPLAINSELECT*FROMtb_userFORCEINDEX(idx_phone)WHEREphoneISNOTNULL;实操提示生产中尽量避免字段为NULL用默认值如’、0替代减少NULL相关的查询问题。场景7order by/group by 字段未建索引导致filesort/temporary失效SQL示例order by/group by的字段未建索引-- 失效order by字段未建索引出现Using filesortEXPLAINSELECT*FROMtb_orderWHEREuser_id10086ORDERBYcreate_time;-- 失效group by字段未建索引出现Using temporaryEXPLAINSELECTuser_id,COUNT(*)FROMtb_orderGROUPBYuser_id;通俗原理order by/group by 需要对数据排序若排序字段未建索引MySQL会进行文件排序filesort或创建临时表temporary性能极差相当于索引失效。优化SQL核心将排序/分组字段加入索引-- 优化1order by字段加入联合索引结合where条件CREATEINDEXidx_userid_createtimeONtb_order(user_id,create_time);EXPLAINSELECT*FROMtb_orderWHEREuser_id10086ORDERBYcreate_time;-- 优化2group by字段建索引CREATEINDEXidx_useridONtb_order(user_id);EXPLAINSELECTuser_id,COUNT(*)FROMtb_orderGROUPBYuser_id;实操提示用EXPLAIN查看若Extra出现filesort或temporary优先优化order by/group by的索引。场景8JOIN关联查询关联字段未建索引/类型不匹配失效SQL示例a.join b on a.id b.a_idb.a_id未建索引/类型不匹配-- 失效1被关联表b的关联字段a_id未建索引EXPLAINSELECT*FROMtb_order aJOINtb_user bONa.user_idb.id;-- 失效2关联字段类型不匹配a.user_id是intb.id是varcharEXPLAINSELECT*FROMtb_order aJOINtb_user bONa.user_idb.id;通俗原理JOIN查询时被关联表如b表的关联字段未建索引会导致全表扫描关联字段类型不匹配会触发隐式类型转换导致索引失效。优化SQL2个核心步骤-- 优化1给被关联表的关联字段建索引CREATEINDEXidx_b_idONtb_user(id);-- 优化2保证关联字段类型一致均为intALTERTABLEtb_userMODIFYCOLUMNidINT;实操提示JOIN关联时优先给小表的关联字段建索引提升查询效率字段类型必须严格一致避免隐式转换。场景9数据分布极端倾斜索引存在但失效容易被忽略失效SQL示例status为索引字段status1的记录占比90%-- 失效数据分布倾斜优化器认为全表扫描更快EXPLAINSELECT*FROMtb_orderWHEREstatus1;通俗原理即使建了索引若某字段的某个值占比过高如超过20%MySQL优化器会认为“走索引的成本高于全表扫描”主动放弃索引导致索引失效。关键判断指标是索引的基数Cardinality基数越低索引效果越差。优化SQL2种方案-- 方案1强制走索引适合必须用该条件且数据量较大的场景EXPLAINSELECT*FROMtb_orderFORCEINDEX(idx_status)WHEREstatus1;-- 方案2优化查询条件增加高选择性字段如结合create_timeEXPLAINSELECT*FROMtb_orderWHEREstatus1ANDcreate_time2026-01-01;实操提示通过SHOW INDEX FROM tb_order查看索引基数基数过低的索引如status可考虑删除或结合其他字段建联合索引。四、2026生产级避坑清单10个高频坑踩过的都懂结合MySQL 8.4版本特性和生产实测整理10个高频避坑点避开这些慢SQL优化成功率100%避免踩坑返工坑1盲目建索引认为“索引越多越好”—— 索引会降低插入/更新/删除效率生产中一个表的索引不超过5个坑2依赖MySQL 8.4的“跳过扫描”特性随意跳过联合索引中间列—— 仅特定场景支持生产环境优先遵循最左匹配坑3字符串字段查询不加引号依赖隐式类型转换—— 必导致索引失效严格保持字段类型一致坑4用select * 查询导致索引无法实现覆盖索引—— 只查需要的字段减少回表提升效率坑5索引字段用函数/运算却不建函数索引—— 要么优化SQL写法要么建函数索引慎用坑6JOIN查询时给主表关联字段建索引忽略被关联表—— 被关联表的关联字段才是索引重点坑7数据分布倾斜时强行建索引—— 先优化查询条件再考虑索引坑8MySQL 8.4版本未利用索引碎片自动清理特性—— 8.4及以上无需手动执行optimize table避免锁表坑9用OR条件时只给一侧字段建索引—— 必须给两侧字段都建索引或用UNION替代坑10优化后不验证效果—— 优化后必须用EXPLAIN重新分析确认type、key、Extra符合预期。五、实战案例完整慢SQL优化流程可直接参考结合生产真实场景完整演示从定位慢SQL到优化落地的全流程小白可直接照搬场景电商订单表tb_order500万条数据慢SQL如下-- 慢SQL查询用户10086近3个月已支付的订单执行时间5.8秒SELECTid,order_no,order_amount,create_timeFROMtb_orderWHEREuser_id10086ANDdelete_flag0ANDcreate_time2026-01-01ANDpay_status1;优化步骤步骤1用EXPLAIN分析问题EXPLAIN SELECT id, order_no, order_amount, create_time FROM tb_order WHERE user_id 10086 AND delete_flag 0 AND create_time 2026-01-01 AND pay_status 1;分析结果typeALL全表扫描keyNULL未走索引ExtraUsing where; Using filesort排序失效。步骤2定位索引失效原因当前仅给user_id建了普通索引查询条件包含user_id、create_time、delete_flag、pay_status未遵循联合索引最左匹配且order by未建索引导致索引失效文件排序。步骤3执行优化操作-- 建联合索引遵循最左匹配包含查询和排序字段实现覆盖索引CREATE INDEX idx_userid_createtime_paystatus_deleteflag ON tb_order(user_id, create_time, pay_status, delete_flag);– 优化SQL只查需要的字段避免select *SELECT id, order_no, order_amount, create_timeFROM tb_orderWHERE user_id 10086AND delete_flag 0AND create_time ‘2026-01-01’AND pay_status 1;步骤4验证优化效果用EXPLAIN重新分析结果typeref走索引keyidx_userid_createtime_paystatus_deleteflag使用新建索引ExtraUsing index覆盖索引无需回表执行时间从5.8秒降至0.02秒优化成功。六、总结与2026实操建议CSDN骨灰用户专属慢SQL优化的核心逻辑很简单用EXPLAIN定位问题用索引解决问题用避坑清单规避问题。2026年MySQL 8.4普及后索引机制更高效但索引失效的核心坑点没变重点关注版本专属特性如索引碎片自动清理、跳过扫描避免盲目依赖。给不同角色的实操建议开发工程师写SQL时先想索引是否生效——字符串加引号、避免函数/运算、遵循联合索引最左匹配写完用EXPLAIN校验避免上线后出问题运维工程师长期开启慢查询日志定期用mysqldumpslow分析慢SQL淘汰无用索引监控索引使用率MySQL 8.4无需手动清理索引碎片面试者重点记EXPLAIN 3个核心字段、9类索引失效场景带优化方案结合本文实战案例面试时能说出“定位-分析-优化-验证”的全流程必加分。最后提醒慢SQL优化预防大于治疗——写SQL时多花1分钟校验索引能避免后续大量的排查和优化工作。互动提问你在优化慢SQL时踩过哪些印象最深的坑评论区留言一起交流解决方案助力大家避开雷区

更多文章