MySQL查询优化完整指南:从理论到实践
本文从MySQL查询的基础机制出发,深入探讨单表查询访问方法、联表查询策略、成本计算原理、基于规则的优化技术,最后通过实际案例展示慢SQL的诊断和优化过程。
目录
- 一、单表查询的访问方法
- 二、联表查询机制
- 三、查询成本计算
- 四、基于规则的查询优化
- 五、执行计划分析
- 六、慢SQL治理实战案例
一、单表查询的访问方法
MySQL 的访问方法定义了查询语句的执行方式,类似于从起点到终点的路线选择。查询优化器根据条件、索引和统计信息选择最优访问方法,以最小化执行成本(I/O、CPU、内存等)。虽然查询结果相同,但不同访问方法的效率差异显著。
1.1 访问方法性能对比
不同访问方法的性能差异如下图所示,从左到右效率递减:
1.2 具体访问方法详解
1.2.1 const - 最高效访问
- 描述: 通过主键或唯一二级索引与常数等值比较,定位单条记录
- 性能: 效率最高(“坐火箭”),成本为常数级别,无需扫描或回表
- 适用场景: 主键或唯一索引的等值查询,联合索引需所有列等值匹配
- 注意: 唯一索引查询 NULL 值(如
key IS NULL
)无法使用const
,因 NULL 可重复 - 示例:
SELECT * FROM table WHERE id = 100;
使用主键,定位单条记录
1.2.2 ref - 普通索引等值查询
- 描述: 使用普通二级索引进行等值查询,可能匹配多条记录,需回表获取完整记录
- 性能: 效率次于
const
(“坐高铁”),适合选择性高的索引,成本随匹配记录数增加 - 适用场景: 普通二级索引或联合索引最左连续列等值匹配,包括 NULL 值查询
- 示例:
SELECT * FROM table WHERE key1 = 'abc';
使用普通索引idx_key1
1.2.3 ref_or_null - 扩展NULL查询
- 描述: 扩展
ref
,额外查找索引列为 NULL 的记录 - 性能: 效率略低于
ref
,因需处理 NULL 检查 - 适用场景: 等值查询结合 NULL 检查,如
WHERE key1 = 'abc' OR key1 IS NULL
- 注意: 若无 NULL 值,可能降级为
ref
1.2.4 range - 范围查询
- 描述: 使用索引(聚簇或二级索引)进行范围查询,条件对应数轴上的区间(单点或连续区间)
- 性能: 效率依赖范围大小,选择性高时接近
ref
,需回表 - 适用场景: 支持
>
,<
,IN
,BETWEEN
,LIKE
(前缀匹配)等操作符 - 示例:
SELECT * FROM table WHERE key1 IN ('a', 'b');
生成单点区间
1.2.5 index_merge - 多索引合并
-
描述: 使用多个二级索引合并结果,分为三种算法:
- Intersection: 取索引结果交集,适用于等值匹配或主键范围匹配,需按主键排序
- Union: 取索引结果并集,适用于 OR 连接的等值匹配
- Sort-Union: 范围查询结果按主键排序后取并集,成本略高
-
性能: 效率低于单索引查询(
range
/ref
),因涉及多索引操作和回表,但优于全表扫描 -
适用场景: 复杂条件涉及多个索引,如
WHERE key1 = 'a' OR key2 = 'b'
-
注意: 优化器根据成本选择是否使用,联合索引可替代以降低成本
1.2.6 index - 索引全扫描
- 描述: 直接扫描二级索引全部叶子节点,查询列和条件均包含在索引中,免回表
- 性能: 优于全表扫描,因索引记录小,但仍需全索引扫描
- 适用场景: 查询列和条件全在索引中,如
SELECT key1 FROM table WHERE key1 = 'a';
- 注意: 需确保查询不涉及非索引列
1.2.7 all - 全表扫描
- 描述: 扫描整个聚簇索引,逐行检查条件
- 性能: 效率最低(“坐乌龟”),扫描全表记录,I/O 成本高
- 适用场景: 无索引可用或条件选择性低
- 示例:
SELECT * FROM table WHERE non_indexed_col = 1;
1.3 重要注意事项
二级索引与回表
通常使用单个二级索引,先定位记录,再回表获取完整数据。非索引条件在回表后过滤。
范围区间确定
AND
连接取交集,OR
连接取并集- 无法使用索引的条件替换为
TRUE
,简化区间计算 - 复杂条件需逐一分析,提取有效区间
联合索引优化
多列查询可通过联合索引替代 index_merge
,但需平衡其他查询对单列索引的需求。
优化器决策
优化器基于统计信息(ANALYZE TABLE
更新)选择最低成本访问方法,通过合理设计索引和查询,MySQL 优化器能选择高效访问方法,显著提升查询性能。
二、联表查询机制
2.1 联表查询的本质
联表查询将多个表的记录组合成笛卡尔积,通过过滤条件生成结果集。笛卡尔积是各表记录逐一匹配形成的组合,记录数为各表行数的乘积(如两表各 100 行,生成 100×100=10000 行)。MySQL 语法简单,在 FROM
子句列出表名即可,但需通过过滤条件(涉及表间比较,如 table1.col1 = table2.col2
)控制结果集规模。
2.2 联表查询的分类
2.2.1 内连接(INNER JOIN)
- 仅保留符合连接条件和过滤条件的记录组合,驱动表与被驱动表可互换
- 示例:
SELECT * FROM table1 INNER JOIN table2 ON table1.col1 = table2.col2;
仅返回匹配记录(默认的联表查询)
2.2.2 外连接(OUTER JOIN)
- 驱动表记录即使无匹配也保留,被驱动表字段补
NULL
- 左外连接: 左表为驱动表,
SELECT * FROM table1 LEFT JOIN table2 ON ... [WHERE ...]
- 右外连接: 右表为驱动表,
SELECT * FROM table1 RIGHT JOIN table2 ON ...
2.3 联表查询执行过程
联表查询由优化器驱动,执行过程如下图所示:
查询驱动表:
- 选择第一个表(驱动表),使用单表访问方法(如
const
,ref
,all
)执行查询,获取符合单表条件的记录 - 示例:
SELECT * FROM table1, table2 WHERE table1.col1 > 10 AND table1.col1 = table2.col2;
先查询table1
满足col1 > 10
的记录。
查询被驱动表:
- 对驱动表结果集的每条记录,查询被驱动表,应用两表条件(如
table1.col1 = table2.col2
)和被驱动表单表条件 - 被驱动表访问次数等于驱动表结果集记录数
2.4 连接算法与优化
2.4.1 嵌套循环连接(Nested Loop Join)
- 驱动表查询一次,被驱动表查询多次(次数等于驱动表结果集记录数)
- 多表连接时,上一轮结果集作为新驱动表,重复过程
- 特点: 简单但效率低,被驱动表可能多次全表扫描
- 性能: 成本随驱动表记录数和被驱动表访问方式增加
2.4.2 索引优化策略
- 被驱动表查询可利用索引加速,参考常见访问方法
- 建议: 为被驱动表的关键列(如连接条件中的列)添加索引,确保选择性高,查询列表仅包含必要列以触发
index
2.4.3 基于块的嵌套循环连接(Block Nested Loop Join)
- 被驱动表数据量大时,多次全表扫描导致高 I/O 成本
- 采用
join buffer
(默认 256KB,可通过join_buffer_size
调整)缓存驱动表结果集记录 - 具体过程:
- 将驱动表查询列和条件放入
join buffer
- 扫描被驱动表,记录一次性与
join buffer
中多条记录匹配 - 减少被驱动表 I/O,最佳情况为
join buffer
容纳所有驱动表记录,仅需扫描被驱动表一次
- 将驱动表查询列和条件放入
三、查询成本计算
3.1 MySQL 查询成本构成
MySQL 查询的执行成本主要分为I/O成本和CPU成本:
3.1.1 I/O 成本
从磁盘加载数据或索引页面到内存的耗时,以页面(page)为基本单位,默认页面大小为 16KB,成本常数默认为 1.0
,出现在涉及表数据或索引的加载这样的场景中,例如全表扫描或索引范围查询。
3.1.2 CPU 成本
读取记录、检测搜索条件、排序等操作的耗时,读取并检测一条记录是否符合条件的默认成本为 0.2
(无论是否需要条件检测),主要包括条件比较、结果集排序等。
3.1.3 成本常数
1.0
(页面读取)和 0.2
(记录检测)是默认值,硬编码在 MySQL 源码中,部分微调值(如 1.1
或 0.01
)用于调整成本估算。
3.2 单表查询成本计算流程
MySQL 查询优化器在执行单表查询前会评估所有可能执行方案的成本,选择成本最低的方案作为执行计划:
具体步骤包括:
- 根据搜索条件,找出可能使用的索引
- 计算全表扫描的成本
- 计算使用索引的成本
- 考虑索引合并
- 选择成本最低的执行方案
3.3 联表查询成本计算
总成本公式:
总成本 = 单次访问驱动表的成本 + 驱动表扇出 × 单次访问被驱动表的成本
其中:
- 驱动表扇出: 驱动表查询结果的记录数
- 当无法直接确定扇出时,优化器通过启发式规则(heuristic)估算剩余条件的过滤效果
3.3.1 联表查询成本评估示例
以内连接方式举例:
SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.common_field
WHERE s1.key2 > 10 AND s1.key2 < 1000 AND s2.key2 > 1000 AND s2.key2 < 2000;
MySQL 优化器评估两种连接顺序(s1驱动s2或s2驱动s1),计算对应的总成本。
3.4 成本计算总结
MySQL 查询优化器通过比较全表扫描和索引访问的 I/O 和 CPU 成本,选择成本最低的执行计划。联表查询中,优化器评估不同连接顺序和访问方法,结合条件过滤和统计数据提高估算准确性。
四、基于规则的查询优化
MySQL 查询优化器通过基于规则的优化手段,将用户编写的复杂或低效查询语句重写为更高效的形式(包括条件化简、外连接消除和子查询优化等),以提升查询性能。
4.1 条件化简
MySQL 查询优化器会对查询中的搜索条件(表达式)进行化简,以减少计算复杂度和提高执行效率。
4.1.1 移除不必要的括号
优化器会移除查询中多余的括号,简化表达式,例如:
-- 原始查询
((a = 5 AND b = c) OR ((a > c) AND (c < 5)))-- 优化后
(a = 5 AND b = c) OR (a > c AND c < 5)
4.1.2 常量传递
当某个列与常量等值匹配,并且与其他表达式通过 AND 连接时,优化器会将常量值传递到其他表达式中:
-- 原始查询
a = 5 AND b > a-- 优化后
a = 5 AND b > 5
4.1.3 等值传递
当多个列之间存在等值关系时,优化器会将等值条件传递:
-- 原始查询
a = b AND b = c AND c = 5-- 优化后
a = 5 AND b = 5 AND c = 5
4.1.4 移除无用条件
对于恒为 TRUE 或 FALSE 的条件,优化器会直接移除:
-- 原始查询
(a < 1 AND b = b) OR (a = 6 OR 5 != 5)-- 优化后
a < 1 OR a = 6
4.1.5 表达式计算
对于只包含常量的表达式,优化器会在查询执行前计算其值:
-- 原始查询
a = 5 + 1-- 优化后
a = 6
但对于涉及复杂函数或非单独列的表达式(如 ABS(a) > 5
),优化器不会进行化简。
4.1.6 HAVING 和 WHERE 子句合并
如果查询不包含聚合函数(如 SUM、MAX)或 GROUP BY 子句,优化器会将 HAVING 子句合并到 WHERE 子句中。
4.2 外连接消除
外连接(LEFT/RIGHT JOIN)与内连接(INNER JOIN)的区别在于,驱动表的记录即使在被驱动表中找不到匹配记录,也会被保留(被驱动表字段填充为 NULL)。
4.2.1 空值拒绝(Null Rejection)
如果 WHERE 子句中指定被驱动表的列不为 NULL(如 t2.n2 IS NOT NULL
),外连接效果等价于内连接:
-- 原始查询
SELECT * FROM t1 LEFT JOIN t2 ON t1.m1 = t2.m2 WHERE t2.n2 IS NOT NULL;-- 等价于
SELECT * FROM t1 INNER JOIN t2 ON t1.m1 = t2.m2;
这种转换允许优化器调整连接顺序,降低查询成本。
4.3 子查询优化
子查询是嵌套在查询中的子查询,优化器通过物化表和半连接等策略优化其执行效率。
4.3.1 子查询出现位置
- SELECT 子句:如
(SELECT m1 FROM t1 LIMIT 1)
- FROM 子句:作为派生表,如
(SELECT m2 + 1 AS m, n2 AS n FROM t2 WHERE m2 > 2) AS t
- WHERE 或 ON 子句:如
WHERE m1 IN (SELECT m2 FROM t2)
- ORDER BY 或 GROUP BY 子句:较少使用
4.3.2 子查询分类
按返回结果集分类:
- 标量子查询:返回单一值(如
(SELECT m1 FROM t1 LIMIT 1)
) - 行子查询:返回单条记录,包含多列(如
(SELECT m2, n2 FROM t2 LIMIT 1)
) - 列子查询:返回单列多行(如
(SELECT m2 FROM t2)
) - 表子查询:返回多行多列(如
(SELECT m2, n2 FROM t2)
)
按与外层查询关系分类:
- 不相关子查询:独立执行,不依赖外层查询值
- 相关子查询:执行依赖外层查询值,如
WHERE m1 IN (SELECT m2 FROM t2 WHERE n1 = n2)
4.3.3 子查询在布尔表达式中的使用
- 比较操作符(=、>、<、等):与标量子查询或行子查询结合
- IN/NOT IN、ANY/SOME、ALL:与列子查询或表子查询结合
- EXISTS/NOT EXISTS:判断子查询结果集是否为空
4.3.4 子查询优化策略
MySQL优化器通过不同的策略优化各类子查询,主要优化路径如下:
主要优化策略说明:
物化表(Materialization):
- 将不相关 IN 子查询结果集写入临时表(物化表),对物化表记录去重,建立哈希索引(内存)或 B+ 树索引(磁盘)
- 将外层查询与物化表进行内连接,由优化器评估连接顺序成本
- 适用于子查询结果集较大时,物化表通过索引加速 IN 判断
半连接(Semi-join):
- 将 IN 子查询转换为半连接,仅保留外层查询表的记录,不关心被驱动表匹配的记录数
- 适用于子查询在 WHERE/ON 子句中,与 IN 语句结合,或者子查询为单一查询(无 UNION、GROUP BY、HAVING 等)
- 不适用于子查询与 OR 连接、使用 NOT IN或者子查询在 SELECT 子句中
执行策略包括:
- 子查询查询列表为主键/唯一索引列时,将子查询表上拉到外层查询的 FROM 子句
- 使用临时表记录外层查询记录 ID,消除重复
- 对子查询表使用松散索引扫描,仅取索引值相同的首条记录
- 逐条检查外层查询记录,找到第一条匹配的子查询记录即停止
- 物化子查询后与外层查询表连接
4.4 优化规则总结
MySQL 查询优化器通过条件化简、外连接消除和子查询优化等基于规则的优化手段,将用户编写的低效查询转换为高效形式。这些规则包括移除冗余条件、合并子句、将外连接转为内连接、以及通过物化表和半连接优化 IN 子查询。开发者可以通过编写简洁的 SQL 语句、利用索引和主键、避免冗余操作等方式,进一步配合优化器提升查询性能。
五、执行计划分析
MySQL采用EXPLAIN
分析SQL查询的执行计划,帮助优化查询性能。它展示MySQL如何执行查询,包括表访问顺序、索引使用情况等。
5.1 EXPLAIN 输出字段详解
运行EXPLAIN
语句后,会返回一个结果集,包含多个字段:
5.1.1 查询标识字段
- id:表示查询中每个子查询的执行顺序。值越大,优先级越高,越先执行。相同
id
表示同一执行层级 - select_type:查询类型,常见值包括:
SIMPLE
:简单查询,无子查询或联合PRIMARY
:最外层查询SUBQUERY
:子查询DERIVED
:派生表(如FROM子句中的子查询)UNION
:UNION操作中的查询
5.1.2 表和分区信息
- table:显示查询涉及的表名或别名
- partitions:显示查询涉及的分区(如果表分区了)。无分区表时为空
5.1.3 访问方法和索引信息
-
type:访问类型(即前面所说的访问方法),反映查询效率,常见值从优到劣:
system
:表只有一行数据const
:通过主键或唯一索引直接定位一行eq_ref
:通过主键或唯一索引进行等值匹配ref
:通过非唯一索引进行等值匹配range
:索引范围扫描(如>
、<
、IN
)index
:全索引扫描ALL
:全表扫描(效率最低)
-
possible_keys:MySQL可能使用的索引列表。为空表示没有可用索引
-
key:实际使用的索引。为空表示未使用索引
-
key_len:使用索引的长度(字节),帮助判断联合索引的使用情况。值越小,索引选择性越高
5.1.4 查询条件和统计信息
- ref:显示与
key
索引比较的列或常量。例如,const
表示常量值,表名.列名表示关联表的列 - rows:估计扫描的行数,值越小越好,反映查询效率
- filtered:表示过滤后的行数百分比(0-100%)。值越高,表示过滤效果越好
5.1.5 额外信息
- Extra:额外信息,常见值:
Using index
:仅使用索引完成查询(覆盖索引)Using where
:在WHERE条件中过滤Using temporary
:使用了临时表(可能影响性能)Using filesort
:需要额外排序(可能影响性能)Using join buffer
:使用了连接缓冲区
5.2 执行计划分析要点
执行计划分析时,需要重点关注以下几个关键字段:
分析时着重点:
- 关注
type
,尽量避免ALL
或index
,优先const
、eq_ref
、ref
- 检查
key
和possible_keys
,确保使用合适的索引 - 观察
rows
和filtered
,减少扫描行数 - 注意
Extra
中的Using temporary
和Using filesort
,可能需要优化
5.3 实际分析示例
通过分析这些字段,可定位查询瓶颈,优化索引或重写SQL以提高性能。
六、慢SQL治理实战案例
以下是5个经过脱敏处理的实际慢SQL案例,展示了从问题发现到优化解决的完整过程。
6.1 慢SQL治理流程图
慢SQL治理是一个系统性的过程,需要遵循科学的流程来确保优化效果:
慢SQL治理的关键在于系统性思考和渐进式优化:
- 发现阶段:建立完善的监控体系,及时发现性能问题
- 分析阶段:深入分析根本原因,避免头痛医头、脚痛医脚
- 方案阶段:制定多层次的优化方案,从SQL到架构全方位考虑
- 实施阶段:谨慎实施,确保系统稳定性
- 验证阶段:客观评估优化效果,确保问题真正解决
- 监控阶段:持续观察,防止问题反弹
6.2 案例一:模糊匹配优化
问题SQL(脱敏后)
SELECT count(*)
FROM `脱敏表`
WHERE (xxx_id = XXX) AND (`name` LIKE '%关键词%')
执行计划分析
字段 | 值 | 含义 |
---|---|---|
id | 1 | 单表查询 |
select_type | SIMPLE | 简单查询 |
table | 脱敏表 | 查询目标表 |
type | ref | 使用了索引,但非最优 |
key | idx_xxxid_xxxuserid | 使用的索引不含 name |
rows | 589328 | 预计扫描 58 万行 |
filtered | 11.11 | 低过滤率 |
Extra | Using where | 没有使用 name 字段索引,存在回表操作 |
性能瓶颈分析
问题点 | 描述 |
---|---|
%关键词% 前模糊匹配 | 无法使用 B+Tree 索引 |
name 无索引 | 即使改为前缀匹配 LIKE ‘xxx%’ 也无法优化 |
使用了错误索引 | 当前索引 (xxx_id, xxxuserid) 无法支持 name 查询 |
高数据量下回表扫描 | xxx_id = 177 的数据量大(58w),过滤性能差 |
优化方案
去掉前模糊匹配,改成 LIKE '关键词%'
,并为 name 字段建立索引。
6.3 案例二:复杂条件查询优化
问题SQL(脱敏后)
SELECT count(*)
FROM `脱敏表`
WHERE creator IN ('ID1', 'ID2', 'ID3', 'ID4', 'ID5', 'ID6', 'ID7') AND (xxx_id = YYY) AND (status = 1) AND (`name` LIKE '%活动关键词%')
性能瓶颈分析
字段 | 值 | 解读 |
---|---|---|
type | ref | 使用了 corp_id 索引,但效率不高 |
key | idx_xxxid_xxxuserid | 说明使用的是 (corp_id, userid) 的索引 |
rows | 518218 | 预估要扫描 50 万条数据(即 corp_id=156 的数据量) |
filtered | 0.56 | 超低过滤率,说明大多数数据都是"扫了但没用上" |
Extra | Using where | 多条件联合过滤 + 无法使用 name 的索引,导致回表严重 |
问题分析表
问题点 | 描述 |
---|---|
%模糊匹配% | LIKE ‘%素质学习活动%’ 无法走索引,依旧是性能杀手 |
无 name 索引 | 和慢 SQL 1 一样,name 字段未建立索引 |
使用错误索引 | 当前使用的是 (xxx_id, xxxuserid),和查询条件不匹配 |
多个 WHERE 条件 | creator IN (…) + status=1 + xxx_id=156 增加了查询复杂度 |
扫描行数巨大 | 扫描了 50 万行,过滤率不到 1%,极其低效 |
优化方案
使用搜索引擎(如Elasticsearch)进行模糊匹配,将复杂的文本搜索从数据库中分离出来。
6.4 案例三:URL字段查询优化
问题SQL(脱敏后)
SELECT `id`, `media_id`
FROM `脱敏表`
WHERE (xxx_id = XXX) AND (user_id = ZZZ) AND (url = 'https://example.com/feed?...') LIMIT 1
执行计划分析
字段 | 值 | 含义 |
---|---|---|
type | ref | 使用了索引但是模糊匹配级别,不够高效 |
key | idx_xxxid_xxxuserid | 当前使用的是 (corp_id, user_id) 索引 |
rows | 27622 | 即便用了索引,预估扫描仍达 2 万+ 行 |
filtered | 10% | 表示大部分数据被过滤掉了(说明 url 没有参与索引) |
Extra | Using where | 表示 MySQL 仍需逐行判断 url=xxx 条件 |
优化方案
添加新字段(如哈希值),改写SQL:
SELECT id, media_id
FROM 脱敏表
WHERE xxx_id = XXXAND xxxuser_id = ZZZAND url_hash = '哈希值'AND url = 'https://example.com/feed?...'LIMIT 1
6.5 案例四:高并发系统级性能问题
问题SQL(脱敏后)
SELECT id, xxxuser_id1, xxxuser_id2, xxx_id, task_id, ...
FROM table_task
WHERE xxxuser_id2 = '用户ID'AND xxx_id = XXXAND create_time >= 时间戳AND task_status = 2;
执行计划分析
字段 | 值 | 含义 |
---|---|---|
type | range | 范围扫描,说明部分使用了索引 |
key | idx_wx_user_time | 使用了某个复合索引(猜测为 xxx_userid + create_time) |
rows | 68794 | 预估需要扫描约 6.8 万行 |
filtered | 1.00 | 过滤率很低(接近全扫描) |
Extra | Using index condition; Using where | 没有使用覆盖索引,部分条件未被索引消化 |
问题分析
这条 SQL 单条执行并不慢,索引命中了(走的是 (xxx_userid, create_time)
),本质的问题是高并发下的系统级退化。
问题类型 | 原因 |
---|---|
✅ 数据库连接池耗尽 | 协程数超出 max_open_conns,大量阻塞 |
✅ 单连接响应慢 | 数据量大,每条查询虽然不慢,但处理行数多,连接占用时间长 |
✅ Goroutine 累积 | 协程爆炸,CPU、内存调度竞争,影响系统吞吐 |
✅ MySQL CPU 使用高 | 并发下 10w+ 次筛选,造成 DB CPU 持续高压 |
✅ 索引虽走,但行数多 | rows=68794 表示每次查近 7 万条,I/O 和网络传输压力大 |
优化方案
- 限制协程并发度(从4降到2)
- 查询范围从10天减少到3天
- 执行频率从10分钟一次改为30分钟一次
6.6 案例五:数据倾斜与并发优化
问题SQL(脱敏后)
SELECT count(*)
FROM `脱敏表`
WHERE (xxx_id = YYY) AND (user_id = AAA) AND (state = '状态值') AND (del_way != 0) AND (delete_time >= 时间戳1) AND (delete_time < 时间戳2)
执行计划分析
字段 | 值 | 含义 |
---|---|---|
key | idx_xxxuserid_followtime | 使用了 (xxxuser_id, follow_time) 联合索引,但不完全匹配查询条件 |
key_len | 8 | 实际只使用了索引的第一列 xxxuser_id(类型为 BIGINT,占用 8 字节) |
ref | const | xxxuser_id = 51967 为常量匹配,用于索引过滤 |
rows | 53472 | 预估扫描行数约为 5.3 万,说明该 xxxuser_id 拥有大量数据 |
filtered | 0.00 | 预估过滤率为 0%,说明几乎所有行都需进一步筛选,回表严重 |
Extra | Using where | 表示需回表做完整 WHERE 条件判断,未能使用覆盖索引或索引条件下推 |
问题分析
问题 | 描述 |
---|---|
❗️数据倾斜 | 部分 xxxuser_id 拥有极多数据(几十万级),SQL执行时间呈不均衡状态 |
❗️并发导致放大 | 每个用户都执行这条 SQL,量一大就撑爆连接池或 DB 资源 |
❗️索引不命中 delete_time / state | 这两个高过滤条件字段只能靠回表判断,扫描行数增多 |
❗️逻辑层并发扫描 | 多个用户并发执行时,每人扫几万,系统瞬间承压(4核但代码设置了10个协程数) |
优化方案
理想索引:
CREATE INDEX idx_xxxuserid_delway_state_deltime
ON 脱敏表(xxxuser_id, del_way, state, delete_time);
实际采用的3种优化:
优化手段 | 背景/动机 | 预期收益 |
---|---|---|
清理数据库表中的双删数据 | 当前 del_way != 0 过滤范围太大,无效数据长期滞留影响查询效率;部分旧数据可归档或物理删除 | 降低数据量,减少每次扫描行数,提升过滤效率 |
降低协程并发数 | 当前每个 xxxuser_id 并发触发多条 SQL,容易造成连接池耗尽、系统抖动 | 控制系统资源消耗,更稳定运行、更平滑负载 |
将时间范围由 3 天缩小到 1 天 | 原先扫描范围广、数据量大,导致执行时间不可控;改为每日定时执行或滚动聚合 | 明确可控的数据窗口,降低查询压力、支持更频繁调度 |
6.7 慢SQL治理总结
类型 | 内容 |
---|---|
数据优化 | 清理无效数据、减少扫描量 |
查询优化 | 改写 SQL 表达式、提升执行计划 |
系统优化 | 限制并发、平滑调度 |
窗口优化 | 缩小时间范围、减少数据量 |
总结
本文从MySQL查询的基础机制出发,系统性地介绍了查询优化的理论基础和实践方法:
- 访问方法:7种不同的访问方法为查询提供了从最高效的
const
到最低效的all
的性能选择 - 联表查询:理解笛卡尔积本质和连接算法,合理设计索引和选择驱动表
- 成本计算:掌握I/O成本和CPU成本的计算原理,理解优化器的决策依据
- 规则优化:利用条件化简、外连接消除等规则,让查询更高效
- 执行计划:通过
EXPLAIN
分析瓶颈,指导索引设计和SQL优化 - 实战案例:5个真实案例展示了从问题发现到解决的完整流程
通过理论学习和实践应用相结合,可以更好地理解MySQL查询优化的精髓,在实际开发中写出高性能的SQL语句