🔥你好我是fengxin_rou这是我的个人主页fengxin_rou的主页

❄️欢迎查看我的专栏我的专栏

《Java后端学习》《JAVASE基础》《JUC并发》《redis》《JVM虚拟机》《MYSQL》《黑马点评》《rabbitmq》《JavaWeb+AI的talis学习系统》《苍穹外卖》

目录

前言

一、SQL 完整执行流程与 Explain 执行计划解析

二、慢查询日志查看与全局优化思路

2.1 慢查询日志解读

2.2 慢查询整体优化思路

三、分页、排序与分组高频场景深度优化

3.1 Limit 深偏移分页优化

3.2 Order By 排序原理与优化

3.3 Group By 分组原理与优化

1. 索引分组(最快,推荐)

2. 临时表 + 排序分组(慢,最坏)

四、Join 连接原理与大表联表优化

1. 内连接 (INNER JOIN)

2. 左外连接 (LEFT JOIN)

3. 右外连接 (RIGHT JOIN)

4. 全外连接 (FULL JOIN)

5. 大表 Join 优化策略

结语


前言

MySQL 作为主流关系型数据库,SQL 执行效率直接决定系统吞吐与响应速度。日常开发中慢查询、分页卡顿、排序聚合耗时、大表联表卡死等问题频发。本文从 SQL 完整执行流程切入,详解执行计划字段含义、效率层级,覆盖慢查询分析、分页、排序、分组、联表五大高频优化场景,兼顾原理剖析与实战调优,助力开发者快速排查并解决数据库性能瓶颈。

一、SQL 完整执行流程与 Explain 执行计划解析

一条 SQL 语句从提交到返回结果,遵循固定执行链路,依次为客户端发送请求→连接器权限校验→查询缓存命中判断→解析器语法语义解析→优化器生成最优执行计划→存储引擎执行 SQL→结果返回客户端。查询缓存 MySQL8.0 已彻底移除,实际生产无需考虑该环节。

想要预判 SQL 执行开销,核心依靠Explain执行计划,在查询语句前追加该关键字即可获取执行详情,各核心字段含义如下。

img

  1. id:查询序列号,标识 SQL 执行顺序,id 越大越先执行,相同 id 从上至下执行。
  2. select_type:查询类型,区分普通查询、子查询、联合查询等场景。
  3. 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 通常用于多表联查中
  1. key:实际最终使用的索引,无索引则为 null。
  2. rows:预估扫描数据行数,数值越小查询性能越好。
  3. 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';

主要观看第三行的信息

  1. Query_time > 1s:执行太慢。
  2. Rows_examined / Rows_sent > 100:扫描太多、返回太少,索引严重失效
  3. 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. 临时表 + 排序分组(慢,最坏)

没有合适索引时:

  1. 创建临时表(Using temporary)
  2. 读取所有数据排序(Using filesort)
  3. 按排序结果分组
  4. 销毁临时表

数据一大,会直接卡死

想要优化就需要知道什么时候是最快?

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. 减少扫描行数
  2. 被驱动表必须加索引
  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万表

解决方案:

  1. 分表
  2. 冗余字段(反范式)
  3. 程序里分两次查询再合并

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 真实执行顺序是:

  1. 先从 A 表取出 create_time > '2025-01-01'少量数据
  2. 再用这少量数据去和 B 表匹配
  3. 速度极快

7. ** 不要 SELECT *,只查需要字段

减少内存、减少 IO、减少排序,能快 30%~50%

结语

本文完整梳理 SQL 从执行流程、执行计划解读,到慢查询排查、分页排序分组、联表查询四大核心优化体系。type 访问类型、索引有效性是判断 SQL 性能的核心标尺,深偏移分页、文件排序、无索引联表是开发高频性能坑点。

实际调优中优先借助 Explain 定位问题,再结合业务增设合理索引、规范 SQL 写法,大表操作尽量缩减数据处理范围。后续可进阶学习 MySQL 索引底层结构、事务锁机制、分库分表方案,进一步应对海量数据场景下的数据库性能挑战。

Logo

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

更多推荐