一、问题场景
业务背景:订单列表查询,显示用户的所有订单
sql
-- 查询用户订单列表
SELECT * FROM orders
WHERE user_id = 12345
AND status IN (1, 2, 3)
AND create_time >= '2024-01-01'
ORDER BY create_time DESC
LIMIT 10;
问题表现:
- 执行时间:5.2秒
- 返回数据:10条
- 表数据量:500万条
二、问题定位
2.1 使用EXPLAIN分析
sql
EXPLAIN SELECT * FROM orders
WHERE user_id = 12345
AND status IN (1, 2, 3)
AND create_time >= '2024-01-01'
ORDER BY create_time DESC
LIMIT 10;
结果:
sql
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 5000000 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
关键信息:
type = ALL:全表扫描(最差)key = NULL:没有使用索引rows = 5000000:扫描了500万行!
2.2 查看当前索引
sql
SHOW INDEX FROM orders;
结果:
sql
+-------+------------+----------+--------------+-------------+-----------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation |
+-------+------------+----------+--------------+-------------+-----------+
| orders| 1 | PRIMARY | 1 | id | A |
+-------+------------+----------+--------------+-------------+-----------+
发现问题:只有主键索引,没有其他索引!
三、优化方案
3.1 添加复合索引
lua
-- 添加复合索引
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);
为什么是这个顺序?
ini
索引 idx_user_status_time(user_id, status, create_time)
查询条件:
WHERE user_id = 12345 ✓ 最左前缀匹配
AND status IN (1, 2, 3) ✓ 可以匹配
AND create_time >= '2024-01-01' ✓ 可以匹配
ORDER BY create_time DESC ✓ 利用索引排序
能使用的查询:
✅ WHERE user_id = 12345
✅ WHERE user_id = 12345 AND status = 1
✅ WHERE user_id = 12345 AND status IN (1, 2, 3)
✅ WHERE user_id = 12345 AND create_time >= '2024-01-01'
不能使用的查询:
✗ WHERE status = 1 (跳过了user_id)
✗ WHERE create_time >= '2024-01-01' (跳过了user_id和status)
3.2 再次EXPLAIN
sql
EXPLAIN SELECT * FROM orders
WHERE user_id = 12345
AND status IN (1, 2, 3)
AND create_time >= '2024-01-01'
ORDER BY create_time DESC
LIMIT 10;
结果:
sql
+----+-------------+--------+-------+----------------------+--------------------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+----------------------+--------------------+---------+------+------+-------------+
| 1 | SIMPLE | orders | range | idx_user_status_time | idx_user_status_time | 17 | NULL | 1500 | Using where |
+----+-------------+--------+-------+----------------------+--------------------+---------+------+------+-------------+
优化效果:
type = range:范围查询(比ALL好很多)key = idx_user_status_time:使用了索引rows = 1500:扫描了1500行(比500万行少很多)- 执行时间:从5.2秒降到0.03秒,提升173倍!
四、进一步优化
4.1 问题:SELECT * 查询所有字段
sql
-- ❌ 不好:查询所有字段
SELECT * FROM orders WHERE user_id = 12345;
-- ✅ 好:只查询需要的字段
SELECT id, order_no, amount, status, create_time
FROM orders WHERE user_id = 12345;
4.2 问题:LIMIT深度分页
sql
-- 问题:LIMIT 1000000, 10 需要扫描100万行
SELECT * FROM orders
WHERE user_id = 12345
ORDER BY create_time DESC
LIMIT 1000000, 10;
-- 解决方案1:使用子查询
SELECT * FROM orders
WHERE id >= (
SELECT id FROM orders
WHERE user_id = 12345
ORDER BY create_time DESC
LIMIT 1000000, 1
)
ORDER BY create_time DESC
LIMIT 10;
-- 解决方案2:记录上次的最大ID
SELECT * FROM orders
WHERE user_id = 12345 AND id > 1234567
ORDER BY create_time DESC
LIMIT 10;
4.3 问题:索引覆盖
sql
-- 如果查询的字段都在索引中,就不需要回表查询
-- 当前索引:idx_user_status_time(user_id, status, create_time)
-- 查询字段:id, order_no, amount, status, create_time
-- ❌ 不好的查询:需要回表
SELECT id, order_no, amount, status, create_time
FROM orders WHERE user_id = 12345;
-- ✅ 优化:添加覆盖索引
CREATE INDEX idx_user_status_time_cover ON orders(user_id, status, create_time, id, order_no, amount);
-- 再次EXPLAIN:
-- Extra: Using index(不需要回表,直接从索引获取)
五、真实案例优化总结
| 优化项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 执行时间 | 5.2秒 | 0.03秒 | 173倍 |
| 扫描行数 | 500万行 | 1500行 | 3333倍 |
| 是否使用索引 | 否 | 是 | - |
| type类型 | ALL | range | - |
六、EXPLAIN字段详解
| 字段 | 说明 | 好的值 | 差的值 |
|---|---|---|---|
| id | 查询序列号 | - | - |
| select_type | 查询类型 | SIMPLE | SUBQUERY、UNION |
| type | 访问类型 | const、eq_ref、ref、range | ALL |
| key | 实际使用的索引 | 有索引 | NULL |
| key_len | 索引长度 | - | - |
| rows | 扫描行数 | 少 | 多 |
| Extra | 额外信息 | Using index | Using filesort、Using temporary |
type类型优劣排序:
sql
const > eq_ref > ref > range > index > ALL
最好 最差
七、常见慢查询场景及优化
7.1 场景1:使用了函数
sql
-- ❌ 索引失效
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01';
-- ✅ 使用索引
SELECT * FROM orders
WHERE create_time >= '2024-01-01 00:00:00'
AND create_time <= '2024-01-01 23:59:59';
7.2 场景2:LIKE模糊查询
sql
-- ❌ 索引失效
SELECT * FROM orders WHERE order_no LIKE '%2024%';
-- ✅ 使用索引
SELECT * FROM orders WHERE order_no LIKE '2024%';
7.3 场景3:OR条件
sql
-- ❌ 索引失效
SELECT * FROM orders WHERE user_id = 1 OR status = 1;
-- ✅ 使用UNION ALL
SELECT * FROM orders WHERE user_id = 1
UNION ALL
SELECT * FROM orders WHERE status = 1;
7.4 场景4:NOT、!=、<>
sql
-- ❌ 索引失效
SELECT * FROM orders WHERE status != 1;
-- ✅ 使用索引
SELECT * FROM orders WHERE status > 1;
八、慢查询监控
8.1 开启慢查询日志
sql
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2; -- 超过2秒的查询记录
-- 查看慢查询日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';
8.2 分析慢查询
bash
# 查看慢查询日志
tail -f /var/log/mysql/slow.log
# 使用mysqldumpslow工具分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s t: 按时间排序
# -t 10: 显示前10条
九、总结
今天我们学到了:
| 要点 | 说明 |
|---|---|
| 问题定位 | EXPLAIN分析type、key、rows |
| 核心优化 | 添加合适的索引(复合索引) |
| 索引顺序 | 遵循最左前缀原则 |
| 覆盖索引 | 索引包含查询字段,避免回表 |
| 常见坑 | 函数、LIKE、OR、NOT |
今日互动:
你的项目有过慢查询吗?是用什么方法优化的?