以下是全面、实用、分层次的 MySQL 性能优化解决方案,以及相关原理。

以 MySQL 5.7 为例说明。

常用的优化方案

SQL 语句优化(最直接有效)

  1. 避免全量查询

    避免查询多余的数据,尤其是敏感和 text 类型数据

    1
    2
    3
    4
    5
    -- 不推荐
    SELECT * FROM users;

    -- 推荐:只查询需要的字段
    SELECT id, name, email FROM users;
  2. 使用 EXPLAIN 分析执行计划

    分析 SQL 执行情况,以及索引利用率和连表查询性能等。

    1
    2
    -- 关注:`type`(是否全表扫描)、`key`(是否用索引)、`rows`(扫描行数)、`Extra`(是否用临时表/文件排序)
    EXPLAIN SELECT * FROM orders WHERE user_id = 100;
  3. 避免在 WHERE 中对字段进行函数操作或计算

    低版本 MySQL 不支持函数索引,会导致索引失效。MySQL8 有所改善。

    1
    2
    3
    4
    5
    -- 索引失效
    SELECT * FROM users WHERE YEAR(created_at) = 2024;

    -- 改写为范围查询
    SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
  4. 避免使用 OR

    会导致索引失效,MySQL8 有所改善。可改写为 UNION 或 IN,或其他字段添加索引。

    1
    2
    3
    4
    5
    6
    7
    -- 可能导致索引失效
    SELECT * FROM users WHERE name = 'Alice' OR age = 25;

    -- 改用 UNION(如果字段都有索引)
    SELECT * FROM users WHERE name = 'Alice'
    UNION
    SELECT * FROM users WHERE age = 25;
  5. 使用 LIMIT 分页优化(避免 OFFSET 大偏移)

    涉及"深度分页"问题,可参照 海量数据的深度分页查询解决方案

    1
    2
    3
    4
    5
    -- 慢(深度分页):OFFSET 100000
    SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

    -- 优化:记住上一页最大ID
    SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;
  6. 避免复杂 CASE WHEN / IF 判断(尤其在 WHERE 或 JOIN 中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    -- 性能差:
    SELECT *
    FROM orders
    WHERE CASE
    WHEN status = 'paid' AND amount > 100 THEN 1
    WHEN status = 'pending' AND created_at > NOW() - INTERVAL 1 DAY THEN 1
    ELSE 0
    END = 1;

    -------------------------------------------------------
    -- 改写为逻辑表达式:
    SELECT *
    FROM orders
    WHERE (status = 'paid' AND amount > 100)
    OR (status = 'pending' AND created_at > NOW() - INTERVAL 1 DAY);
  7. 避免高频的统计计算和字符截取

    如果某些计算是高频使用的,可以在写入时计算好并存储。或者定时或实时建立统计表(按天、按月)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    -- 每次查询都计算:
    SELECT YEAR(create_time), MONTH(create_time), COUNT(*)
    FROM orders
    GROUP BY YEAR(create_time), MONTH(create_time);

    ---------------------------预计算 + 冗余字段(空间换时间)-------------------------------------
    -- 建表时或通过触发器/应用层写入
    ALTER TABLE orders ADD COLUMN order_year_month INT;

    -- 查询时直接使用
    SELECT order_year_month, COUNT(*)
    FROM orders
    GROUP BY order_year_month;

    -- 对该字段加索引
    CREATE INDEX idx_order_ym ON orders(order_year_month);
    1
    2
    3
    4
    5
    6
    -- 使用生成列:MySQL 支持虚拟列或存储列,自动计算并可加索引
    ALTER TABLE orders
    ADD COLUMN order_year_month INT
    GENERATED ALWAYS AS (YEAR(create_time) * 100 + MONTH(create_time)) STORED;

    CREATE INDEX idx_order_ym ON orders(order_year_month);
  8. 使用临时表 / 中间表预处理复杂逻辑

    可分步优化、避免单条 SQL 过于复杂。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    -- Step 1: 预处理中间结果到临时表
    CREATE TEMPORARY TABLE temp_filtered_orders AS
    SELECT id, user_id, amount,
    CASE WHEN amount > 100 THEN 'high' ELSE 'low' END AS level
    FROM orders
    WHERE create_time >= '2024-01-01';

    -- Step 2: 对临时表加索引(可选)
    CREATE INDEX idx_temp_user ON temp_filtered_orders(user_id);

    -- Step 3: 基于临时表做后续 JOIN / GROUP / 统计
    SELECT u.name, t.level, COUNT(*)
    FROM temp_filtered_orders t
    JOIN users u ON t.user_id = u.id
    GROUP BY u.name, t.level;

索引优化(性能提升关键)

不适合建立索引的列

  • tinyint 或 用以标致类型的int列。比如用以保存性别,选择性过大,最小都是超过25%了,因此没有设置索引的必要。

  • 更新频繁的字段。

  • 不会出现在where子句中的字段。

  • 数据可能为null或空或0的字段

  • 存储占用过大的,如:text,json

适合建立索引的列

  • 字段离散度越高,越适合选作索引的关键字。如主键和唯一字段(主键索引自动创建)。

  • 很少更新的字段(因为也会牵连更新索引)。

  • 占用存储空间少的字段。

  • 存储空间固定的字段。

  • Where子句中经常使用的字段,分组字段、排序字段和多表连接字段(外键索引自动创建)。

  • 尽量使用联合索引(前缀)

  1. 为常用 WHERE、JOIN、ORDER BY 字段建索引

    索引建立的规则需要遵守以上,否则可能适得其反,会被优化器所优化。

    1
    2
    CREATE INDEX idx_user_email ON users(email);
    CREATE INDEX idx_order_user_status ON orders(user_id, status);
  2. 使用复合索引(联合索引),注意最左前缀原则

    优先考虑使用联合索引,尤其是有类型状态的字段。

    1
    2
    3
    -- 查询条件 WHERE a=? AND b=? → 建立 (a, b)
    -- 查询条件 WHERE b=? AND a=? → 同样可用 (a, b),MySQL 会自动优化顺序(优化器)
    -- 查询条件 WHERE b=? → 无法使用 (a, b) 索引,MySQL8 中有所优化,可能会用到
  3. 避免过多索引(写操作变慢,空间占用大)

    一般单表索引不超过 5 个,根据查询频率和区分度选择,优先使用联合索引

  4. 使用覆盖索引(Covering Index)避免回表

    尽量将查询字段都包含在索引中。

    1
    2
    3
    4
    -- 如果索引包含 SELECT 所有字段,无需回表查主键
    CREATE INDEX idx_covering ON orders(user_id, status, amount);

    SELECT user_id, status, amount FROM orders WHERE user_id = 100; -- 走覆盖索引
  5. 定期分析和重建索引(碎片整理)

    1
    2
    3
    ANALYZE TABLE orders;  -- 更新统计信息
    OPTIMIZE TABLE orders; -- 重建表和索引(锁表,谨慎使用)
    -- 或使用在线工具:pt-online-schema-change / gh-ost

表结构与设计优化(思想习惯)

  1. 选择合适的数据类型

    • 使用 TINYINT 代替 INT 存布尔值
    • 使用 VARCHAR(N) 而不是 TEXT(如果长度可控)
    • 使用 DATETIME 而不是字符串存时间
    • 使用 UNSIGNED 无符号整数扩大范围
  2. 避免 NULL,设置默认值

    NULL 会增加判断复杂度,索引效率略低,且可能存在歧义性。

  3. 拆分大字段或大表(垂直/水平分表)

    对于半结构化 JSON 大数据,可以选择存储在非关系型数据库中。

    • TEXT/BLOB 字段可拆到扩展表

    • 历史数据归档(如 orders → orders_2024, orders_2023)

  4. 使用合适存储引擎

    只有插入和查询的 日志类数据,可以使用 MyISAM 引擎

    • 一般用 InnoDB(支持事务、行锁、外键)

    • 读多写少且无事务可用 MyISAM(已基本淘汰)

MySQL 配置参数优化(底层原理)

MySQL的innodb_flush_method参数控制着innodb数据文件及redo log的打开、刷写模式

  • redo log 物理日志(保证持久性),记录的是数据页的物理修改,而不是某一行或某几行修改,它用来恢复提交后的物理数据页(只能恢复到最后一次提交的位置)。

  • undo 逻辑日志 (保证原子性),用来回滚行记录到某个版本,根据每行记录进行记录。

缓冲池(最重要)

1
innodb_buffer_pool_size = 70%-80% of total RAM (专用数据库服务器)

Buffer Pool 大小,会影响缓存命中率

  1. 在事务执行过程中,所有对数据页的修改都会先生成 redo log 记录,并写入 log buffer;在事务提交时,根据配置决定是否将 log buffer 中的日志刷入磁盘上的 redo log 文件(ib_logfile)。

  2. 事务执行期间,数据页的修改是在 Buffer Pool 中的“脏页”上直接进行的;事务提交时,并不会立即将这些脏页写回磁盘,而是标记为“可刷盘”,由后台线程(如 checkpoint、LRU flusher)异步写入。

日志与刷盘策略(平衡性能与安全)

1
2
3
4
# 安全(默认),设为2可提速(小概率丢数据)
innodb_flush_log_at_trx_commit = 1
# 安全,设为0或N可提速
sync_binlog = 1

这两个参数是控制MySQL 磁盘写入策略以及数据安全性的关键参数:

InnoDB默认开启内部的XA事务(基于redo log和undo log),采用日志先行的策略。

在未开启binlog的情况下,数据变更(增,删,改)会在内存中操作,并将事务顺序写入到redo log中,这时就会认为事务已经完成,响应事务提交成功。然后在以下参数设置条件下,才将内存中数据刷新合并到磁盘中。

在持久化到磁盘的过程中,如果服务器宕机等导致内存中数据丢失,该数据就会丢失。这种丢失是可以通过 recovery 重做日志,找回数据的。

缓存刷新的参数: innodb_flush_log_at_trx_commit(刷新方式) innodb_flush_log_at_timeout(刷新频率)

  • innodb_flush_log_at_trx_commit=1(默认),每次事务提交,把log buffer刷到文件系统中去,并且调用文件系统的“flush”操作将缓存刷新到磁盘(如果底层硬件提供的IOPS比较差,MySQL并发会由于硬件IO而无法提升)。

  • innodb_flush_log_at_trx_commit=0,每隔一秒,把log buffer刷到文件系统,并且调用文件系统的“flush”将缓存刷新到磁盘(可能丢失1秒的事务数据)。

  • innodb_flush_log_at_trx_commit=2(推荐),每次事务提交,把log buffer刷到文件系统,但每隔一秒调用文件系统的“flush”将缓存刷新到磁盘(如果只是MySQL数据库挂掉了,由于文件系统没有问题,那么对应的事务数据并没有丢失。如果操作系统挂掉或重启,可能丢失1秒的事务数据)

sync_binlog

sync_binlog 的默认值是0,像操作系统刷其他文件的机制一样,MySQL不会同步到磁盘中而是依赖操作系统来刷新binary log。

当sync_binlog =N (N>0) ,MySQL 在每写 N次 二进制日志binary log时,会使用fdatasync()函数将它的写二进制日志binary log同步到磁盘中去。

注: 如果启用了autocommit,那么每一个语句statement就会有一次写操作;否则每个事务对应一个写操作。

脏页控制(数据页)

把内存中被修改过,跟磁盘中的数据页不一致的数据页称为脏页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 告知磁盘 I/O 能力:默认 200,SSD 建议 1000~5000
innodb_io_capacity=2000
# 控制突发刷盘能力:默认 2000,可设为 io_capacity 的 2~5 倍
innodb_io_capacity_max=3000

# 脏页比例软上限:默认 75%(5.7)或 90%(8.0),建议 70~85%
innodb_max_dirty_pages_pct=80
# 脏页刷盘起始水位:默认 0,建议设为 10~20,避免突增抖动
innodb_max_dirty_pages_pct_lwm=10

# 自适应刷盘:默认 ON,推荐保持开启
innodb_adaptive_flushing=ON
# 监控刷盘延迟:越大表示刷盘越滞后,应 < 70% of redo log size
Innodb_checkpoint_age=
  • innodb_io_capacity 参数用于告诉 InnoDB 存储引擎底层磁盘设备的 I/O 能力(单位:IOPS),InnoDB 会据此控制后台线程(如 Page Cleaner)每秒刷脏页的数量,从而避免过度消耗 I/O 资源或刷盘不足。 该参数值设置过小,会导致 InnoDB 低估磁盘 I/O 能力,从而限制后台刷脏页的速度,使得脏页积累速度超过刷盘速度,最终引发 Redo Log 空间不足、Checkpoint 延迟、写入阻塞等问题。

  • innodb_max_dirty_pages_pct 是 Buffer Pool 中脏页比例的“软上限”,默认值在 MySQL 5.6+ 是 75%,在 MySQL 8.0 中默认是 90%。当脏页比例达到或超过该值时,InnoDB 会主动加大刷盘力度,但不会完全禁止新脏页产生。

  • InnoDB 内部会根据当前脏页比例 M,结合 innodb_max_dirty_pages_pctinnodb_max_dirty_pages_pct_lwm(低水位线,默认 0),通过一个内部算法(非公开公式)计算出一个“刷盘压力因子”,用于动态调节 Page Cleaner 线程的刷盘速度。这个因子影响每秒刷多少页,但不是简单的 0~100 百分比映射。

  • 页比例是监控指标,不是配置参数。通过查询 Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_total 来监控当前脏页比例,但不能“通过它去设置”任何参数 —— 设置上限应使用 innodb_max_dirty_pages_pct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 监控指标:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';
SHOW GLOBAL STATUS LIKE 'Innodb_checkpoint_age';
-- checkpoint_age 越大,说明刷盘越滞后

-- 查看当前脏页比例
SELECT
VARIABLE_VALUE AS dirty_pages
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';

SELECT
VARIABLE_VALUE AS total_pages
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';

-- 计算比例
SELECT ROUND(
(dirty_pages / total_pages) * 100, 2
) AS dirty_pct
FROM (
SELECT
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty') AS dirty_pages,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_total') AS total_pages
) t;

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_%';

脏页刷盘触发的条件:

触发条件 是否字段 是否阻塞用户线程 说明
Checkpoint 推进 否(异步) 最主要机制,由 redo log 空间或脏页比例驱动
Buffer Pool 满(LRU淘汰) 是(可能等待) 高并发写 + 缓冲池小 → 性能瓶颈
脏页比例超阈值 否(异步) innodb_max_dirty_pages_pct控制
Page Cleaner 定时任务 每秒后台刷脏,受 I/O capacity 控制
实例关闭(SHUTDOWN) 是(阻塞) Sharp Checkpoint,刷所有脏页
手动命令(FLUSH/OPTIMIZE) 运维操作,谨慎执行
Redo Log 空间不足 可能阻塞写入 强制推进 checkpoint,刷对应脏页

mysql 连坐机制:如果要刷盘的脏页相邻的数据页恰好也是脏页,就一起写入磁盘,如果邻居的邻居也是如此。

在机械硬盘时代这个策略可以减少随机IO,但如果使用固态硬盘的话随机IO的性能往往比较高,所以使用这个策略反而拖累了查询性能。因此可以通过 innodb_flush_neighbors 关闭这个“连坐”的策略。

连接与线程

1
2
3
4
# 根据业务调整
max_connections = 500
# 缓存线程避免频繁创建
thread_cache_size = 50

临时表与排序

1
2
3
4
tmp_table_size = 64M
max_heap_table_size = 64M
# 每连接分配,不宜过大
sort_buffer_size = 2M

修改后重启或 SET GLOBAL 生效,建议压测验证。

监控与慢查询优化(手段穷极)

  1. 开启慢查询日志

    1
    2
    3
    4
    slow_query_log = 1
    slow_query_log_file = /var/log/mysql/slow.log
    long_query_time = 1 # 超过1秒记录
    log_queries_not_using_indexes = 1 # 记录未用索引的查询
  2. 使用工具分析慢查询

    • mysqldumpslow(MySQL自带)
    • pt-query-digest(Percona Toolkit,推荐)
    1
    pt-query-digest /var/log/mysql/slow.log
  3. 监控工具

    • 实时监控SHOW PROCESSLIST, SHOW STATUS, SHOW ENGINE INNODB STATUS
    • 可视化监控:
      • Prometheus + Grafana + mysqld_exporter
      • Percona PMM(推荐)
      • MySQL Workbench Performance Dashboard

架构层优化(高并发/大数据)

  1. 读写分离

    主库写,多个从库读(通过 ProxySQL / MaxScale / 中间件路由)

  2. 分库分表

    • 使用 ShardingSphere / MyCat / Vitess 等中间件
    • 按用户ID、时间等维度拆分
  3. 引入缓存层

    • Redis / Memcached 缓存热点数据
    • 缓存查询结果或对象,减轻数据库压力
  4. 异步化与队列

  • 非核心写操作走消息队列(如 Kafka、RabbitMQ)

  • 延迟写、批量写优化吞吐

  1. 数据归档与冷热分离

  • 历史数据迁移到归档库或数仓(ClickHouse / Hive)

  • 在线库只保留热数据

实用的优化方案

实际中经常碰到的问题和优化方案。

如何提高数据插入效率

  • 合并insert语句(批量插入)。日志量减少(binlog、undolog),日志刷盘数量和频率就减少,SQL解析也减少。

  • 适当调高缓冲区参数值 bulk_insert_buffer_size。批量插入时支持更多数据。

  • 设置 事务日志刷盘策略 innodb_flush_log_at_trx_commit = 0,默认为1。

    =1:每个事务提交都会刷新日志(buffer -> file),和文件同步磁盘。
    =0:每秒执行一次刷新,和文件到磁盘同步。
    =2:每个事务会刷新日志(buffer -> file),但不会立即同步磁盘。文件到磁盘同步,是每秒一次

  • 手动提交事务(执行完成后,统一提交事务)。

频繁更新数据的优化方案

频繁更新数据库可通过索引、减少锁定时间、使用批量更新、优化SQL查询、分区表和分布式数据库等手段提高性能和效率。

其中,优化索引是一个最为基础且有效的方法,通过对数据库表进行适当的索引设计,可以显著减少查询和更新操作的时间。

索引优化不仅能加快查询速度,还能减少数据库的负载。选择合适的索引类型(如B-树索引、哈希索引等)以及正确地为表中的关键字段设置索引,可以使数据库在处理频繁更新操作时更加高效。特别是对于主键和外键的索引优化,能极大地提高数据访问和修改的速度。

优化索引

  • 选择合适的索引类型

    • B-树索引:适用于大多数查询操作,特别是在范围查询和排序操作中表现优异。
    • 哈希索引:适用于等值查询,但不适用于范围查询。
    • 全文索引:适用于文本搜索操作,如在文章中搜索关键词。
  • 为关键字段设置索引

    对于主键和外键字段,设置索引可以极大地提高数据的访问和修改速度。

    • 主键索引:主键通常是唯一的,可以显著加快数据查询和更新操作。

    • 外键索引:外键索引可以加快表之间的关联查询操作,提高数据一致性。

减少锁定时间

  • 使用行级锁:只锁定被修改的行,减少锁定冲突,锁定时间越短,数据库并发性能就越高。

  • 使用乐观锁:乐观锁是一种减少锁定时间的策略,它假设数据冲突的概率较低,在提交数据时才进行冲突检测。

使用批量更新

  • 批量插入和更新:将多条插入操作合并为一条,可以减少数据库的I/O操作,可以显著提高数据库的性能。

  • 使用事务控制:使用事务控制可以确保数据的完整性和一致性,同时可以减少锁定时间,提高数据库的性能。

优化SQL查询

  • 避免使用复杂的嵌套查询:复杂的嵌套查询会导致数据库性能下降,尽量避免使用复杂的嵌套查询。

  • 使用查询缓存:查询缓存是一种提高数据库性能的有效方法,它将查询结果缓存到内存中,减少数据库I/O操作。

分区表和分布式数据库

  • 分区表:分区表是一种将大表拆分为多个小表的策略,减少查询和更新操作的时间,提高数据库的性能。

  • 分布式数据库:分布式数据库是一种将数据分布到多个节点上的策略,可以显著提高数据库的性能和可扩展性。

使用缓存

  • 应用层缓存:在应用层使用缓存可以减少对数据库的访问,提高系统的性能。

  • 数据库层缓存:在数据库层使用缓存可以减少数据库的I/O操作,提高数据库的性能。

数据库集群和负载均衡

  • 数据库集群:数据库集群是一种将多个数据库服务器组合在一起的策略,可以显著提高数据库的性能和可用性。

  • 负载均衡:负载均衡是一种将请求分发到多个服务器上的策略,可以显著提高系统的性能和可用性。

数据库监控和调优

  • 数据库监控:数据库监控是一种实时监控数据库性能的策略,可以及时发现和解决性能问题。

  • 数据库调优:数据库调优是一种通过调整数据库配置和参数来提高性能的策略。

使用高效的存储引擎

  • 选择合适的存储引擎:不同的存储引擎适用于不同的应用场景,选择合适的存储引擎可以显著提高数据库的性能。

  • 优化存储引擎配置:优化存储引擎的配置可以提高数据库的性能和稳定性。

数据归档和清理

  • 定期归档数据:定期归档不常用的数据,可以减少数据库的负载,提高数据库的性能。

  • 定期清理数据:定期清理无用的数据,可以减少数据库的存储空间,提高数据库的性能。

数据库设计优化

  • 规范化数据库设计:通过规范化数据库设计,可以减少数据冗余,提高数据库的性能和一致性。

  • 反规范化设计:在某些情况下,通过反规范化设计可以提高数据库的查询性能。

内存碎片的优化方案

MySQL碎片指的是MySQL数据文件中一些不连续的空白空间,这些空间无法再被全部利用,久而久之越来越多越来越零碎。

MySQL 碎片的本质是“空间分配与回收不同步”或“空间利用率下降”。

碎片产生的原因

MySQL 中的“碎片”通常是指由于数据的频繁增删改操作,导致存储空间未能被高效利用,从而在磁盘或内存中产生不连续、空闲但无法被有效重用的空间。碎片主要影响性能和存储效率。

当执行插入操作时,MySQL会尝试使用空白空间,如果插入的数据刚好按照索引排序落在被删除数据的区间,可能会复用这个位置,但如果某个空白空间一直没有被大小合适的数据占用,仍然无法将其彻底占用,就形成了碎片;

【MySQL的几种删除情况】

  • drop、truncate 不管是InnoDB还是MyISAM都立刻释放磁盘空间

  • delete from table_name 删除全部表数据,MyISAM立刻释放磁盘空间,InnoDB不会立刻释放磁盘空间

  • delete from table_name where xxx 带条件的删除不管是InnoDB还是MyISAM都不会立刻释放

  • delete from table_name 虽然未立刻释放磁盘空间,但下次插入的时候仍然可以使用这部分空间

以下是 MySQL 碎片产生的主要原因:

一、数据删除操作(DELETE)

  • 删除记录后,原数据页中的空间并不会立即归还给操作系统,而是标记为“可重用”。磁盘的文件大小不会收缩。InnoDB的Purge线程会异步的来清理这些没用的索引键和行。

  • 如果后续插入的数据大小不匹配或插入位置不连续,就会形成“空洞”,即碎片。

  • 大量 DELETE 操作后未进行优化,碎片会持续累积。

二、数据更新导致行变长(UPDATE)

  • 当 UPDATE 操作使某行数据长度增加(如 VARCHAR 字段内容变长),原位置空间不足时,MySQL 会将该行迁移到新的数据页,原位置留下空洞。

  • 尤其在 InnoDB 中,行溢出或页分裂会加剧碎片。

Innodb的最小物理存储分配单位是页(page),而UPDATE也可能导致页分裂,频繁的页分裂,页会变得稀疏,并且被不规则的填充,所以最终数据会有碎片。例如原始数据长度varchar(100),大规模更新数据长度为50,这样的话,有50的空间被空白了,新入库的数据不能完全利用剩余的50,这就会产生碎片。

三、频繁的插入和删除交替操作(INSERT/DELETE)

  • 高频的写入和删除会导致数据页中留下大量不连续的小块空闲空间。

  • 这些空间可能因为太小或位置分散,无法容纳新插入的行,造成空间浪费。

四、自增主键中断或非顺序插入

  • 按顺序写入可以减少碎片,但如果中间有删除或使用 UUID、随机主键等无序插入,会导致页内或页间数据不连续。

  • 非顺序插入容易引发页分裂(Page Split),进而产生碎片。

五、页分裂(Page Split) —— InnoDB 特有

  • 当一个数据页已满,而新数据需要插入到该页的中间位置时,InnoDB 会将该页一分为二(页分裂),腾出空间。

  • 分裂后两个页可能都未填满,形成内部碎片。

  • 频繁分裂会加剧碎片化并影响查询性能。

六、表结构变更(如 ALTER TABLE)

  • 某些 ALTER 操作(如添加字段、修改类型)可能导致表重建或行格式变化,产生临时碎片。

  • 在线 DDL 操作过程中也可能因中间状态产生碎片。

七、未及时重建表

  • MySQL 不会自动整理碎片,长期运行的表若未定期优化,碎片会持续积累。

  • OPTIMIZE TABLE 或 ALTER TABLE … ENGINE=InnoDB 可重建表、整理碎片,但需手动或定时执行。

八、存储引擎特性(如 MyISAM vs InnoDB)

  • MyISAM:删除记录后留下空洞,碎片明显,需定期执行 OPTIMIZE。

  • InnoDB:虽然有“插入缓冲”和“页合并”机制,但高并发写入或大量更新仍会产生碎片,尤其在独立表空间(file-per-table)模式下。

九、大对象字段的存储和更新(BLOB/TEXT)

  • 大字段可能导致行溢出(off-page storage),更新时容易造成原页空间浪费。

  • 频繁更新大字段会加剧碎片问题。

十、事务回滚或 MVCC 机制残留

  • InnoDB 的多版本并发控制(MVCC)会保留旧版本数据,直到无事务引用。

  • 回滚段或 undo log 中旧数据占用空间,若清理不及时,也可能形成逻辑碎片(不影响数据页,但占用存储)。

碎片会带来什么问题

MySQL 中的“碎片”虽然不会直接导致数据丢失或系统崩溃,但会带来一系列性能下降、资源浪费和运维复杂性增加的问题。

以下是碎片带来的主要负面影响:

一、性能下降(最核心问题)

  1. 查询性能降低

    • 碎片导致数据在磁盘或内存中物理不连续,查询时需要读取更多数据页才能获取完整结果。

    • 尤其影响范围查询(BETWEEN><)和全表扫描,I/O 次数增加,响应时间变长。

  2. 索引效率降低

    • 索引结构(如 B+ 树)若因碎片导致节点分散,会增加树的高度或节点访问次数。

    • 查询时需要加载更多索引页到内存,缓存命中率下降,执行计划变慢。

  3. 缓冲池利用率下降(Buffer Pool)

    • InnoDB 缓冲池缓存的是数据页。碎片页中有效数据少,大量缓存空间被“空洞”占用。

    • 导致真正需要的数据页无法被缓存,频繁从磁盘读取,拖慢整体性能。

二、存储空间浪费

  1. 占用额外磁盘空间

    • Data_free(表中未使用但未释放的空间)持续增长,实际数据量远小于表文件大小。

    • 举例:一个 10GB 的表,可能只有 6GB 是真实数据,其余 4GB 是碎片空洞。

  2. 备份/迁移成本增加

    • 备份工具(如 mysqldump、xtrabackup)会备份整个表文件(含碎片),导致备份文件更大、耗时更长。

    • 数据迁移、复制、同步也会因体积膨胀而效率降低。

三、写入性能受影响

  1. 插入效率降低

    • 当新数据无法填入现有碎片空洞时,MySQL 需分配新页,增加 I/O 和页管理开销。

    • 频繁页分裂(Page Split)也会拖慢 INSERT/UPDATE 操作。

  2. 更新操作成本上升:如果更新导致行迁移(Migration),需要写入新页 + 维护指针 + 清理旧位置,开销更大。

四、维护成本增加

  1. 需要定期人工干预

    • 必须定期执行 OPTIMIZE TABLE 或重建表,否则碎片持续累积。

    • 大表优化耗时长、锁表风险高(尤其 MyISAM),影响业务可用性。

  2. 监控和诊断复杂化

    • 需监控 information_schema.TABLES.Data_free、表大小增长率、查询执行时间变化等指标。

    • 性能问题排查时,碎片可能成为“隐藏元凶”,增加诊断难度。

五、扩展性和高可用受影响

  1. 主从复制延迟可能加剧:大量碎片表在主库执行 OPTIMIZE 会产生大量 binlog,从库重放时会造成复制延迟。

  2. 云数据库/容器环境成本上升

  • 在按存储计费的云环境(如 AWS RDS、阿里云 RDS)中,碎片导致“虚胖”表,多花冤枉钱。

  • 容器存储卷空间被无效占用,影响弹性扩缩容。

六、极端情况:系统稳定性风险

  • 表空间文件(如 ibdata1 或 .ibd 文件)过度膨胀,可能占满磁盘,导致数据库写入失败或服务中断。

  • 自动扩容机制若未配置,可能引发严重生产事故。

如何清理碎片

清理 MySQL 中的碎片是数据库日常维护的重要任务,目的是回收无效空间、提升查询性能、减少存储占用、优化缓存效率

不同存储引擎(如 InnoDB、MyISAM)清理方式略有不同,以下是完整、实用的碎片清理方案:

一、OPTIMIZE TABLE(通用方法)

推荐用于 MyISAM 和部分 InnoDB,Optimize语句可以重新组织表和索引的物理存储,减少存储空间,提高访问的IO效率

1
2
3
4
5
6
7
8
9
OPTIMIZE TABLE table_name;

-- 示例:
OPTIMIZE TABLE orders;
-- +----------------+----------+----------+---------------------------------------------------+
-- | Table | Op | Msg_type | Msg_text
-- +----------------+----------+----------+---------------------------------------------------+
-- | test.orders | optimize | status | OK
-- +----------------+----------+----------+---------------------------------------------------+
  • 重建表结构、整理碎片、回收未使用空间。

  • 对 MyISAM:效果显著,会重建索引和数据文件。

  • 对 InnoDB(独立表空间模式 innodb_file_per_table=1)才有作用:等价于 ALTER TABLE ... FORCE,重建表和索引。

注意事项

  • 锁表操作:执行期间表会被锁定(MyISAM 全锁,InnoDB 可能短暂锁)。数据量越大的表,优化耗时越长,百万条数据大约耗时30s(约25000-30000行/秒)。在磁盘优化时,所有的增删操作将受限。

  • 空间要求:需要额外磁盘空间存放临时表(约等于原表大小)。剩余空间必须 > 被optimize的表大小

  • 权限要求:使用这个语句需要对目标表具有select、insert权限

  • 主从复制:会记录到 binlog,在从库重放,可能引起复制延迟。

  • 大表慎用:建议在业务低峰期执行。

二、重建表(InnoDB 专用方法)

更灵活、推荐用于生产环境

1
2
3
4
5
6
7
8
9
10
-- 使用 ALTER TABLE 重建,本质上是recreate,期间支持DML查询和更新操作
ALTER TABLE table_name ENGINE=InnoDB;
-- MySQL 5.7+ 支持的语法,明确表示强制重建表,等价于更改引擎为当前引擎
ALTER TABLE table_name FORCE;

----------------------------------------------------------------------

-- 使用在线 DDL(MySQL 5.6+,推荐)
-- ALGORITHM=INPLACE:原地重建,减少空间占用。LOCK=NONE:尽量不锁表(取决于操作类型和版本)。
ALTER TABLE table_name ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;
  • 更可控,支持在线操作(部分场景)。

  • 可结合其他 DDL 一起执行(如加索引、改字段)。

操作过程

  1. 获取MDL(meta data lock)写锁,innodb内部创建与原表结构相同的临时文件

  2. 拷贝数据前,MDL写锁退化成读锁,支持DML更新操作

  3. 根据主键递增顺序,将数据读出并写到临时文件,直到全部写入,并且拷贝期间的DML更新操作会记录到Row log中

  4. 上锁,再将Row log中的数据应用到临时文件

  5. 互换原表和临时表的名字

  6. 删除临时表

三、分区表碎片清理(针对 RANGE/LIST 分区)

如果表是分区表,可以只优化特定碎片严重的分区,减少影响:

1
2
3
ALTER TABLE table_name OPTIMIZE PARTITION partition_name;
-- 或
ALTER TABLE table_name REBUILD PARTITION partition_name;

适用于日志表、时间分表等场景,按月/日分区,只清理历史分区。

四、批量清理多个表的碎片

方法1:SQL 脚本自动生成优化语句

1
2
3
4
5
SELECT CONCAT('OPTIMIZE TABLE ', table_schema, '.', table_name, ';') AS sql_stmt
FROM information_schema.tables
WHERE table_schema = 'your_database'
AND data_free > 1024 * 1024 * 100 -- 碎片大于100MB的表
AND engine IN ('InnoDB', 'MyISAM');

方法2:Shell 脚本 + MySQL 命令行(定时任务)

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
DB_USER="root"
DB_PASS="password"
DB_NAME="your_db"

mysql -u$DB_USER -p$DB_PASS -Nse "
SELECT CONCAT('OPTIMIZE TABLE ', table_schema, '.', table_name, ';')
FROM information_schema.tables
WHERE table_schema = '$DB_NAME'
AND data_free > 100*1024*1024
AND engine IN ('InnoDB','MyISAM');
" | mysql -u$DB_USER -p$DB_PASS

建议每周或每月低峰期执行一次。

五、监控碎片程度(执行前判断是否需要清理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 查看表碎片大小(Data_free):
SELECT
table_name,
engine,
data_length,
index_length,
data_free,
ROUND(data_free / (data_length + index_length) * 100, 2) AS frag_percent
FROM information_schema.tables
WHERE table_schema = 'your_database'
AND data_free > 0
ORDER BY data_free DESC;

-- 通过 `show table status like '表名'` 查看表的状态,如果data_free字段不为0则表示有碎片存在
SELECT
CONCAT( TRUNCATE ( SUM( data_length ) / 1024 / 1024, 2 ), 'MB' ) AS data_size,
CONCAT( TRUNCATE ( SUM( data_free ) / 1024 / 1024, 2 ), 'MB' ) AS data_free,
CONCAT( TRUNCATE ( SUM( index_length ) / 1024 / 1024, 2 ), 'MB' ) AS index_size
FROM
information_schema.TABLES
WHERE
TABLE_NAME = 'tableName';

建议:碎片率 > 10% 或碎片大小 > 1GB 的表优先优化。

六、特殊情况处理

  1. 共享表空间碎片(ibdata1)

    此操作风险高,仅建议在维护窗口或新系统部署时进行。

    • 如果使用系统表空间(innodb_file_per_table=OFF),所有表数据存在 ibdata1,碎片无法单独清理。

    • 解决方案:

      1. 备份所有数据(mysqldump)
      2. 停止 MySQL,删除 ibdata1、ib_logfile*
      3. 启用 innodb_file_per_table=ON
      4. 恢复数据 → 每个表独立 .ibd 文件,可单独优化
  2. 大表优化太慢?

    使用 pt-online-schema-change(Percona Toolkit)工具在线重建表,几乎无锁:支持大表在线优化,不影响业务读写。

    1
    pt-online-schema-change --alter "ENGINE=InnoDB" D=your_db,t=your_table --execute

预防碎片的最佳实践(治本之策)

方法 说明
使用自增主键 保证数据顺序插入,减少页分裂
避免频繁 UPDATE 大字段 如 TEXT/BLOB,更新易导致行迁移
定期归档历史数据 减少活跃数据量,降低碎片产生速度
合理设置页填充因子 InnoDB 默认已优化,一般无需调整
使用 SSD + 合理 buffer pool 减轻碎片对 I/O 的影响
监控 + 自动化脚本 定期检查碎片,自动触发优化

碎片清理操作速查表

场景 推荐命令 说明
小表 / MyISAM 表 OPTIMIZE TABLE t; 简单直接,会锁表
InnoDB 表重建 ALTER TABLE t ENGINE=InnoDB; 更灵活,支持在线选项
在线无锁优化 ALTER TABLE t ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE; MySQL 5.6+
分区表优化 ALTER TABLE t OPTIMIZE PARTITION p0; 针对性清理
批量清理 用 SQL 生成脚本 + 定时任务 自动化运维
超大表在线优化 pt-online-schema-change 第三方工具,推荐生产环境
  • 监控先行:不要盲目优化,先查碎片率和大小。

  • 低峰操作:选择业务低谷期执行,避免影响线上服务。

  • 备份保障:重要表优化前做备份。

  • 自动化 + 预防:建立定期维护机制 + 优化表设计,从源头减少碎片。

其他实用技巧

  • 使用连接池:避免频繁创建/销毁连接(HikariCP、Druid)

  • 避免大事务:拆分成小事务,减少锁竞争和回滚段压力

  • 合理使用锁:避免 SELECT ... FOR UPDATE 滥用

  • 批量操作:INSERT/UPDATE 使用批量语句,减少交互次数