MySQL实战篇1:慢查询优化实战-4道题的真实优化记录
📅 2025年10月8日
💻 测试环境:HeidiSQL + MySQL 8.0
📊 数据量:users表1000条,orders表5000+条,order_items表10000+条
⏰ 用时:约1小时
今天做了4道SQL优化题,有做对的,也有不太理解的。特别是第3题和第4题,遇到了一些问题,记录一下整个过程。
题目1:深度分页优化 ✅
原始SQL
SELECT * FROM users
ORDER BY id
LIMIT 900, 10;
因为我的测试数据只有1000条,所以改成查询第900-910条。
优化前的表现
执行时间:0.016秒(因为数据少,看不出差异)
EXPLAIN分析:
关键信息:
- type: index(扫描整个索引)
- rows: 910(需要读910行)
- Extra: -
虽然时间不长,但我看EXPLAIN发现:MySQL要扫描910行,然后跳过前900行,只返回10行。
这意味着什么?
- 如果数据是10万条,LIMIT 90000, 10 就要扫描90010行
- 偏移量越大,浪费越多
- 深度分页的核心问题:不是查询慢,是"跳过"慢
我的优化方案
我查了资料,用子查询 + 覆盖索引的方式:
SELECT * FROM users
WHERE id >= (SELECT id FROM users ORDER BY id LIMIT 900, 1)
ORDER BY id LIMIT 10;
思路:
- 内层查询:只查id,找到第900条的id
- 外层查询:从这个id开始,取10条
优化后的表现
执行时间:0.008秒
EXPLAIN分析:
关键改进:
- 内层查询:type=index, rows=901, Extra: Using index ← 覆盖索引
- 外层查询:type=range, rows≈10-20
我的疑问和理解
疑问1:为什么子查询就快了?
我一开始不太理解,后来想明白了:
- 内层
SELECT id FROM users
只查id字段 - id是主键,索引已经包含了id的值
- MySQL不需要回表查数据,只扫描索引树
- EXPLAIN显示"Using index"就是这个意思,这叫覆盖索引
疑问2:type是index还是range?
- 内层查询:type=index(扫描索引树)
- 外层查询:type=range(范围扫描)
- range比index更优,因为只扫描需要的范围
性能对比
版本 | 扫描行数 | 执行时间 | 说明 |
---|---|---|---|
原始 | 910行 | 0.016秒 | 全部扫描再跳过 |
优化后 | 901行(内层) + 10行(外层) | 0.008秒 | 覆盖索引+范围扫描 |
虽然我的数据少,时间差异不明显,但如果是10万、100万数据:
- 原始:扫描90010行 → 可能要5-10秒
- 优化:扫描901行(覆盖索引,快) + 10行 → 可能只要0.05秒
- 性能提升:100倍+
我的收获
- 深度分页的问题本质是"跳过"太多行
- 覆盖索引的意思:查询的字段在索引里,不用回表
- 小数据量看EXPLAIN,大数据量看执行时间
- 生产环境要避免深度分页,可以用游标分页
题目2:JOIN查询优化 ✅
原始SQL
SELECT u.name, COUNT(o.id) as order_count, SUM(o.amount) as total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
GROUP BY u.id
ORDER BY total_amount DESC
LIMIT 20;
优化前的表现
执行时间:0.032秒
EXPLAIN分析:
table | type | key | rows |
---|---|---|---|
users | ALL | NULL | 1000 |
orders | ALL | NULL | 5000+ |
我看到的问题:
- orders表没有索引,JOIN时全表扫描
- 用了LEFT JOIN,但WHERE条件过滤了NULL
我发现的问题
问题1:缺少索引
查看索引:
orders表的user_id列没有索引,JOIN时要全表扫描。
问题2:LEFT JOIN用错了
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
这里用LEFT JOIN,但WHERE条件o.status = 'completed'
会过滤掉o为NULL的记录。
实际上等同于INNER JOIN,用LEFT JOIN反而更慢。
我的优化方案
第1步:创建索引
CREATE INDEX idx_user_id ON orders(user_id);
思路:
- users表是小表(1000条)
- orders表是大表(5000+条)
- 小表驱动大表,在大表的连接列建索引
第2步:改写SQL
SELECT u.name, COUNT(o.id) as order_count, SUM(o.amount) as total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
GROUP BY u.id
ORDER BY total_amount DESC
LIMIT 20;
把LEFT JOIN改成INNER JOIN。
优化后的表现
执行时间:0.000
EXPLAIN分析:
关键改进:
- orders表:type从ALL变成eq_ref(最优的JOIN类型)
- rows:从5000+变成1
- 使用了索引:key = idx_user_id
性能对比
版本 | orders表type | orders表rows | 执行时间 |
---|---|---|---|
优化前 | ALL | 5000+ | 0.031秒 |
优化后 | eq_ref | 1 | 0.015秒 |
我的收获
- 小表驱动大表,在大表建索引
- LEFT JOIN不总是必要的,能用INNER JOIN就用INNER JOIN
- eq_ref是JOIN最优的访问类型
- 索引对JOIN性能影响巨大
题目3:索引失效问题 ⚠️(遇到问题)
原始SQL
SELECT * FROM orders
WHERE YEAR(created_at) = 2024;
我先创建了索引
CREATE INDEX idx_created_at ON orders(created_at);
优化前的表现
EXPLAIN分析:
- type: ALL(全表扫描)
- key: NULL(没用索引)
- rows: 5008
问题很明显:索引失效了!
原因:YEAR(created_at)
在索引列上用了函数,索引失效。
我的优化尝试
我把SQL改成范围查询:
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
这样就不在索引列上用函数了。
但是遇到问题了!
EXPLAIN分析:
结果:还是type=ALL,rows=5008!
我懵了:
- SQL改对了(没用函数)
- 索引也创建了
- 为什么还是全表扫描?
我的分析
后来我想了想,可能的原因:
原因1:数据分布问题
可能2024年的数据占了大部分(比如80-90%)。
MySQL优化器会判断:
- 走索引:扫描索引树 + 回表查数据 → 大量随机I/O
- 全表扫描:顺序读表 → 连续I/O
如果数据量大,全表扫描可能更快,优化器就不用索引了。
原因2:统计信息没更新
我尝试更新统计信息:
ANALYZE TABLE orders;
但EXPLAIN结果还是一样。
我的困惑
这道题让我困惑的是:
- 我的SQL改写是对的
- 但优化器选择了全表扫描
- 这算优化成功还是失败?
我现在的理解:
- 索引不是一定会用
- 要看数据分布和查询范围
- 如果查询范围太大(>30%数据),全表扫描可能是对的
- 虽然EXPLAIN还是ALL,但SQL改写本身是正确的
这道题没达到预期效果,不知道哪里有问题。
题目4:综合优化挑战 ❌(有点不会)
原始SQL
SELECT DISTINCT u.id, u.name, u.email
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
WHERE oi.product_name LIKE '%Phone%'AND o.status = 'completed'AND u.city = '深圳';
优化前的表现
执行时间:0.047秒
EXPLAIN分析:
table | type | key | rows |
---|---|---|---|
users | eq_ref | PRIMARY | 1 |
orders | eq_ref | idx_user_id | 1 |
order_items | ALL | NULL | 10000 |
我看到的问题:
- order_items表:type=ALL,全表扫描
- 其他两表:type=eq_ref(有索引)
我一开始的尝试
我想:product_name没索引,那创建一个索引吧。
CREATE INDEX idx_product_name ON order_items(product_name);
但后来发现:这个索引根本用不上!
为什么用不上?
因为:
WHERE oi.product_name LIKE '%Phone%'
前后都有通配符%,任何索引都失效!
LIKE 'Phone%'
→ 可以用索引(前缀匹配)LIKE '%Phone'
→ 索引失效(后缀匹配)LIKE '%Phone%'
→ 索引完全失效(模糊匹配)
我的困惑
这道题我有点不会了:
问题1:LIKE ‘%xxx%’ 怎么优化?
- 普通索引用不上
- 是不是要用全文索引?
- 还是改变数据库设计?
问题2:三表JOIN的顺序对吗?
- 现在是users → orders → order_items
- 要不要调整顺序?
问题3:DISTINCT影响性能吗?
- DISTINCT会去重
- 是不是影响性能?
我查了一些资料
方案1:全文索引
ALTER TABLE order_items ADD FULLTEXT INDEX ft_product_name(product_name);-- 改写查询
WHERE MATCH(oi.product_name) AGAINST('Phone' IN NATURAL LANGUAGE MODE)
但我没试过全文索引,不确定效果。
方案2:改变设计
- 增加product_category字段
- 或者增加product_id字段
- 用精确匹配代替LIKE
方案3:改变驱动顺序
如果product_name条件筛选很多数据,可以先过滤order_items。
但我不太确定怎么改。
这道题的现状
我尝试了一些方案,但效果不理想。
数据统计:
- users表:1000条
- orders表:5000+条
- order_items表:10000+条
这道题我没做好,还需要继续学习。
我的总体收获
这次实战做了4道题:
- ✅ 题目1(深度分页):做对了,理解了覆盖索引
- ✅ 题目2(JOIN优化):做对了,理解了小表驱动大表
- ⚠️ 题目3(索引失效):SQL改对了,但优化器没用索引,不太理解
- ❌ 题目4(综合优化):LIKE ‘%xxx%’ 不会优化
我现在理解的知识点
1. 深度分页优化
- 核心问题:跳过太多行
- 解决方案:子查询 + 覆盖索引
- 关键概念:覆盖索引 = 查询字段在索引里,不回表
2. JOIN优化
- 小表驱动大表
- 在大表的连接列建索引
- 能用INNER JOIN就不用LEFT JOIN
3. 索引失效场景
- 在索引列上用函数:
YEAR(date)
→ 失效 - LIKE前后通配符:
LIKE '%xxx%'
→ 失效 - 表达式计算:
col + 1 = value
→ 失效
我的薄弱环节
-
优化器的选择逻辑
- 为什么题目3优化器不用索引?
- 什么情况下全表扫描比索引快?
- 怎么判断优化器的选择是否合理?
-
LIKE模糊查询优化
- LIKE ‘%xxx%’ 怎么办?
- 全文索引怎么用?
- 数据库设计层面怎么避免?
-
复杂查询的优化思路
- 多表JOIN怎么调整驱动顺序?
- DISTINCT对性能影响多大?
- 怎么判断查询已经优化到极致?
下次要注意
- 先看数据分布,再判断优化效果
- EXPLAIN的type=ALL不一定是坏事
- 有些优化问题需要改数据库设计
- 遇到不会的要继续学习,不能停
写于:2025年10月8日晚
数据量:测试环境小数据量
用时:约1小时
这次实战让我对SQL优化有了更深的理解,但也发现了自己的不足。题目3和题目4的问题,我要继续研究。
学习是个持续的过程,做错了也是收获。 💪
附录:测试环境信息
数据库版本: MySQL 8.0
表结构:
- users:1000条数据
- orders:5000+条数据
- order_items:10000+条数据
索引情况:
工具: HeidiSQL可视化工具
相关博客
- MySQL实战篇0:数据库设计实战----从需求到建表的完整流程
- MySQL实战篇2:篇1第四题深入——MySQL模糊查询LIKE ‘%xxx%‘优化深度研究
- MySQL学习笔记07:MySQL SQL优化与EXPLAIN分析实战指南(上):执行计划深度解析
- MySQL学习笔记08:MySQL SQL优化与EXPLAIN分析实战指南(下):JOIN优化到慢查询监控完整实战