MySQL 常用优化方法
MySQL 性能优化不能简单理解为“加索引”。SQL、索引、数据量、执行计划、表结构以及业务访问方式都会影响最终性能。
下面整理一些实际开发中比较常用的 MySQL 优化方法。
1. 使用 EXPLAIN 分析 SQL
分析慢 SQL 时,首先应该查看执行计划:
EXPLAIN
SELECT *
FROM orders
WHERE user_id = 10001
AND status = 1
ORDER BY created_at DESC
LIMIT 20;
重点关注:
| 字段 | 说明 |
|---|---|
type | 表的访问方式,如 const、ref、range、ALL |
possible_keys | 可能使用的索引 |
key | 实际使用的索引 |
key_len | 实际使用的索引长度 |
rows | 优化器估算需要扫描的行数 |
filtered | 过滤后的数据比例 |
Extra | 额外执行信息 |
重点关注:
type = ALL
rows 很大
key = NULL
Using filesort
Using temporary
但这些结果并不代表一定有问题,需要结合实际数据量和执行时间判断。
MySQL 8.x 还可以使用:
EXPLAIN ANALYZE
SELECT ...;
查看实际执行情况,并对比优化器的估算值与实际值。
官方文档:MySQL EXPLAIN
2. 避免 SELECT *
尽量明确指定需要的字段:
SELECT id, name, status
FROM users
WHERE id = 10001;
而不是:
SELECT *
FROM users
WHERE id = 10001;
这样可以减少不必要的:
磁盘 I/O
网络传输
内存消耗
数据处理
同时,在部分场景下也更容易利用覆盖索引。
3. 合理使用 LIMIT
如果业务只需要一条数据,可以使用:
SELECT id, name
FROM users
WHERE email = 'test@example.com'
LIMIT 1;
LIMIT 1 可以让数据库在找到满足条件的数据后停止继续读取。
不过需要注意:
LIMIT 1并不是为了让EXPLAIN的type变成const。
const 主要取决于查询条件是否能够通过唯一索引等方式唯一定位记录。
4. 合理设计索引
索引应该围绕真实查询设计,而不是“字段越多索引越多”。
例如经常执行:
SELECT *
FROM orders
WHERE user_id = ?
AND status = ?
ORDER BY created_at DESC
LIMIT 20;
可以考虑:
CREATE INDEX idx_user_status_created
ON orders(user_id, status, created_at);
联合索引的字段顺序需要结合:
WHERE
JOIN
ORDER BY
GROUP BY
以及字段选择性综合判断。
5. 遵循联合索引的最左匹配原则
例如:
INDEX idx_user_status_created(user_id, status, created_at)
通常可以有效支持:
WHERE user_id = ?
以及:
WHERE user_id = ?
AND status = ?
但单独:
WHERE status = ?
通常无法充分利用该联合索引进行定位。
因此设计联合索引时,要根据实际查询模式确定字段顺序,而不是简单地把多个字段放在一起。
6. 注意索引中的范围查询
例如:
INDEX(a, b, c)
查询:
WHERE a = 10
AND b > 100
AND c = 20
当联合索引中出现范围条件后,后续列通常不能继续用于进一步的索引范围定位。
因此设计联合索引时,需要特别关注:
等值条件
→ 范围条件
→ 排序
但“范围之后的字段完全失效”并不准确,MySQL 仍可能通过其他机制使用相关索引信息,实际情况应以 EXPLAIN / EXPLAIN ANALYZE 为准。
7. 避免让索引列参与计算
例如:
SELECT *
FROM users
WHERE age * 2 = 36;
如果 age 有索引,这种写法可能影响索引的有效利用。
更合理:
SELECT *
FROM users
WHERE age = 18;
类似情况包括:
WHERE price + 10 > 100
WHERE YEAR(created_at) = 2026
WHERE id + 1 = 100
对于时间查询,推荐改成范围查询:
WHERE created_at >= '2026-01-01'
AND created_at < '2027-01-01'
这样通常更有利于使用 created_at 索引。
8. 避免隐式类型转换
例如:
id BIGINT
却使用字符串参数:
WHERE id = '10001'
虽然 MySQL 很多情况下可以正常处理,但类型不一致可能导致隐式转换,并影响索引使用。
应用层应该尽量保证:
数据库字段类型
=
程序参数类型
特别是在 JOIN 条件中,两边关联字段的数据类型也应该保持一致。
9. 谨慎使用 LIKE ‘%xxx%’
例如:
WHERE name LIKE '%mysql%'
普通 B+Tree 索引通常无法直接利用字符串前缀进行快速定位。
而:
WHERE name LIKE 'mysql%'
通常可以利用索引进行前缀匹配。
如果业务确实需要:
关键词搜索
全文搜索
模糊搜索
可以考虑 MySQL FULLTEXT 或 Elasticsearch 等更适合搜索场景的方案。
10. 合理使用 IN
例如:
SELECT *
FROM users
WHERE id IN (1, 2, 3, 4, 5);
少量值的 IN 通常没有问题。
真正需要关注的是:
WHERE id IN (...)
里面包含几千、几万甚至更多值。
这种场景可能带来:
SQL 体积过大
优化器处理成本增加
网络传输增加
执行计划复杂
应用层参数处理成本增加
如果数据量较大,可以考虑:
临时表
批量查询
JOIN
批处理
不要简单认为“连续 ID 就一定应该改成 BETWEEN”。
11. 尽量使用 UNION ALL
例如:
SELECT id FROM users WHERE status = 1
UNION ALL
SELECT id FROM users WHERE status = 2;
如果可以确定两个结果集不存在重复数据,优先使用:
UNION ALL
而不是:
UNION
因为 UNION 需要进行去重处理,会增加额外的计算成本。
因此:
确定不需要去重 → UNION ALL
需要去重 → UNION
12. 谨慎使用 OR
例如:
WHERE user_id = 10001
OR order_no = 'A10001'
OR 是否影响索引,需要结合具体执行计划判断。
不要简单认为:
只要出现 OR 就不会使用索引。
如果 OR 两边条件差异较大,可以尝试:
UNION ALL
例如:
SELECT ...
FROM users
WHERE user_id = 10001
UNION ALL
SELECT ...
FROM users
WHERE email = 'test@example.com';
但前提是两部分结果不会重复;如果可能重复,则需要使用 UNION 或其他去重方案。
13. 避免 ORDER BY RAND()
下面这种写法在数据量较大时性能较差:
SELECT *
FROM products
ORDER BY RAND()
LIMIT 10;
因为 MySQL 需要为大量数据生成随机值并进行排序。
如果只是随机抽取少量数据,可以根据业务设计其他方案,例如:
随机 ID
随机偏移
预生成随机字段
随机主键范围
应用层随机
具体方案需要根据数据是否连续、是否允许删除、是否存在分页等业务条件选择。
14. 优化深分页
下面这种分页:
SELECT id, name
FROM products
ORDER BY id
LIMIT 1000000, 20;
随着 OFFSET 增大,数据库需要处理越来越多的数据。
如果按照递增 ID 分页,可以使用 Keyset Pagination:
SELECT id, name
FROM products
WHERE id > 1000000
ORDER BY id
LIMIT 20;
对于大量数据的列表、订单、日志等场景,这种方式通常比深度 OFFSET 分页更加稳定。
15. 合理控制查询范围
例如查询日志:
SELECT *
FROM operation_logs
WHERE created_at >= '2026-01-01';
如果表已经有几亿条数据,一次查询几个月甚至几年的数据,本身就可能产生大量扫描和处理。
可以根据业务进行:
时间分段
分页查询
批量处理
异步任务
数据归档
例如程序按天查询:
2026-09-01
2026-09-02
2026-09-03
...
最后再汇总结果。
16. 避免无意义的 NULL 判断
不能简单地说:
WHERE column IS NULL
“一定导致索引失效”。
实际上,MySQL 对 IS NULL 可以使用索引,具体情况取决于索引和查询条件。
真正需要避免的是没有必要的 NULL 处理逻辑,以及对索引字段进行不利于索引使用的函数、计算或转换。
如果业务明确不允许 NULL,可以在表结构层面使用:
NOT NULL
并设置合理的默认值。
17. 合理使用覆盖索引
例如:
SELECT user_id, created_at
FROM orders
WHERE user_id = 10001;
如果存在:
INDEX(user_id, created_at)
查询所需要的字段已经包含在索引中,就可能直接从索引获取数据。
这种方式称为:
覆盖索引。
可以通过:
EXPLAIN
观察是否出现:
Using index
但不要为了覆盖索引把几十个字段全部塞进索引。
索引过宽会增加:
磁盘空间
INSERT 成本
UPDATE 成本
DELETE 成本
18. 不要轻易 FORCE INDEX
MySQL 优化器会根据统计信息和成本模型选择索引。
如果:
EXPLAIN
发现 MySQL 没有选择预期索引,不应该第一时间:
FORCE INDEX
应该先分析:
数据分布
索引选择性
统计信息
查询条件
联合索引设计
必要时可以:
ANALYZE TABLE orders;
更新统计信息。
只有在明确知道优化器选择存在问题,并且经过测试确认后,才考虑:
FORCE INDEX
因为数据量和数据分布发生变化后,强制索引也可能变成性能问题。
19. JOIN 优化
多表 JOIN 时重点关注:
JOIN 条件是否有索引
字段类型是否一致
驱动表是否合理
过滤条件是否尽可能提前
JOIN 后扫描的数据量
例如:
SELECT o.id, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 1;
应该确保:
users.id
orders.user_id
等关联字段具备合理的索引。
对于 INNER JOIN,MySQL 可以根据成本选择合适的连接顺序;而 LEFT JOIN 等外连接受到连接语义约束,不能简单认为“左表一定更小”或者“左表一定更快”。
因此 JOIN 优化最终还是应该通过:
EXPLAIN
观察实际执行计划。
20. 不要机械限制 JOIN 表数量
“JOIN 超过 3 张表就一定性能差”并不是一个准确的优化原则。
例如:
A JOIN B JOIN C
可能非常快。
而:
A JOIN B
也可能非常慢。
真正影响性能的是:
数据量
索引
连接条件
过滤条件
JOIN 顺序
中间结果集大小
排序
聚合
临时表
如果一个查询需要 JOIN 多张大表,并且执行频率很高,可以进一步考虑:
SQL 优化
索引优化
汇总表
缓存
数据冗余
异步计算
读写分离
而不是简单地把 JOIN 拆到程序代码中。
总结
MySQL 优化的核心并不是记住多少条“SQL 禁忌”,而是建立正确的分析流程:
发现慢 SQL
↓
EXPLAIN
↓
分析执行计划
↓
检查 SQL 和索引
↓
优化 SQL / 索引 / 数据访问方式
↓
再次 EXPLAIN
↓
EXPLAIN ANALYZE
↓
验证实际执行效果
实际开发中,可以优先关注这几个方面:
1. 是否扫描了大量数据
2. 是否使用了合适的索引
3. 联合索引顺序是否合理
4. 是否存在深分页
5. 是否存在大量排序或临时操作
6. JOIN 条件是否有索引
7. SQL 是否存在不必要的计算、转换
8. 优化器估算与实际执行是否存在明显偏差
最重要的一点是:
不要为了优化而优化,一切优化都应该以真实 SQL、真实数据和实际执行计划为依据。
相比“看到 ALL 就加索引”、“看到 Using filesort 就改 SQL”这种机械优化方式,使用 EXPLAIN 和 EXPLAIN ANALYZE 验证执行计划,才是更加可靠的 MySQL 性能优化方法。