跳到主要内容

MySQL 性能优化

性能优化是数据库管理中最重要的技能之一。本章将详细介绍 MySQL 性能优化的方法和技巧。

性能优化概述

MySQL 性能优化是一个系统工程,涉及多个层面:

┌─────────────────────────────────────────────────────────────┐
│ 性能优化层次 │
├─────────────────────────────────────────────────────────────┤
│ 1. SQL 优化 → 查询语句优化、索引使用 │
│ 2. 表结构优化 → 数据类型、范式设计、反范式 │
│ 3. 参数优化 → Buffer Pool、连接数、日志配置 │
│ 4. 硬件优化 → 内存、磁盘、CPU │
│ 5. 架构优化 → 读写分离、分库分表、缓存 │
└─────────────────────────────────────────────────────────────┘

EXPLAIN 执行计划分析

EXPLAIN 是 MySQL 提供的查询分析工具,用于查看 MySQL 如何执行 SQL 语句。

基本使用

-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE name = '张三';

-- MySQL 8.0.18+ 支持更详细的分析
EXPLAIN ANALYZE SELECT * FROM users WHERE name = '张三';

-- 以 JSON 格式输出(更详细)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE name = '张三';

-- 以树形结构输出
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE name = '张三';

EXPLAIN 输出列详解

执行 EXPLAIN 后会返回以下列:

列名说明
id查询标识符,相同 id 表示同一查询
select_type查询类型
table访问的表
partitions匹配的分区
type访问类型(重要)
possible_keys可能使用的索引
key实际使用的索引
key_len使用的索引长度
ref索引比较的列
rows预估扫描行数
filtered条件过滤的百分比
Extra额外信息

type 访问类型(从优到差)

type 列表示 MySQL 如何查找数据,是判断查询效率的关键指标:

type 值说明性能
NULL不需要访问表,在优化阶段就完成最佳
system表只有一行(系统表)极好
const主键或唯一索引的常量查询极好
eq_ref主键或唯一索引的关联查询很好
ref非唯一索引的等值查询
fulltext全文索引查询
ref_or_null类似 ref,额外查找 NULL一般
index_merge索引合并优化一般
range索引范围扫描一般
index全索引扫描较差
ALL全表扫描最差

各类型示例

-- const:主键查询
EXPLAIN SELECT * FROM users WHERE id = 1;
-- type = const

-- eq_ref:JOIN 时使用主键
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- type = eq_ref(对于 users 表)

-- ref:非唯一索引等值查询
CREATE INDEX idx_name ON users(name);
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- type = ref

-- range:范围查询
EXPLAIN SELECT * FROM users WHERE id BETWEEN 1 AND 100;
-- type = range

-- index:全索引扫描
EXPLAIN SELECT id FROM users;
-- type = index(覆盖索引)

-- ALL:全表扫描(需要优化)
EXPLAIN SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- type = ALL(函数导致索引失效)

Extra 额外信息

Extra 列提供额外的执行信息:

Extra 值说明建议
Using index使用覆盖索引,不回表
Using where服务器层过滤正常
Using temporary使用临时表需关注
Using filesort文件排序需优化
Using index condition索引下推优化
Using join buffer使用连接缓冲区需关注
Impossible WHEREWHERE 条件不可能为真检查逻辑

Extra 信息示例

-- Using index:覆盖索引
CREATE INDEX idx_name_email ON users(name, email);
EXPLAIN SELECT name, email FROM users WHERE name = '张三';
-- Extra: Using index

-- Using filesort:需要额外排序
EXPLAIN SELECT * FROM users ORDER BY created_at;
-- Extra: Using filesort

-- Using temporary:使用临时表
EXPLAIN SELECT DISTINCT status FROM orders;
-- Extra: Using temporary

-- Using index condition:索引下推
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age > 20;
-- Extra: Using index condition

EXPLAIN ANALYZE(MySQL 8.0.18+)

EXPLAIN ANALYZE 会实际执行查询并显示真实的时间开销:

EXPLAIN ANALYZE SELECT * FROM users WHERE name = '张三';

-- 输出示例:
-> Index lookup on users using idx_name (name='张三')
(cost=0.35 rows=1) (actual time=0.015..0.016 rows=1 loops=1)

-- 解读:
-- cost=0.35:预估成本
-- rows=1:预估行数
-- actual time=0.015..0.016:实际执行时间(毫秒)
-- rows=1:实际返回行数
-- loops=1:循环次数

注意EXPLAIN ANALYZE 会真正执行查询,对于大数据量修改操作要谨慎使用。

索引优化

索引是提升查询性能最有效的方法之一。

索引类型

MySQL 支持多种索引类型:

-- 主键索引(聚簇索引)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);

-- 唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);

-- 普通索引
CREATE INDEX idx_name ON users(name);

-- 复合索引
CREATE INDEX idx_name_age ON users(name, age);

-- 全文索引
CREATE FULLTEXT INDEX idx_content ON articles(content);

-- 前缀索引
CREATE INDEX idx_name_prefix ON users(name(10));

索引设计原则

1. 选择合适的列

-- 好的索引列选择:
-- 1. WHERE 条件中的列
-- 2. JOIN 关联的列
-- 3. ORDER BY 排序的列
-- 4. 区分度高的列

-- 查看列的区分度
SELECT
COUNT(DISTINCT name) / COUNT(*) AS name_selectivity,
COUNT(DISTINCT status) / COUNT(*) AS status_selectivity
FROM users;
-- selectivity 越接近 1,区分度越高,越适合建索引

2. 遵循最左前缀原则

-- 复合索引
CREATE INDEX idx_name_age_city ON users(name, age, city);

-- 能使用索引的查询
WHERE name = '张三'
WHERE name = '张三' AND age = 25
WHERE name = '张三' AND age = 25 AND city = '北京'

-- 不能使用索引的查询
WHERE age = 25 -- 跳过了 name
WHERE city = '北京' -- 跳过了 name 和 age
WHERE name = '张三' AND city = '北京' -- 跳过了 age,只能用到 name

3. 覆盖索引优化

-- 普通查询需要回表
SELECT * FROM users WHERE name = '张三';

-- 覆盖索引避免回表
CREATE INDEX idx_name_age ON users(name, age);
SELECT name, age FROM users WHERE name = '张三';
-- Extra: Using index(无需回表)

-- 尽量只查询需要的列
-- 不好的写法
SELECT * FROM users WHERE name = '张三';

-- 好的写法
SELECT id, name, email FROM users WHERE name = '张三';

索引失效场景

以下情况会导致索引失效:

-- 1. 在索引列上使用函数
-- 索引失效
WHERE YEAR(created_at) = 2024
-- 优化
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'

-- 2. 隐式类型转换
-- 假设 phone 是 VARCHAR 类型
-- 索引失效
WHERE phone = 13800138000
-- 优化
WHERE phone = '13800138000'

-- 3. LIKE 以通配符开头
-- 索引失效
WHERE name LIKE '%张%'
-- 能用索引
WHERE name LIKE '张%'

-- 4. 使用 OR(部分情况)
-- 如果 OR 前后列不同,可能失效
WHERE name = '张三' OR age = 25
-- 优化为 UNION
WHERE name = '张三'
UNION
WHERE age = 25

-- 5. 使用 NOT IN、NOT EXISTS、<>
-- 可能导致索引失效
WHERE status NOT IN (1, 2)
-- 优化
WHERE status IN (0, 3, 4, 5)

-- 6. 对索引列进行计算
-- 索引失效
WHERE age + 1 = 26
-- 优化
WHERE age = 25

-- 7. IS NULL 和 IS NOT NULL
-- 可能导致索引失效(取决于数据分布)
WHERE name IS NOT NULL

索引维护

-- 查看索引使用情况
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
COUNT_READ,
COUNT_FETCH
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'your_database';

-- 查看未使用的索引
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE INDEX_NAME IS NOT NULL
AND COUNT_READ = 0
AND COUNT_FETCH = 0;

-- 分析表(更新统计信息)
ANALYZE TABLE users;

-- 优化表(重建表,释放空间)
OPTIMIZE TABLE users;

-- 检查索引基数
SHOW INDEX FROM users;

慢查询日志

慢查询日志是定位性能问题的重要工具。

启用慢查询日志

-- 查看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 动态启用(无需重启)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒的查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 记录未使用索引的查询
SET GLOBAL log_queries_not_using_indexes = 'ON';

配置文件方式

[mysqld]
# 启用慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log

# 慢查询阈值(秒,支持小数)
long_query_time = 1

# 记录未使用索引的查询
log_queries_not_using_indexes = 1

# 限制每分钟记录的索引查询数量
log_throttle_queries_not_using_indexes = 10

# 记录慢管理语句
log_slow_admin_statements = 1

# 输出目标(FILE、TABLE 或 FILE,TABLE)
log_output = 'FILE'

分析慢查询日志

# 使用 mysqldumpslow 汇总分析
# 按平均查询时间排序,显示前 10 条
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log

# 按总查询时间排序
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 按查询次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

# 按平均扫描行数排序
mysqldumpslow -s ar -t 10 /var/log/mysql/slow.log

mysqldumpslow 输出示例

Count: 1523  Time=2.45s (3735s)  Lock=0.00s  Rows=1.0 (1523), webapp[webapp]@localhost
SELECT * FROM orders WHERE customer_id = N AND status = 'S'
  • Count: 查询执行次数
  • Time: 平均时间(总时间)
  • Lock: 锁等待时间
  • Rows: 返回行数

查询慢日志表

如果 log_output = 'TABLE',可以查询表:

-- 查询最近的慢查询
SELECT
start_time,
user_host,
query_time,
lock_time,
rows_sent,
rows_examined,
sql_text
FROM mysql.slow_log
ORDER BY start_time DESC
LIMIT 10;

-- 查询扫描行数过多的查询
SELECT
sql_text,
rows_examined,
rows_sent,
rows_examined / rows_sent AS ratio
FROM mysql.slow_log
WHERE rows_sent > 0
ORDER BY ratio DESC
LIMIT 10;

使用 pt-query-digest

Percona Toolkit 提供更强大的分析工具:

# 安装 Percona Toolkit
sudo apt-get install percona-toolkit

# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log > analysis.txt

# 只分析最近 1 小时的查询
pt-query-digest --since '1h' /var/log/mysql/slow.log

# 分析特定数据库
pt-query-digest --filter '$event->{db} =~ /mydb/' /var/log/mysql/slow.log

查询优化技巧

1. 只查询需要的列

-- 不好:SELECT *
SELECT * FROM users WHERE id = 1;

-- 好:只查询需要的列
SELECT id, name, email FROM users WHERE id = 1;

2. 使用 LIMIT 限制结果

-- 限制返回行数
SELECT * FROM orders WHERE status = 'pending' LIMIT 100;

-- 分页查询
SELECT * FROM orders ORDER BY id LIMIT 0, 20;
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 0;

3. 优化分页查询

-- 传统分页(偏移量大时性能差)
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

-- 优化方案 1:使用 WHERE 子查询
SELECT * FROM orders
WHERE id > 1000000
ORDER BY id LIMIT 20;

-- 优化方案 2:使用 INNER JOIN
SELECT o.* FROM orders o
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t
ON o.id = t.id;

4. 避免 SELECT DISTINCT

-- DISTINCT 需要排序和去重
SELECT DISTINCT name FROM users;

-- 使用 GROUP BY 可能更高效
SELECT name FROM users GROUP BY name;

-- 或确保索引覆盖
CREATE INDEX idx_name ON users(name);

5. 使用 UNION ALL 代替 UNION

-- UNION 会去重,需要临时表
SELECT name FROM users WHERE age > 20
UNION
SELECT name FROM admins WHERE age > 20;

-- UNION ALL 不去重,性能更好
SELECT name FROM users WHERE age > 20
UNION ALL
SELECT name FROM admins WHERE age > 20;

6. 批量插入优化

-- 单条插入(效率低)
INSERT INTO users (name, email) VALUES ('张三', '[email protected]');
INSERT INTO users (name, email) VALUES ('李四', '[email protected]');

-- 批量插入(效率高)
INSERT INTO users (name, email) VALUES
('张三', '[email protected]'),
('李四', '[email protected]'),
('王五', '[email protected]');

-- 使用 LOAD DATA INFILE(最快)
LOAD DATA INFILE '/tmp/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(name, email);

7. 使用 JOIN 代替子查询

-- 子查询(可能效率低)
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');

-- JOIN(通常更高效)
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';

8. 避免大事务

-- 大事务会长时间持有锁
BEGIN;
-- 大量操作...
UPDATE orders SET status = 'completed' WHERE status = 'pending';
-- 更多操作...
COMMIT;

-- 分批处理
-- 每次处理 1000 条
UPDATE orders SET status = 'completed'
WHERE status = 'pending'
LIMIT 1000;

服务器参数优化

Buffer Pool 配置

[mysqld]
# InnoDB Buffer Pool 大小(建议物理内存的 70-80%)
innodb_buffer_pool_size = 4G

# Buffer Pool 实例数(每个实例至少 1GB)
innodb_buffer_pool_instances = 4

# 预读优化
innodb_read_ahead_threshold = 56
-- 查看 Buffer Pool 状态
SHOW STATUS LIKE 'Innodb_buffer_pool%';

-- 计算 Buffer Pool 命中率
SELECT
(1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)) * 100
AS hit_rate
FROM (
SELECT
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') AS Innodb_buffer_pool_reads,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') AS Innodb_buffer_pool_read_requests
) t;
-- 命中率应该在 99% 以上

连接参数优化

[mysqld]
# 最大连接数
max_connections = 500

# 等待超时(秒)
wait_timeout = 28800
interactive_timeout = 28800

# 连接缓冲区
join_buffer_size = 4M
sort_buffer_size = 4M
read_buffer_size = 2M
read_rnd_buffer_size = 2M

日志配置

[mysqld]
# 慢查询日志
slow_query_log = 1
long_query_time = 1

# 二进制日志
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7

# 错误日志
log_error = /var/log/mysql/error.log

# InnoDB 日志
innodb_log_file_size = 256M
innodb_log_buffer_size = 16M
innodb_flush_log_at_trx_commit = 1

性能监控

使用 Performance Schema

-- 启用 Performance Schema
-- 在 my.cnf 中确保:
-- performance_schema = ON

-- 查看等待事件
SELECT
EVENT_NAME,
COUNT_STAR,
SUM_TIMER_WAIT / 1000000000 AS total_seconds
FROM performance_schema.events_waits_summary_global_by_event_name
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

-- 查看语句执行统计
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_TIMER_WAIT / 1000000000 AS total_seconds,
AVG_TIMER_WAIT / 1000000 AS avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

使用 sys 数据库

MySQL 8.0 提供了 sys 数据库,简化性能分析:

-- 查看最慢的 SQL
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile;

-- 查看全表扫描的 SQL
SELECT * FROM sys.statements_with_full_table_scans;

-- 查看索引使用情况
SELECT * FROM sys.schema_index_statistics;

-- 查看表访问统计
SELECT * FROM sys.schema_table_statistics;

-- 查看内存使用
SELECT * FROM sys.memory_global_by_current_bytes;

小结

本章我们学习了:

  1. EXPLAIN 执行计划:分析查询的执行方式
  2. 索引优化:设计原则、索引失效场景
  3. 慢查询日志:定位和分析慢查询
  4. 查询优化技巧:实用的 SQL 优化方法
  5. 服务器参数优化:Buffer Pool、连接、日志配置
  6. 性能监控:Performance Schema 和 sys 数据库

练习

  1. 使用 EXPLAIN 分析一个复杂查询的执行计划
  2. 为一个慢查询设计合适的索引
  3. 配置慢查询日志并分析结果
  4. 计算当前数据库的 Buffer Pool 命中率

参考资源