MySQL 8.0性能调优实战:慢查询诊断到索引优化全流程

MySQL性能调优的切入点与诊断流程

MySQL性能问题的排查遵循一个固定路径:先定位瓶颈(CPU、IO还是锁),再分析原因(慢查询、索引缺失还是配置不当),最后实施优化。数据库运维工作中,80%的性能问题来自慢查询和缺失索引,剩余20%来自参数配置和硬件资源瓶颈。

慢查询日志的配置与分析

开启慢查询日志是诊断的第一步:

-- my.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
log_queries_not_using_indexes = 1

-- 动态开启(无需重启)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;

用mysqldumpslow汇总慢查询TOP 10:

mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

输出按总耗时排序,直接看到最耗时的SQL模式。如果需要更细粒度的分析,用pt-query-digest:

pt-query-digest /var/log/mysql/slow.log > slow_report.txt

Percona的pt-query-digest会按查询指纹分组,展示每类SQL的执行次数、总耗时、锁等待时间、返回行数等维度的统计。

EXPLAIN执行计划深度解读

拿到慢SQL后,用EXPLAIN分析执行计划:

EXPLAIN FORMAT=JSON 
SELECT o.order_id, o.amount, u.name 
FROM orders o 
JOIN users u ON o.user_id = u.id 
WHERE o.status = 'pending' AND o.created_at > '2026-07-01';

重点关注以下字段:

type列:表示访问类型,从优到差依次为system > const > eq_ref > ref > range > index > ALL。出现ALL意味着全表扫描,必须加索引优化。

Extra列:Using index表示走了覆盖索引(最优),Using filesort表示额外排序(需优化),Using temporary表示使用了临时表(需优化),Using where表示存储引擎返回数据后在Server层过滤。

rows列:MySQL估算的扫描行数。如果rows远大于实际返回行数,说明索引选择性差,大量行被扫描后又丢弃。

索引优化策略与常见误区

策略一:遵循最左前缀原则

复合索引(a,b,c)可以服务a、(a,b)、(a,b,c)的查询,但不能服务(b,c)的查询。索引列的顺序应按选择性从高到低排列:

-- 选择性计算
SELECT 
  COUNT(DISTINCT status) / COUNT(*) AS status_sel,
  COUNT(DISTINCT user_id) / COUNT(*) AS user_sel,
  COUNT(DISTINCT created_at) / COUNT(*) AS created_sel
FROM orders;

-- 如果 user_sel > status_sel > created_sel
-- 复合索引应为 (user_id, status, created_at)
ALTER TABLE orders ADD INDEX idx_user_status_created(user_id, status, created_at);

策略二:覆盖索引避免回表

当索引列包含查询所需的所有字段时,直接从索引返回数据,不需要回查聚簇索引:

-- 原查询:回表
SELECT id, status, amount FROM orders WHERE user_id = 100;

-- 覆盖索引:不回表
ALTER TABLE orders ADD INDEX idx_user_status_amount(user_id, status, amount);

误区:索引越多越好

每个索引都占用磁盘空间,且INSERT/UPDATE/DELETE时需要维护索引。一张表的索引数量建议控制在5-8个以内,冗余索引应及时清理。识别冗余索引用sys.schema_unused_indexes视图查看从未使用的索引。

InnoDB Buffer Pool调优

Buffer Pool是InnoDB最核心的内存区域,缓存数据和索引页。配置建议:

[mysqld]
innodb_buffer_pool_size = 物理内存的70-80%
innodb_buffer_pool_instances = 8

对于32GB内存的独占数据库服务器,设为24-26GB。多实例配置下,每个instance管理独立的LRU链表,减少并发访问时的锁争用。

SQL查询优化实战案例

一个典型的分页查询优化场景:

-- 原始写法:深分页时扫描大量行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

-- 优化方案一:延迟关联
SELECT o.* FROM orders o 
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t 
ON o.id = t.id;

-- 优化方案二:游标分页(推荐)
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;

延迟关联的原理是子查询只走索引扫描到ID,再回表取20行完整数据。游标分页则完全跳过了OFFSET,但要求前端记录上次查询的最后一个ID。分库分表方案中,跨分片的深分页问题更为复杂,通常需要引入汇总表或搜索引擎(如Elasticsearch)来处理。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql80-xing-neng-diao-you-shi-zhan-man-cha-xun-zhen-duan/

(0)
小编小编
上一篇 2026年8月7日
下一篇 2026年8月7日

相关推荐

MySQL 8.0性能调优实战:慢查询诊断、索引优化与执行计划深度解读

MySQL性能问题的排查起点:慢查询日志

数据库慢了,不要猜,先开慢查询日志。这是性能诊断的事实数据源,靠经验猜测只会浪费时间。MySQL 8.0默认未开启慢查询日志,需要手动配置:

-- 在线开启(重启失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;     -- 超过0.5秒记录
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 没用索引也记录
SET GLOBAL min_examined_row_limit = 100;  -- 扫描少于100行不记录

-- 持久化到配置文件 /etc/my.cnf
[mysqld]
slow_query_log = 1
long_query_time = 0.5
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = 1

mysqldumpslow是慢查询日志的快速分析工具:

# 按查询时间排序,取前10条
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 按锁定时间排序
mysqldumpslow -s l -t 10 /var/log/mysql/slow.log

# 按查询次数排序(找出热点查询)
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

pt-query-digest比mysqldumpslow更强大,能生成详细的查询分析报告:

pt-query-digest /var/log/mysql/slow.log > slow_report.txt

EXPLAIN执行计划:读懂MySQL的查询路径

拿到慢SQL后,第一步不是加索引,是读懂执行计划:

EXPLAIN ANALYZE 
SELECT o.order_id, o.amount, u.user_name
FROM orders o 
JOIN users u ON o.user_id = u.id
WHERE o.created_at > '2024-01-01' 
  AND o.status = 'PAID'
ORDER BY o.amount DESC
LIMIT 20;

EXPLAIN ANALYZE(MySQL 8.0.18+)比传统EXPLAIN多了实际执行时间,是真正的性能数据而非优化器估算。关键字段解读:

type列——访问类型,从好到差排序:

system/const:单行查找,主键或唯一索引等值查询
eq_ref:JOIN时使用主键或唯一索引
ref:非唯一索引等值查询
range:索引范围扫描(BETWEEN, >, <)
index:全索引扫描
ALL:全表扫描,必须优化

Extra列——额外信息,重点关注的几种:

Using index:覆盖索引,不回表,性能最优
Using where:在存储引擎返回数据后过滤,说明有未走索引的条件
Using filesort:额外排序,无法利用索引顺序
Using temporary:使用了临时表,GROUP BY或DISTINCT的常见代价
Using index condition:索引下推(ICP),MySQL 5.6+的优化

rows列——预估扫描行数。filtered列是过滤比例。实际返回行数 = rows × filtered%。如果rows很大但filtered很低,说明扫描了大量无用数据,索引不够精准。

索引优化的四个实战场景

场景1:复合索引的列顺序

最左前缀原则决定了复合索引的列顺序。选择性高的列放前面:

-- 查询条件:WHERE status = 'PAID' AND created_at > '2024-01-01'
-- status选择性低(几种状态),created_at选择性高(每天不同值)

-- 差索引:status在前,只用到status的等值过滤
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);

-- 对比:创建时间在前
-- 但这样的话status条件无法利用索引
-- 正确做法:根据实际查询条件组合决定

-- 用EXPLAIN验证
EXPLAIN SELECT * FROM orders 
WHERE status = 'PAID' AND created_at > '2024-01-01';
-- 如果idx_status_created的key_len只包含status(4字节),
-- 说明created_at没走索引,需要调整或创建新索引

场景2:覆盖索引避免回表

-- 查询只需要order_id和amount
SELECT order_id, amount FROM orders WHERE status = 'PAID';

-- 覆盖索引:所有查询列都在索引中
ALTER TABLE orders ADD INDEX idx_cover_paid (status, order_id, amount);

-- EXPLAIN结果中Extra列应该显示"Using index"
-- 不需要回表查主键索引,IO量大幅减少

场景3:ORDER BY的索引优化

-- 按创建时间倒序分页
SELECT * FROM orders WHERE user_id = 123 
ORDER BY created_at DESC LIMIT 10;

-- 索引要包含排序列
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at DESC);

-- MySQL 8.0支持降序索引,创建时指定DESC
-- 这样ORDER BY可以直接利用索引顺序,无需filesort

场景4:深度分页优化

-- 传统分页:越往后越慢
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;
-- 扫描100010行,丢掉前100000行

-- 方案1:游标分页(推荐)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;
-- 只扫描10行,但要求页码连续

-- 方案2:延迟JOIN
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) tmp
ON o.id = tmp.id;
-- 子查询走覆盖索引,只取10个id再回表
-- 速度提升5-50倍取决于表大小

MySQL 8.0参数调优:InnoDB核心配置

[mysqld]
# 缓冲池大小 - 物理内存的60-70%(专用数据库服务器)
innodb_buffer_pool_size = 12G

# 缓冲池实例数 - 每个实例至少1GB
innodb_buffer_pool_instances = 12

# 日志文件大小 - 影响checkpoint频率
innodb_log_file_size = 2G
innodb_log_buffer_size = 64M

# 刷新策略 - 根据数据安全级别选择
# 1 = 每次事务提交刷盘(最安全,最慢)
# 2 = 每次提交写日志,每秒刷盘(折中)
# 0 = 每秒刷盘(最快,崩溃可能丢1秒数据)
innodb_flush_log_at_trx_commit = 1

# 并发线程控制
innodb_thread_concurrency = 0    # 0=自动(推荐8.0+)
innodb_read_io_threads = 8
innodb_write_io_threads = 8

# 自适应哈希索引 - 等值查询多时开启
innodb_adaptive_hash_index = ON

# Change Buffer - 非唯一二级索引的插入优化
innodb_change_buffering = all
innodb_change_buffer_max_size = 25

buffer_pool_size的精细化调优:不要一次性设满。留出内存给操作系统文件缓存和临时表。观察Buffer Pool Wait指标,如果经常出现等待,说明不够大。如果Buffer Pool Read(磁盘读)比例低于1%,当前大小已经足够。

-- 查看缓冲池命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- Innodb_buffer_pool_read_requests / 
-- (Innodb_buffer_pool_read_requests + Innodb_buffer_pool_reads)
-- 应该 > 99.9%

线上诊断工具箱

查看当前正在执行的SQL

SELECT * FROM information_schema.PROCESSLIST 
WHERE Command != 'Sleep' AND Time > 5
ORDER BY Time DESC;

-- MySQL 8.0+用performance_schema更精准
SELECT * FROM performance_schema.events_statements_current
WHERE TIMER_WAIT/1000000000000 > 5;  -- 超过5秒

查看锁等待

SELECT 
  r.trx_id AS waiting_trx,
  r.trx_mysql_thread_id AS waiting_thread,
  b.trx_id AS blocking_trx,
  b.trx_mysql_thread_id AS blocking_thread,
  r.trx_query AS waiting_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;

InnoDB状态监控

SHOW ENGINE INNODB STATUS\G
-- 关注几个段落:
-- LATEST DETECTED DEADLOCK:最近死锁
-- TRANSACTIONS:事务状态
-- BUFFER POOL AND MEMORY:缓冲池使用情况
-- ROW OPERATIONS:行操作统计

MySQL性能调优是数据驱动的工程问题。用慢查询日志找到瓶颈,用EXPLAIN分析路径,用索引优化消除全表扫描,用参数调优压出硬件极限。每一步都有数据支撑,不要靠猜。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql80-xing-neng-diao-you-shi-zhan-man-cha-xun-zhen-duan/

(0)
小编小编
上一篇 2026年7月30日
下一篇 2026年7月30日

相关推荐

MySQL 8.0性能调优实战:慢查询诊断、执行计划分析与索引优化全流程

MySQL慢查询定位:从开关到根因

数据库性能问题的80%由20%的慢SQL导致。定位这些SQL是调优的起点。

开启慢查询日志并设置阈值:

-- 临时开启(重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 记录未走索引的查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 永久生效:my.cnf
[mysqld]
slow_query_log = 1
long_query_time = 1
log_queries_not_using_indexes = 1
slow_query_log_file = /var/log/mysql/slow.log

用mysqldumpslow分析Top 10慢查询:

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

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

MySQL 8.0还提供了performance_schema直接查询慢查询:

SELECT DIGEST_TEXT,
       COUNT_STAR,
       AVG_TIMER_WAIT/1000000000 AS avg_ms,
       SUM_ROWS_EXAMINED,
       SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000  -- 平均超过1秒
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;

这种方式比慢日志更实时,且不产生磁盘IO开销。

EXPLAIN执行计划深度解读

拿到慢SQL后,第一步是看执行计划:

EXPLAIN FORMAT=TREE
SELECT o.order_id, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID' AND o.created_at > '2026-07-01'
ORDER BY o.created_at DESC
LIMIT 20;

关注几个关键字段:

type:访问类型,从好到差依次为:system > const > eq_ref > ref > range > index > ALL。出现ALL(全表扫描)必须优化。

key:实际使用的索引。如果为NULL说明没走索引。

rows:预估扫描行数。这个值和实际差距可能很大,但量级参考有意义。

Extra:附加信息。重点关注:

– Using filesort:额外排序,大量数据时严重拖慢查询
– Using temporary:使用临时表,常见于GROUP BY无索引
– Using index:覆盖索引,性能最优

索引优化:从建索引到避免索引失效

复合索引的最左前缀原则

-- 索引定义
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);

-- 能走索引
WHERE status = 'PAID'
WHERE status = 'PAID' AND created_at > '2026-07-01'

-- 不能走索引(跳过了status列)
WHERE created_at > '2026-07-01'

-- 部分走索引(只有status走索引,created_at需要回表过滤)
WHERE status = 'PAID' AND amount > 100

覆盖索引消除回表

如果查询只需要索引中包含的列,MySQL直接从索引返回数据,不需要回表查主键:

-- 原查询:需要回表拿amount
SELECT order_id, amount FROM orders WHERE status = 'PAID';

-- 优化:建立覆盖索引
ALTER TABLE orders ADD INDEX idx_status_amount (status, order_id, amount);

-- EXPLAIN中Extra显示Using index,表示覆盖索引生效

索引失效的常见陷阱

-- 1. 对索引列使用函数
WHERE DATE(created_at) = '2026-07-28'   -- 索引失效
WHERE created_at >= '2026-07-28' AND created_at < '2026-07-29'  -- 索引生效

-- 2. 隐式类型转换
WHERE user_id = '123'   -- user_id是INT,字符串触发隐式转换,索引失效
WHERE user_id = 123     -- 正确

-- 3. LIKE前缀通配符
WHERE name LIKE '%zhang'   -- 索引失效
WHERE name LIKE 'zhang%'   -- 索引生效

-- 4. OR条件列无索引
WHERE status = 'PAID' OR remark = 'urgent'  -- remark无索引,整条查询索引失效
-- 解决:UNION ALL改写
SELECT * FROM orders WHERE status = 'PAID'
UNION ALL
SELECT * FROM orders WHERE remark = 'urgent' AND status != 'PAID'

InnoDB Buffer Pool调优

Buffer Pool是InnoDB性能的命脉。核心参数:

-- 查看当前配置
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

-- 生产环境建议:物理内存的60-80%(独占MySQL实例)
-- 32G内存服务器
SET GLOBAL innodb_buffer_pool_size = 21474836480;  -- 20GB

-- 多个Buffer Pool实例减少锁争用
SET GLOBAL innodb_buffer_pool_instances = 8;  -- 每个实例至少1GB

监控Buffer Pool命中率:

SELECT
  (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100 AS hit_rate
FROM (
  SELECT variable_value AS Innodb_buffer_pool_reads
  FROM performance_schema.global_status
  WHERE variable_name = 'Innodb_buffer_pool_reads'
) r,
(
  SELECT variable_value AS Innodb_buffer_pool_read_requests
  FROM performance_schema.global_status
  WHERE variable_name = 'Innodb_buffer_pool_read_requests'
) rr;

命中率低于95%说明Buffer Pool不足,频繁磁盘读取。此时优先扩容Buffer Pool,而非加索引。

连接池与线程缓存

-- 最大连接数
SET GLOBAL max_connections = 500;

-- 线程缓存(减少线程创建销毁开销)
SET GLOBAL thread_cache_size = 64;

-- 查看线程缓存命中率
SHOW STATUS LIKE 'Threads_created';
SHOW STATUS LIKE 'Connections';
-- 命中率 = 1 - Threads_created / Connections

连接池配置(以HikariCP为例):

spring:
  datasource:
    hikari:
      maximum-pool-size: 50      # CPU核心数 * 2 + 磁盘数
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

maximum-pool-size不是越大越好。MySQL每个连接占用一个线程,线程数超过CPU核心数后,上下文切换开销急剧上升。50个连接在8核服务器上是合理的上限。

在线DDL与大表变更

MySQL 8.0支持ALGORITHM=INPLACE的在线DDL,大部分索引添加操作不阻塞读写:

ALTER TABLE orders ADD INDEX idx_created_at (created_at),
  ALGORITHM=INPLACE, LOCK=NONE;

但以下操作仍需拷表(ALGORITHM=COPY),会锁表:

- 修改列类型
- 添加自增列
- 修改字符集

大表(千万级)变更建议使用gh-ost或pt-online-schema-change工具,通过创建影子表+增量同步方式实现无锁变更。

MySQL性能调优没有万能参数,核心是定位瓶颈:慢查询靠索引优化,吞吐量靠Buffer Pool,并发靠连接池。一个稳定的MySQL实例,95%的查询应在100ms内返回,否则先从索引和SQL写法找问题,再考虑硬件扩容。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql80-xing-neng-diao-you-shi-zhan-man-cha-xun-zhen-duan/

(0)
小编小编
上一篇 2026年7月28日
下一篇 2026年7月28日

相关推荐

MySQL 8.0性能调优实战:慢查询诊断、执行计划分析与索引优化全流程

MySQL慢查询定位:从开关到根因

数据库性能问题的80%由20%的慢SQL导致。定位这些SQL是调优的起点。

开启慢查询日志并设置阈值:

-- 临时开启(重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 记录未走索引的查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 永久生效:my.cnf
[mysqld]
slow_query_log = 1
long_query_time = 1
log_queries_not_using_indexes = 1
slow_query_log_file = /var/log/mysql/slow.log

用mysqldumpslow分析Top 10慢查询:

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

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

MySQL 8.0还提供了performance_schema直接查询慢查询:

SELECT DIGEST_TEXT,
       COUNT_STAR,
       AVG_TIMER_WAIT/1000000000 AS avg_ms,
       SUM_ROWS_EXAMINED,
       SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000  -- 平均超过1秒
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;

这种方式比慢日志更实时,且不产生磁盘IO开销。

EXPLAIN执行计划深度解读

拿到慢SQL后,第一步是看执行计划:

EXPLAIN FORMAT=TREE
SELECT o.order_id, o.amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID' AND o.created_at > '2026-07-01'
ORDER BY o.created_at DESC
LIMIT 20;

关注几个关键字段:

type:访问类型,从好到差依次为:system > const > eq_ref > ref > range > index > ALL。出现ALL(全表扫描)必须优化。

key:实际使用的索引。如果为NULL说明没走索引。

rows:预估扫描行数。这个值和实际差距可能很大,但量级参考有意义。

Extra:附加信息。重点关注:

– Using filesort:额外排序,大量数据时严重拖慢查询
– Using temporary:使用临时表,常见于GROUP BY无索引
– Using index:覆盖索引,性能最优

索引优化:从建索引到避免索引失效

复合索引的最左前缀原则

-- 索引定义
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);

-- 能走索引
WHERE status = 'PAID'
WHERE status = 'PAID' AND created_at > '2026-07-01'

-- 不能走索引(跳过了status列)
WHERE created_at > '2026-07-01'

-- 部分走索引(只有status走索引,created_at需要回表过滤)
WHERE status = 'PAID' AND amount > 100

覆盖索引消除回表

如果查询只需要索引中包含的列,MySQL直接从索引返回数据,不需要回表查主键:

-- 原查询:需要回表拿amount
SELECT order_id, amount FROM orders WHERE status = 'PAID';

-- 优化:建立覆盖索引
ALTER TABLE orders ADD INDEX idx_status_amount (status, order_id, amount);

-- EXPLAIN中Extra显示Using index,表示覆盖索引生效

索引失效的常见陷阱

-- 1. 对索引列使用函数
WHERE DATE(created_at) = '2026-07-28'   -- 索引失效
WHERE created_at >= '2026-07-28' AND created_at < '2026-07-29'  -- 索引生效

-- 2. 隐式类型转换
WHERE user_id = '123'   -- user_id是INT,字符串触发隐式转换,索引失效
WHERE user_id = 123     -- 正确

-- 3. LIKE前缀通配符
WHERE name LIKE '%zhang'   -- 索引失效
WHERE name LIKE 'zhang%'   -- 索引生效

-- 4. OR条件列无索引
WHERE status = 'PAID' OR remark = 'urgent'  -- remark无索引,整条查询索引失效
-- 解决:UNION ALL改写
SELECT * FROM orders WHERE status = 'PAID'
UNION ALL
SELECT * FROM orders WHERE remark = 'urgent' AND status != 'PAID'

InnoDB Buffer Pool调优

Buffer Pool是InnoDB性能的命脉。核心参数:

-- 查看当前配置
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

-- 生产环境建议:物理内存的60-80%(独占MySQL实例)
-- 32G内存服务器
SET GLOBAL innodb_buffer_pool_size = 21474836480;  -- 20GB

-- 多个Buffer Pool实例减少锁争用
SET GLOBAL innodb_buffer_pool_instances = 8;  -- 每个实例至少1GB

监控Buffer Pool命中率:

SELECT
  (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100 AS hit_rate
FROM (
  SELECT variable_value AS Innodb_buffer_pool_reads
  FROM performance_schema.global_status
  WHERE variable_name = 'Innodb_buffer_pool_reads'
) r,
(
  SELECT variable_value AS Innodb_buffer_pool_read_requests
  FROM performance_schema.global_status
  WHERE variable_name = 'Innodb_buffer_pool_read_requests'
) rr;

命中率低于95%说明Buffer Pool不足,频繁磁盘读取。此时优先扩容Buffer Pool,而非加索引。

连接池与线程缓存

-- 最大连接数
SET GLOBAL max_connections = 500;

-- 线程缓存(减少线程创建销毁开销)
SET GLOBAL thread_cache_size = 64;

-- 查看线程缓存命中率
SHOW STATUS LIKE 'Threads_created';
SHOW STATUS LIKE 'Connections';
-- 命中率 = 1 - Threads_created / Connections

连接池配置(以HikariCP为例):

spring:
  datasource:
    hikari:
      maximum-pool-size: 50      # CPU核心数 * 2 + 磁盘数
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

maximum-pool-size不是越大越好。MySQL每个连接占用一个线程,线程数超过CPU核心数后,上下文切换开销急剧上升。50个连接在8核服务器上是合理的上限。

在线DDL与大表变更

MySQL 8.0支持ALGORITHM=INPLACE的在线DDL,大部分索引添加操作不阻塞读写:

ALTER TABLE orders ADD INDEX idx_created_at (created_at),
  ALGORITHM=INPLACE, LOCK=NONE;

但以下操作仍需拷表(ALGORITHM=COPY),会锁表:

- 修改列类型
- 添加自增列
- 修改字符集

大表(千万级)变更建议使用gh-ost或pt-online-schema-change工具,通过创建影子表+增量同步方式实现无锁变更。

MySQL性能调优没有万能参数,核心是定位瓶颈:慢查询靠索引优化,吞吐量靠Buffer Pool,并发靠连接池。一个稳定的MySQL实例,95%的查询应在100ms内返回,否则先从索引和SQL写法找问题,再考虑硬件扩容。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql80-xing-neng-diao-you-shi-zhan-man-cha-xun-zhen-duan/

(0)
小编小编
上一篇 2026年7月28日
下一篇 2026年7月28日

相关推荐

MySQL 8.0性能调优实战:慢查询诊断与索引优化全流程

MySQL 8.0性能诊断:从慢查询日志到根因分析

MySQL性能问题的表象多种多样——接口超时、CPU飙高、磁盘IO打满——但根因往往落在三类问题上:低效SQL、索引缺失或失效、锁等待。诊断的起点不是猜测,而是数据。慢查询日志是MySQL性能调优的第一手数据源。

慢查询日志配置与分析

开启慢查询日志

-- 动态开启(无需重启)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;    -- 超过500ms记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 未用索引也记录
SET GLOBAL min_examined_row_limit = 100;  -- 扫描行少于100不记录

-- 持久化配置(my.cnf)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
log_queries_not_using_indexes = 1

使用mysqldumpslow做快速统计

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

# 按锁定时间排序
mysqldumpslow -s l -t 10 /var/log/mysql/slow.log

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

使用pt-query-digest做深度分析

# 生成查询分析报告
pt-query-digest /var/log/mysql/slow.log > slow_report.txt

# 指定时间段分析
pt-query-digest --since '2026-07-25 00:00:00' --until '2026-07-25 06:00:00' \
  /var/log/mysql/slow.log

# 输出关键信息:
# - Rank:查询排名
# - Query ID:查询指纹
# - Response time:总响应时间及占比
# - Calls:执行次数
# - R/Call:平均每次响应时间
# - V/M:方差/均值比,越高说明性能越不稳定

EXPLAIN执行计划深度解读

找到慢查询后,用EXPLAIN分析执行计划。MySQL 8.0推荐使用EXPLAIN FORMAT=TREE获得更直观的执行树:

EXPLAIN FORMAT=TREE
SELECT o.order_id, o.amount, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.create_time > '2026-07-01'
  AND o.status = 'PAID'
ORDER BY o.amount DESC
LIMIT 20;

关键字段判断逻辑

type列(从优到差):

  • system/const:单行查找,最优
  • eq_ref:唯一索引关联,次优
  • ref:非唯一索引查找
  • range:索引范围扫描
  • index:全索引扫描(比ALL好,但仍然慢)
  • ALL:全表扫描,必须优化

Extra列关键信息:

  • Using index:覆盖索引,不需要回表
  • Using where:在存储引擎返回数据后做过滤
  • Using filesort:需要额外排序(大结果集时极慢)
  • Using temporary:创建临时表(GROUP BY无索引时常见)

索引优化实战:从理论到操作

索引选择性计算

索引的选择性 = 该列不同值数量 / 总行数。选择性越高,索引越有效:

-- 计算各列的选择性
SELECT 
  COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
  COUNT(DISTINCT customer_id) / COUNT(*) AS customer_selectivity,
  COUNT(DISTINCT create_time) / COUNT(*) AS time_selectivity,
  COUNT(*) AS total_rows
FROM orders;

-- 示例结果:
-- status_selectivity: 0.0003  (极低,只有几个状态值)
-- customer_selectivity: 0.12   (中等)
-- time_selectivity: 0.85       (高,几乎每条记录时间不同)

复合索引的列顺序

复合索引遵循最左前缀原则。列顺序由以下因素决定:

  1. 等值条件列放在前面
  2. 范围条件列放在后面
  3. 排序列放在最后
-- 查询条件:status = 'PAID' AND create_time > '2026-07-01'
-- 排序:ORDER BY amount DESC

-- 错误索引:create_time在前
CREATE INDEX idx_time_status ON orders(create_time, status);
-- type=range,status过滤依赖Using where,无法利用索引

-- 正确索引:status等值在前,time范围在后,amount排序最后
CREATE INDEX idx_status_time_amount ON orders(status, create_time, amount);
-- type=range,Using index condition,排序可用索引

函数索引(MySQL 8.0+)

-- 对JSON字段创建函数索引
CREATE INDEX idx_data_json_extract 
ON orders((CAST(JSON_EXTRACT(data, '$.region') AS CHAR(20))));

-- 对日期列创建函数索引
CREATE INDEX idx_create_date 
ON orders((DATE(create_time)));

锁等待诊断与优化

锁等待监控

-- 查看当前锁等待
SELECT 
  r.trx_id AS waiting_trx,
  r.trx_mysql_thread_id AS waiting_thread,
  r.trx_query AS waiting_query,
  b.trx_id AS blocking_trx,
  b.trx_mysql_thread_id AS blocking_thread,
  b.trx_query AS blocking_query,
  b.trx_started AS blocking_started
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;

-- MySQL 8.0更简洁的写法
SELECT * FROM performance_schema.data_lock_waits\G

死锁分析

-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS\G
-- 在LATEST DETECTED DEADLOCK段查看详情

-- 开启死锁完整日志记录
SET GLOBAL innodb_print_all_deadlocks = ON;
-- 死锁信息会写入error log,便于事后分析

常见锁优化策略

-- 1. 缩小事务粒度:大事务拆分
-- 反模式:一个事务中做太多操作
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 锁行
-- ... 执行大量业务逻辑 ...
UPDATE orders SET status = 'PAID' WHERE id = 100;  -- 长时间持锁
COMMIT;

-- 正模式:按依赖关系拆分事务
-- 事务1:只做余额扣减
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- 事务2:只做订单状态更新
BEGIN;
UPDATE orders SET status = 'PAID' WHERE id = 100;
COMMIT;

-- 2. 按固定顺序访问资源,避免死锁
-- 所有事务按id升序更新
UPDATE inventory SET stock = stock - 1 WHERE product_id = LEAST(pid1, pid2);
UPDATE inventory SET stock = stock - 1 WHERE product_id = GREATEST(pid1, pid2);

数据库高可用架构:读写分离与MGR

MySQL Group Replication配置

# my.cnf - MGR单主模式配置
[mysqld]
plugin_load_add = 'group_replication.so'
group_replication_group_name = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
group_replication_start_on_boot = OFF
group_replication_local_address = "10.0.0.1:33061"
group_replication_group_seeds = "10.0.0.1:33061,10.0.0.2:33061,10.0.0.3:33061"
group_replication_single_primary_mode = ON
group_replication_enforce_update_everywhere_checks = OFF

# 启动MGR
SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;
CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='password' 
  FOR CHANNEL 'group_replication_recovery';
START GROUP_REPLICATION;

ProxySQL读写分离

-- 配置后端MySQL服务器
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight)
VALUES (10, '10.0.0.1', 3306, 1);   -- 写
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight)
VALUES (20, '10.0.0.2', 3306, 1);   -- 读
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight)
VALUES (20, '10.0.0.3', 3306, 1);   -- 读

-- 配置读写分离规则
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT.*FOR UPDATE', 10, 1);  -- SELECT FOR UPDATE走写
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, '^SELECT', 20, 1);              -- 普通SELECT走读

LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;

数据迁移实战:大表在线变更

# 使用gh-ost做在线DDL(无锁变更)
gh-ost \
  --user="admin" --password="xxx" --host="10.0.0.1" \
  --database="production" --table="orders" \
  --alter="ADD COLUMN region VARCHAR(20) DEFAULT 'CN'" \
  --allow-on-master \
  --initial-rows-estimate=50000000 \
  --chunk-size=5000 \
  --max-load='Threads_running=100' \
  --critical-load='Threads_running=500' \
  --execute

# 关键参数说明:
# --chunk-size:每批处理行数,根据服务器负载动态调整
# --max-load:达到此负载暂停复制
# --critical-load:达到此负载中止操作

MySQL性能调优是一个持续迭代的过程。从慢查询日志定位问题SQL,用EXPLAIN验证执行计划,通过索引优化和SQL改写降低资源消耗,再用读写分离和在线DDL应对高并发和变更需求。每次调优后都要用压测数据验证效果,避免”感觉快了”的假象。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql80-xing-neng-diao-you-shi-zhan-man-cha-xun-zhen-duan/

(0)
小编小编
上一篇 2026年7月25日
下一篇 2026年7月27日

相关推荐

MySQL 8.0性能调优实战:慢查询诊断与索引优化全流程

MySQL 8.0性能诊断:从慢查询日志到根因分析

MySQL性能问题的表象多种多样——接口超时、CPU飙高、磁盘IO打满——但根因往往落在三类问题上:低效SQL、索引缺失或失效、锁等待。诊断的起点不是猜测,而是数据。慢查询日志是MySQL性能调优的第一手数据源。

慢查询日志配置与分析

开启慢查询日志

-- 动态开启(无需重启)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;    -- 超过500ms记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 未用索引也记录
SET GLOBAL min_examined_row_limit = 100;  -- 扫描行少于100不记录

-- 持久化配置(my.cnf)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
log_queries_not_using_indexes = 1

使用mysqldumpslow做快速统计

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

# 按锁定时间排序
mysqldumpslow -s l -t 10 /var/log/mysql/slow.log

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

使用pt-query-digest做深度分析

# 生成查询分析报告
pt-query-digest /var/log/mysql/slow.log > slow_report.txt

# 指定时间段分析
pt-query-digest --since '2026-07-25 00:00:00' --until '2026-07-25 06:00:00' \
  /var/log/mysql/slow.log

# 输出关键信息:
# - Rank:查询排名
# - Query ID:查询指纹
# - Response time:总响应时间及占比
# - Calls:执行次数
# - R/Call:平均每次响应时间
# - V/M:方差/均值比,越高说明性能越不稳定

EXPLAIN执行计划深度解读

找到慢查询后,用EXPLAIN分析执行计划。MySQL 8.0推荐使用EXPLAIN FORMAT=TREE获得更直观的执行树:

EXPLAIN FORMAT=TREE
SELECT o.order_id, o.amount, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.create_time > '2026-07-01'
  AND o.status = 'PAID'
ORDER BY o.amount DESC
LIMIT 20;

关键字段判断逻辑

type列(从优到差):

  • system/const:单行查找,最优
  • eq_ref:唯一索引关联,次优
  • ref:非唯一索引查找
  • range:索引范围扫描
  • index:全索引扫描(比ALL好,但仍然慢)
  • ALL:全表扫描,必须优化

Extra列关键信息:

  • Using index:覆盖索引,不需要回表
  • Using where:在存储引擎返回数据后做过滤
  • Using filesort:需要额外排序(大结果集时极慢)
  • Using temporary:创建临时表(GROUP BY无索引时常见)

索引优化实战:从理论到操作

索引选择性计算

索引的选择性 = 该列不同值数量 / 总行数。选择性越高,索引越有效:

-- 计算各列的选择性
SELECT 
  COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
  COUNT(DISTINCT customer_id) / COUNT(*) AS customer_selectivity,
  COUNT(DISTINCT create_time) / COUNT(*) AS time_selectivity,
  COUNT(*) AS total_rows
FROM orders;

-- 示例结果:
-- status_selectivity: 0.0003  (极低,只有几个状态值)
-- customer_selectivity: 0.12   (中等)
-- time_selectivity: 0.85       (高,几乎每条记录时间不同)

复合索引的列顺序

复合索引遵循最左前缀原则。列顺序由以下因素决定:

  1. 等值条件列放在前面
  2. 范围条件列放在后面
  3. 排序列放在最后
-- 查询条件:status = 'PAID' AND create_time > '2026-07-01'
-- 排序:ORDER BY amount DESC

-- 错误索引:create_time在前
CREATE INDEX idx_time_status ON orders(create_time, status);
-- type=range,status过滤依赖Using where,无法利用索引

-- 正确索引:status等值在前,time范围在后,amount排序最后
CREATE INDEX idx_status_time_amount ON orders(status, create_time, amount);
-- type=range,Using index condition,排序可用索引

函数索引(MySQL 8.0+)

-- 对JSON字段创建函数索引
CREATE INDEX idx_data_json_extract 
ON orders((CAST(JSON_EXTRACT(data, '$.region') AS CHAR(20))));

-- 对日期列创建函数索引
CREATE INDEX idx_create_date 
ON orders((DATE(create_time)));

锁等待诊断与优化

锁等待监控

-- 查看当前锁等待
SELECT 
  r.trx_id AS waiting_trx,
  r.trx_mysql_thread_id AS waiting_thread,
  r.trx_query AS waiting_query,
  b.trx_id AS blocking_trx,
  b.trx_mysql_thread_id AS blocking_thread,
  b.trx_query AS blocking_query,
  b.trx_started AS blocking_started
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;

-- MySQL 8.0更简洁的写法
SELECT * FROM performance_schema.data_lock_waits\G

死锁分析

-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS\G
-- 在LATEST DETECTED DEADLOCK段查看详情

-- 开启死锁完整日志记录
SET GLOBAL innodb_print_all_deadlocks = ON;
-- 死锁信息会写入error log,便于事后分析

常见锁优化策略

-- 1. 缩小事务粒度:大事务拆分
-- 反模式:一个事务中做太多操作
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 锁行
-- ... 执行大量业务逻辑 ...
UPDATE orders SET status = 'PAID' WHERE id = 100;  -- 长时间持锁
COMMIT;

-- 正模式:按依赖关系拆分事务
-- 事务1:只做余额扣减
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- 事务2:只做订单状态更新
BEGIN;
UPDATE orders SET status = 'PAID' WHERE id = 100;
COMMIT;

-- 2. 按固定顺序访问资源,避免死锁
-- 所有事务按id升序更新
UPDATE inventory SET stock = stock - 1 WHERE product_id = LEAST(pid1, pid2);
UPDATE inventory SET stock = stock - 1 WHERE product_id = GREATEST(pid1, pid2);

数据库高可用架构:读写分离与MGR

MySQL Group Replication配置

# my.cnf - MGR单主模式配置
[mysqld]
plugin_load_add = 'group_replication.so'
group_replication_group_name = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
group_replication_start_on_boot = OFF
group_replication_local_address = "10.0.0.1:33061"
group_replication_group_seeds = "10.0.0.1:33061,10.0.0.2:33061,10.0.0.3:33061"
group_replication_single_primary_mode = ON
group_replication_enforce_update_everywhere_checks = OFF

# 启动MGR
SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;
CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='password' 
  FOR CHANNEL 'group_replication_recovery';
START GROUP_REPLICATION;

ProxySQL读写分离

-- 配置后端MySQL服务器
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight)
VALUES (10, '10.0.0.1', 3306, 1);   -- 写
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight)
VALUES (20, '10.0.0.2', 3306, 1);   -- 读
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight)
VALUES (20, '10.0.0.3', 3306, 1);   -- 读

-- 配置读写分离规则
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT.*FOR UPDATE', 10, 1);  -- SELECT FOR UPDATE走写
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, '^SELECT', 20, 1);              -- 普通SELECT走读

LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;

数据迁移实战:大表在线变更

# 使用gh-ost做在线DDL(无锁变更)
gh-ost \
  --user="admin" --password="xxx" --host="10.0.0.1" \
  --database="production" --table="orders" \
  --alter="ADD COLUMN region VARCHAR(20) DEFAULT 'CN'" \
  --allow-on-master \
  --initial-rows-estimate=50000000 \
  --chunk-size=5000 \
  --max-load='Threads_running=100' \
  --critical-load='Threads_running=500' \
  --execute

# 关键参数说明:
# --chunk-size:每批处理行数,根据服务器负载动态调整
# --max-load:达到此负载暂停复制
# --critical-load:达到此负载中止操作

MySQL性能调优是一个持续迭代的过程。从慢查询日志定位问题SQL,用EXPLAIN验证执行计划,通过索引优化和SQL改写降低资源消耗,再用读写分离和在线DDL应对高并发和变更需求。每次调优后都要用压测数据验证效果,避免”感觉快了”的假象。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql80-xing-neng-diao-you-shi-zhan-man-cha-xun-zhen-duan/

(0)
小编小编
上一篇 2026年7月25日
下一篇 2026年7月25日

相关推荐