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

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

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

写在前面

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

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

阅读本文后,你将收获:

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

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

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

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

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

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

2.1 EXPLAIN:你的 SQL 体检报告

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

sql 复制代码
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 字段图解:

scss 复制代码
性能差 ←------------------------------------------------------------------------------------------→ 性能好
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 种经典场景

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

sql 复制代码
-- ① 左模糊查询(索引失效)
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 可以直接从索引返回数据,无需回表

sql 复制代码
-- 索引: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。

sql 复制代码
-- 联合索引 (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 改写技巧

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+Tree InnoDB/MyISAM 默认索引,支持等值和范围查询 绝大多数场景
Hash Memory 等值查询极快,不支持范围 精确查找,如 KV 缓存
Full-Text InnoDB/MyISAM 全文检索 文章搜索、关键词匹配
Spatial InnoDB/MyISAM GIS 地理数据 位置服务、距离计算

3.2 三星索引(Three-Star Index)

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

  • 一星:索引包含所有等值 WHERE 条件的列
  • 二星:索引顺序与 ORDER BY 一致,避免 filesort
  • 三星:索引包含所有查询列(覆盖索引)
sql 复制代码
-- 查询
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 说明索引效果越好。

sql 复制代码
-- 查看索引基数
SHOW INDEX FROM user;

经验法则

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

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

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

能走索引的情况:

css 复制代码
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 ORDER BY b  -- 利用索引排序

不能走索引的情况:

ini 复制代码
WHERE b = 2
WHERE c = 3
WHERE a = 1 AND c = 3  -- 跳过 b,只能用到 a

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

四、数据库结构优化

4.1 范式 vs 反范式

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

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

4.2 表分区(Partitioning)

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

sql 复制代码
-- 按年份 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 垂直分表

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

ini 复制代码
user_0 (id % 4 = 0)
user_1 (id % 4 = 1)
user_2 (id % 4 = 2)
user_3 (id % 4 = 3)

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

bash 复制代码
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 UNSIGNED INT、VARCHAR
状态(0-255) TINYINT ENUM
IP 地址 INT(用 INET_ATON) VARCHAR(15)
金额 DECIMAL(10,2) FLOAT、DOUBLE
文本 VARCHAR(变长) CHAR(定长浪费)

五、架构优化

5.1 读写分离

架构图:

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

实现方式

  • 代码层:配置多个数据源,用 AOP 切面路由
  • 中间件:ShardingSphere-JDBC、MyCAT
    代价:从库有复制延迟,不适合对实时性要求极高的场景。

5.2 缓存策略

多级缓存架构:

markdown 复制代码
请求 → 本地缓存(Caffeine)→ Redis → MySQL
         ↑                      ↑
      命中返回               命中返回

缓存更新策略

  • Cache Aside(旁路缓存):读时更新,写时删除缓存
  • Write Through:写时同时更新缓存和数据库
  • Write Behind:先写缓存,异步刷到数据库
    注意:MySQL 8.0 已移除 Query Cache,不要在数据库层做查询缓存。

5.3 连接池配置

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

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

yaml 复制代码
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_size 1-4 GB Redo 日志大小,影响写入性能
max_connections 500-2000 最大连接数,过大增加上下文切换
innodb_flush_log_at_trx_commit 1(强一致)/2(高性能) 刷盘策略
tmp_table_size / max_heap_table_size 64-256 MB 内存临时表上限,超过用磁盘

6.2 Linux 层面的优化

bash 复制代码
# 修改 open files 限制
ulimit -n 65535
​
# 禁用 swap(数据库应该尽量使用内存)
sysctl -w vm.swappiness=1
​
# 文件系统推荐 XFS 或 EXT4,开启 noatime
mount -o noatime,nodiratime /dev/sda1 /data

七、监控与诊断工具

7.1 慢查询日志

sql 复制代码
-- 开启慢查询日志
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 复制代码
-- 查看当前正在执行的 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 加索引、改 SQL 10-100 倍
2 改查询逻辑(缓存、分页) 5-10 倍
3 调整 buffer_pool 大小 2-5 倍
4 读写分离 2-3 倍(读场景)
5 表分区 2-5 倍
6 水平分表 5-10 倍
7 升级硬件 1.5-2 倍 高(贵)

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

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

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

优化步骤

Step 1:看执行计划

sql 复制代码
EXPLAIN SELECT ... 

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

Step 2:加索引

sql 复制代码
ALTER TABLE user ADD INDEX idx_time (create_time);

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

Step 4:优化 SQL 写法

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 不是越短越好,优化不是越复杂越好。适合业务场景的方案才是最好的。

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

相关推荐
程序员契奇21 分钟前
Tools工具使用
人工智能·后端
IT_陈寒37 分钟前
SpringBoot自动配置没生效?你可能漏了这个注解
前端·人工智能·后端
长明42 分钟前
C#项目组织与概念梳理
后端·c#
xn71331 小时前
个人网站站外分发怎么做归因?我给 XBSTACK 补了一套 UTM 追踪规则
后端·低代码
用户2330713074791 小时前
JUC 并发容器与工具
后端
冰暮流星1 小时前
flask之模版渲染
后端·python·flask
威武的花瓣1 小时前
细说ASP.NET的各种异步操作
后端·asp.net·php
漂亮的摩托1 小时前
如何编写一个SpringBoot项目告警推送的Starter
java·spring boot·后端
任性的芝麻1 小时前
ASP.NET MVC 中的异步方式
后端·asp.net·mvc
雨师@2 小时前
go语言项目--实例化(图书管理)--006
开发语言·后端·golang