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

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

相关推荐
开心就好20252 小时前
苹果iOS应用开发上架与推广完整教程
后端·ios
四千岁2 小时前
Ollama+OpenWebUI 最佳组合:本地大模型可视化交互方案
前端·javascript·后端
Carsene2 小时前
AutoScan Spring Boot Starter v1.3.0 发布:高级过滤与环境配置新特性
spring boot·后端
程序员柒叔2 小时前
OpenCode 一周动态-2026-W15
后端·github
星辰_mya2 小时前
Spring Cloud服务熔断与降级
后端·spring·spring cloud
星浩AI2 小时前
Claude Code 项目实战:多 Agent 流程编排,从原型到可运行 ChatBot
后端·claude·vibecoding
小蜜蜂dry2 小时前
nestjs实战 - 拦截器,统一处理接口请求与响应结果
前端·后端·nestjs
胖纳特2 小时前
业务系统深度集成:基于OnlyOffice中国版连接器实现合同生成、AI写作与报表自动化
前端·后端