【MySQL SQL 执行全链路剖析】:执行计划、慢查询与经典场景优化指南
🔥你好我是fengxin_rou这是我的个人主页fengxin_rou的主页
❄️欢迎查看我的专栏我的专栏
《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWeb+AI的talis学习系统》、《苍穹外卖》

目录
前言
MySQL 作为主流关系型数据库,SQL 执行效率直接决定系统吞吐与响应速度。日常开发中慢查询、分页卡顿、排序聚合耗时、大表联表卡死等问题频发。本文从 SQL 完整执行流程切入,详解执行计划字段含义、效率层级,覆盖慢查询分析、分页、排序、分组、联表五大高频优化场景,兼顾原理剖析与实战调优,助力开发者快速排查并解决数据库性能瓶颈。
一、SQL 完整执行流程与 Explain 执行计划解析
一条 SQL 语句从提交到返回结果,遵循固定执行链路,依次为客户端发送请求→连接器权限校验→查询缓存命中判断→解析器语法语义解析→优化器生成最优执行计划→存储引擎执行 SQL→结果返回客户端。查询缓存 MySQL8.0 已彻底移除,实际生产无需考虑该环节。
想要预判 SQL 执行开销,核心依靠Explain执行计划,在查询语句前追加该关键字即可获取执行详情,各核心字段含义如下。

- id:查询序列号,标识 SQL 执行顺序,id 越大越先执行,相同 id 从上至下执行。
- select_type:查询类型,区分普通查询、子查询、联合查询等场景。
- type:访问类型,衡量查询效率核心指标,优先级从差到优排序:
all<index<range<ref<eq_ref<const<system。
type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
- All(全表扫描):在这些情况里,all 是最坏的情况,因为采用了全表扫描的方式。
- index(全索引扫描):index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。所以,要尽量避免全表扫描和全索引扫描。
- range(索引范围扫描):range 表示采用了索引范围扫描,一般在 where 子句中使用 < 、>、in、between 等关键词,只检索给定范围的行,属于范围查找。从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。
- ref(非唯一索引扫描):ref 类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀,返回数据返回可能是多条。因为虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
- eq_ref(唯一索引扫描):eq_ref 类型是使用主键或唯一索引时产生的访问方式,通常使用在多表联查中。比如,对两张表进行联查,关联条件是两张表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 进行执行计划查看的时候,type 就会显示 eq_ref。
- const(结果只有一条的主键或唯一索引扫描):const 类型表示使用了主键或者唯一索引与常量值进行比较,比如 select name from product where id=1。需要说明的是 const 类型和 eq_ref 都使用了主键或唯一索引,不过这两个类型有所区别,const 是与常量进行比较,查询效率会更快,而 eq_ref 通常用于多表联查中。
- key:实际最终使用的索引,无索引则为 null。
- rows:预估扫描数据行数,数值越小查询性能越好。
- Extra:额外执行信息,包含排序、临时表、索引使用等关键提示。
- Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。
- Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。
- Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。
基础查询示例:
-- 查看单表查询执行计划
EXPLAIN SELECT id,name FROM user WHERE id=10;
二、慢查询日志查看与全局优化思路
2.1 慢查询日志解读
慢查询日志是 MySQL 记录超时 SQL 的日志文件,专门捕获执行时长超出阈值的低效语句,是性能排查首要依据。 开启后可通过配置参数管控日志规则:slow_query_log控制开关,long_query_time设定超时阈值,默认 1 秒。
这是一条完整的慢查询日志
# Time: 2026-05-24T12:34:56.789012Z
# User@Host: appuser[appuser] @ 192.168.1.100 []
# Query_time: 3.212345 Lock_time: 0.000123 Rows_sent: 10 Rows_examined: 10000
SET timestamp=1779785696;
SELECT * FROM orders WHERE user_id=12345 AND status='pending';
主要观看第三行的信息
- Query_time > 1s:执行太慢。
- Rows_examined / Rows_sent > 100:扫描太多、返回太少,索引严重失效。
- Lock_time > 0.5s:锁等待久,并发冲突。
然后根据sql去执行explain插排是否全表扫描,索引是否失效等问题
2.2 慢查询整体优化思路
遵循标准化调优流程,层层递进解决性能问题。 第一步开启慢查询日志,捕获所有超时异常 SQL; 第二步使用 Explain 分析执行计划,定位索引失效、全表扫描问题; 第三步根据业务场景优化索引,删减无效索引、建立联合索引; 第四步改写 SQL 语句,规避模糊查询、隐式类型转换等踩坑写法; 第五步验证优化效果,对比执行耗时与扫描行数,确认性能提升。
还有一种方式是优化数据库表,可以把表分成大表和小表,减轻单个表的查询压力;也可以把常用的字段分出来,不常用的分到另一个表
三、分页、排序与分组高频场景深度优化
3.1 Limit 深偏移分页优化
limit 1000000,10属于深偏移分页,MySQL 会先扫描前 100 万条无效数据,再截取后 10 条,海量偏移下耗时急剧飙升。
常用两种实战优化方案。
- 主键定位分页(游标分页):利用主键有序特性,通过上一页最后主键作为起始边界,避免全量扫描。
-- 低效写法
SELECT * FROM user LIMIT 1000000,10;
-- 优化写法
SELECT * FROM user WHERE id>1000000 LIMIT 10;
- 延迟关联:先仅查询主键,再关联查询完整字段,减少磁盘 IO 开销。
3.2 Order By 排序原理与优化
排序分为索引排序与文件排序两种模式。 数据依托有序索引完成排序,无需额外资源;当无排序索引、排序字段无索引、多字段混合排序时,触发Using filesort文件排序。
文件排序会在内存或磁盘生成临时文件排序数据,资源消耗极高。
优化核心:为排序字段建立索引,减少排序字段数量,避免select *查询冗余字段,大分页一定要用 游标分页(id 定位)
3.3 Group By 分组原理与优化
分组有两种方式:
1. 索引分组(最快,推荐)
索引本身是排好序的 B+ 树,相同值挨在一起。 MySQL 直接顺序扫描索引,遇到值变化就切分组,不需要排序、不需要临时表。
特点:
- EXPLAIN Extra 无 Using temporary
- EXPLAIN Extra 无 Using filesort
- 速度极快
2. 临时表 + 排序分组(慢,最坏)
没有合适索引时:
- 创建临时表(Using temporary)
- 读取所有数据排序(Using filesort)
- 按排序结果分组
- 销毁临时表
数据一大,会直接卡死
想要优化就需要知道什么时候是最快?
WHERE 等值条件 + GROUP BY 字段,完全匹配联合索引的左前缀顺序
例子:
SELECT user_id, COUNT(*) FROM order
WHERE status = 1
GROUP BY user_id;
最优索引:
INDEX idx_status_userid (status, user_id)
所以可以得出优化方案两个方面:建立索引和保护索引不失效
1. 必做:WHERE + GROUP BY 建联合索引
2.不要对函数分组(会导致索引失效)
3. 尽量不要用 SELECT *
4. 只需要去重就用 DISTINCT 替代,能索引都一样快
本质结果一样都是:把重复 user_id 去掉,只留唯一值。
-- 写法1:DISTINCT 去重
SELECT DISTINCT user_id FROM orders;
-- 写法2:GROUP BY 分组
SELECT user_id FROM orders GROUP BY user_id;
5. 大表分组必须走索引,否则必慢查询
四、Join 连接原理与大表联表优化
这里建两个表
表 1:employees(员工表)
| id | name | department_id | |
|---|---|---|---|
| 1 | 张三 | 10 | |
| 2 | 李四 | 20 | |
| 3 | 王五 | NULL | → 没有部门 |
| 4 | 赵六 | 30 | → 部门 30 不存在 |
表 2:departments(部门表)
| id | name | |
|---|---|---|
| 10 | 技术部 | |
| 20 | 产品部 | |
| 50 | 市场部 | → 没有员工 |
1. 内连接 (INNER JOIN)
内连接返回两个表中有匹配关系的行。示例:
SELECT employees.name, departments.name
FROM employees
INNER JOIN departments
ON employees.department_id = departments.id;
这个查询返回每个员工及其所在的部门名称。
结果(只保留两边都匹配的行)
| name | name |
|---|---|
| 张三 | 技术部 |
| 李四 | 产品部 |
2. 左外连接 (LEFT JOIN)
左外连接返回左表中的所有行,即使在右表中没有匹配的行。未匹配的右表列会包含NULL。示例:
SELECT employees.name, departments.name
FROM employees
LEFT JOIN departments
ON employees.department_id = departments.id;
这个查询返回所有员工及其部门名称,包括那些没有分配部门的员工。
结果(左表全部保留,右表没匹配填 NULL)
| name | name |
|---|---|
| 张三 | 技术部 |
| 李四 | 产品部 |
| 王五 | NULL |
| 赵六 | NULL |
3. 右外连接 (RIGHT JOIN)
右外连接返回右表中的所有行,即使左表中没有匹配的行。未匹配的左表列会包含NULL。示例:
SELECT employees.name, departments.name
FROM employees
RIGHT JOIN departments
ON employees.department_id = departments.id;
这个查询返回所有部门及其员工,包括那些没有分配员工的部门。
结果(右表全部保留,左表没匹配填 NULL)
| name | name |
|---|---|
| 张三 | 技术部 |
| 李四 | 产品部 |
| NULL | 市场部 |
4. 全外连接 (FULL JOIN)
全外连接返回两个表中所有行,包括非匹配行,在MySQL中,FULL JOIN 需要使用 UNION 来实现,因为 MySQL 不直接支持 FULL JOIN。示例:
SELECT employees.name, departments.name
FROM employees
LEFT JOIN departments
ON employees.department_id = departments.id
UNION
SELECT employees.name, departments.name
FROM employees
RIGHT JOIN departments
ON employees.department_id = departments.id;
这个查询返回所有员工和所有部门,包括没有匹配行的记录。
结果(两张表所有数据都保留,缺的填 NULL)
| name | name |
|---|---|
| 张三 | 技术部 |
| 李四 | 产品部 |
| 王五 | NULL |
| 赵六 | NULL |
| NULL | 市场部 |
联表查询底层采用嵌套循环连接算法,驱动表逐行匹配被驱动表数据。
- JOIN 的本质:把两张表按条件 “配对”
- 谁驱动谁:左连接 = 左表驱动;右连接 = 右表驱动
- 驱动表的所有行必须全部出现
5. 大表 Join 优化策略
大表 JOIN 慢是因为:被驱动表没索引 + 扫描行数太多。
所以优化目标只有 3 个:
- 减少扫描行数
- 被驱动表必须加索引
- 尽量让小表驱动大表
1. 被驱动表的关联字段必须建索引(重要)
A LEFT JOIN B ON A.b_id = B.id
- A = 驱动表
- B = 被驱动表
- B.id 必须建索引!
如果没索引 → 每次匹配都要全表扫描 B 表 → 大表直接卡死。
2. 小表驱动大表(内连接核心)
小表 INNER JOIN 大表
MySQL 优化器会自动选小表做驱动表,循环次数最少,最快。
比如:
- 小表 100 行 → 循环 100 次
- 大表 1000 万行 → 被驱动,走索引
速度差距 10~1000 倍。
3. 禁止大表 JOIN 大表
业务上一定要避免:
1000万表 JOIN 1000万表
解决方案:
- 分表
- 冗余字段(反范式)
- 程序里分两次查询再合并
4. 优先使用覆盖索引,避免回表
SELECT user_id, name FROM A JOIN B ON A.user_id = B.id
如果 B 表索引包含 (id, name) → 直接从索引取数据,不用回表,速度提升巨大。
这叫 覆盖索引。
5. 连接条件只允许 等值匹配(=)
A JOIN B ON A.id = B.id ✅ 最快(可以用索引)
A JOIN B ON A.id > B.id ❌ 慢死(不能用索引)
JOIN 条件必须是 =,不能是 > < like !=
6. 过滤条件优先写在 WHERE 里,先缩小数据量
SELECT * FROM A
JOIN B ON A.b_id = B.id
WHERE A.create_time > '2025-01-01' ✅ 先过滤,再JOIN
先把 A 表数据变小,再 JOIN,速度极快。
MySQL 真实执行顺序是:
- 先从 A 表取出
create_time > '2025-01-01'的少量数据 - 再用这少量数据去和 B 表匹配
- 速度极快
7. ** 不要 SELECT *,只查需要字段
减少内存、减少 IO、减少排序,能快 30%~50%。
结语
本文完整梳理 SQL 从执行流程、执行计划解读,到慢查询排查、分页排序分组、联表查询四大核心优化体系。type 访问类型、索引有效性是判断 SQL 性能的核心标尺,深偏移分页、文件排序、无索引联表是开发高频性能坑点。
实际调优中优先借助 Explain 定位问题,再结合业务增设合理索引、规范 SQL 写法,大表操作尽量缩减数据处理范围。后续可进阶学习 MySQL 索引底层结构、事务锁机制、分库分表方案,进一步应对海量数据场景下的数据库性能挑战。

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)