1. MySQL慢查询优化实战:从原理到避坑指南
从事数据库开发这么多年,我处理过的慢查询问题少说也有上百个。很多看似简单的SQL语句,背后可能藏着让你抓狂的性能陷阱。今天我们就来聊聊那些年我踩过的坑,以及如何用正确的方式打开MySQL查询优化。
2. 索引设计中的经典陷阱
2.1 组合索引的顺序玄机
上周排查的一个生产案例让我印象深刻:一个简单的用户查询接口,响应时间从50ms突然飙升到3秒。查看SQL后发现是这样的:
sql复制SELECT * FROM users
WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'
AND status = 'ACTIVE'
表上有两个索引:
- Index A: (create_time, status)
- Index B: (status, create_time)
你猜优化器选了哪个?结果是Index A,扫描了220万行。而如果强制使用Index B,只需要扫描5万行。这是因为:
当范围条件列作为索引前缀时,后面的等值条件无法有效参与范围扫描优化。这就是为什么(status, create_time)的组合在这种情况下表现更好。
我常用的验证方法是使用EXPLAIN FORMAT=JSON查看optimizer trace:
json复制"range_scan_alternatives": [
{
"index": "idx_create_time_status",
"ranges": ["2023-01-01 <= create_time <= 2023-12-31"],
"rows": 2200000,
"cost": 287654
},
{
"index": "idx_status_create_time",
"ranges": ["status = 'ACTIVE' AND 2023-01-01 <= create_time <= 2023-12-31"],
"rows": 50000,
"cost": 6234
}
]
2.2 类型不匹配的隐形杀手
有一次凌晨被叫起来处理一个"数据库挂掉"的告警,发现是一条简单的JOIN查询拖垮了整个实例:
sql复制SELECT * FROM orders o JOIN users u ON o.user_id = u.id
WHERE o.status = 'PENDING'
问题出在o.user_id是varchar(32),而u.id是bigint。MySQL在执行时会强制转换varchar为bigint,导致无法使用user_id上的索引。解决方案很简单:
sql复制-- 修改orders表的user_id字段类型
ALTER TABLE orders MODIFY user_id BIGINT;
-- 或者强制类型转换(临时方案)
SELECT * FROM orders o JOIN users u ON CAST(o.user_id AS SIGNED) = u.id
类型不一致的问题在字符集上也会出现。曾经有个跨境项目,订单表用utf8mb4而用户表用utf8,JOIN性能差了10倍。统一字符集后查询时间从1.2秒降到120ms。
3. 表达式计算的代价
3.1 不要在索引列上做计算
这是新手常犯的错误:
sql复制-- 错误示例
SELECT * FROM products WHERE YEAR(create_time) = 2023;
-- 正确写法
SELECT * FROM products
WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31';
我在性能测试中发现,前者需要全表扫描,而后者能用上create_time的索引,性能相差两个数量级。
3.2 隐式类型转换的坑
这个例子更隐蔽:
sql复制SELECT * FROM transactions
WHERE account_id = 12345;
-- account_id是varchar类型
MySQL会隐式将account_id转换为数字,等效于:
sql复制SELECT * FROM transactions
WHERE CAST(account_id AS SIGNED) = 12345;
解决方法要么改字段类型,要么保持类型一致:
sql复制SELECT * FROM transactions
WHERE account_id = '12345';
4. 深度解析Range Optimizer
4.1 内存限制参数实战
处理过一个电商系统的促销查询,SQL如下:
sql复制SELECT * FROM products
WHERE category_id IN (1,2,3,...,5000);
突然某天开始超时,查看MySQL错误日志发现:
code复制Warning: Memory capacity of 8388608 bytes for 'range_optimizer_max_mem_size' exceeded.
这是因为IN列表太长,超过了range_optimizer_max_mem_size的默认值8MB。解决方案:
sql复制-- 临时调整(需要SUPER权限)
SET SESSION range_optimizer_max_mem_size=33554432; -- 32MB
-- 更好的方案是重构查询
SELECT * FROM products
WHERE category_id BETWEEN 1 AND 100
OR category_id IN (SELECT id FROM hot_categories);
4.2 Index Dive的奥秘
当执行这样的查询时:
sql复制SELECT * FROM orders
WHERE user_id IN (1001, 1002, ..., 1050);
MySQL默认会对每个值执行Index Dive来精确估算行数。但当IN列表超过eq_range_index_dive_limit(默认200)时,就会改用NDV统计估算。
我曾遇到一个案例:某user_id有10万订单,其他用户平均50单。当IN列表包含201个用户时,优化器低估了扫描行数,选择了错误的执行计划。解决方法:
sql复制-- 调低阈值(谨慎使用)
SET SESSION eq_range_index_dive_limit=50;
-- 更好的方案是拆解查询
SELECT * FROM orders WHERE user_id = 1001;
SELECT * FROM orders WHERE user_id IN (1002,...,1050);
5. 实战优化技巧汇编
5.1 执行计划分析三板斧
-
EXPLAIN基础版:
sql复制EXPLAIN SELECT * FROM table WHERE condition;重点关注type列(ALL→index→range→ref→eq_ref→const)和rows列
-
EXPLAIN FORMAT=JSON:
sql复制EXPLAIN FORMAT=JSON SELECT ...可以查看更详细的cost计算和备选方案
-
Optimizer Trace:
sql复制SET optimizer_trace="enabled=on"; SELECT * FROM table WHERE condition; SELECT * FROM information_schema.optimizer_trace;
5.2 索引设计黄金法则
- 高频查询优先
- 区分度高列在前
- 避免过度索引(写性能下降)
- 定期使用
ANALYZE TABLE更新统计信息 - 长字符串考虑前缀索引:
sql复制ALTER TABLE users ADD INDEX (email(20));
5.3 慢查询日志配置
ini复制# my.cnf配置
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1 # 超过1秒的记录
log_queries_not_using_indexes = 1
分析工具推荐:
bash复制# 使用pt-query-digest分析
pt-query-digest /var/log/mysql/mysql-slow.log
6. 真实案例复盘
6.1 电商订单分页优化
原始查询:
sql复制SELECT * FROM orders
WHERE user_id=123
ORDER BY create_time DESC
LIMIT 10000, 20;
问题:越往后翻页越慢,到第500页需要5秒。
优化方案:
sql复制-- 方案1:记住最后一条的create_time
SELECT * FROM orders
WHERE user_id=123 AND create_time < '2023-06-01'
ORDER BY create_time DESC
LIMIT 20;
-- 方案2:使用覆盖索引
SELECT o.* FROM orders o
JOIN (
SELECT id FROM orders
WHERE user_id=123
ORDER BY create_time DESC
LIMIT 10000, 20
) tmp ON o.id=tmp.id;
6.2 千万级用户标签查询
需求:查找具有标签A和B的所有用户。
错误写法:
sql复制SELECT DISTINCT user_id FROM user_tags
WHERE tag_id IN ('A','B')
GROUP BY user_id
HAVING COUNT(DISTINCT tag_id)=2;
优化写法:
sql复制SELECT a.user_id
FROM user_tags a JOIN user_tags b
ON a.user_id = b.user_id
WHERE a.tag_id = 'A' AND b.tag_id = 'B';
配合复合索引(tag_id, user_id),查询时间从8秒降到80ms。