写在前面
大家好,欢迎来到MySQL全面教学系列的第13天。经过了前面12天的学习,我们已经掌握了MySQL的方方面面。今天,我们要进入一个实战性极强 的话题------性能优化。
性能优化是MySQL面试的重中之重,也是实际工作中最常遇到的挑战。很多开发者面对慢查询时手足无措,不知道从何下手。今天,我将带你掌握一套完整的性能优化方法论,从发现问题到解决问题,让你在面对性能问题时胸有成竹。
让我们一起开启性能优化之旅!

目录
-
- 写在前面
- 一、性能优化方法论
-
- [1.1 性能优化的完整流程](#1.1 性能优化的完整流程)
- [1.2 优化的层次](#1.2 优化的层次)
- [1.3 优化原则](#1.3 优化原则)
- 二、慢查询日志
-
- [2.1 开启慢查询日志](#2.1 开启慢查询日志)
- [2.2 慢查询日志格式](#2.2 慢查询日志格式)
- [2.3 使用pt-query-digest分析](#2.3 使用pt-query-digest分析)
- [2.4 使用mysqldumpslow(内置工具)](#2.4 使用mysqldumpslow(内置工具))
- 三、SQL优化技巧
-
- [3.1 避免SELECT *](#3.1 避免SELECT *)
- [3.2 小表驱动大表](#3.2 小表驱动大表)
- [3.3 批量操作代替单条](#3.3 批量操作代替单条)
- [3.4 避免在WHERE中对字段进行运算](#3.4 避免在WHERE中对字段进行运算)
- [3.5 避免隐式类型转换](#3.5 避免隐式类型转换)
- [3.6 用UNION ALL代替UNION](#3.6 用UNION ALL代替UNION)
- [3.7 优化LIMIT分页](#3.7 优化LIMIT分页)
- 四、索引优化
-
- [4.1 覆盖索引](#4.1 覆盖索引)
- [4.2 最左前缀原则](#4.2 最左前缀原则)
- [4.3 索引下推(ICP)](#4.3 索引下推(ICP))
- [4.4 索引失效场景](#4.4 索引失效场景)
- [4.5 索引优化实战](#4.5 索引优化实战)
- 五、表结构优化
-
- [5.1 字段类型选择](#5.1 字段类型选择)
- [5.2 垂直拆分](#5.2 垂直拆分)
- [5.3 水平拆分](#5.3 水平拆分)
- [5.4 分区表](#5.4 分区表)
- 六、连接池优化
-
- [6.1 为什么需要连接池](#6.1 为什么需要连接池)
- [6.2 HikariCP参数配置](#6.2 HikariCP参数配置)
- [6.3 连接池大小设置](#6.3 连接池大小设置)
- 七、实战:一个慢查询的优化过程
-
- [7.1 问题发现](#7.1 问题发现)
- [7.2 问题分析](#7.2 问题分析)
- [7.3 优化过程](#7.3 优化过程)
- [7.4 最终优化方案](#7.4 最终优化方案)
- [7.5 优化效果](#7.5 优化效果)
- 八、踩坑提醒
-
- [8.1 过早优化是万恶之源](#8.1 过早优化是万恶之源)
- [8.2 先定位瓶颈再优化](#8.2 先定位瓶颈再优化)
- [8.3 索引不是越多越好](#8.3 索引不是越多越好)
- [8.4 EXPLAIN的坑](#8.4 EXPLAIN的坑)
- 九、面试高频考点
-
- [9.1 如何定位慢SQL?](#9.1 如何定位慢SQL?)
- [9.2 大表分页怎么优化?](#9.2 大表分页怎么优化?)
- [9.3 如何分析SQL执行计划?](#9.3 如何分析SQL执行计划?)
- [9.4 什么情况下索引会失效?](#9.4 什么情况下索引会失效?)
- [9.5 如何优化COUNT(*)查询?](#9.5 如何优化COUNT(*)查询?)
- [9.6 连接池大小怎么设置?](#9.6 连接池大小怎么设置?)
- 十、总结
- 十一、下一步预告
- 参考资料
- 互动话题
一、性能优化方法论
1.1 性能优化的完整流程
性能优化不是盲目地修改代码,而是要有科学的方法论:
┌─────────────────────────────────────────────────────────────┐
│ 性能优化流程图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 发现问题 ──→ 2. 定位瓶颈 ──→ 3. 分析原因 ──→ 4. 优化实施 │
│ ↑ │ │
│ └──────────────── 5. 效果验证 ←───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 优化的层次
| 优化层次 | 优化内容 | 效果 | 难度 |
|---|---|---|---|
| SQL层 | SQL语句优化、索引优化 | 显著 | 低 |
| 表结构层 | 字段类型、表拆分 | 明显 | 中 |
| 架构层 | 读写分离、分库分表 | 显著 | 高 |
| 系统层 | 服务器配置、OS参数 | 中等 | 高 |
1.3 优化原则
sql
-- 经验之谈:性能优化的黄金法则
-- 1. 先定位,再优化
-- 不要凭感觉优化,要有数据支撑
-- 2. 先优化SQL,再优化架构
-- 80%的性能问题可以通过SQL优化解决
-- 3. 小步快跑,持续验证
-- 每次只改一处,验证效果后再继续
-- 4. 监控先行
-- 没有监控的优化是盲目的
-- 5. 预防胜于治疗
-- 在开发阶段就考虑性能
二、慢查询日志
2.1 开启慢查询日志
慢查询日志是发现性能问题的第一道防线。
sql
-- 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志(临时)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录
-- 永久配置(my.cnf)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1 -- 记录未使用索引的查询
2.2 慢查询日志格式
# Time: 2024-01-15T10:30:45.123456Z
# User@Host: root[root] @ localhost [] Id: 15
# Query_time: 2.345678 Lock_time: 0.000123 Rows_sent: 10 Rows_examined: 1000000
SET timestamp=1705314645;
SELECT * FROM orders WHERE create_time > '2024-01-01';
字段说明:
| 字段 | 说明 |
|---|---|
| Time | 查询执行时间 |
| User@Host | 执行用户和主机 |
| Query_time | 查询总耗时(秒) |
| Lock_time | 等待锁的时间 |
| Rows_sent | 返回的行数 |
| Rows_examined | 扫描的行数 |
2.3 使用pt-query-digest分析
pt-query-digest是Percona Toolkit中的慢查询分析工具,功能强大。
bash
# 安装Percona Toolkit
yum install percona-toolkit
# 分析慢查询日志
pt-query-digest /var/lib/mysql/slow.log
# 输出到文件
pt-query-digest /var/lib/mysql/slow.log > slow_query_report.txt
# 分析最近1小时的慢查询
pt-query-digest --since 1h /var/lib/mysql/slow.log
# 只显示最慢的10条
pt-query-digest --limit 10 /var/lib/mysql/slow.log
pt-query-digest输出示例:
# 总体统计
# 110.5s user time, 2.3s system time, 25.56M rss, 78.34M vsz
# Current date: Mon Jan 15 10:35:00 2024
# Hostname: mysql-server
# Files: /var/lib/mysql/slow.log
# Overall: 1.23k total, 45 unique, 2.45 QPS, 0.35x concurrency
# 查询排名
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ============== ===== ====== ===== ====
# 1 0x3A... 125.3450 45.2% 234 0.5356 0.12 SELECT orders
# 2 0x8B... 78.2340 28.3% 156 0.5015 0.08 SELECT users
2.4 使用mysqldumpslow(内置工具)
bash
# 查看最慢的10条SQL
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log
# 查看访问次数最多的10条SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log
# 参数说明
# -s: 排序方式 (t:时间, c:次数, l:锁定时间, r:返回记录数)
# -t: 显示前N条
# -g: 正则匹配
三、SQL优化技巧
3.1 避免SELECT *
sql
-- 不推荐:查询所有列
SELECT * FROM employees WHERE department_id = 10;
-- 推荐:只查询需要的列
SELECT employee_id, first_name, last_name, email
FROM employees
WHERE department_id = 10;
-- 原因:
-- 1. 减少网络传输
-- 2. 减少内存使用
-- 3. 可能使用覆盖索引
3.2 小表驱动大表
sql
-- 不推荐:大表驱动小表
SELECT * FROM huge_table h
INNER JOIN small_table s ON h.id = s.huge_id
WHERE s.status = 'ACTIVE';
-- 推荐:小表驱动大表(使用STRAIGHT_JOIN强制顺序)
SELECT * FROM small_table s
STRAIGHT_JOIN huge_table h ON h.id = s.huge_id
WHERE s.status = 'ACTIVE';
-- 或者确保优化器选择正确的驱动表
SELECT * FROM small_table s
INNER JOIN huge_table h ON h.id = s.huge_id
WHERE s.status = 'ACTIVE';
3.3 批量操作代替单条
sql
-- 不推荐:循环插入
INSERT INTO logs (message) VALUES ('log1');
INSERT INTO logs (message) VALUES ('log2');
INSERT INTO logs (message) VALUES ('log3');
-- ... 1000次
-- 推荐:批量插入
INSERT INTO logs (message) VALUES
('log1'), ('log2'), ('log3'), ...;
-- 推荐:使用LOAD DATA
LOAD DATA INFILE '/tmp/data.csv'
INTO TABLE logs
FIELDS TERMINATED BY ',';
-- 批量大小建议:1000-10000条/批次
3.4 避免在WHERE中对字段进行运算
sql
-- 不推荐:对字段进行运算,无法使用索引
SELECT * FROM employees WHERE YEAR(hire_date) = 2023;
SELECT * FROM orders WHERE amount * 0.9 > 1000;
-- 推荐:将运算移到右侧
SELECT * FROM employees
WHERE hire_date >= '2023-01-01' AND hire_date < '2024-01-01';
SELECT * FROM orders WHERE amount > 1000 / 0.9;
3.5 避免隐式类型转换
sql
-- 表结构:phone VARCHAR(20),有索引
-- 不推荐:隐式类型转换,索引失效
SELECT * FROM users WHERE phone = 13800138000;
-- 推荐:类型一致
SELECT * FROM users WHERE phone = '13800138000';
3.6 用UNION ALL代替UNION
sql
-- 不推荐:UNION会去重,消耗性能
SELECT name FROM table_a
UNION
SELECT name FROM table_b;
-- 推荐:UNION ALL不去重,性能更好
SELECT name FROM table_a
UNION ALL
SELECT name FROM table_b;
-- 如果确定没有重复数据,一定用UNION ALL
3.7 优化LIMIT分页
sql
-- 不推荐:深分页性能差
SELECT * FROM orders
ORDER BY create_time DESC
LIMIT 1000000, 10;
-- 推荐1:使用覆盖索引+子查询
SELECT * FROM orders o
INNER JOIN (
SELECT order_id FROM orders
ORDER BY create_time DESC
LIMIT 1000000, 10
) tmp ON o.order_id = tmp.order_id;
-- 推荐2:使用游标/书签(上一页的最大ID)
SELECT * FROM orders
WHERE create_time < '2024-01-15 10:00:00'
ORDER BY create_time DESC
LIMIT 10;
-- 推荐3:使用冗余字段优化
-- 添加page_num字段,按页查询
四、索引优化
4.1 覆盖索引
覆盖索引:查询的所有列都在索引中,无需回表查询。
sql
-- 创建复合索引
CREATE INDEX idx_employee_dept_name ON employees(department_id, first_name, last_name);
-- 覆盖索引查询(Extra: Using index)
SELECT first_name, last_name
FROM employees
WHERE department_id = 10;
-- 非覆盖索引查询(需要回表)
SELECT first_name, last_name, email -- email不在索引中
FROM employees
WHERE department_id = 10;
4.2 最左前缀原则
sql
-- 创建复合索引 (a, b, c)
CREATE INDEX idx_abc ON table_name(a, b, c);
-- 可以使用索引的情况
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND c = 3 -- a使用索引,c不使用
WHERE a = 1 AND b > 2 AND c = 3 -- a,b使用索引,c不使用
-- 不能使用索引的情况
WHERE b = 2
WHERE b = 2 AND c = 3
WHERE c = 3
4.3 索引下推(ICP)
MySQL 5.6+引入的优化,将WHERE条件下推到存储引擎层过滤。
sql
-- 索引:(name, age)
SELECT * FROM users WHERE name LIKE '张%' AND age = 20;
-- 无ICP:先根据name找到所有记录,再回表过滤age
-- 有ICP:在存储引擎层就用age过滤,减少回表次数
查看ICP是否开启:
sql
SHOW VARIABLES LIKE 'optimizer_switch';
-- 确保index_condition_pushdown=on
4.4 索引失效场景
sql
-- 1. 使用函数或表达式
WHERE YEAR(create_time) = 2023 -- 失效
WHERE create_time >= '2023-01-01' -- 有效
-- 2. 隐式类型转换
WHERE phone = 13800138000 -- 失效(字符串字段用数字查)
WHERE phone = '13800138000' -- 有效
-- 3. LIKE以通配符开头
WHERE name LIKE '%张%' -- 失效
WHERE name LIKE '张%' -- 有效
-- 4. OR条件使用不当
WHERE id = 1 OR age = 20 -- 可能失效(除非都有索引)
-- 5. 不等于、NOT IN
WHERE status != 1 -- 可能失效
WHERE status NOT IN (1, 2) -- 可能失效
-- 6. IS NULL / IS NOT NULL
WHERE name IS NULL -- 可能失效(取决于数据分布)
-- 7. 范围查询后失效
-- 索引(a, b, c)
WHERE a = 1 AND b > 2 AND c = 3 -- c不使用索引
4.5 索引优化实战
sql
-- 场景:优化订单查询
-- 原表:orders(order_id, user_id, status, create_time, amount)
-- 需求1:根据用户查询订单
CREATE INDEX idx_user_id ON orders(user_id);
-- 需求2:根据状态和创建时间查询
CREATE INDEX idx_status_time ON orders(status, create_time);
-- 需求3:根据用户和状态查询(覆盖索引)
CREATE INDEX idx_user_status ON orders(user_id, status, amount, create_time);
-- 查询示例
SELECT amount, create_time
FROM orders
WHERE user_id = 100 AND status = 1;
-- 使用覆盖索引,无需回表
五、表结构优化
5.1 字段类型选择
| 数据类型 | 推荐用法 | 不推荐 |
|---|---|---|
| INT | 整数ID、状态码 | 不要用INT存手机号 |
| BIGINT | 大整数、时间戳 | 小数据量不要用 |
| VARCHAR | 变长字符串 | 长度固定用CHAR |
| CHAR | 固定长度(MD5、手机号) | 变长数据不要用 |
| DECIMAL | 精确小数(金额) | 不要用FLOAT/DOUBLE |
| DATETIME | 日期时间 | 时间戳用INT/BIGINT |
| TIMESTAMP | 记录时间(自动更新) | 范围有限(1970-2038) |
| JSON | 灵活结构数据 | 不要存大量JSON |
sql
-- 经验之谈:字段类型选择原则
-- 1. 能用INT不用BIGINT(节省空间)
-- 2. 金额用DECIMAL(19,4),不要用FLOAT
-- 3. 状态用TINYINT,不要用VARCHAR
-- 4. 定长用CHAR,变长用VARCHAR
-- 5. 大文本用TEXT,不要存在主表
5.2 垂直拆分
将宽表拆分成多个窄表,减少IO。
sql
-- 原表:users(user_id, name, email, phone, address, profile, settings, ...)
-- 拆分后:
-- 主表:频繁访问的核心字段
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20),
status TINYINT DEFAULT 1,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 扩展表:不常访问的大字段
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY,
address TEXT,
bio TEXT,
avatar_url VARCHAR(500),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- 设置表:用户配置
CREATE TABLE user_settings (
user_id BIGINT PRIMARY KEY,
notification_enabled TINYINT DEFAULT 1,
theme VARCHAR(20) DEFAULT 'light',
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
5.3 水平拆分
将大表按某种规则拆分成多个小表。
sql
-- 按用户ID取模拆分
-- users_0, users_1, users_2, users_3
-- 获取表名
SET @table_index = user_id % 4;
SET @sql = CONCAT('SELECT * FROM users_', @table_index, ' WHERE user_id = ?');
PREPARE stmt FROM @sql;
EXECUTE stmt USING @user_id;
DEALLOCATE PREPARE stmt;
-- 或者使用分库分表中间件:ShardingSphere、MyCat
5.4 分区表
MySQL分区表将大表在物理上分成多个小文件。
sql
-- 按范围分区
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
create_time DATETIME NOT NULL
) PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION pfuture VALUES LESS THAN MAXVALUE
);
-- 按HASH分区
CREATE TABLE logs (
log_id BIGINT PRIMARY KEY,
message TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) PARTITION BY HASH(log_id) PARTITIONS 4;
-- 查询只访问特定分区(分区裁剪)
SELECT * FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01';
-- 只扫描p2023分区
六、连接池优化
6.1 为什么需要连接池
数据库连接是昂贵的资源,频繁创建销毁会严重影响性能。连接池可以复用连接,减少开销。
6.2 HikariCP参数配置
HikariCP是Java生态中性能最好的连接池。
properties
# HikariCP配置示例
# 基础配置
spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/mydb
spring.datasource.hikari.username=root
spring.datasource.hikari.password=password
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
# 连接池大小(黄金公式:connections = ((core_count * 2) + effective_spindle_count))
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=10
# 连接超时
spring.datasource.hikari.connection-timeout=30000
# 空闲连接超时
spring.datasource.hikari.idle-timeout=600000
# 连接最大生命周期
spring.datasource.hikari.max-lifetime=1800000
# 连接测试
spring.datasource.hikari.connection-test-query=SELECT 1
6.3 连接池大小设置
连接池大小计算公式(来自PostgreSQL建议,同样适用于MySQL):
connections = ((core_count * 2) + effective_spindle_count)
示例:
- 4核CPU,单块SSD
- connections = (4 * 2) + 1 = 9
实际建议:
- 小型应用:10-20
- 中型应用:20-50
- 大型应用:50-100(配合读写分离)
注意:连接数不是越大越好!
七、实战:一个慢查询的优化过程
7.1 问题发现
sql
-- 业务反馈:订单查询页面加载很慢,需要10秒以上
-- 慢查询日志发现:
SELECT
o.order_id,
o.user_id,
o.amount,
o.status,
o.create_time,
u.username,
u.email,
p.product_name,
p.price
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
WHERE o.create_time > '2023-01-01'
ORDER BY o.create_time DESC
LIMIT 10000, 10;
-- Query_time: 10.234567
-- Rows_examined: 5000000
7.2 问题分析
使用EXPLAIN分析执行计划:
sql
EXPLAIN SELECT ...;
-- 输出:
-- id | select_type | table | type | possible_keys | key | rows | Extra
-- 1 | SIMPLE | o | ALL | NULL | NULL | 5000000 | Using where; Using filesort
-- 1 | SIMPLE | u | eq_ref| PRIMARY | PRI | 1 | NULL
-- 1 | SIMPLE | oi | ref | order_id | ord | 3 | NULL
-- 1 | SIMPLE | p | eq_ref| PRIMARY | PRI | 1 | NULL
问题诊断:
- orders表全表扫描(type=ALL)
- 没有使用索引
- 使用filesort排序
- 深分页LIMIT 10000, 10
7.3 优化过程
第一步:添加索引
sql
-- 为create_time添加索引
CREATE INDEX idx_create_time ON orders(create_time);
-- 效果:type从ALL变为range,但仍有filesort
第二步:优化分页
sql
-- 使用覆盖索引+子查询
SELECT
o.order_id,
o.user_id,
o.amount,
o.status,
o.create_time,
u.username,
u.email
FROM (
SELECT order_id, user_id, amount, status, create_time
FROM orders
WHERE create_time > '2023-01-01'
ORDER BY create_time DESC
LIMIT 10000, 10
) o
LEFT JOIN users u ON o.user_id = u.user_id;
-- 订单详情单独查询(避免JOIN过多)
第三步:使用书签分页
sql
-- 前端记录上一页最后一条的create_time
-- 第一次查询
SELECT order_id, create_time
FROM orders
WHERE create_time > '2023-01-01'
ORDER BY create_time DESC
LIMIT 10;
-- 返回最后一条create_time = '2023-12-01 10:00:00'
-- 下一页查询
SELECT order_id, create_time
FROM orders
WHERE create_time < '2023-12-01 10:00:00' -- 使用书签
ORDER BY create_time DESC
LIMIT 10;
7.4 最终优化方案
sql
-- 1. 创建复合索引
CREATE INDEX idx_ct_uid ON orders(create_time, user_id);
-- 2. 优化后的查询
SELECT
o.order_id,
o.user_id,
o.amount,
o.status,
o.create_time,
u.username,
u.email
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE o.create_time > '2023-01-01'
AND o.create_time < '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
-- 3. 分页优化(使用书签)
-- 前端维护create_time游标
7.5 优化效果
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 查询时间 | 10.2秒 | 10毫秒 | 1000倍 |
| 扫描行数 | 500万 | 100 | 50000倍 |
| 使用索引 | 否 | 是 | - |
| 回表次数 | 500万 | 100 | - |
八、踩坑提醒
8.1 过早优化是万恶之源
sql
-- 经验之谈:不要过度优化
-- 1. 先让系统跑起来,再考虑优化
-- 2. 优化要有数据支撑,不要凭感觉
-- 3. 80/20法则:优化最关键的20%代码
-- 反例:为了"优化"而写的复杂SQL
SELECT * FROM (
SELECT * FROM (
SELECT ... FROM ...
) t1
) t2;
-- 可能还不如简单查询快
8.2 先定位瓶颈再优化
sql
-- 使用SHOW PROFILE定位瓶颈
SET profiling = 1;
-- 执行查询
SELECT * FROM large_table WHERE ...;
-- 查看分析结果
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
-- 输出:
-- Starting: 0.000010
-- Opening tables: 0.000005
-- System lock: 0.000003
-- Table lock: 0.000004
-- init: 0.000015
-- optimizing: 0.000008
-- statistics: 0.000050 <-- 这里耗时多,可能是统计信息不准
-- preparing: 0.000012
-- executing: 0.000002
-- Sending data: 2.345678 <-- 主要耗时在这里
-- ...
8.3 索引不是越多越好
sql
-- 每个索引都会增加写操作的开销
-- 索引过多会导致:
-- 1. INSERT/UPDATE/DELETE变慢
-- 2. 占用更多磁盘空间
-- 3. 优化器选择困难
-- 建议:
-- 1. 单表索引不超过5个
-- 2. 复合索引字段不超过3个
-- 3. 定期清理无用索引
-- 查找无用索引
SELECT
t.TABLE_SCHEMA,
t.TABLE_NAME,
s.INDEX_NAME,
s.CARDINALITY,
t.TABLE_ROWS,
ROUND(s.CARDINALITY / t.TABLE_ROWS, 2) AS selectivity
FROM information_schema.STATISTICS s
JOIN information_schema.TABLES t ON s.TABLE_SCHEMA = t.TABLE_SCHEMA
AND s.TABLE_NAME = t.TABLE_NAME
WHERE t.TABLE_SCHEMA = 'your_db'
AND s.CARDINALITY / t.TABLE_ROWS < 0.1 -- 选择性低于10%
ORDER BY selectivity;
8.4 EXPLAIN的坑
sql
-- EXPLAIN显示的行数是估算值,不是实际扫描行数
-- 要以实际执行为准
-- 使用EXPLAIN ANALYZE(MySQL 8.0.18+)获取实际执行信息
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;
-- 或者使用SHOW STATUS查看实际行数
FLUSH STATUS;
SELECT * FROM orders WHERE user_id = 100;
SHOW STATUS LIKE 'Handler_read%';
九、面试高频考点
9.1 如何定位慢SQL?
答案:
- 慢查询日志:开启slow_query_log,设置long_query_time
- SHOW PROCESSLIST:查看正在执行的慢查询
- performance_schema:使用events_statements_history_long表
- 业务监控:应用层记录慢查询API
- pt-query-digest:分析慢查询日志
9.2 大表分页怎么优化?
答案:
- 覆盖索引+子查询:先查ID,再关联详情
- 书签/游标分页:记录上一页边界值
- 限制分页深度:只允许翻到100页以内
- 使用搜索引擎:Elasticsearch等
- 延迟关联:先分页取ID,再JOIN取详情
9.3 如何分析SQL执行计划?
答案:
使用EXPLAIN查看执行计划,关注以下字段:
- type:访问类型,从好到差:system > const > eq_ref > ref > range > index > ALL
- key:实际使用的索引
- rows:估算扫描行数
- Extra:额外信息,关注Using filesort、Using temporary、Using index
9.4 什么情况下索引会失效?
答案:
- 对索引列使用函数或表达式
- 隐式类型转换
- LIKE以%开头
- OR条件使用不当
- 不等于(!=、<>)和NOT IN
- IS NULL(取决于数据分布)
- 复合索引不满足最左前缀
9.5 如何优化COUNT(*)查询?
答案:
- 使用近似值:SHOW TABLE STATUS
- 使用缓存:Redis缓存计数
- 使用汇总表:定时统计写入
- 使用覆盖索引:COUNT(索引列)
- 限制条件:先过滤再COUNT
9.6 连接池大小怎么设置?
答案:
公式:connections = ((core_count * 2) + effective_spindle_count)
实际建议:
- 4核8G:10-20连接
- 8核16G:20-50连接
- 配合读写分离:每个实例50-100连接
注意:连接数不是越大越好,过多会导致CPU上下文切换开销。
十、总结
今天我们学习了MySQL性能优化的完整方法论:
核心知识点
-
性能优化流程:发现问题 → 定位瓶颈 → 分析原因 → 优化实施 → 效果验证
-
慢查询分析:
- 开启慢查询日志
- 使用pt-query-digest分析
- 关注Query_time、Rows_examined
-
SQL优化技巧:
- 避免SELECT *
- 小表驱动大表
- 批量操作代替单条
- 优化深分页
-
索引优化:
- 覆盖索引
- 最左前缀原则
- 避免索引失效
-
表结构优化:
- 合理选择字段类型
- 垂直拆分
- 水平拆分/分区
优化原则
- 先定位,再优化:用数据说话
- 先SQL,再架构:80%问题在SQL层
- 小步快跑:每次只改一处,持续验证
- 监控先行:没有监控的优化是盲目的
十一、下一步预告
Day14 - MySQL备份与恢复
在下一篇文章中,我们将学习:
- 备份的重要性(删库跑路的故事)
- mysqldump和xtrabackup的使用
- 主从复制架构
- 数据恢复实战
- 制定完善的备份策略
数据安全是DBA的生命线,备份恢复是必备技能,敬请期待!
参考资料
MySQL 8.0 Reference Manual - Optimization
互动话题
- 你在工作中遇到过哪些性能问题?是如何解决的?
- 对于深分页问题,你还有什么好的解决方案?
- 分享一个你优化SQL的成功案例,优化前后性能提升了多少?
欢迎在评论区留言讨论,如果觉得文章有帮助,别忘了点赞收藏哦!我们下节课见!
本文是MySQL全面教学系列第13篇,系列文章持续更新中...