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
相关推荐
身如柳絮随风扬8 小时前
MySQL核心知识
数据库·mysql
551只玄猫9 小时前
【数据库原理 实验报告1】创建和管理数据库
数据库·sql·学习·mysql·课程设计·实验报告·数据库原理
q5431470879 小时前
MySQL SQL100道基础练习题
数据库·mysql
zhoupenghui16810 小时前
mysql 中如果条件where中有or,则要求or两边的字段都必须有索引,否则不能用到索引, 为什么?
数据库·mysql·索引
eggwyw11 小时前
完美解决phpstudy安装后mysql无法启动
数据库·mysql
java修仙传11 小时前
MySQL 事务隔离级别详解
数据库·mysql·oracle
Irissgwe11 小时前
MySQL存储过程和触发器专题
数据库·mysql·oracle
skiy14 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
创世宇图14 小时前
Alibaba Cloud Linux 安装生产环境-mysql
linux·mysql
重庆小透明15 小时前
【搞定面试之mysql】第一篇:mysql的优化和索引
mysql·面试·职场和发展