核心技术点:​

  1. 执行计划深度解析:不只是看type字段,更要看懂Extra里的"潜台词"​
  2. 索引失效的真相:统计信息、成本估算与优化器的"小心思"​
  3. 连接查询优化:从Nested Loop Join到Batched Key Access的演进

一、Explain:看懂执行计划才是优化的开始

        很多工程师用Explain就是扫一眼type字段,看到ALL就喊"没走索引",看到index就觉得"没问题"。这实在太肤浅了。Explain就像医生的CT报告,得会看细节才能确诊。

1.1 type字段:查询的"访问路径"​

        type字段确实重要,它表示MySQL决定如何查找数据。从好到差大概是这样的:

  • system/const:​​ 通过主键或唯一索引直接找到一条记录,最优。
  • eq_ref:​​ 关联查询时,使用主键或唯一索引进行匹配。
  • ref:​​ 使用普通索引进行等值查询。
  • range:​​ 索引范围扫描(BETWEEN、IN、>、<等)。
  • index:​​ 全索引扫描(比ALL好点,但也要小心)。
  • ALL:​​ 全表扫描,性能最差。

        但我要特别提醒的是,​不是看到index或ALL就一定是坏事。比如你要查SELECT COUNT(*) FROM table,如果有个二级索引比主键索引小(字节数少),MySQL选择扫描整个二级索引(index)反而比扫描主键索引(ALL?不,这里其实也是index,但扫描的索引树更小)更快。

1.2 Extra字段:藏着"魔鬼"的地方

        这里才是执行计划的精华所在,很多关键信息都藏在这里:

  • Using where:​​ 服务器层在存储引擎返回数据后进行了过滤。这意味着索引可能没有完全覆盖查询条件。
  • Using index:​​ 使用了覆盖索引,这是性能最好的情况之一。
  • Using temporary:​​ 使用了临时表,常见于GROUP BY、ORDER BY等操作,需要警惕。
  • Using filesort:​​ 需要额外的排序操作,如果数据量大会很耗性能。
  • Select tables optimized away:​​ 比如用MIN()、MAX()查询时,MySQL可以直接从索引中获取值而不需要真正执行查询。

踩坑经历:一次"Using index"的假象

        记得有一次,我们有个分页查询突然变慢。Explain显示type是ref,Extra是Using index,看起来完美利用了索引。但查询就是需要2-3秒。

排查过程:​

  1. 首先怀疑是数据量大了,但表总共就几十万数据,不应该这么慢。
  2. 仔细分析Extra字段,虽然显示Using index,但后面还有个"Using where"。
  3. 原来这个查询是SELECT * FROM table WHERE status = 1 LIMIT 800000, 20。虽然status字段有索引,但MySQL需要先从索引中找出80万条满足条件的记录对应的主键ID,然后回表80万次,最后才返回20条数据。
  4. 这个"Using index"只是用索引来定位记录,但大量的回表操作(随机IO)才是性能杀手。

解决方案:​
        我们把分页改成了基于游标的方式:WHERE id > 上一页最大ID AND status = 1 LIMIT 20,同时建立了(status, id)的联合索引。这样MySQL可以直接通过索引定位到起始位置,然后顺序扫描20条记录即可。

独家见解:​

  • 不要迷信Using index:​​ 要区分是覆盖索引(最优)还是仅仅用索引来查找记录(可能伴随大量回表)。
  • 分页查询的坑:​​ LIMIT M, N在M很大时性能极差,应该使用基于游标的分页。
  • 联合索引的顺序很重要:​​ 等值查询字段放前面,范围查询字段放后面。

二、索引失效的真相:优化器不是总聪明

        很多人背八股文说"LIKE以%开头索引失效",但不知道为什么。其实根本原因是B+树索引的有序性——如果开头是通配符,索引的有序性就无法利用,只能全扫描。

2.1 统计信息的陷阱

        MySQL优化器是基于成本估算的,它依赖表的统计信息来决定使用哪个索引。但这些统计信息可能不准确:

  • 当表数据变化超过一定比例时,统计信息会自动更新
  • 但自动更新可能不及时,或者采样率设置不合理
  • 这会导致优化器错误地估算每个索引的成本

踩坑经历:优化器的"错误选择"​

        我们遇到过最诡异的问题:一个运行良好的SQL突然变慢,Explain发现它居然放弃了原本高效的索引,选择了全表扫描。

排查过程:​

  1. 首先用FORCE INDEX强制使用原来的索引,查询立即恢复正常
  2. 这说明不是索引的问题,而是优化器选错了索引
  3. 检查发现,昨晚有大量数据清理操作,表数据量减少了70%
  4. 但统计信息没有及时更新,优化器还按照之前的数据分布来估算成本

解决方案:​
        我们执行了ANALYZE TABLE来重新生成统计信息。之后优化器就做出了正确的选择。

预防措施:​

  • 对于数据变化频繁的表,可以适当提高统计信息的采样率
  • 在重大数据变更后,手动执行ANALYZE TABLE
  • 考虑使用更精确的统计信息收集方式

2.2 成本估算的局限性

        优化器的成本模型基于一些假设,可能不总是准确:

  • 它认为顺序IO比随机IO快,但具体快多少取决于硬件
  • 它对数据分布的理解可能不准确,特别是当有数据倾斜时
  • 它无法预知缓存命中率,而是假设每次读取都需要磁盘IO

独家见解:​

  • 不要完全相信优化器:​​ 当发现执行计划不理想时,可以尝试用FORCE INDEX
  • 理解数据分布:​​ 对于有严重数据倾斜的字段,可能需要特殊的索引策略
  • 监控执行计划变化:​​ 重要的查询应该定期检查执行计划

三、连接查询的优化艺术

        连接查询是另一个容易出性能问题的地方,特别是当多张大表关联时。

3.1 连接算法的选择

        MySQL主要使用Nested Loop Join:

  1. 驱动表(小表)的每条记录
  2. 去被驱动表(大表)中查找匹配记录
  3. 被驱动表的连接字段必须有索引,否则性能是灾难性的

踩坑经历:一次JOIN操作拖垮整个数据库

        我们有个报表查询需要5张表关联,平时运行良好。但某个月初,这个查询突然需要运行5分钟,期间数据库CPU持续100%。

排查过程:​

  1. 发现优化器选择了一个错误表作为驱动表
  2. 这个驱动表虽然记录数少,但连接字段在被驱动表上没有索引
  3. 导致对被驱动表进行了数百万次的全表扫描

解决方案:​

  • 给被驱动表的连接字段添加索引
  • 使用STRAIGHT_JOIN强制指定连接顺序
  • 考虑将部分关联逻辑移到应用层处理

3.2 子查询的陷阱

        很多开发喜欢写子查询,觉得逻辑清晰。但子查询可能产生临时表,导致性能问题:

-- 不好的写法
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);

-- 更好的写法
SELECT u.* FROM users u 
JOIN orders o ON u.id = o.user_id 
WHERE o.amount > 1000;

独家见解:​

  • 能用JOIN就不用子查询:​​ 大多数情况下JOIN性能更好
  • 注意连接顺序:​​ 小表作为驱动表,大表要有索引
  • 考虑反范式化:​​ 某些场景下,适当的数据冗余可以避免复杂的连接查询

四、实战优化策略清单

        基于这些年的经验,我总结了一些实用的优化准则:

4.1 索引设计原则

  • 只为查询需要的列建索引
  • 考虑索引的选择性(唯一值越多,索引效果越好)
  • 联合索引要注意字段顺序
  • 避免过度索引,影响写性能

4.2 查询编写规范

  • 避免SELECT *,只取需要的字段
  • 小心使用OR,可能导致索引失效
  • 注意IN列表的长度,太长会影响性能
  • 避免在WHERE子句中对字段进行函数操作

4.3 架构层面优化

  • 读写分离,将查询压力分散到从库
  • 适当的数据分片,避免单表过大
  • 使用缓存减少数据库压力
  • 考虑使用列式存储处理分析型查询

结尾

        MySQL查询优化是个系统工程,需要从索引设计、SQL编写、执行计划分析等多个角度综合考虑。最重要的不是记住各种规则,而是理解其背后的原理——特别是B+树索引的工作原理和优化器的成本估算逻辑。

        我经常跟团队说,优化就像破案,要有耐心看线索(执行计划),理解动机(优化器的选择),最后才能找到真凶(性能瓶颈)。每个数据库都是独特的,需要根据实际的数据分布和查询模式来制定优化策略。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐