SQL优化实战经验指南

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 NULLIS 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;

最左前缀原则的两个层面

  1. 联合索引的字段匹配

    • 创建联合索引(a,b,c)相当于创建了三个索引:(a)(a,b)(a,b,c)
    • 查询条件必须从最左侧字段开始,才能有效利用索引
  2. 字符串的前缀匹配

    • 对于字符串字段,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);

优点 :查询性能稳定,存储空间固定
缺点:增加存储和计算开销,不支持范围查询


大表索引添加的安全策略

对于千万级以上数据量的大表,直接添加索引可能导致长时间锁表,影响业务正常运行。

安全的索引添加流程
  1. 创建结构相同的新表

    sql 复制代码
    CREATE TABLE user_new LIKE user;
  2. 在新表上添加所需索引

    sql 复制代码
    ALTER TABLE user_new ADD INDEX idx_new_field(field_name);
  3. 数据迁移

    sql 复制代码
    -- 分批迁移数据,避免长时间锁表
    INSERT INTO user_new SELECT * FROM user LIMIT 100000;
  4. 原子性切换表名

    sql 复制代码
    RENAME TABLE user TO user_old, user_new TO user;
  5. 验证数据完整性后删除旧表

    sql 复制代码
    DROP TABLE user_old;

注意事项:这个过程需要在业务低峰期进行,并且要做好数据备份和回滚方案。


总结与最佳实践

优化思路总结

  1. 识别瓶颈:通过慢查询日志和监控工具定位性能问题
  2. 分析执行计划:使用EXPLAIN深入分析SQL的执行过程
  3. 索引优化:合理设计和使用索引,避免索引失效
  4. 查询重写:根据业务特点优化查询逻辑
  5. 持续监控:建立性能监控机制,及时发现和解决问题

性能优化检查清单

SQL编写规范
  • 避免使用 SELECT *,明确指定查询字段
  • 使用标准SQL语法,避免简写形式
  • 确定单条结果的查询添加 LIMIT 1
  • 遵循小表驱动大表原则
  • 避免深分页查询
索引设计原则
  • 根据查询频率和选择性合理创建索引
  • 利用联合索引,遵循最左前缀原则
  • 避免冗余索引,定期清理无用索引
  • 对于大表的索引变更,采用安全的操作流程
查询优化要点
  • 避免在WHERE子句中对索引字段使用函数
  • 注意数据类型匹配,避免隐式转换
  • 合理使用覆盖索引,减少回表操作
  • 对于复杂查询,考虑拆分为多个简单查询

参考资料

技术文档

相关工具

  • 慢查询分析:MySQL Slow Query Log, pt-query-digest
  • 性能监控:Druid, MySQL Workbench Performance Reports
  • 执行计划分析:EXPLAIN, EXPLAIN FORMAT=JSON
相关推荐
vortex53 小时前
在 Kali Linux 上配置 MySQL 服务器并实现 Windows 远程连接
linux·数据库·mysql
杨云龙UP4 小时前
CentOS 7上离线部署MySQL 8.0.X操作指南(二进制压缩包部署+独立目录部署,不在自动默认路径配置下安装)
linux·运维·服务器·mysql·centos
全栈工程师修炼指南5 小时前
DBA | MySQL 数据库基础数据操作学习实践笔记
数据库·笔记·学习·mysql·dba
牛奶咖啡136 小时前
通过keepalived搭建MySQL双主模式的MySQL集群
数据库·mysql·mysql双主互备模式架构·mysql双主互备模式搭建·mysql主主互备模式·mysql双主互备实现高可用·keepalived高可用
DemonAvenger6 小时前
深入 Redis Set:从功能优势到项目实战的最佳实践
redis·性能优化·nosql
leo_yu_yty7 小时前
Mysql DBA学习笔记(日志)
学习·mysql·dba
凸头12 小时前
解决慢SQL问题
java·mysql
lang2015092812 小时前
MySQL缓冲池秒热技巧:告别冷启动
数据库·mysql
Go高并发架构_王工17 小时前
MySQL内存优化:缓冲池与查询缓存调优技术详解
数据库·mysql·缓存