资讯动态

MySQL性能诊断核心:Explain执行计划七字段实战解读与优化

发布时间:2026/10/13 9:20:14 来源:尧图企业网站定制
1. 这不是“调优秘籍”而是我踩过三年坑后整理的MySQL性能诊断工作流你点开这个标题大概率正被线上慢查询报警搞得焦头烂额或者刚被DBA甩过来一份Explain执行计划截图满屏typeALL、rows2894367、Extra里赫然写着Using filesort——心里一沉知道今晚又得加班了。别急这不是什么高深莫测的数据库内核源码分析而是一套我在多个中型业务系统里反复验证、打磨出来的可落地、可复现、可传承的MySQL性能问题处理路径。核心就两件事用Explain看懂SQL到底在干什么再用六类常见优化手段精准干预。它不讲索引原理的数学推导不堆砌InnoDB缓冲池参数而是聚焦于你每天真实面对的场景为什么加了索引没生效为什么ORDER BY突然变慢为什么JOIN操作耗时飙升这些答案全藏在Explain输出的每一行里。适合两类人一是刚接手老系统的开发面对千行SQL无从下手二是想摆脱“改完SQL就丢给DBA”的被动状态真正掌握数据层话语权的后端工程师。整套方法论不需要你背诵B树结构但要求你养成一个习惯任何上线前的SQL必须先看Explain任何线上告警的慢查第一反应不是改代码而是抓执行计划。接下来的内容就是我把这个习惯拆解成你能立刻上手的步骤、参数、陷阱和判断依据。2. Explain不是“翻译器”而是SQL执行过程的X光片逐字段解读与关键信号识别很多人把Explain当成一个简单的“SQL翻译工具”输入一条语句得到一张表然后扫一眼type是不是const就完事。这就像拿着CT片只看有没有骨折却忽略了软组织损伤、血管走向和潜在炎症。Explain真正的价值在于它揭示了MySQL优化器实际选择的执行路径而这个路径往往和你写的SQL字面意思南辕北辙。我见过太多案例开发者自信满满地写了WHERE user_id ? AND status active以为走了联合索引结果Explain显示typerangekeynull——因为status字段的区分度太低优化器直接放弃了索引。所以解读Explain必须带着“质疑精神”它说的真的是最优解吗我们来逐字段拆解那些真正决定性能生死的关键信号。2.1 id字段执行顺序的“时间轴”而非简单编号id字段常被误解为“查询编号”其实它是执行依赖关系的时间轴。当id值相同时表示这些行属于同一个SELECT按table列顺序执行当id值递增时表示这是子查询或UNION数值越大执行越靠后。最危险的情况是id出现NULL——这意味着该行是UNION的结果集MySQL需要创建临时表合并数据这是性能杀手。我处理过一个报表查询原始SQL用UNION ALL拼接三个渠道数据Explain里id列全是NULLrows总和超500万临时表撑爆内存。解决方案不是优化单个子查询而是重构为单表JOINCASE WHEN聚合id恢复为连续数字执行时间从47秒降到1.2秒。这里的关键洞察是id的连续性是执行计划是否“线性可控”的第一道门槛。如果看到大量NULL或跳跃式id优先考虑SQL结构重构而不是索引优化。2.2 select_type字段识别“嵌套陷阱”的预警灯select_type告诉你当前这一行SQL的“身份”。除了常见的SIMPLE简单查询、PRIMARY最外层查询要高度警惕SUBQUERY和DERIVED。SUBQUERY出现在WHERE或SELECT子句中的子查询比如SELECT name FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount 100)。Explain会为子查询单独生成一行select_typeSUBQUERY。问题在于这类子查询往往无法利用外部表的索引且可能重复执行。我曾优化一个用户画像查询原逻辑是主表JOIN子查询结果Explain显示subquery执行了12次对应主表12行每次扫描订单表全量。改成EXISTS重写后子查询变成DEPENDENT SUBQUERY利用主表id索引执行次数降为1次耗时减少83%。DERIVED则更隐蔽它代表FROM子句中的派生表子查询结果集MySQL会将其物化为临时表。某次优化商品搜索接口Explain显示DERIVED typeALL意味着整个子查询结果被当成了全表扫描对象。最终方案是将派生表逻辑下推到WHERE条件避免临时表生成。记住当select_type出现SUBQUERY或DERIVED时你的第一反应不应该是加索引而是问这个逻辑能否用JOIN或EXISTS等价重写2.3 type字段性能分水岭从ALL到const的七级阶梯type字段是Explain里最核心的性能指标它描述了MySQL如何查找表中行。它的取值从差到优大致分为七级ALL全表扫描→ index全索引扫描→ range范围扫描→ ref非唯一索引匹配→ eq_ref唯一索引等值匹配→ const/system常量查询。重点来了不要只盯着是否为const而要看“为什么不是ref或eq_ref”。比如一个WHERE user_id ?的查询理论上应该走到eq_ref但如果user_id字段是VARCHAR类型而传入的参数是数字如WHERE user_id 123MySQL会触发隐式类型转换导致索引失效type退化为ALL。我在线上抓到过一个典型案例用户登录接口Explain显示typeALL排查发现前端传参是字符串123后端MyBatis参数类型定义为Integer框架自动做了类型转换触发了索引失效。解决方案不是改SQL而是统一前后端数据类型契约。另一个高频陷阱是使用函数WHERE DATE(create_time) 2023-01-01DATE()函数让create_time索引完全失效。正确写法是WHERE create_time 2023-01-01 00:00:00 AND create_time 2023-01-02 00:00:00。这里的关键原则是type的每一次降级背后都对应着一个可修复的具体技术动作而不是玄学的“索引没建好”。2.4 possible_keys与key字段索引“备选池”与“最终决策”的博弈possible_keys列出优化器认为可能用上的索引key显示最终选定的索引。这两者的差异就是优化器“思考过程”的快照。当possible_keys有值而key为NULL时说明优化器觉得索引“不划算”宁可全表扫描。常见原因有三一是索引区分度太低如性别字段建索引二是查询条件未覆盖索引最左前缀联合索引idx_a_b_cWHERE b1 AND c1跳过了a三是统计信息过期。我处理过一个订单查询possible_keys显示有idx_status_user_id但keyNULL执行极慢。执行ANALYZE TABLE orders更新统计信息后key变为该索引性能恢复正常。另一个经典场景是OR条件WHERE a1 OR b2即使a、b各自有索引优化器也可能放弃所有索引走ALL。此时应改用UNION(SELECT * FROM t WHERE a1) UNION ALL (SELECT * FROM t WHERE b2)让每个分支都能走独立索引。这里的经验是当possible_keys丰富但key为空时先检查统计信息和查询条件写法而不是盲目新建索引。2.5 key_len字段索引“使用深度”的毫米级刻度key_len显示MySQL实际使用索引的字节数。这个看似冷门的字段能精准定位索引是否“用到位”。计算规则是对于VARCHAR(n)utf8mb4编码下为n*422字节存长度INT为4字节DATETIME为8字节。以联合索引idx_user_id_status_create_time为例若key_len4说明只用了user_idINT4若key_len8说明用了user_idstatus假设status是TINYINT1但需对齐实际可能为8若key_len16则三个字段全用上了。我曾优化一个消息列表接口Explain显示key_len4但查询条件是WHERE user_id ? AND status read明显应该用到两个字段。排查发现status字段定义为VARCHAR(20)而查询条件传的是字符串read但索引中该字段实际存储了更多字节因填充导致优化器只敢用user_id部分。解决方案是将status改为ENUM或TINYINT减小字段宽度key_len升至8查询速度提升5倍。key_len就像索引使用的“游标卡尺”它告诉你优化器到底“吃”进了索引的多少营养而不是简单回答“有没有吃”。2.6 rows字段优化器的“估算谎言”但却是调优的起点rows字段显示优化器预估需要扫描的行数。注意这是“估算”不是精确值尤其在数据分布不均或统计信息陈旧时误差巨大。但它依然是调优的黄金起点。当rows远大于实际返回结果数如SELECT COUNT(*)结果为1000rows显示500000说明优化器严重误判大概率是索引失效或统计信息问题。我遇到过最离谱的案例一个分页查询LIMIT 10 OFFSET 10000Explain显示rows1000000但实际业务中该条件匹配数据极少。原因是OFFSET过大优化器认为跳过10000行成本太高不如全表扫描。解决方案不是硬扛而是改用游标分页记录上一页最后一条记录的ID下一页查询WHERE id last_id ORDER BY id LIMIT 10。rows从百万级降至个位数。另一个重要技巧是结合EXPLAIN FORMATJSON查看filtered字段过滤后剩余百分比如果rows大但filtered接近100%说明WHERE条件基本没起作用该检查字段是否有索引或数据质量。rows的价值不在于其绝对数值而在于它与你业务认知的偏差程度——偏差越大越值得深挖。2.7 Extra字段隐藏在幕后的“执行导演”记录所有暗箱操作Extra字段是Explain的“幕后花絮”记录了优化器未在其他字段体现的额外操作。这里藏着最多性能陷阱。最常见的有Using filesort表示MySQL需要额外排序未利用索引完成ORDER BY。例如WHERE a1 ORDER BY b但索引是idx_a没有包含b。解决方案是创建联合索引idx_a_b。Using temporary表示创建了临时表通常由GROUP BY、DISTINCT、UNION或某些JOIN触发。某次优化报表GROUP BY date(created_at)导致Using temporary改成GROUP BY DATE(created_at)函数索引后消失。Using index condition好消息表示使用了索引条件下推ICP即WHERE条件中能用索引的部分先在存储引擎层过滤减少回表次数。Using where; Using index最佳状态表示覆盖索引查询所有数据从索引中获取无需回表。Impossible WHERE查询条件自相矛盾如WHERE id1 AND id2MySQL直接返回空结果性能最好。我曾在一个电商搜索中Explain显示Using filesort Using temporary耗时3秒。分析发现是ORDER BY price LIMIT 20但price字段无索引。添加联合索引idx_status_pricestatus用于WHEREprice用于ORDER BY后Extra变为Using index耗时降至0.08秒。Extra字段就像汽车仪表盘的故障灯亮起时不必恐慌但必须立即停车检查——它指向的是最直接、最可操作的优化点。3. 六类实战优化手段从“索引设计”到“架构规避”的完整链条Explain只是诊断工具真正的战斗力在于优化手段。很多教程把优化手段罗列为“加索引、用缓存、读写分离”等泛泛而谈的名词但实际落地时你会卡在无数细节里联合索引字段顺序怎么排什么时候该用覆盖索引分库分表的临界点在哪里下面这六类手段是我从上百个真实案例中提炼出的、带具体参数、场景和取舍逻辑的实操指南每一条都经过生产环境验证。3.1 索引设计不是“越多越好”而是“恰到好处”的精密工程索引设计是MySQL优化的基石但也是误区最多的领域。新手常犯两大错误一是对所有WHERE字段都建单列索引二是盲目追求“全覆盖”。实际上索引的本质是空间换时间而空间成本磁盘IO、内存占用、写入延迟必须被精确计算。我们以一个典型用户表users为例业务查询模式包括按user_id查详情、按email登录、按statuscreated_at查批量用户、按city分组统计。正确的索引策略如下主键索引强制user_id作为主键聚簇索引天然存在。高频等值查询索引email字段建唯一索引UNIQUE INDEX idx_email ON users(email)因为登录查询是最高频场景且email具有强唯一性索引效率最高。联合索引的“最左前缀”实践针对WHERE status ? AND created_at ?的查询建联合索引idx_status_created ON users(status, created_at)。关键点在于字段顺序status区分度低如active/inactivecreated_at区分度高但WHERE条件中status是等值created_at是范围根据“等值在前范围在后”原则status必须放前面。如果反过来建idx_created_status优化器只能用到created_at部分status条件将失效。覆盖索引的精准投放针对SELECT count() FROM users WHERE status active建覆盖索引idx_status_count ON users(status, id)。这里id是主键必然存在索引中只存status和id查询count()时无需回表且status等值查询能高效定位。经实测该查询从120ms降至8ms。索引宽度控制避免在长文本字段如TEXT、VARCHAR(1000)上建索引。曾有一个日志表开发者在content字段建了前缀索引idx_content_100(content(100))但业务查询多为全文模糊匹配前缀索引完全无效反而拖慢写入。最终方案是引入Elasticsearch处理全文检索MySQL只负责结构化查询。提示索引不是建完就完事。定期用SELECT table_name, index_name, seq_in_index, column_name FROM information_schema.statistics WHERE table_schema your_db AND table_name users ORDER BY table_name, index_name, seq_in_index; 检查索引字段顺序和冗余情况。删除三个月内未被使用的索引可通过performance_schema.table_io_waits_summary_by_index_usage监控。3.2 SQL重写用“语法糖”绕过优化器的思维盲区MySQL优化器很强大但也有其固有的“思维定式”。SQL重写就是用它能理解的语法表达你想要的逻辑。这比调优器参数更直接有效。以下是四个高频、高回报的重写技巧IN替换OR但要控制数量WHERE id 1 OR id 2 OR id 3... 效率低下优化器可能放弃索引。改用WHERE id IN (1,2,3)。但注意IN列表不宜过长一般不超过500个否则解析开销大。某次优化批量订单查询IN列表达2000个耗时飙升。解决方案是分批查询每500个一批或改用临时表JOIN。EXISTS替代IN子查询对于“查找存在关联记录的主表数据”EXISTS通常比IN更优。例如SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id u.id AND o.status paid)。EXISTS在找到第一条匹配记录后即停止而IN可能需生成完整子查询结果集。实测在用户量100万、订单量500万的场景下EXISTS比IN快3.2倍。**避免SELECT ***明确指定所需字段。这不仅是减少网络传输更是影响执行计划的关键。例如如果只需要user_id和name建覆盖索引idx_user_id_name ON users(user_id, name)查询将走Using index避免回表。而SELECT * 即使有该索引也必须回表取其他字段索引价值大打折扣。日期范围查询替代函数WHERE YEAR(create_time) 2023 效率极低。重写为 WHERE create_time 2023-01-01 AND create_time 2024-01-01。前者无法用索引后者可充分利用create_time索引。我处理过一个财务对账脚本原SQL用MONTH()函数日均执行12次每次耗时42秒重写后降至0.8秒日省CPU时间超8分钟。注意SQL重写后务必用Explain验证执行计划是否真的改善。有时看似合理的重写可能因数据分布变化导致优化器选择不同路径。3.3 分页优化告别OFFSET拥抱游标与延迟关联LIMIT M,N尤其是N很大时是MySQL最经典的性能黑洞。原理很简单OFFSET M意味着MySQL必须先扫描并跳过前M行才能取N行。当M100000时即使最终只返回10行MySQL也要“数”100000次。这是典型的“为结果付出远超必要的代价”。解决方案有二游标分页推荐适用于有序列表如按时间倒序。核心思想是用上一页最后一条记录的排序字段值作为下一页的起点。例如第一页SELECT * FROM articles ORDER BY created_at DESC LIMIT 20假设最后一条created_at为2023-01-01 10:00:00则第二页SELECT * FROM articles WHERE created_at 2023-01-01 10:00:00 ORDER BY created_at DESC LIMIT 20。此方案Explain显示rows极小性能稳定。某新闻APP采用此方案后第1000页加载时间从12秒降至0.15秒。延迟关联适用于复杂查询当分页查询涉及多表JOIN且WHERE条件复杂时先用子查询获取主键ID再JOIN取详情。例如SELECT a.* FROM articles a INNER JOIN (SELECT id FROM articles WHERE status published ORDER BY created_at DESC LIMIT 10000, 20) AS tmp ON a.id tmp.id。子查询只扫描索引id速度快主查询只JOIN 20条记录。相比直接LIMIT 10000,20性能提升一个数量级。实操心得游标分页要求排序字段绝对唯一或组合唯一。如果created_at可能重复需加入主键id作为第二排序字段ORDER BY created_at DESC, id DESC并在WHERE中写WHERE (created_at, id) (2023-01-01 10:00:00, 12345)。3.4 表结构优化从“能用”到“好用”的底层改造表结构是性能的底层土壤。很多慢查询根源不在SQL而在设计之初就埋下的隐患。以下三点改造投入小、见效快字段类型精准化用最小够用的类型。例如用户状态status用TINYINT(1)0/1/2代替VARCHAR(20)active/inactive/pending存储空间从20字节降至1字节索引大小同步缩减缓存命中率提升。某社交App将用户等级level从VARCHAR(10)改为TINYINT相关查询QPS提升17%。避免NULL字段NULL值在索引中特殊处理增加存储和比较开销。除非业务逻辑确实需要“未知”状态否则所有字段设为NOT NULL并用默认值如0、代替。ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NOT NULL DEFAULT 。垂直拆分冷热数据将访问频率差异巨大的字段拆到不同表。例如用户基本信息id, name, email高频访问而用户详细资料address, bio, avatar_url低频访问。建users_basic和users_detail两张表用id关联。这样高频查询只扫描小表Buffer Pool压力骤减。某电商平台实施后用户中心接口P99延迟下降40%。警告表结构变更需谨慎。大表修改ADD COLUMN可能锁表。务必在业务低峰期使用pt-online-schema-change等工具在线执行。3.5 查询缓存与应用层缓存让“重复劳动”归零MySQL自带的Query Cache已从8.0移除因粒度粗整条SQL为单位、失效频繁任一相关表DML即失效而饱受诟病。现代优化应转向更精细的缓存策略应用层缓存首选用Redis缓存热点查询结果。关键在于缓存Key的设计。例如用户详情查询Key为user:detail:{user_id}Value为JSON序列化的用户对象。设置合理过期时间如2小时并配合主动更新用户资料修改时删除对应Key。某直播平台用此方案用户信息查询QPS从3000降至200DB负载下降93%。结果集缓存针对聚合查询报表类查询如每日销售额结果变化慢可缓存数小时。Key为report:daily_sales:{date}避免每次请求都扫描订单表。连接池与预编译应用层使用HikariCP等高性能连接池开启prepareStatementCacheSize如256复用预编译语句减少SQL解析开销。实测可降低单次查询耗时15%-20%。注意缓存不是万能药。必须解决缓存穿透查不存在的Key、缓存雪崩大量Key同时过期、缓存击穿热点Key失效瞬间大量请求打到DB三大问题。常用方案空值缓存、随机过期时间、互斥锁Mutex Lock。3.6 架构升级当单机MySQL成为瓶颈时的理性选择当以上所有手段都用尽QPS仍持续增长或数据量突破单机极限如表行数超5000万单表大小超100GB就需要考虑架构升级。这不是“一步登天”而是有清晰路径的渐进式演进读写分离第一道防线主库写多个从库读。通过中间件如ShardingSphere或应用层路由实现。关键点是解决主从延迟对实时性要求高的查询如用户刚下单立即查订单强制走主库对报表、搜索等允许秒级延迟的查询走从库。某电商大促期间将80%的读流量切到从库主库CPU从95%降至60%。分库分表终极方案当单表过大读写都成为瓶颈。核心原则是“找最均匀的拆分键”。用户类业务user_id是天然选择订单类业务order_id或user_id均可。工具选型轻量级用ShardingSphere-JDBC客户端分片重量级用ShardingSphere-Proxy服务端分片。某支付系统分库分表后单库数据量从8TB降至200GB查询P99从2.3秒降至120ms。异构存储卸载特定压力将不适合MySQL的场景剥离。例如海量日志用Elasticsearch实时消息用Kafka复杂图关系用Neo4j。MySQL专注其最强项强一致性事务和结构化查询。经验之谈分库分表是“伤筋动骨”的大手术务必在业务低峰期进行并做好充分压测和回滚预案。90%的团队其实卡在读写分离和应用层缓存这一关就已解决大部分性能问题。4. 完整实操流程从发现慢查询到上线验证的七步闭环再好的理论不落地都是空谈。下面是我日常处理一个线上慢查询的标准七步法每一步都有明确动作、工具和验收标准确保可复制、可追踪、可复盘。4.1 步骤一捕获慢查询工具slow_query_log pt-query-digest第一步不是打开MySQL而是确认“慢”在哪里。开启MySQL慢查询日志slow_query_logON, long_query_time1.0并用Percona Toolkit的pt-query-digest分析日志。命令pt-query-digest /var/lib/mysql/slow.log --since 2023-01-01 00:00:00 --limit 10。它会生成TOP 10最耗时的SQL列表包含平均耗时、执行次数、锁等待时间等。关键输出是“Query_time”和“Rows_examined”。例如某条SQL平均耗时3.2秒Rows_examined1245890这就是我们的目标。4.2 步骤二复现与Explain工具MySQL CLI EXPLAIN FORMATJSON在测试环境用相同参数复现该SQL。执行EXPLAIN [SQL]并务必加上FORMATJSONEXPLAIN FORMATJSON SELECT * FROM orders WHERE user_id 123 AND status shipped。JSON格式提供比传统表格更丰富的信息如“used_columns”、“index_condition_pushdown”、“filtered”等。重点关注type是否为ALL/rangekey_len是否合理rows是否远超预期Extra是否有filesort/temporary4.3 步骤三根因分析工具information_schema SHOW INDEX根据Explain结果深入分析。例如typeALL检查possible_keys是否为空若为空用SHOW INDEX FROM orders查看索引是否存在若存在检查WHERE条件是否符合最左前缀。若key_len异常小用SELECT column_name, data_type, character_maximum_length FROM information_schema.columns WHERE table_name orders AND column_name IN (user_id, status)确认字段类型和长度。这一步的目标是写出一句确定性的结论如“因status字段为VARCHAR(20)查询条件传入字符串shipped导致user_id索引无法使用优化器选择全表扫描”。4.4 步骤四制定优化方案工具纸上推演 小规模验证基于根因提出1-3个可行方案并评估ROI。例如方案A为user_id,status建联合索引预计耗时2分钟风险低方案B将status改为TINYINT需改代码和数据迁移风险高收益中方案C重写SQL为EXISTS需改业务逻辑风险中收益高。通常选择方案A先行。在测试库执行CREATE INDEX idx_user_status ON orders(user_id, status)然后再次EXPLAIN确认type变为refkey_len8rows大幅下降。4.5 步骤五性能压测工具sysbench 自定义脚本索引建好不代表万事大吉。用sysbench模拟线上流量sysbench oltp_read_only --db-drivermysql --mysql-host127.0.0.1 --mysql-port3306 --mysql-userroot --mysql-passwordpass --mysql-dbtest --tables1 --table-size1000000 --threads32 --time60 run。对比优化前后QPS、平均延迟、95分位延迟。验收标准QPS提升≥30%或P95延迟下降≥50%。某次优化索引后QPS从1200升至2100达标。4.6 步骤六上线与灰度工具pt-online-schema-change 监控生产环境建索引必须在线。使用pt-online-schema-changept-online-schema-change --alter ADD INDEX idx_user_status (user_id, status) Dtest,torders --execute。它会创建影子表同步数据最后原子切换。上线后立即在监控系统如PrometheusGrafana中观察MySQL Threads_running、Innodb_buffer_pool_read_requests、Slow_queries等指标是否平稳。灰度10%流量观察15分钟无异常再全量。4.7 步骤七效果验证与文档沉淀工具业务监控 Confluence上线后24小时内回到第一步的慢查询日志确认该SQL是否消失或耗时显著下降。同时将整个过程问题现象、Explain截图、根因分析、优化方案、压测数据、上线步骤写成Confluence文档标题为“[日期]订单表user_id_status查询优化”。这不仅是知识沉淀更是下次遇到同类问题时新人能快速上手的“作战手册”。我团队坚持此流程新人入职两周内就能独立完成一次完整优化。5. 常见问题与避坑指南那些文档里不会写的血泪教训纸上得来终觉浅绝知此事要躬行。以下是我和团队踩过的坑浓缩成可直接抄作业的避坑清单每一条都对应一个真实的、让人拍桌的瞬间。问题现象根本原因排查技巧解决方案我的教训Explain显示key有值但查询依然慢索引区分度极低如status只有Y/N两个值优化器认为全表扫描更快执行SELECT COUNT(DISTINCT status) FROM orders; 若结果≤3该字段不适合作为索引前导列删除该单列索引若必须用改为联合索引的后置字段曾为一个只有两种状态的字段建索引浪费了2GB磁盘空间还拖慢了INSERT速度加了索引Explain显示key_len正常但rows没变少MySQL统计信息过期优化器基于错误数据做决策执行SHOW INDEX FROM table_name; 查看Cardinality值若远低于实际行数或为0执行ANALYZE TABLE table_name; 强制更新统计信息某次数据批量导入后忘记ANALYZE导致优化器一直走错路排查了3小时才想到这点ORDER BY ... LIMIT N查询Explain显示Using filesort但加索引无效ORDER BY字段上有函数如ORDER BY UPPER(name)或排序方向不一致ASC/DESC混用检查ORDER BY子句确认是否为纯字段名执行SHOW CREATE TABLE table_name; 看索引定义的排序方向创建函数索引MySQL 8.0CREATE INDEX idx_upper_name ON table_name ((UPPER(name))); 或统一排序方向在MySQL 5.7上折腾半天最后发现是版本不支持函数索引果断升级到8.0分页查询LIMIT 10000,10Explain rows10000但实际执行慢MySQL必须扫描并丢弃前10000行IO和CPU开销巨大执行SELECT COUNT(*) FROM table WHERE ...; 对比COUNT结果与OFFSET值若COUNT OFFSET说明查询无意义改用游标分页或在应用层限制最大OFFSET如1000报错一个管理后台允许用户手动输入OFFSET被恶意刷到100万直接拖垮DB后来加了校验主从延迟飙升从库Seconds_Behind_Master 300s大事务如DELETE大量历史数据在主库执行从库单线程回放慢在主库执行SHOW PROCESSLIST; 找到State为Sending to client或Copying to tmp table的长事务拆分大事务为小批次如DELETE FROM logs WHERE create_time 2022-01-01 LIMIT 10000或在从库启用并行复制slave_parallel_workers 0一次清理半年日志没分批主从延迟12小时老板电话打爆从此养成分批执行的习惯实操心得永远相信Explain但不要迷信它。Explain展示的是“优化器的计划”而实际执行受数据分布、并发、硬件等多重因素影响。我的固定动作是Explain之后一定跟一句SELECT ... FOR UPDATE加锁或SELECT SLEEP(1)模拟并发观察真实耗时。有时候Explain说很快但线上一并发就卡死那一定是锁竞争问题这时就要看INFORMATION_SCHEMA.INNODB_TRX和INNODB_LOCK_WAITS了。6. 性能优化的终点建立可持续的“观测-分析-优化”循环优化不是一锤子买卖而是一个永不停歇的循环。我见过太多团队花一周时间优化了一个慢查询上线后欢呼雀跃然后回归日常三个月后又被新的慢查询淹没。真正的高手不在于单次优化多漂亮而在于构建了一套让问题“自动浮现、自动分析、自动预警”的机制。这是我们团队正在运行的“三阶防御体系”第一阶实时观测Detection在所有应用的DAO层统一注入SQL执行耗时埋点如Spring AOP拦截JdbcTemplate上报到ELK。设置阈值单条SQL耗时500ms或全表扫描rows 10000

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑