SQL优化实战经验指南
慢查询识别与定位
什么是慢查询
慢查询(Slow Query)是指执行时间超过预设阈值的SQL语句。这类查询不仅会显著拖慢数据库整体性能,还可能引发连锁反应:响应时间增长、资源占用过高、阻塞其他正常操作,最终影响用户体验和系统稳定性。
MySQL中获取慢查询语句
临时开启慢查询日志
对于线上环境的临时排查,可以通过以下命令快速开启慢查询监控:
sql
-- 切换到目标数据库
USE dist_schema;
-- 开启慢查询日志
SET GLOBAL slow_query_log = 1;
-- 设置慢查询阈值为1秒
SET GLOBAL long_query_time = 1;
-- 验证慢查询是否成功开启
SHOW VARIABLES LIKE '%slow_query_log%';
-- 确认当前的慢查询阈值设置
SHOW GLOBAL VARIABLES LIKE 'long_query_time';
永久开启慢查询(生产环境需谨慎)
如果需要长期监控,可以修改配置文件,但建议在生产环境中谨慎使用:
ini
# my.cnf 配置文件
[mysqld]
# 启用慢查询日志
slow_query_log = ON
# 指定慢查询日志文件路径(文件不存在时会自动创建)
slow_query_log_file = /var/lib/mysql/slow.log
# 设置慢查询阈值,单位:秒
long_query_time = 1
查看慢查询记录
通过以下命令可以查看记录的慢查询语句:
bash
cat /var/lib/mysql/c65f6d071766-slow.log

使用Druid监控慢查询
对于使用了Druid连接池的应用,可以通过其Web控制台更直观地查看SQL执行情况:
访问地址:http://<host>:<port>/[context-path]/druid/sql.html


通过Druid控制台,我们可以清晰地看到每个SQL语句的执行次数、平均耗时、最大耗时等关键指标,为后续优化提供数据支撑。
SQL优化策略与实践
SQL查询执行流程解析
理解MySQL的查询执行流程是进行SQL优化的基础。下图展示了一条SQL语句从接收到返回结果的完整过程:

解析器(Parser)
解析器负责对SQL语句进行词法和语法分析,构建语法树:

优化器(Optimizer)
优化器是MySQL中负责SQL性能优化的核心模块。它的主要职责是:
- 通过分析系统统计信息,为每个查询请求生成最优执行计划
- 选择最合适的数据检索方式
- 决定索引的使用策略和表的连接顺序
注意:优化器认为的"最优"方案未必是DBA眼中的最优方案,这也是为什么有时需要人工干预优化的原因。
基于查询流程的优化技巧
1. 避免使用 SELECT *
使用 SELECT *
会带来多重性能损耗:
- 解析成本高:需要查询数据字典获取所有列信息
- 数据传输开销大:传输不必要的字段数据
- 内存占用增加:缓存更多不需要的数据
- 无法利用覆盖索引:降低索引优化效果
推荐做法:明确指定需要查询的字段
sql
-- 避免
SELECT * FROM user WHERE id = 1;
-- 推荐
SELECT id, name, email FROM user WHERE id = 1;
2. 使用标准SQL语法,避免简写
MySQL底层会将所有简写形式转换为完整语法,这个转换过程会增加额外的解析开销。
sql
-- 简写形式(不推荐)
SELECT name "姓名" FROM user;
SELECT * FROM t1, t2 WHERE t1.f = t2.f;
-- 标准语法(推荐)
SELECT name AS "姓名" FROM user;
SELECT * FROM t1 AS t1 INNER JOIN t2 AS t2 ON t1.f = t2.f;
3. 明确单条查询时使用 LIMIT
当确定查询结果只有一条记录时,添加 LIMIT 1
可以让MySQL在找到匹配记录后立即停止扫描:
sql
-- 普通查询
SELECT * FROM t WHERE name = 'zhang';
-- 优化后(确定只有一条结果时)
SELECT * FROM t WHERE name = 'zhang' LIMIT 1;
这样优化后,MySQL在匹配到第一条符合条件的记录时就会停止扫描,而不是继续扫描整个表。
4. 遵循"小表驱动大表"原则
在多表关联查询中,让数据量较小的表作为驱动表,可以显著减少循环次数和内存消耗。
示例对比:
sql
-- 大表驱动小表(性能较差)
SELECT * FROM test_sql_user u
LEFT JOIN test_sql_tenant_actor tu ON tu.actor_id = u.id
LEFT JOIN test_sql_tenant t ON tu.tenant_id = t.id
LIMIT 1000;

sql
-- 小表驱动大表(性能更优)
SELECT SQL_NO_CACHE * FROM test_sql_tenant t
LEFT JOIN test_sql_tenant_actor tu ON tu.tenant_id = t.id
LEFT JOIN test_sql_user u ON tu.actor_id = u.id
LIMIT 1000;

从执行结果可以看出,通过调整表的连接顺序,查询性能得到了明显提升。
5. 控制关联表的数量
连表查询时应尽量控制关联表的数量,建议不超过3-4张表:
- 数据量呈指数增长:每增加一张表,中间结果集可能呈几何级数增长
- 索引优化难度增加:涉及的表越多,优化器越难选择最优的执行计划
- 维护复杂度上升:后期修改和调试的难度显著增加
6. 避免深分页查询
深分页查询是常见的性能杀手:
sql
-- 性能很差的深分页
SELECT xx, xx, xx FROM yyy LIMIT 100000, 10;
这条SQL相当于查询第10万页的数据,MySQL实际执行过程是:先查询出100010条数据,然后丢弃前面的100000条,只返回最后10条。这个过程极其浪费资源。
优化方案:使用游标分页
sql
-- 基于主键或排序字段进行分页
SELECT xx, xx, xx FROM yyy WHERE id > last_id ORDER BY id LIMIT 10;
索引优化详解
索引的本质与原理
假设我们的数据是按照下图方式存储的:

如果要查询 id=8
的记录,在没有索引的情况下,必须从头到尾遍历所有数据,直到找到所有匹配的记录。
索引的定义:索引是对数据库表中一列或多列的值进行排序的一种数据结构,通过索引可以快速定位到特定数据,避免全表扫描。
InnoDB存储引擎的索引结构
InnoDB使用B+树作为索引结构,其特点是:
- 聚簇索引:叶子节点直接存储完整的数据行
- 数据文件即索引文件:表数据和主键索引存储在同一个文件中

二级索引(辅助索引)
对于非主键字段建立的索引,叶子节点存储的是主键值,需要通过主键值回到聚簇索引中获取完整数据:

索引的分类
根据不同的分类标准,索引可以分为以下几种类型:
按功能特性分类
- 唯一索引:确保索引列的值唯一性,允许NULL值,一张表可以有多个唯一索引
- 普通索引:最基础的索引类型,无唯一性限制,主要用于加速查询
- 前缀索引:针对字符串类型字段,只对前N个字符建立索引,节省存储空间
- 全文索引:用于在大文本字段中进行关键字检索,常用于搜索引擎场景
按实现方法分类
- B-Tree索引:基于B+树结构的多路平衡查找树,是InnoDB的默认索引类型
- Hash索引:基于哈希表实现,查询速度极快(接近O(1)),但只适用于等值查询
索引失效的常见情况
了解索引失效的场景对于SQL优化至关重要。以下是导致索引失效的主要情况:
1. 模糊查询导致的失效
sql
-- 这些情况会导致索引失效
SELECT * FROM user WHERE name LIKE '%张%'; -- 左右模糊
SELECT * FROM user WHERE name LIKE '%张'; -- 左模糊
-- 这种情况可以使用索引
SELECT * FROM user WHERE name LIKE '张%'; -- 右模糊(前缀匹配)
原因:左模糊匹配无法确定从B+树的哪个位置开始查找,只能进行全表扫描。
2. 函数运算导致的失效
sql
-- 索引失效
SELECT * FROM order WHERE DATE(create_time) = '2023-10-01';
-- 索引有效
SELECT * FROM order WHERE create_time >= '2023-10-01' AND create_time < '2023-10-02';
3. 数据类型隐式转换
sql
-- phone字段为varchar类型,但查询时使用数字,会导致隐式转换
SELECT * FROM user WHERE phone = 13812345678; -- 索引失效
-- 正确写法
SELECT * FROM user WHERE phone = '13812345678'; -- 索引有效
4. 联合索引的最左前缀原则
对于联合索引 (a, b, c)
,以下查询条件的索引使用情况:
sql
-- 可以使用索引
WHERE a = 1 -- 使用索引a
WHERE a = 1 AND b = 2 -- 使用索引a,b
WHERE a = 1 AND b = 2 AND c = 3 -- 使用索引a,b,c
-- 无法使用索引
WHERE b = 2 -- 索引失效
WHERE c = 3 -- 索引失效
WHERE b = 2 AND c = 3 -- 索引失效
5. 其他常见失效场景
- OR条件:WHERE子句中包含OR,且OR条件中有非索引列
- 负向查询 :使用
!=
、<>
、NOT IN
等操作符 - NULL值判断 :
IS NULL
或IS NOT NULL
可能导致索引失效 - 字段运算 :对索引字段进行
+
、-
、*
、/
运算 - 优化器选择:当MySQL认为全表扫描比使用索引更快时,会放弃使用索引
EXPLAIN执行计划详解
EXPLAIN是MySQL提供的SQL分析工具,能够显示查询的执行计划,是SQL优化的重要工具。
基本语法 :EXPLAIN <SQL语句>
示例输出格式
sql
mysql> EXPLAIN SELECT * FROM test_sql_user u;
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------+
| 1 | SIMPLE | u | NULL | ALL | NULL | NULL | NULL | NULL | 644930 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-------+
1 row in set, 1 warning (0.01 sec)
关键字段解析
1. id字段
表示执行计划中操作的ID,决定执行顺序:
- 值相同:按从上到下的顺序执行
- 值不同:ID值越大,执行优先级越高
- 值为NULL:通常是UNION结果,最后执行
示例:
sql
-- ID值相同,按顺序执行
SELECT * FROM test_sql_user u
LEFT JOIN test_sql_tenant_actor tu ON tu.actor_id = u.id
LEFT JOIN test_sql_tenant t ON tu.tenant_id = t.id
LIMIT 1000;
-- ID值不同,子查询优先执行
SELECT * FROM test_sql_user u
WHERE u.id IN (
SELECT actor_id FROM test_sql_tenant_actor ta
WHERE ta.is_main = 'Y' AND ta.actor_id LIKE '012ec46995e011e%'
)
LIMIT 1000;
2. select_type字段
表示查询的类型:
- SIMPLE:简单查询,不包含子查询或UNION
- PRIMARY:复杂查询中的最外层查询
- SUBQUERY:包含在SELECT列表中的子查询
- DERIVED:包含在FROM子句中的子查询(派生表)
- UNION:UNION查询中的第二个及后续的SELECT语句
- UNION RESULT:UNION查询的结果集
3. table字段
显示当前操作的表名,可能是:
- 物理表名
- 子查询的别名(如
<derived2>
表示第二个派生表) - UNION结果的表示(如
<union1,2>
)

4. type字段(重要)
表示表的访问方式,这是判断查询效率的关键指标:
- system:表中只有一行数据,这是const的特例
- const:通过主键或唯一索引等值查询,最多返回一条记录
- eq_ref:连接查询中,被驱动表通过主键或唯一索引匹配一行
- ref:通过非唯一索引等值查询
- fulltext:使用全文索引查询
- range:基于索引进行范围查询(BETWEEN、<、>、IN等)
- index:全索引扫描,遍历整个索引树
- ALL:全表扫描,性能最差
性能排序 (从优到劣): system > const > eq_ref > ref > fulltext > range > index > ALL
优化目标:尽量让type字段达到ref级别以上,避免出现index和ALL。
5. possible_keys 和 key字段
- possible_keys:MySQL分析认为可能会使用的索引
- key:实际使用的索引
分析要点:
- 如果key为NULL,说明索引未被使用,可能存在索引失效问题
- possible_keys有值但key为NULL,通常表示索引虽然存在但未被优化器选中
6. ref字段
显示索引查找时的参考值:
- const:使用常量值进行查询
- 具体字段名:使用该字段的值进行索引查找

7. rows字段
预估的扫描行数,这个值对性能评估很重要:
- 对于InnoDB引擎,这个数值是估算值,但具有很强的参考价值
- 数值越大,查询效率越低
- 结合type字段,可以判断查询的整体效率
8. extra字段(重要)
extra字段提供了查询执行的额外信息,对索引优化具有重要的参考价值:
常见的关键信息:
- Using index:使用了索引覆盖,无需回表,性能最佳
- Using where:需要在存储引擎层进行数据过滤
- Using temporary:使用了临时表处理查询,通常出现在GROUP BY或ORDER BY中
- Using filesort:无法使用索引排序,需要进行文件排序,性能较差
- Using index condition:使用了索引下推优化
- Using join buffer:连接查询中使用了Join Buffer来加速访问
需要注意的信息:
- Using temporary + Using filesort:查询效率很低,需要重点优化
- Using where; Using index:索引覆盖但需要条件过滤
- NULL:查询较为简单,通过主键直接访问数据
索引优化的高级技巧
最左前缀原则详解
最左前缀原则是联合索引优化的核心概念。
示例场景:
sql
SELECT u.* FROM test_sql_user u
LEFT JOIN test_sql_tenant_actor tu ON tu.actor_id = u.id
LEFT JOIN test_sql_tenant t ON tu.tenant_id = t.id
WHERE u.phone LIKE '13896622%'
AND u.area > 7
AND u.continuous_visit_days < 10;


最左前缀原则的两个层面:
-
联合索引的字段匹配:
- 创建联合索引
(a,b,c)
相当于创建了三个索引:(a)
、(a,b)
、(a,b,c)
- 查询条件必须从最左侧字段开始,才能有效利用索引
- 创建联合索引
-
字符串的前缀匹配:
- 对于字符串字段,
LIKE '前缀%'
这种前缀匹配可以使用索引 - 例如:
WHERE name LIKE '张%'
可以有效使用name字段的索引
- 对于字符串字段,
索引选择性分析
为什么有索引却不走索引?
sql
SELECT u.* FROM test_sql_user u
LEFT JOIN test_sql_tenant_actor tu ON tu.actor_id = u.id
LEFT JOIN test_sql_tenant t ON tu.tenant_id = t.id
WHERE sex > 0 AND u.area > 7 AND u.continuous_visit_days < 10;
原因分析: 当MySQL优化器分析发现,使用索引的成本可能比全表扫描更高时,会选择放弃使用索引。这通常发生在:
- 查询结果集占总数据的比例较大
- 索引的选择性较差(重复值太多)
- 表的数据量较小时
覆盖索引与回表优化
什么是回表操作? 当使用二级索引查询时,如果查询的列不完全包含在索引中,就需要通过主键值回到聚簇索引中获取完整数据,这个过程叫做回表。
覆盖索引的优势:
sql
-- 这个查询可能实现覆盖索引,避免回表
SELECT u.sex, u.area, u.continuous_visit_days FROM test_sql_user u
LEFT JOIN test_sql_tenant_actor tu ON tu.actor_id = u.id
LEFT JOIN test_sql_tenant t ON tu.tenant_id = t.id
WHERE sex > 0 AND u.area > 7 AND u.continuous_visit_days < 10;

覆盖索引的优化价值:
- 避免回表操作,减少磁盘I/O
- 减少数据传输量
- 提高查询并发能力
- 显著提升查询性能
字符串索引的优化策略
对于字符串类型的字段建立索引时,有多种策略可供选择:
1. 完整字段索引
sql
-- 为整个字符串字段建立索引
CREATE INDEX idx_email ON user(email);
优点 :查询效率高,支持所有查询类型
缺点:占用存储空间大,尤其是长字符串
2. 前缀索引
sql
-- 只对字符串的前10个字符建立索引
CREATE INDEX idx_email_prefix ON user(email(10));
优点 :节省存储空间,适用于前缀区分度高的场景
缺点:可能增加扫描行数,不支持覆盖索引
3. 倒序存储 + 前缀索引
sql
-- 先将字符串倒序存储,再建立前缀索引
UPDATE user SET email_reverse = REVERSE(email);
CREATE INDEX idx_email_reverse_prefix ON user(email_reverse(10));
适用场景:原字符串前缀区分度不够时的解决方案
4. Hash字段索引
sql
-- 增加hash字段,对hash值建立索引
ALTER TABLE user ADD COLUMN email_hash VARCHAR(32);
UPDATE user SET email_hash = MD5(email);
CREATE INDEX idx_email_hash ON user(email_hash);
优点 :查询性能稳定,存储空间固定
缺点:增加存储和计算开销,不支持范围查询
大表索引添加的安全策略
对于千万级以上数据量的大表,直接添加索引可能导致长时间锁表,影响业务正常运行。
安全的索引添加流程
-
创建结构相同的新表
sqlCREATE TABLE user_new LIKE user;
-
在新表上添加所需索引
sqlALTER TABLE user_new ADD INDEX idx_new_field(field_name);
-
数据迁移
sql-- 分批迁移数据,避免长时间锁表 INSERT INTO user_new SELECT * FROM user LIMIT 100000;
-
原子性切换表名
sqlRENAME TABLE user TO user_old, user_new TO user;
-
验证数据完整性后删除旧表
sqlDROP TABLE user_old;
注意事项:这个过程需要在业务低峰期进行,并且要做好数据备份和回滚方案。
总结与最佳实践
优化思路总结
- 识别瓶颈:通过慢查询日志和监控工具定位性能问题
- 分析执行计划:使用EXPLAIN深入分析SQL的执行过程
- 索引优化:合理设计和使用索引,避免索引失效
- 查询重写:根据业务特点优化查询逻辑
- 持续监控:建立性能监控机制,及时发现和解决问题
性能优化检查清单
SQL编写规范
- 避免使用
SELECT *
,明确指定查询字段 - 使用标准SQL语法,避免简写形式
- 确定单条结果的查询添加
LIMIT 1
- 遵循小表驱动大表原则
- 避免深分页查询
索引设计原则
- 根据查询频率和选择性合理创建索引
- 利用联合索引,遵循最左前缀原则
- 避免冗余索引,定期清理无用索引
- 对于大表的索引变更,采用安全的操作流程
查询优化要点
- 避免在WHERE子句中对索引字段使用函数
- 注意数据类型匹配,避免隐式转换
- 合理使用覆盖索引,减少回表操作
- 对于复杂查询,考虑拆分为多个简单查询
参考资料
技术文档
相关工具
- 慢查询分析:MySQL Slow Query Log, pt-query-digest
- 性能监控:Druid, MySQL Workbench Performance Reports
- 执行计划分析:EXPLAIN, EXPLAIN FORMAT=JSON