SQL 优化是数据库性能调优的核心环节。合理的 SQL 优化可以显著提升查询性能,减少资源消耗,提高系统整体响应速度。本文总结了常见的 SQL 优化方法,涵盖索引优化、查询优化、系统调优等多个方面。
一、索引优化
1.1 为常用字段创建索引
为在 WHERE、JOIN、ORDER BY、GROUP BY 子句中频繁使用的列创建索引,可以避免全表扫描,大幅提升查询性能。
示例:
1 | -- 为 WHERE 条件字段创建索引 |
1.2 合理使用复合索引
当查询经常在多个列一起过滤、排序或分组时,创建复合索引。注意复合索引的列顺序:应将最具选择性的列放在前面。
示例:
1 | -- 查询:WHERE status = 'active' AND user_id = 123 ORDER BY created_at |
最左前缀原则:
- 复合索引
(a, b, c)可以用于查询a、a, b、a, b, c - 但不能用于查询
b、c、b, c
1.3 避免过度索引
索引虽然能加快读操作,但会拖慢写操作(INSERT/UPDATE/DELETE),增加存储和维护成本。
优化建议:
- 定期检查未使用的索引:
SELECT * FROM sys.schema_unused_indexes; - 删除冗余索引
- 使用 MySQL 8.0+ 的隐形索引(Invisible Index)功能测试移除索引的影响
示例:
1 | -- 将索引设为隐形,测试性能影响 |
1.4 选择合适的数据类型
使用尽量小、最合适的数据类型,避免索引大字段或含有很多 NULL 的字段。
建议:
- 整型(INT、BIGINT)比字符串快
- VARCHAR 比 TEXT 好
- 日期类型不要存成字符串
- 避免在 TEXT、BLOB 类型上创建索引
1.5 使用覆盖索引
覆盖索引是指查询所需的所有列都在索引中,无需回表查询,可以显著提升性能。
示例:
1 | -- 查询只需要 id 和 name,索引包含这两列 |
1.6 更新统计信息
定期使用 ANALYZE TABLE 更新表的统计信息,避免旧统计误导优化器。
1 | ANALYZE TABLE orders; |
二、查询优化
2.1 使用 EXPLAIN 分析执行计划
使用 EXPLAIN 或 EXPLAIN ANALYZE(MySQL 8.0+)分析 SQL 执行计划,找出性能瓶颈。
关键字段:
type:访问类型,应避免ALL(全表扫描)key:使用的索引rows:扫描的行数Extra:额外信息,如Using filesort、Using temporary
示例:
1 | EXPLAIN SELECT * FROM orders WHERE user_id = 123; |
type 类型(性能从好到差,并说明性能差异):
system:极快,仅适用于系统表(类似于只有一行的空表),这种类型几乎没有访问代价,耗时和资源消耗最小,MySQL 直接从表中获取结果并返回。const:非常快,表示通过主键或唯一索引可以直接定位到一行数据(常用于主键/唯一索引 + 常量条件),不需要扫描多行,通常只读一次磁盘或直接从内存缓存返回,性能和 system 接近。eq_ref:性能好,适用于多表连接中,对等连接唯一索引(如 join 时主表的某一行在从表中最多对应一行),MySQL 能高效用索引查到结果,扫描和返回的行很少。ref:性能较好,使用了非唯一索引或前缀索引,可能返回多行。扫描的行比 eq_ref 多,依然是基于索引做查找,但无法精确到唯一一行,随着匹配行数的增多,性能下降。range:性能一般,利用索引做范围扫描(如 BETWEEN、>、<、IN),只扫描满足范围条件的索引部分,相较于全表/全索引扫描性能高,但会随着范围的扩大扫描行数增多。index:性能较差,扫描整个索引树(全索引扫描),不会回表。虽然比全表扫描快(因为只读索引而不读全表数据行),但如果数据量大同样耗资源。常见于只用到索引字段,未用到表中其他字段的查询。ALL:最慢,表示全表扫描,即逐行遍历整张表,索引完全未被利用。数据量越大,性能损耗越明显,极易成为瓶颈,应尽量避免。
小结:system ≈ const > eq_ref > ref > range > index > ALL。后者性能依次递减,越靠后的类型扫描/处理的数据量越大,占用的 CPU、IO 资源越多。
2.2 避免 SELECT *
只选择必要的列,减少 I/O、网络传输和内存使用。
示例:
1 | -- 不推荐 |
2.3 避免在索引列上使用函数
在索引列上使用函数会导致索引失效,应尽量避免。
示例:
1 | -- 不推荐:索引失效 |
2.4 避免 LIKE ‘%abc’ 前导通配符
前导通配符会导致索引失效,应尽量避免。
示例:
1 | -- 不推荐:索引失效 |
2.5 使用 JOIN 替代子查询
在大多数情况下,JOIN 比子查询性能更好。
示例:
1 | -- 不推荐:使用子查询 |
2.6 优化 LIMIT 分页
对于大表的分页查询,使用基于排序键的分页(keyset pagination)替代 OFFSET。
示例:
1 | -- 不推荐:OFFSET 越大性能越差 |
2.7 使用 UNION ALL 替代 UNION
如果不需要去重,使用 UNION ALL 比 UNION 性能更好。
示例:
1 | -- 不推荐:会去重,性能较差 |
2.8 避免使用 NOT IN 和 !=
NOT IN 和 != 通常会导致全表扫描,应尽量避免。
示例:
1 | -- 不推荐 |
2.9 合理使用 EXISTS
对于判断存在性的查询,EXISTS 通常比 IN 性能更好。
示例:
1 | -- 推荐:使用 EXISTS |
2.10 批量操作优化
对于批量 INSERT、UPDATE、DELETE 操作,使用批量语句可以提升性能。
示例:
1 | -- 不推荐:逐条插入 |
三、MySQL 优化器优化操作
MySQL 优化器是数据库执行查询前的核心组件,负责将 SQL 语句转换为高效的执行计划。优化器会在多个阶段对查询进行优化,包括常量折叠、查询重写、子查询优化等。了解优化器的工作机制有助于编写更高效的 SQL。
3.1 常量折叠(Constant Folding)
常量折叠是指优化器在编译阶段对表达式中的常量进行计算和简化,将常量比较转换为更简单的形式,甚至直接移除永远为真或永远为假的条件。
优化示例:
1 | -- 原始查询 |
常量传播:
1 | -- 原始查询 |
3.2 查询重写(Query Rewrite)
优化器会将 SQL 查询在逻辑上等价地重写为另一种结构,以简化执行路径或降低资源开销。
3.2.1 IN 转换为 EXISTS
1 | -- 原始查询 |
3.2.2 子查询转换为 JOIN(Semijoin)
对于 IN、EXISTS 类子查询,优化器会尝试转换为 semijoin,将子查询的表结构”拉出”到外层查询。
1 | -- 原始查询 |
Semijoin 策略:
- Table Pullout(表拉出):将子查询中的表移到外层,充分利用连接顺序优化
- Duplicate Weedout(去重):避免 semijoin 返回重复值
- FirstMatch:扫描子查询表时一旦找到匹配就停止
- LooseScan:使用索引挑选每组中的第一个值
3.2.3 派生表合并(Derived Table Merge)
对于 FROM 子句中的子查询(派生表),优化器会尝试将其合并到外层查询。
1 | -- 原始查询 |
优势:
- 可以将外层的 WHERE 条件”下推”到派生表中
- 减少中间结果集的大小
- 避免创建临时表
3.2.4 视图合并(View Merge)
类似于派生表合并,优化器会尝试将视图定义合并到查询中。
1 | -- 定义视图 |
3.3 子查询优化(Subquery Optimization)
3.3.1 子查询物化(Materialization)
对于非相关子查询(不依赖外层查询),优化器可以将子查询先执行一次,将结果存入临时表,然后外层查询通过临时表进行 JOIN 或查找。
1 | -- 原始查询 |
适用条件:
- 子查询是非相关的(不引用外层表的列)
- 子查询结果集不会太大
- 子查询执行成本低于多次执行
3.3.2 标量子查询优化
对于返回单个值的标量子查询,优化器可以将其展开或提前计算。
1 | -- 原始查询 |
3.3.3 条件下推(Predicate Pushdown)
优化器会将条件推到最早执行的位置,减少中间结果集。
1 | -- 原始查询 |
3.4 JOIN 优化
3.4.1 JOIN 顺序优化
优化器会根据表的统计信息、索引情况、表大小等因素,选择最优的 JOIN 顺序。
1 | -- 原始查询 |
3.4.2 JOIN 算法选择
优化器会根据表大小、索引情况选择不同的 JOIN 算法:
- Nested Loop Join:适用于小表或索引良好的情况
- Block Nested Loop Join:适用于没有索引的情况
- Hash Join(MySQL 8.0.18+):适用于大表 JOIN
3.5 索引优化选择
3.5.1 索引选择
优化器会根据查询条件、表统计信息、索引选择性等因素,选择最优的索引。
1 | -- 查询 |
3.5.2 索引合并(Index Merge)
当单个索引无法满足查询需求时,优化器可能使用多个索引,然后合并结果。
1 | -- 查询 |
3.6 其他优化操作
3.6.1 ORDER BY 优化
1 | -- 如果 ORDER BY 的列有索引,可能直接使用索引排序 |
3.6.2 GROUP BY 优化
1 | -- 如果 GROUP BY 的列有索引,可能使用索引进行分组 |
3.6.3 DISTINCT 优化
1 | -- 如果 DISTINCT 的列有唯一索引,优化器知道结果必然唯一 |
3.6.4 LIMIT 优化
1 | -- 如果 ORDER BY 和 WHERE 条件都有索引,可能提前停止扫描 |
3.7 优化器控制开关
MySQL 提供了 optimizer_switch 变量来控制优化器的行为:
1 | -- 查看当前优化器开关 |
3.8 查看优化器优化过程
3.8.1 使用 EXPLAIN
1 | -- 查看执行计划 |
3.8.2 使用 Optimizer Trace
1 | -- 启用优化器跟踪 |
3.9 优化器限制与注意事项
统计信息准确性:优化器依赖表的统计信息,需要定期执行
ANALYZE TABLE更新统计信息优化器可能选择错误:在某些情况下,优化器可能选择非最优的执行计划,可以使用优化器提示(hints)强制指定
复杂查询优化限制:对于非常复杂的查询(如多表 JOIN、复杂子查询),优化器可能无法找到最优计划
版本差异:不同 MySQL 版本的优化器能力不同,新版本通常有更好的优化能力
3.10 优化器提示(Optimizer Hints)
当优化器选择非最优计划时,可以使用优化器提示:
1 | -- 强制使用特定索引 |
四、系统级优化
3.1 InnoDB Buffer Pool 调优
innodb_buffer_pool_size 是 InnoDB 最重要的参数,通常应设置为系统内存的 70-80%。
1 | -- 查看当前配置 |
3.2 慢查询日志
启用慢查询日志,定期分析慢 SQL。
1 | -- 启用慢查询日志 |
3.3 使用 Performance Schema
使用 Performance Schema 监控查询性能、锁等待、I/O 等情况。
1 | -- 查看最耗时的查询 |
3.4 事务优化
保持事务尽可能短,避免长时间占用锁资源。
建议:
- 事务中只包含必要的操作
- 避免在事务中进行耗时操作(如网络请求、文件操作)
- 合理设置事务隔离级别
3.5 连接池配置
合理配置连接池大小,避免连接数过多或过少。
1 | -- 查看最大连接数 |
五、表结构优化
4.1 规范化与反规范化
- 规范化:减少数据冗余,对写操作有利
- 反规范化:适当冗余,对读操作有利,但需要控制一致性成本
根据业务场景选择合适的策略。
4.2 使用分区表
对于大表,使用分区可以缩小扫描范围,提升查询性能。
示例:
1 | -- 按时间范围分区 |
4.3 垂直分割
将不常用的列拆分到单独的表中,减少主表大小。
示例:
1 | -- 主表:常用字段 |
六、优化实践流程
5.1 诊断阶段
- 启用慢查询日志,收集慢 SQL
- 使用
EXPLAIN分析执行计划 - 使用 Performance Schema 分析资源消耗
5.2 索引优化
- 确保主键存在
- 为常用 WHERE、JOIN、ORDER BY、GROUP BY 字段创建索引
- 创建合适的复合索引
- 删除未使用的索引
5.3 SQL 重写
- 避免 SELECT *
- 避免在索引列上使用函数
- 使用 JOIN 替代子查询
- 优化分页查询
- 使用批量操作
5.4 系统调优
- 调整 InnoDB Buffer Pool 大小
- 优化连接池配置
- 调整其他系统参数
5.5 架构优化
- 读写分离
- 使用缓存(Redis、Memcached)
- 分库分表
- 使用 CDN 加速静态资源
七、常见优化场景
6.1 大表查询优化
1 | -- 场景:查询最近 30 天的订单 |
6.2 JOIN 优化
1 | -- 场景:查询用户及其订单信息 |
6.3 GROUP BY 优化
1 | -- 场景:统计每个用户的订单数量 |
八、监控与维护
7.1 定期检查索引使用情况
1 | -- 查看未使用的索引(MySQL 8.0+) |
7.2 定期更新统计信息
1 | -- 更新表统计信息 |
7.3 监控慢查询
定期分析慢查询日志,找出需要优化的 SQL。
1 | # 使用 pt-query-digest 分析慢查询日志 |
九、总结
SQL 优化是一个持续的过程,需要:
- 诊断:使用工具找出性能瓶颈
- 优化:从索引、SQL、系统配置等多方面优化
- 测试:验证优化效果
- 监控:持续监控性能指标
- 迭代:根据业务变化不断调整
记住:过早优化是万恶之源。应该先找出真正的性能瓶颈,再进行有针对性的优化。同时,优化时要考虑业务场景,不能为了性能而牺牲数据一致性和业务逻辑的正确性。