1. 慢SQL优化实战:从定位到解决的完整指南
作为经历过多次生产环境SQL性能问题的老兵,我深知一条糟糕的SQL能让整个系统瘫痪。记得有一次凌晨3点被叫醒处理一个查询超时问题,最终发现只是一个缺少复合索引的订单查询。本文将分享我在10年开发生涯中总结的慢SQL优化方法论,包含10个真实案例的完整分析过程。
2. SQL性能问题定位三板斧
2.1 慢查询日志:精准捕获问题SQL
慢查询日志是MySQL内置的问题定位工具,配置参数如下:
sql复制slow_query_log = ON
long_query_time = 1 # 超过1秒的查询
log_queries_not_using_indexes = ON # 记录未走索引的查询
关键分析技巧:
- 使用mysqldumpslow工具统计高频慢查询
- 重点关注Lock_time和Rows_examined字段
- 生产环境建议设置1-2秒阈值,测试环境可设为0.1秒
注意:长期开启慢查询日志会有约5%性能损耗,建议在问题排查期间开启
2.2 EXPLAIN执行计划深度解读
执行计划是优化SQL的核心工具,需要重点关注的字段:
| 字段 | 说明 | 优化方向 |
|---|---|---|
| type | 访问类型 | 至少达到range级别 |
| key_len | 使用索引长度 | 检查是否充分利用复合索引 |
| rows | 预估扫描行数 | 超过1万行需要警惕 |
| Extra | 额外信息 | 出现Using filesort必须优化 |
type字段的完整效率排序(优→劣):
- system:系统表单行查询
- const:主键/唯一索引等值查询
- eq_ref:关联查询主键匹配
- ref:非唯一索引等值查询
- range:索引范围扫描
- index:全索引扫描
- ALL:全表扫描
2.3 高级诊断工具组合拳
2.3.1 SHOW PROFILE分析
sql复制SET profiling = 1;
执行目标SQL;
SHOW PROFILES;
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;
典型问题特征:
- converting HEAP to MyISAM:内存不足
- Creating tmp table:临时表滥用
- copying to tmp table:磁盘临时表
2.3.2 OPTIMIZER TRACE追踪
sql复制SET optimizer_trace="enabled=on";
SET optimizer_trace_max_mem_size=1000000;
执行SQL;
SELECT * FROM information_schema.optimizer_trace;
输出重点查看:
- considered_execution_plans:优化器考虑的方案
- chosen_plan_access_path:最终选择的访问路径
- attributes_attached_to_plan:选择依据
3. 十大经典案例深度剖析
3.1 最左匹配原则陷阱
问题SQL:
sql复制SELECT * FROM orders WHERE order_no = '12345'
索引配置:
sql复制KEY `idx_shop_order` (`shop_id`,`order_no`)
原因分析:
复合索引遵循最左前缀原则,缺少shop_id条件时:
- 相当于只使用索引的第一列
- 对于(
a,b)索引,WHERE b=1无法使用索引
解决方案:
- 修改查询条件包含shop_id
- 调整索引顺序为(
order_no,shop_id) - 单独建立order_no索引
实战经验:复合索引字段顺序 = 等值查询字段在前 + 范围查询/排序字段在后
3.2 隐式类型转换危机
问题SQL:
sql复制SELECT * FROM users WHERE mobile = 13800138000
索引配置:
sql复制KEY `idx_mobile` (`mobile`) # mobile是varchar类型
性能影响:
- 导致全表扫描,执行时间从10ms→800ms
- 在百万级数据表上CPU飙升100%
原理剖析:
当发生类型转换时:
sql复制WHERE mobile = 13800138000
# 实际执行:
WHERE CAST(mobile AS signed int) = 13800138000
正确写法:
sql复制SELECT * FROM users WHERE mobile = '13800138000'
3.3 大分页性能优化方案
原始SQL:
sql复制SELECT * FROM orders
WHERE status=1
ORDER BY create_time DESC
LIMIT 100000, 10;
问题分析:
- 需要先排序100010条记录
- 然后丢弃前100000条
- 在500万数据表上执行需要2.3秒
优化方案一:游标分页
sql复制SELECT * FROM orders
WHERE status=1 AND id < last_id
ORDER BY id DESC
LIMIT 10;
优化方案二:延迟关联
sql复制SELECT t.* FROM orders t
JOIN (
SELECT id FROM orders
WHERE status=1
ORDER BY create_time DESC
LIMIT 100000, 10
) tmp ON t.id = tmp.id;
性能对比:
| 方案 | 执行时间 | 扫描行数 |
|---|---|---|
| 原始方案 | 2300ms | 100010 |
| 游标分页 | 15ms | 10 |
| 延迟关联 | 45ms | 100010 |
3.4 IN查询优化策略
问题SQL:
sql复制SELECT * FROM products
WHERE category_id IN(1,2,3)
ORDER BY price DESC
LIMIT 10;
索引配置:
sql复制KEY `idx_cat_price` (`category_id`,`price`)
优化技巧:
- 控制IN列表数量(建议<200)
- 使用JOIN替代大IN查询
- 对于固定分类可改用多个UNION
优化后SQL:
sql复制SELECT * FROM products t1
JOIN (
SELECT id FROM products
WHERE category_id IN(1,2,3)
ORDER BY price DESC
LIMIT 10
) t2 ON t1.id = t2.id;
3.5 索引失效的边界条件
典型场景:
sql复制WHERE create_time > '2023-01-01' AND status = 1
索引设计误区:
sql复制KEY `idx_time_status` (`create_time`,`status`)
正确设计:
sql复制KEY `idx_status_time` (`status`,`create_time`)
原理说明:
- 范围查询右侧的索引列失效
- 等效于只使用status索引
- 调整顺序后可充分利用复合索引
3.6 索引下推(ICP)技术应用
MySQL 5.6+特性:
sql复制SET optimizer_switch='index_condition_pushdown=on';
适用场景:
sql复制SELECT * FROM users
WHERE name LIKE '张%'
AND age > 18;
执行流程对比:
- 无ICP:先回表再过滤age
- 有ICP:在存储引擎层直接过滤
性能提升:
- 减少70%-90%的回表操作
- 对于text/blob列效果更明显
3.7 强制索引使用技巧
语法示例:
sql复制SELECT * FROM orders FORCE INDEX(idx_status_time)
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10;
适用场景:
- 优化器选错索引时
- 统计信息过期导致误判
- 需要测试不同索引性能时
注意事项:
- 先使用ANALYZE TABLE更新统计信息
- 检查索引选择性(Cardinality)
- 不同数据分布可能需要不同策略
4. 高级优化策略
4.1 查询重写技术
案例:OR条件优化
sql复制# 原始SQL
SELECT * FROM logs
WHERE type='error' OR create_time > '2023-01-01';
# 优化方案
SELECT * FROM logs WHERE type='error'
UNION ALL
SELECT * FROM logs
WHERE create_time > '2023-01-01'
AND type!='error';
4.2 临时表优化方案
问题特征:
- Extra出现Using temporary
- 磁盘临时表(Created_tmp_disk_tables)
优化方法:
- 增加tmp_table_size(默认16MB)
- 优化GROUP BY/ORDER BY字段顺序
- 使用SQL_BIG_RESULT提示
4.3 连接查询优化
执行策略对比:
| 算法 | 适用场景 | 优化方法 |
|---|---|---|
| Nested Loop | 小表驱动大表 | 确保驱动表有索引 |
| Hash Join | MySQL 8.0+ | 控制join_buffer_size |
| BNL | 块嵌套循环 | 避免大表关联 |
5. 实战问题排查手册
5.1 性能骤降排查流程
- 检查QPS/CPU/锁等待情况
- 分析慢查询日志
- 确认执行计划是否变化
- 检查表统计信息
- 排查是否有隐式转换
5.2 索引失效场景速查
| 场景 | 示例 | 解决方案 |
|---|---|---|
| 函数操作 | WHERE YEAR(create_time)=2023 | 使用范围查询 |
| 数学运算 | WHERE price*2 > 100 | 提前计算 |
| 隐式转换 | WHERE code = 100(code是varchar) | 类型一致 |
| OR条件 | WHERE a=1 OR b=2 | 改用UNION |
5.3 参数调优建议
ini复制# my.cnf关键参数
innodb_buffer_pool_size = 总内存的70%
innodb_io_capacity = 2000 # SSD建议值
innodb_stats_on_metadata = OFF
optimizer_search_depth = 4 # 控制优化器复杂度
6. 架构级解决方案
当单条SQL优化到达瓶颈时,需要考虑:
- 读写分离:将分析查询路由到从库
- 数据分片:按时间/ID范围拆分
- 异构存储:
- Elasticsearch:全文搜索/复杂查询
- 列式存储:分析型查询
- 缓存层:热点数据加速
在最近的一个电商项目中,我们通过将订单报表查询迁移到ClickHouse,使查询时间从12秒降低到800毫秒,同时主库负载下降40%。