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 性能优化方法。