MySQL慢SQL优化全流程:从定位到索引设计实战
1. 全流程思路慢SQL从发现到根治到底该怎么走做了这么多年MySQL调优我发现很多同学一上来就盯着索引使劲或者看见慢查询日志就慌其实这是本末倒置。MySQL性能优化有一套完整的链路核心是先定位、再分析、后设计、终验证缺一步都容易白忙活。先说个常见场景某天早上你收到告警数据库CPU打满业务接口响应时间从50ms飙到3秒。这时候如果你直接去加索引大概率是盲人摸象——因为你不确定是哪些SQL在拖垮数据库也不知道它们为什么慢更不知道加了索引之后会不会引入新的问题。我在实际运维中反复踩过这种坑之后总结出一个相对稳妥的落地流程分享给你参考。整个全流程可以拆成四个环节慢查询定位、执行计划分析、索引设计优化、验证回归。慢查询定位解决的是哪些SQL有问题执行计划分析解决的是为什么有问题索引设计优化解决的是怎么改验证回归解决的是改了之后到底有没有效果、有没有副作用。这四个环节环环相扣缺了任何一个优化都可能变成一场赌博。我自己处理过的模拟项目X就是一个典型例子某后台管理系统的列表查询在数据量到200万行之后出现明显卡顿业务方天天催一开始怀疑是服务器配置不够后来发现CPU和内存其实都很闲真正的问题是某条关联查询走了全表扫描每次查询要扫描百万行数据。经过慢查询日志抓取、EXPLAIN分析、索引重构三步操作之后查询耗时从1.8秒降到30毫秒。这个过程就是上面说的全流程我下面把每个环节的实操细节都拆开讲一遍。为了让你对这个流程有一个整体认知我先用一张结构化的工作流清单把各阶段的关键动作列出来后续每一节会展开细说。阶段核心动作关键产出常见误区慢查询定位开启慢查询日志、抓取TOP SQL疑似问题SQL清单只看耗时不看扫描行数执行计划分析EXPLAIN逐列解读、关注type/key/rows/Extra性能瓶颈判定只盯type列忽略rows基数索引设计优化区分度计算、复合索引组合、覆盖索引设计索引调整方案无脑给所有字段加索引验证回归前后耗时对比、执行计划对比、线上灰度观察优化效果确认测试环境验证完直接上生产这套流程的核心价值在于每一步都有明确的输入和输出不会让你在优化过程中迷失方向。下面的篇幅我会按照这个流程一步步走每一个环节都会附上真实的命令、参数和踩坑经验。2. 慢查询定位实操三种抓取手法与效率对比2.1 慢查询日志最基础也最容易被忽略的配置慢查询日志是所有排查工作的起点。很多开发同学可能到现在都没开过这个开关或者在排查问题的时候才想起来临时开这其实已经错过了最佳时机。我建议任何线上MySQL实例从上线第一天就开启慢查询日志哪怕刚开始没有慢查询这些数据也是后续容量规划和性能分析的宝贵素材。开启方式很简单修改MySQL配置文件my.cnf或my.ini在[mysqld]段下追加以下几项slow_query_log ON slow_query_log_file /var/log/mysql/slow-query.log long_query_time 1 log_queries_not_using_indexes ON参数含义我逐个说明slow_query_log是总开关slow_query_log_file指定日志文件路径注意MySQL的运行用户需要对目录有写权限long_query_time表示超过多少秒算慢查询我把阈值设为1秒这是因为线上业务正常情况下查询都在毫秒级超过1秒的问题SQL已经有优化价值了没必要等到10秒才记录那时候业务早就被打挂了log_queries_not_using_indexes这是一个容易被忽略但是很有价值的参数它会把所有没有走索引的查询都记录下来不管耗时是否超过阈值。配置完成后重启MySQL生效。不过如果你是线上环境不方便重启也可以用SET GLOBAL动态调整SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;注意long_query_time修改后对已经建立的连接不生效需要重新连接或者利用定时任务连接一次才会触发实际测试中你会发现新发起的查询才会按新阈值记录。日志文件会一直增长所以建议配置日志轮转。MySQL 5.7以上可以用SET GLOBAL slow_query_log OFF先关闭然后mv文件再重新开启或者直接用mysqladmin flush-logs刷新日志。Linux下也可以用logrotate按天切割这个很多文章都会写但真正执行的时候要小心权限和信号问题。2.2 用pt-query-digest把日志变成报表慢查询日志是纯文本格式如果SQL量大直接看根本看不过来。我常用的工具是Percona Toolkit里的pt-query-digest它能把慢查询日志汇总成一份按耗时和次数排序的报告瞬间定位到最值得优化的SQL。安装方式不赘述Debian系可以直接apt install percona-toolkit或者从官网下载对应发行版的包。分析命令很简单pt-query-digest /var/log/mysql/slow-query.log slow_report.txt生成的报告分为三部分总体统计、按查询维度分组统计、每个查询的详细样本。我最关注的是第二部分它把相同的SQL参数不一致但结构相同归为一组按总耗时排序。假设报告里有一个这样的条目# Query 3: 4.97k QPS, 1.24x卡片 # Time range: 2024-01-01T10:00:00 to 2024-01-01T11:00:00 # Attribute pct total min max avg 95% stddev median # # Count 3 145 # Exec time 78 647s 2s 8s 4s 6s 1s 3s # Lock time 6 97s 0 5s 1s 1s 1s 0 # Rows sent 1 2.15k 1 45 14.83 44.24 9.32 5.84 # Rows examine 75 1.01M 1.22k 796.14k 40.83k 81.22k 21.59k 21.73k注意看Rows examine和Rows sent的比值如果Rows examine远大于Rows sent说明这条SQL扫描了大量数据行却只返回很少的结果这是典型的索引缺失或索引设计不合理导致的假性能问题。这类SQL即使耗时没超过阈值也应该纳入优化队列因为它消耗了大量磁盘IO和CPU资源。2.3 实时定位SHOW PROCESSLIST与performance_schema慢查询日志是事后分析的利器但线上情况瞬息万变。有时候突发的慢查询会把数据库打到濒临崩溃这时候没时间去看日志文件需要在问题发生的当下立刻抓到正在执行的SQL。首选命令是SHOW FULL PROCESSLIST它能看到当前所有正在执行的连接和SQL语句SHOW FULL PROCESSLIST;输出的关键字段是Id连接ID、Time执行时间、State当前状态、InfoSQL内容。如果发现某个连接的Time特别大比如超过10秒那基本可以断定它有问题。这时候可以先KILL {Id}把连接干掉止损优先。不过SHOW FULL PROCESSLIST的缺点是它只能看到当前正在执行的SQL如果SQL执行太快你刷几遍都未必能抓到。更好的方案是使用performance_schema里的events_statements_current表SELECT THREAD_ID, EVENT_NAME, TIMER_WAIT/1000000000 AS wait_ms, SQL_TEXT, ROWS_EXAMINED, ROWS_AFFECTED FROM performance_schema.events_statements_current WHERE SQL_TEXT IS NOT NULL ORDER BY TIMER_WAIT DESC LIMIT 20;这个查询能实时抓取当前正在执行的SQL还附带扫描行数等关键指标。不过要注意performance_schema开关需要提前开启而且开启后有一定内存开销建议在低峰期开启并持续观察。还有一个实战技巧让慢查询日志记录所有未走索引的SQL也就是前面提到的log_queries_not_using_indexes然后在测试环境跑一遍核心业务用例日志里会立刻记录下所有索引失效的SQL这个策略在接手一个老项目时特别管用可以快速摸清系统的索引使用状况。3. EXPLAIN执行计划深读每一列都是体检报告3.1 从一次完整的EXPLAIN输出说起定位到问题SQL之后下一步就是用EXPLAIN分析执行计划。我拿一个模拟的真实场景举例某订单查询SQL关联了用户表和订单表EXPLAIN SELECT o.order_no, u.user_name FROM orders o INNER JOIN users u ON o.user_id u.id WHERE o.status 1 AND o.create_time 2024-01-01 ORDER BY o.pay_time DESC LIMIT 20;执行之后输出大概是这样的idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra1SIMPLEoNULLALLidx_status_time,idx_user_idNULLNULLNULL19823445.26Using where; Using filesort1SIMPLEuNULLeq_refPRIMARYPRIMARY8test.o.user_id1100.00NULL这张表的信息量非常大。我一行一行说type列显示o表是ALL意味着全表扫描198万多行数据全部要过一遍u表是eq_ref走主键等值连接没问题。key列是NULL说明o表虽然存在idx_status_time和idx_user_id两个索引但优化器一个都没用上。Extra列出现了Using where; Using filesort文件排序意味着内存临时表或磁盘排序这也是性能杀手。最直观的性能瓶颈就在o表那一行全表扫描加上文件排序198万行数据做排序再取20条这种查询不慢才怪。任何执行计划分析的核心就是找到这种类型差、rows大、Extra异常的行然后针对性优化。3.2 type列的五级阶梯从ALL到const的性能递进type列是判断单表访问方式的重要指标它从差到优一般遵循这样一个递进关系ALL全表扫描→index全索引扫描→range索引范围扫描→ref非唯一索引等值匹配→eq_ref唯一索引等值匹配→const/system主键或唯一索引等值匹配。我把这几种类型整理成一个速查表方便你对照type值访问方式扫描范围性能评价常见场景ALL全表扫描整张表极差无可用索引或优化器放弃索引index全索引扫描整个索引树较差查询列都在索引中但无法条件过滤range索引范围扫描索引部分区间良好WHERE中有BETWEEN、、、IN等ref非唯一索引等值匹配匹配行集合良好WHERE中普通索引列等值条件eq_ref唯一索引等值匹配最多一行优秀JOIN中主键或唯一索引被等值匹配const主键等值匹配单行极优WHERE中主键常量一个比较反直觉的点type为index也不一定差。如果查询的列全部包含在某个覆盖索引中index扫描虽然遍历整个索引但走的是比数据行更紧凑的索引页在某些统计查询场景下反而比ALL快几个数量级。3.3 rows与filtered优化器眼中的成本账本rows列是优化器预估的需要扫描的行数它直接决定了SQL的大致耗时量级。一般来说rows从几十万降到几百查询耗时通常能从秒级降到毫秒级。但要注意这个值是估算值不是精确值有时候会因为统计信息不准确而出现偏差。filtered表示经过WHERE条件过滤后剩余行数的百分比。比如上面示例中o表的filtered是5.26%说明优化器预估扫描198万行后只有约10万行满足条件1982344 × 5.26%这个性价比极低因为大部分行都在WHERE判断中被丢弃。一个实用技巧当rows和filtered出现明显不合理的组合时先执行ANALYZE TABLE刷新统计信息再重新看EXPLAIN。比如某表有大量UPDATE操作统计信息长时间没更新优化器可能拿着旧数据做判断做出错误的执行计划选择。有时候你会发现刷新统计信息之后执行计划自己就变好了。3.4 Extra列的隐藏密码filesort、temporary、index conditionExtra列包含了很多优化器行为的补充信息其中三类最值得关注Using filesort代表排序无法利用索引顺序需要额外的排序步骤。这通常出现在ORDER BY列没有和索引匹配、或者没有建立合适的联合索引的场景。文件排序如果排序数据量超出sort_buffer_size会使用磁盘临时文件性能急剧下降。解决方法一般是把排序字段纳入索引设计。Using temporary代表查询使用了临时表常见于GROUP BY、DISTINCT、UNION操作。临时表在内存中还好数据量大时会落到磁盘性能非常差。解决思路是让GROUP BY的字段顺序匹配索引顺序或者改写SQL。Using index condition代表触发了索引条件下推Index Condition PushdownICP这是MySQL 5.6以后引入的重要优化。它允许存储引擎层在索引遍历时就过滤不符合条件的记录减少回表次数。看到这个字段说明索引利用得不错但如果你发现某些查询出现了Using where; Using index condition的组合可以进一步优化索引设计让过滤条件全部落到索引字段上从而消掉Using where。3.5 实操心得EXPLAIN不是万能的做了这么多年排查我得说EXPLAIN也有它的局限性。第一它只展示优化器选择的执行计划不展示所有可能选项所以有时候你看不到为什么没走索引A而走了索引B这时需要结合OPTIMIZER_TRACE查看详细决策过程。第二它不显示实际的耗时和扫描行数只能看预估值所以最终验证还是要靠真实执行时间。OPTIMIZER_TRACE的使用方法是在会话内打开开关SET optimizer_trace enabledon; SELECT ...; SELECT * FROM information_schema.OPTIMIZER_TRACE;输出的JSON内容非常详细包括优化器对各索引访问成本的估算、对表关联顺序的考量等。实际工作中我一般只在EXPLAIN结果和直觉不符时才用这个工具深挖日常优化用EXPLAIN已经够用了。4. 索引设计核心方法论从字段选择到组合艺术4.1 区分度决定一切为什么不能给低区分度字段建索引很多刚入门的人有一个误区觉得WHERE里出现的字段就应该建索引。我见过一个典型案例某表有一个status字段只有0和1两个值开发同学给这个字段建了索引结果查询计划还是走了全表扫描。原因是优化器计算后发现使用索引访问的话如果status1能匹配表中50%以上的行优化器认为直接全表扫描成本更低于是放弃索引。这其实是个聪明的选择对于区分度极低的字段索引扫描需要大量回表付出的磁盘IO可能比顺序读表还大。实际操作中怎么判断字段区分度SQL很简单SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity FROM table_name;这个比值就是区分度取值范围在0到1之间。一般来说区分度大于0.1的字段才值得建独立索引。比如一个订单表里有100万行order_no字段基本唯一区分度接近1建索引效果很好而status字段只有几个值区分度可能只有0.00001建索引意义不大。但对于这种低区分度字段也不是完全没办法解决方案是和其他高区分度字段一起建联合索引。比如查询是WHERE status 1 AND create_time 2024-01-01那么建一个(status, create_time)的联合索引是合理的虽然status区分度低但它可以在索引的最左列先过滤掉一半数据剩下的通过create_time继续缩小范围。4.2 联合索引的左前缀法则顺序决定生死联合索引是MySQL索引设计的核心而它的核心法则就是最左前缀原则。MySQL的联合索引本质上是按照索引定义中第一个字段、再第二个字段、再第三个字段的顺序建立的一棵B树。查询的过滤条件必须从索引的最左列开始并且连续匹配才能最大化利用索引。我举个例子。假设有一个orders表建立了联合索引idx_user_status_time(user_id, status, create_time)那么下面这些查询都能很好命中这个索引WHERE user_id 1001 -- 命中只用了最左列 WHERE user_id 1001 AND status 1 -- 命中用了前两列 WHERE user_id 1001 AND status 1 AND create_time 2024-01-01 -- 完整命中三列而下面的查询则无法使用完整索引WHERE status 1 -- 未从最左列开始无法使用索引 WHERE create_time 2024-01-01 AND user_id 1001 -- 条件顺序不影响但只有user_id能用到索引这里有一个容易踩的坑WHERE create_time 2024-01-01 AND user_id 1001虽然SQL里create_time写在前面但优化器会做条件重排把等值条件user_id 1001放到前面所以user_id仍然能命中最左列。真正决定能否用索引的是字段是否连续参与匹配不是SQL书写顺序。设计联合索引的顺序时通用的考量优先级是先等值条件字段再范围条件字段。因为在联合索引中一旦某个字段遇到范围查询、、BETWEEN后面的字段就无法利用索引顺序进行匹配了。比如(user_id, status, create_time)中把create_time放在最前面那status 1这个等值条件就只能靠回表过滤了损失惨重。4.3 覆盖索引把查询按死在索引里覆盖索引指的是查询的所有字段都包含在某个索引中查询时只需要扫描索引页不需要回表取数据行。这是提升查询性能最暴力也最有效的手段之一。举个例子常见的高频查询是SELECT id, order_no, status FROM orders WHERE user_id 1001 ORDER BY create_time DESC;如果只建了idx_user_id(user_id)索引那么查询时通过索引找到user_id匹配的所有主键再逐行回表取出order_no、status、create_time最后在内存排序。如果建了idx_user_orders(user_id, create_time, order_no, status)联合索引那么索引树本身包含了查询所需的所有字段优化器直接遍历索引就返回结果连排序都省了因为索引本身就是按user_id create_time排序的。这里有个细节值得注意不要为了追求覆盖索引而把所有字段都塞进索引。索引越多写入和更新成本越高。实际互联网场景中一般把高频查询返回的字段控制在4~6个以内超过这个数量就要权衡利弊或者考虑反范式设计。4.4 索引下推、MRR与排序优化让索引发挥余热MySQL 5.6之后引入的索引条件下推ICP允许在索引遍历过程中就过滤掉不符合WHERE条件的记录。比如联合索引(name, age)查询WHERE name LIKE 张% AND age 20在没有ICP的情况下只能通过name匹配到所有姓张的记录然后逐条回表判断age有了ICPage的判断在索引层就完成了回表次数大幅减少。MRRMulti-Range Read优化则把从辅助索引拿到的回表主键排序后再回表把随机IO变成相对顺序IO对机械硬盘时代效果显著对SSD也有一定收益。这个优化默认开启不用特别配置但你可以在EXPLAIN的Extra列看到Using MRR来确认。ORDER BY排序优化方面如果排序字段和联合索引顺序匹配就不会出现filesort。比如上面那个ORDER BY create_time DESC的例子如果联合索引是(user_id, create_time)排序字段恰好是索引的第二列同时WHERE的等值条件固定了user_id排序顺序其实已经由索引天然保证。这里注意只把user_id作为等值条件、create_time作为排序字段时是可以利用索引顺序的这里很多人会误以为联合索引的第二个字段不能用于排序其实只要第一个字段是等值条件第二个字段的排序还是能利用索引的。4.5 索引设计的取舍写放大与查询效率的平衡索引不是越多越好这是我在实际项目中反复被验证的一点。每增加一个索引INSERT、UPDATE、DELETE操作都需要同步维护对应的B树写放大效应十分明显。如果你的业务是低写入量、高查询量的读多写少场景可以适当多加索引但如果是高并发写入场景比如下单、日志采集、舆情监控数据落库每多一个索引都可能让TPS下跌好几个百分点。我的习惯做法是控制单表索引数量不超过5个超过就审视一下是不是有冗余索引或者低频索引。有些查询很少用到但索引却建立后一直在消耗写入性能这种就果断删掉。还有一个容易被忽略的点前缀索引。对于超长字符串字段比如URL、备注内容、长文本全字段建索引会导致索引体积巨大内存放不下性能反而差。这时可以只对前N个字符建索引ALTER TABLE articles ADD INDEX idx_url_prefix (url(64));前缀索引的代价是可能无法用于覆盖索引和ORDER BY但这个方案在处理大字段场景下非常值得考虑。前缀长度的选择可以用一个对比查询来辅助判断SELECT COUNT(DISTINCT LEFT(url, 64)) / COUNT(*) AS prefix64, COUNT(DISTINCT LEFT(url, 128)) / COUNT(*) AS prefix128, COUNT(DISTINCT url) / COUNT(*) AS full FROM articles;找到区分度损失最小的前缀长度这是一个可以量化的决策过程不要拍脑袋拍一个数字就上。5. 完整实操案例从秒级到毫秒级的优化全记录5.1 问题SQL还原与基础分析前阵子帮一个团队优化某跨平台系统的订单列表分页接口压测时发现一个分页查询在数据量约300万行的orders表上执行时间达到2.3秒。原始SQL长这样SELECT o.id, o.order_no, o.user_id, o.status, o.total_amount, u.nickname FROM orders o LEFT JOIN users u ON o.user_id u.id WHERE o.status IN (1, 2, 3) AND o.create_time 2024-06-01 ORDER BY o.id DESC LIMIT 20 OFFSET 200000;第一眼就能看出两个可疑点OFFSET 200000意味着要跳过20万行这是深分页问题status IN (1,2,3)范围很大区分度低。业务上又确实需要翻到很后面的页码不能简单限制页码。先用EXPLAIN确认执行计划EXPLAIN SELECT o.id, o.order_no, o.user_id, o.status, o.total_amount, u.nickname FROM orders o LEFT JOIN users u ON o.user_id u.id WHERE o.status IN (1, 2, 3) AND o.create_time 2024-06-01 ORDER BY o.id DESC LIMIT 20 OFFSET 200000;结果中orders表type是range使用了一个状态时间的联合索引rows预估43万Extra列还出现了Using filesort。深分页加文件排序这个查询注定慢索引没有完全适配排序需求。5.2 优化方案延迟关联加覆盖索引的组合拳我设计的方案是两步走。第一步改造SQL采用延迟关联延迟join的思路先通过覆盖索引快速拿到目标主键ID再回原表取完整数据第二步建立合适的覆盖索引支持第一步的快速定位。新SQL如下SELECT o.id, o.order_no, o.user_id, o.status, o.total_amount, u.nickname FROM ( SELECT id FROM orders WHERE status IN (1, 2, 3) AND create_time 2024-06-01 ORDER BY id DESC LIMIT 20 OFFSET 200000 ) tmp JOIN orders o ON tmp.id o.id LEFT JOIN users u ON o.user_id u.id ORDER BY o.id DESC;核心改动是先用子查询只取主键id子查询中可以完全利用覆盖索引避免回表减少排序的数据量然后再用主键去关联取完整行。users表本来就是根据主键关联性能开销不大。配套索引设计为idx_status_time_id(status, create_time, id)注意把id放进了索引末尾。这么设计的原因是WHERE条件用到了status和create_time过滤ORDER BY和深分页使用id排序id放入索引后子查询可以直接从索引中按序拿id连filesort都省了。这个索引同时是覆盖索引因为子查询只需要id列。5.3 优化前后的数据对比优化后我重新执行EXPLAIN验证子查询的执行计划已经不再出现filesorttype为rangerows大幅下降。真实执行测试指标优化前优化后提升比例单次查询耗时2.3秒47毫秒约49倍扫描行数预估43万5.6万87%下降排序方式filesort利用索引顺序无排序开销回表次数大量20次仅最终取数大幅减少这个案例的关键不在于SQL写得多炫技而在于每一步都基于执行计划中的实际指标做决策先发现全表扫描filesort再通过索引设计消除排序再用延迟关联减少无效IO。这套方法比盲目加索引或者改SQL写法要可靠得多。5.4 线上执行DDL的注意事项索引设计完成后线上执行ALTER TABLE也要讲究策略。在几百万行的大表上直接ALTER TABLE ADD INDEX会锁表虽然InnoDB支持在线DDL但具体能不能不阻塞DML还要看版本和操作类型。MySQL 5.6以上支持ALGORITHMINPLACE, LOCKNONE可以做到DDL期间允许并发读写ALTER TABLE orders ADD INDEX idx_status_time_id (status, create_time, id), ALGORITHMINPLACE, LOCKNONE;但即便支持在线DDL大表的索引构建仍会产生大量IO和主从延迟尤其在主从环境下备库产生的延迟可能让读流量感知到数据落后。实操中建议在业务低峰期执行并且先用pt-online-schema-change这类工具做预演或者干脆在备库先执行再切换这种方式更稳妥。我个人的经验是超过千万行的表宁可多花一个小时用工具编排也不要冒锁表的险。5.5 深分页的进一步优化思路上面的延迟关联方案已经能解决很大一部分深分页问题但OFFSET 200000本身还是不够优雅。如果业务场景允许更进一步的做法是改成键值分页keyset pagination也就是记录上一页最后一条记录的id下一页直接用WHERE id 上次最大id ORDER BY id DESC LIMIT 20。这种方式下不管翻到多少页扫描的数据量都是恒定的20条性能不会随着页码增长而恶化。但它牺牲了跳页能力用户不能直接点击第100页。对大多数C端业务来说无限制的跳页本身就不是刚需产品层面改成加载更多的交互模式对后端压力会小很多。这个思路我在给团队培训时经常强调性能优化不只是技术层面的调整也有产品交互层面的可商量空间。6. 常见问题排查实录索引失效场景与陷阱清单6.1 索引失效的常见场景盘点我整理了一个高频踩坑清单每一类都是实际项目中遇到过的问题类型示例写法索引失效原因推荐改写隐式类型转换WHERE phone 13800001111phone字段是VARCHAR数字常量触发转换索引失效WHERE phone 13800001111对索引列使用函数WHERE DATE(create_time) 2024-06-01函数操作破坏索引有序性WHERE create_time 2024-06-01 AND create_time 2024-06-02LIKE前置通配符WHERE name LIKE %张无法从索引定位起始位置改全文索引或考虑后置匹配OR条件有一侧无索引WHERE user_id 1 OR status 2status无索引导致全表扫描拆成UNION或给status建索引联合索引跳列索引(a,b,c)条件只用a,c中间列b缺失c无法利用索引顺序重排索引字段顺序或补中间列条件范围查询后接等值条件索引(a,b)条件a10且b1a范围查询后b无法利用索引顺序调整索引顺序为(b,a)或拆分查询这里面隐式类型转换是最隐蔽的。我遇到过一个案例某用户表phone字段是VARCHAR但代码里传参是Integer类型JDBC预编译时参数类型不匹配MySQL在比较时做了隐式转换导致索引完全失效。加了引号之后同样的SQL耗时从800ms降到60ms一行代码没改就是参数类型的问题。6.2 优化器没走索引的意外情况有时候索引设计没问题、SQL写法也正确但EXPLAIN显示还是全表扫描。最常见的原因是统计信息不准确。大量增删改之后information_schema.statistics里的基数cardinality可能长期未更新优化器基于过期的统计信息做了错误判断。解决办法就是执行ANALYZE TABLEANALYZE TABLE orders;这个操作会重新统计索引的区分度信息代价是会对表加读锁在线业务低峰期做比较合适。我之前遇到过一张500万行的表每次查询都在3秒以上EXPLAIN显示全表扫描但索引明明存在且区分度很高。执行ANALYZE之后执行计划自动改成走索引查询耗时直接降到50ms以下。这个案例给了我一个不小的教训建完索引不代表一劳永逸还得关注统计信息是否新鲜。还有一个容易被忽略的点是innodb_stats_on_metadata参数。默认情况下MySQL在访问information_schema或SHOW INDEX等元数据时会重新计算统计信息如果统计信息计算代价高且频繁触发反而会带来额外开销。生产环境可以关闭这个参数SET GLOBAL innodb_stats_on_metadata OFF改由定时的ANALYZE TABLE来维护统计信息。6.3 写在最后的一点心得MySQL性能优化这件事说到底是用数据驱动决策而不是凭感觉优化。慢查询日志给你线索EXPLAIN给你依据索引设计给你方案验证回归给你闭环。遇到任何性能问题先量化、再分析、后动手千万别上来就改配置加索引。我个人踩过最大的坑就是急于求成看到慢SQL直接加索引结果加完之后查询是快了但整体写入性能下降了又得花时间回滚。后来养成了先看执行计划、先算区分度、先评估影响面再动手的习惯返工率低了很多。希望这篇全流程笔记能帮你少走一些弯路。如果你们团队也有类似的慢查询治理需求建议先用慢查询日志跑一周收集数据把TOP 20的SQL拉出来做一次集中评审你会发现大部分问题都能通过索引设计解决真正需要改SQL逻辑的其实很少。

相关新闻

RabbitMQ、Kafka、RocketMQ选型指南:三大消息中间件深度对比

RabbitMQ、Kafka、RocketMQ选型指南:三大消息中间件深度对比

做技术选型,最怕的不是不知道选哪个,而是拿着一个现成的锤子到处找钉子。我见过不少团队,因为盲目追捧某些中间件,把一个简单的业务集成场景搞得异常复杂;也见过有人贪图某个中间件"轻量易用",硬…

2026/10/13 7:44:11 阅读更多 →
MySQL通配符使用与性能优化:从LIKE语法到索引陷阱

MySQL通配符使用与性能优化:从LIKE语法到索引陷阱

前几天在帮同事排查一条慢查询,一条模糊搜索的SQL把接口拖到了1.2秒,数据量才不到200万行。一看语句, WHERE customer_name LIKE %某某% ,典型的通配符前置写法,直接把索引废掉了。后来改成前缀匹配加覆盖索引&#…

2026/10/13 7:43:11 阅读更多 →
SIFT特征匹配实战:从尺度空间原理到纯Python与OpenCV实现

SIFT特征匹配实战:从尺度空间原理到纯Python与OpenCV实现

简介:SIFT(尺度不变特征变换)算法的 Python 实现学习包,适合正在学习计算机视觉、图像特征提取与匹配的开发者。包内同时提供手写 Python 源码和 OpenCV 调用示例,便于对照理解尺度空间极值检测、关键点定位、方向分配…

2026/10/13 7:43:11 阅读更多 →

最新新闻

JMeter压测实战全解析:核心原理、线程组与结果分析

JMeter压测实战全解析:核心原理、线程组与结果分析

先回答第一个问题:JMeter是一款开源的压力测试工具,最初由某软件团队开发,后来贡献给了开源社区并持续维护至今。它最初的设计目标是做Web应用的功能测试,但发展到今天,已经成了接口测试、性能测试、分布式压测领域绕不…

2026/10/13 8:23:39 阅读更多 →
潜在时空CoT重构VLA推理,端到端自动驾驶如何实现双榜SOTA

潜在时空CoT重构VLA推理,端到端自动驾驶如何实现双榜SOTA

1. 项目整体认知:这不是一次单纯的刷榜 先说结论:如果你和我一样长期混在自动驾驶感知决策这条线里,最近某高校与某企业联合团队放出来的LaST-VLA方案,值得花时间细读。它在业内常用的导航仿真榜单的两个指标体系上都排到了第一梯…

2026/10/13 8:23:39 阅读更多 →
命令行正则辅助工具 rea:让日志解析与文本处理更高效

命令行正则辅助工具 rea:让日志解析与文本处理更高效

我先解释一下这个标题。rea是我最近一个月里写的一个命令行正则表达式辅助工具,全称是 Regular Expression Assistant,缩写取前三个字母。有人以为这个名字跟某个前端框架有什么关系,其实没有,我纯粹是因为它短、敲起来顺&#xf…

2026/10/13 8:23:39 阅读更多 →
多信息参考熵模型:提升端到端图像压缩率失真性能的关键

多信息参考熵模型:提升端到端图像压缩率失真性能的关键

近几年的端到端图像压缩研究里,熵模型一直被当成“房间里的那头大象”——大家都站在它背上做改进,可很少有人正面回答一个问题:潜变量的概率分布到底应该由谁来估计,用什么信息估计,才能让码率逼近真实熵下限。传统JP…

2026/10/13 8:23:39 阅读更多 →
Windows命名管道与JSON实现低延迟进程通信

Windows命名管道与JSON实现低延迟进程通信

1. 项目概述:为什么命名管道是Windows本地进程通信的“隐形高速路”在Windows系统里做跨进程数据交换,很多人第一反应是文件读写、共享内存,或者直接上网络套接字——但其实,有一条被严重低估的“本地专线”常年静默运行&#xff…

2026/10/13 8:23:39 阅读更多 →
操作系统与Linux入门:一文搞懂内核、系统调用与核心机制

操作系统与Linux入门:一文搞懂内核、系统调用与核心机制

开写之前我先说清楚:这篇东西不是那种照本宣科的教材复述,是我在带学生、日常折腾服务器和桌面环境过程中,把“操作系统到底是个什么东西”这件事重新梳理了一遍之后的总结。标题里带“Linux1”,说明这是系列第一讲,那…

2026/10/13 8:22:39 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →