一文吃透 MySQL 性能优化:从执行计划到架构设计

0 阅读13分钟

一文吃透 MySQL 性能优化:从执行计划到架构设计

掌握这 20+ 核心概念,告别“SELECT *”和“全表扫描”

写在前面

作为一名后端开发,你一定遇到过这样的场景:明明功能逻辑没问题,但接口响应时间却长达几秒甚至几十秒。打开慢查询日志一看,罪魁祸首往往是一两句“简单”的 SQL。

MySQL 性能优化不是玄学,而是一套有章可循的方法论。本文将从SQL 优化、索引设计、数据库结构、架构演进、配置调优五个维度,系统梳理 MySQL 性能优化的核心概念,帮助你建立起自己的优化知识体系。

阅读本文后,你将收获:

  • 掌握 EXPLAIN 执行计划的每个关键字段含义
  • 理解 B+Tree 索引的工作原理及失效场景
  • 学会 10+ 条 SQL 改写技巧
  • 了解读写分离、分库分表等架构演进路径

一、优化金字塔:找对发力点

在开始之前,先建立正确的优化优先级认知:

         /\
        /  \     ① SQL 与索引优化(效果最明显,覆盖 80% 问题)
       /____\
      /      \   ② 数据库结构优化(表设计、范式、分区)
     /________\
    /          \ ③ 架构优化(读写分离、缓存、分库分表)
   /____________\
  /              \④ 硬件与配置优化(最后手段,成本高,收益有限)

一个常见误区:上来就申请更高配置的服务器,或者盲目调整几十个数据库参数。实际上,绝大多数性能问题都源于糟糕的 SQL 写法或缺失的索引。


二、SQL 与索引优化(重中之重)

2.1 EXPLAIN:你的 SQL 体检报告

EXPLAIN 是分析 SQL 性能的第一工具,它输出 MySQL 执行计划的每一步。

EXPLAIN SELECT * FROM user WHERE age = 18;

输出字段详解(重点关注加粗部分):

字段含义优化目标
type访问类型,性能从好到差:system > const > eq_ref > ref > range > index > ALL至少达到 range,争取 ref
key实际使用的索引不能为 NULL
rows预估扫描的行数越小越好
Extra额外信息避免 Using filesort、Using temporary
possible_keys可能使用的索引-
filtered过滤后剩余行的百分比越高越好

type 字段图解:

性能差 ←——————————————————————————————→ 性能好
ALL → index → range → ref → eq_ref → const → system
(全表扫描) (全索引扫描) (范围) (等值)   (唯一)   (常量)

Extra 中需要警惕的信号:

  • Using filesort:需要额外排序,通常需要优化 ORDER BY
  • Using temporary:使用临时表,常见于 GROUP BY 或 DISTINCT
  • Using index condition:使用了索引下推(5.6+ 的特性,好事)
  • Using where:用 WHERE 过滤,如果 type 是 ALL,问题很大

2.2 索引失效的 7 种经典场景

索引不是建了就一定生效,以下情况会导致索引失效(面试高频):

-- ① 左模糊查询(索引失效)
SELECT * FROM user WHERE name LIKE '%张三';
-- ✅ 改为右模糊(索引有效)
SELECT * FROM user WHERE name LIKE '张三%';

-- ② 类型隐式转换(索引失效)
SELECT * FROM user WHERE phone = 13800000000;  -- phone 是 varchar
-- ✅ 加引号
SELECT * FROM user WHERE phone = '13800000000';

-- ③ 对索引列使用函数(索引失效)
SELECT * FROM user WHERE DATE(create_time) = '2024-01-01';
-- ✅ 改为范围查询
SELECT * FROM user WHERE create_time BETWEEN '2024-01-01' AND '2024-01-01 23:59:59';

-- ④ 对索引列进行计算(索引失效)
SELECT * FROM user WHERE age + 1 = 18;
-- ✅ 计算移到等号右边
SELECT * FROM user WHERE age = 17;

-- ⑤ OR 连接(两边都有索引才有效)
SELECT * FROM user WHERE name = '张三' OR age = 18;
-- 如果只有 name 有索引,age 没有,整体索引失效

-- ⑥ 联合索引不满足最左前缀
-- 索引为 (a, b, c),以下查询不走索引
SELECT * FROM table WHERE b = 1 AND c = 2;
-- ✅ 必须包含 a
SELECT * FROM table WHERE a = 1 AND b = 2;

-- ⑦ NOT IN、<> 操作(索引可能失效)
SELECT * FROM user WHERE status <> 0;

2.3 覆盖索引与回表

回表(Back to Table):通过二级索引找到主键,再根据主键到聚簇索引查找完整数据行。这个过程需要两次 B+Tree 查找,产生随机 I/O。

覆盖索引(Covering Index):查询需要的所有列都在索引中,MySQL 可以直接从索引返回数据,无需回表

-- 索引:idx_name_age (name, age)

-- 需要回表:因为要查 phone,索引里没有
SELECT name, age, phone FROM user WHERE name = '张三';

-- 覆盖索引:所有需要的数据都在索引中
SELECT name, age FROM user WHERE name = '张三';
-- EXPLAIN 的 Extra 会显示 Using index

优化技巧:尽量让 SELECT 只取需要的列,避免 SELECT *

2.4 索引下推(ICP,Index Condition Pushdown)

MySQL 5.6 引入的重要优化。在没有 ICP 之前,索引只能定位到 name 匹配的记录,然后回表,再用 age 过滤。有了 ICP 后,可以在索引层先过滤 age。

-- 联合索引 (name, age)
SELECT * FROM user WHERE name LIKE '张%' AND age = 20;

没有 ICP:找到所有 张* 的记录 → 回表 → 过滤 age=20
有 ICP:在索引层同时判断 age=20 → 符合条件的才回表

EXPLAIN Extra 中看到 Using index condition,说明 ICP 已生效。

2.5 查询重写:5 条 SQL 改写技巧

-- ① 避免 SELECT *
-- 差:返回不需要的列,浪费 I/O
SELECT * FROM user WHERE id = 1;
-- 好:只取需要的列
SELECT id, name, age FROM user WHERE id = 1;

-- ② 大分页优化
-- 差:offset 100000,MySQL 要扫描 100020 行
SELECT * FROM user ORDER BY id LIMIT 100000, 20;
-- 好:先找到起始位置,再取数据
SELECT * FROM user 
WHERE id > (SELECT id FROM user ORDER BY id LIMIT 100000, 1) 
LIMIT 20;

-- ③ UNION 换成 UNION ALL
-- UNION 会去重(代价高),确定无重复就用 UNION ALL
SELECT id FROM user WHERE status = 1
UNION ALL  -- 而不是 UNION
SELECT id FROM order WHERE status = 1;

-- ④ 用 EXISTS 代替 IN(子表数据量大时)
-- 差:IN 子查询会先执行
SELECT * FROM user WHERE id IN (SELECT user_id FROM order);
-- 好:EXISTS 以主查询驱动
SELECT * FROM user u 
WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.id);

-- ⑤ 批量操作代替循环
-- 差:循环 100 次
UPDATE user SET age = age + 1 WHERE id = 1;
-- 好:一次批量
UPDATE user SET age = age + 1 WHERE id IN (1,2,3,4,5);

三、索引设计优化

3.1 索引类型选择

索引类型引擎支持特点适用场景
B+TreeInnoDB/MyISAM默认索引,支持等值和范围查询绝大多数场景
HashMemory等值查询极快,不支持范围精确查找,如 KV 缓存
Full-TextInnoDB/MyISAM全文检索文章搜索、关键词匹配
SpatialInnoDB/MyISAMGIS 地理数据位置服务、距离计算

3.2 三星索引(Three-Star Index)

这是一个评估索引好坏的经典模型:

  • 一星:索引包含所有等值 WHERE 条件的列
  • 二星:索引顺序与 ORDER BY 一致,避免 filesort
  • 三星:索引包含所有查询列(覆盖索引)
-- 查询
SELECT id, name, age FROM user 
WHERE city = '北京' AND gender = 1 
ORDER BY create_time;

-- 三星索引:(city, gender, create_time, name, age)
-- 一星:city 和 gender 是等值条件
-- 二星:create_time 在索引中,且顺序正确
-- 三星:name 和 age 也在索引中,无需回表

3.3 基数(Cardinality)与选择性

基数:索引列中唯一值的数量。
选择性:基数 / 总行数,越接近 1 说明索引效果越好。

-- 查看索引基数
SHOW INDEX FROM user;

经验法则

  • 性别(基数 2):选择性差,不适合建索引
  • 身份证号(基数 ≈ 总行数):选择性好,适合建索引
  • 状态字段(如 0/1/2):如果大部分查询只查某一种状态,索引可能无效

3.4 联合索引的最左前缀原则

联合索引 (a, b, c) 相当于创建了三个索引:(a)(a,b)(a,b,c)

能走索引的情况:

WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 ORDER BY b  -- 利用索引排序

不能走索引的情况:

WHERE b = 2
WHERE c = 3
WHERE a = 1 AND c = 3  -- 跳过 b,只能用到 a

设计建议:把区分度高的列放在最左边。


四、数据库结构优化

4.1 范式 vs 反范式

范式(3NF)反范式
原则减少冗余,避免更新异常适当增加冗余,减少 JOIN
优点数据一致性好,节省存储查询性能高,无需多表关联
缺点查询需要 JOIN,性能下降更新时需要维护多处,可能不一致
适用写多读少、对一致性要求高读多写少、报表类场景

反范式示例:订单表存商品名称,而不是存商品 ID 去关联商品表。

4.2 表分区(Partitioning)

将一张大表物理拆分成多个文件,逻辑上仍是一张表。

-- 按年份 RANGE 分区
CREATE TABLE orders (
    id INT,
    order_date DATE,
    amount DECIMAL(10,2)
)
PARTITION BY RANGE (YEAR(order_date)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025)
);

适用场景

  • 历史数据可清理(直接 DROP 分区,比 DELETE 快得多)

  • 查询有明显的时间/范围规律

  • 单表数据量超过 1000 万行

    注意:分区键必须在 WHERE 条件中,否则会扫描所有分区。

4.3 水平分表 vs 垂直分表

水平分表:将行分散到多张结构相同的表。

user_0 (id % 4 = 0)
user_1 (id % 4 = 1)
user_2 (id % 4 = 2)
user_3 (id % 4 = 3)

垂直分表:将列拆分到不同表。

user_base (id, name, age)      -- 热数据
user_profile (id, address, bio) -- 冷数据、大字段

常用中间件:ShardingSphere、MyCAT、Vitess

4.4 数据类型优化

核心原则:

  1. 越小越好:能用 TINYINT 不用 INT,能用 INT 不用 BIGINT

  2. 越简单越好:整数比字符串比较快,DATETIME 比 VARCHAR 存时间好

  3. 避免 NULL:NULL 让索引、统计更复杂,尽量 NOT NULL + 默认值

    实战建议:

场景推荐类型不推荐
年龄TINYINT UNSIGNEDINT、VARCHAR
状态(0-255)TINYINTENUM
IP 地址INT(用 INET_ATON)VARCHAR(15)
金额DECIMAL(10,2)FLOAT、DOUBLE
文本VARCHAR(变长)CHAR(定长浪费)

五、架构优化

5.1 读写分离

架构图:

        写操作         主库
         ↓             ↓
    应用程序  →  同步复制  →  从库1
         ↑             ↑
        读操作        从库2

实现方式

  • 代码层:配置多个数据源,用 AOP 切面路由

  • 中间件:ShardingSphere-JDBC、MyCAT

    代价:从库有复制延迟,不适合对实时性要求极高的场景。

5.2 缓存策略

多级缓存架构:

请求 → 本地缓存(Caffeine)→ Redis → MySQL
         ↑                      ↑
      命中返回               命中返回

缓存更新策略

  • Cache Aside(旁路缓存):读时更新,写时删除缓存

  • Write Through:写时同时更新缓存和数据库

  • Write Behind:先写缓存,异步刷到数据库

    注意:MySQL 8.0 已移除 Query Cache,不要在数据库层做查询缓存。

5.3 连接池配置

连接池复用数据库连接,避免频繁的三次握手和四次挥手。

HikariCP 推荐配置(Spring Boot 默认):

spring:
  datasource:
    hikari:
      maximum-pool-size: 20      # 最大连接数
      minimum-idle: 10           # 最小空闲连接
      connection-timeout: 30000  # 连接超时(ms)
      idle-timeout: 600000       # 空闲超时
      max-lifetime: 1800000      # 连接最大生命周期

经验值:连接数不是越大越好,太多会增加上下文切换。一般公式:(CPU 核心数 * 2) + 磁盘数

5.4 消息队列削峰

场景:秒杀、日志写入、订单异步处理

高并发写入 → MQ(Kafka/RocketMQ)→ 消费者批量写入 MySQL

好处:将瞬时高并发变为平稳的持久化写入,保护数据库。


六、服务器配置优化

6.1 最重要的 5 个参数

参数推荐值说明
innodb_buffer_pool_size物理内存的 70-80%InnoDB 缓存数据和索引,最重要
innodb_log_file_size1-4 GBRedo 日志大小,影响写入性能
max_connections500-2000最大连接数,过大增加上下文切换
innodb_flush_log_at_trx_commit1(强一致)/2(高性能)刷盘策略
tmp_table_size / max_heap_table_size64-256 MB内存临时表上限,超过用磁盘

6.2 Linux 层面的优化

# 修改 open files 限制
ulimit -n 65535

# 禁用 swap(数据库应该尽量使用内存)
sysctl -w vm.swappiness=1

# 文件系统推荐 XFS 或 EXT4,开启 noatime
mount -o noatime,nodiratime /dev/sda1 /data

七、监控与诊断工具

7.1 慢查询日志

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;  -- 超过 2 秒记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 记录没走索引的 SQL

-- 查看慢查询日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';

分析工具

  • mysqldumpslow -s t -t 10 slow.log(MySQL 自带)
  • pt-query-digest slow.log(Percona Toolkit,推荐)

7.2 Performance Schema

MySQL 5.6+ 内置的性能监控库。

-- 查看当前正在执行的 SQL
SELECT * FROM performance_schema.threads 
WHERE PROCESSLIST_STATE IS NOT NULL;

-- 查看语句执行阶段的耗时
SELECT * FROM performance_schema.events_statements_summary_by_digest 
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

7.3 第三方监控方案

  • Prometheus + Grafana:可视化监控趋势
  • Percona Monitoring and Management (PMM):Percona 开源的 MySQL 监控全家桶
  • SkyWalking / Pinpoint:APM 工具,可追踪 SQL 到具体接口

八、优化优先级速查表

优先级优化手段预期提升实施难度风险
1加索引、改 SQL10-100 倍
2改查询逻辑(缓存、分页)5-10 倍
3调整 buffer_pool 大小2-5 倍
4读写分离2-3 倍(读场景)
5表分区2-5 倍
6水平分表5-10 倍
7升级硬件1.5-2 倍高(贵)

九、实战案例:优化一个慢查询

问题描述:用户列表页加载 6 秒,SQL 如下:

SELECT * FROM user 
WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY create_time 
LIMIT 100000, 20;

优化步骤

Step 1:看执行计划

EXPLAIN SELECT ... 

输出:type: ALL(全表扫描),rows: 500万Extra: Using filesort

Step 2:加索引

ALTER TABLE user ADD INDEX idx_time (create_time);

Step 3:再看计划 type: rangerows: 200万,仍然有 Using filesort

Step 4:优化 SQL 写法

-- 先找起始 ID,再取数据
SELECT * FROM user 
WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'
AND id > (
    SELECT id FROM user 
    WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'
    ORDER BY create_time LIMIT 100000, 1
)
ORDER BY create_time LIMIT 20;

Step 5:最终结果 执行时间从 6 秒降到 0.05 秒。


写在最后

MySQL 性能优化不是一蹴而就的,而是一个持续迭代的过程。建议从以下路径入手:

  1. 开启慢查询日志,找到问题 SQL

  2. 用 EXPLAIN 分析执行计划,定位瓶颈

  3. 加索引 + 改 SQL,解决 80% 的问题

  4. 监控 + 复盘,防止性能回退

    记住:索引不是越多越好,SQL 不是越短越好,优化不是越复杂越好。适合业务场景的方案才是最好的。


如果觉得有帮助,欢迎点赞、收藏、评论三连支持~