俗话说的好,解铃还需系铃人,我们要通过现象看本质,为什么数据多了性能就会慢呢?为什么为什么为什么呢? 那我们来慢慢分析,我能想到以下内容,欢迎大家补充,我们一起共同进步:
我们在已知问题本质原因条件下,是不是再去想解决方案就会清晰很多了呢?因此我们来将他们逐个击破即可。
那这个时候我们就可以将慢查询语句保存到某个log中,然后通过EXPLAIN分析,光说不练假把式,为了方便我们创建一个名为CBook的表
CREATE TABLE `cbook` (
`m_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`m_isbn` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`m_pressInfo` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`m_price` decimal(10,2) DEFAULT NULL COMMENT '价格',
`m_page` int(11) DEFAULT NULL COMMENT '页数',
`m_author` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`m_num` int(11) DEFAULT NULL COMMENT '数量',
PRIMARY KEY (`m_isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='图书表';同时向表中插入一些数据:
INSERT INTO `cbook` (`m_name`, `m_isbn`, `m_pressInfo`, `m_price`, `m_page`, `m_author`, `m_num`)
VALUES
('数据结构与算法分析:C语言描述', '9787111571768', '机械工业出版社', 59.00, 454, 'Mark Allen Weiss', 10),
('计算机网络(第 7 版)', '9787115493033', '人民邮电出版社', 69.00, 508, 'Adrian·Tanenbaum', 15),
('Java 程序设计:从入门到精通', '9787302613470', '清华大学出版社', 88.00, 436, 'John Smith', 8);再补充几条数据:
INSERT INTO `cbook` VALUES
('平凡的世界', '9787020190512', '人民文学出版社', 39.00, 240, '路遥', 8),
('Python 程序设计', '9787111649068', '机械工业出版社', 119.00, 688, 'Eric Matthes', 12),
('计算机网络架构与原理', '9787302444863', '清华大学出版社', 89.00, 576, '吴功宜', 15),
('数据科学与应用', '9787539982536', '江苏凤凰文艺出版社', 45.00, 320, '张三', 6),
('深度学习基础', '9787544262335', '南海出版公司', 28.00, 192, '李四', 8);在EXPLAIN 输出的 12 个字段中,有六个最关键的字段是需要我们关注的,见下表。
字段 | 技术内涵 | 优化方向 |
|---|---|---|
id | 查询执行顺序标识符。数字越大优先级越高,相同 id 按顺序执行 | 识别嵌套查询层级,优化子查询逻辑 |
select_type | 查询类型(如 SIMPLE/PRIMARY/SUBQUERY)。反映 SQL 结构复杂度 | 简化复杂查询结构,减少临时表生成 |
type | 访问类型性能阶梯:system > const > eq_ref > ref > range > index > ALL | 消除 ALL 类型全表扫描,提升至 range 级别 |
key_len | 索引使用字节数。反映复合索引的有效利用率 | 优化索引字段顺序,减少冗余索引长度 |
rows | 预估扫描行数。与实际行数偏差超过 10 倍需更新统计信息 | 执行 ANALYZE TABLE 更新数据分布统计 |
Extra | 附加执行信息(如 Using index/Using temporary)。揭示隐藏性能问题 | 消除 Using filesort 等非预期操作 |
现在就让我们来使用EXPLAIN来更加具体的认识以上内容。 执行sql语句:
EXPLAIN SELECT * FROM cbook WHERE m_isbn = '9787020190512';结果如下:

从表中看到,我们的Extra字段是为NULL的,而为NULL的原因是: SELECT * 需要所有列数据,主键索引 PRIMARY(m_isbn) 仅包含 ISBN 列因此必须回表查询完整行数据 → 无法使用 Using index,若我们将sql改写为
-- 只查询索引列(Extra 显示 Using index)
EXPLAIN SELECT m_isbn FROM cbook WHERE m_isbn = '9787020190512';执行结果如下:

可以看到,Extra列内容变为了using index,这是因为改写后的sql满足以下 全部条件 时 Extra 才会显示 Using index:
由于我们查询列只需要m_isbn,且m_isbn为主键列,所以满足全部条件。 全字段解析(sql未改写前)
字段 | 当前值 | 技术含义 | 优化建议 |
|---|---|---|---|
type | const | 主键唯一查询最优级别,说明通过主键或唯一索引直接定位到唯一行 | 无需优化 |
key_len | 82 | varchar(20) utf8mb4 计算:20字符 × 4字节/字符 + 长度标识2字节 = 82字节 | 符合预期,索引定义合理 |
rows | 1 | 精确匹配主键的预估行数,统计信息与实际数据一致 | 统计信息准确,无需更新 |
Extra | NULL | 无额外操作说明,具体表现为:- 不需要文件排序- 不需要临时表- 虽需回表但不额外标注 | 正常现象,查询效率最优 |
同时需要大家注意以下内容:
我们再来执行下列sql:
EXPLAIN SELECT * FROM cbook WHERE m_page > 300;执行结果如下:

字段 | 当前值 | 技术含义 | 优化方向 |
|---|---|---|---|
type | ALL | 全表扫描(性能最差级别,需遍历表中所有行) | 为 m_page 字段添加索引 |
key | NULL | 未使用任何索引,查询未命中有效索引结构 | 建立针对性索引(如 INDEX(m_page)) |
rows | 8 | 表当前总行数(实际5行),数据量增大时显示预估扫描行数(如1000万行场景) | 优化统计信息(ANALYZE TABLE) |
Extra | Using where | 在存储引擎层完成数据过滤,未使用索引过滤条件 | 确保过滤条件命中索引 |
接下来我试试使用复合索引,我们先执行以下sql创建复合索引:
ALTER TABLE cbook
ADD INDEX idx_price (m_price),
ADD INDEX idx_author_press (m_author, m_pressInfo);然后执行下列sql语句:
EXPLAIN SELECT m_author, m_pressInfo
FROM cbook
WHERE m_author = 'Eric Matthes'
AND m_pressInfo LIKE '%机械%';执行结果如下:

字段 | 当前值 | 技术含义 | 优化建议 |
|---|---|---|---|
type | ref | 使用非唯一索引扫描(良好级别) | 无需优化 |
key_len | 403 | varchar(20) utf8mb4 计算:m_author varchar(100) = 100*4 + 2 = 402字节m_pressInfo 未被完全使用(因LIKE左模糊导致),null标志位1字节 | 符合预期,索引定义合理 |
rows | 1 | 精确匹配主键的预估行数,统计信息与实际数据一致 | 统计信息准确,无需更新 |
Extra | using index | 无额外操作说明,具体表现为:- 不需要文件排序- 不需要临时表- 虽需回表但不额外标注 | 正常现象,查询效率最优 |
这里需要注意,由于左模糊匹配会导致联合索引中m_pressInfo失效,但m_author仍然生效,我们可以执行以下sql测试:
-- 删除原索引
DROP INDEX idx_author_press ON cbook;
-- 创建反转顺序索引
CREATE INDEX idx_press_author ON cbook(m_pressInfo, m_author);
-- 重写查询(需改变条件顺序)
EXPLAIN SELECT m_author, m_pressInfo
FROM cbook
WHERE m_pressInfo LIKE '机械%' -- 改为右通配
AND m_author = 'Eric Matthes';
可以看到执行结果中,key_len变成了806,因此验证了左模糊匹配会导致联合索引中m_pressInfo失效的问题.
我们再来看看索引失效场景

可以看到,由于我们第一次price字段参与计算导致无法走索引,因此在索引列上进行计算操作,将导致索引失效。
经过以上EXPLAIN使用,相信大家已经学的差不多了,前辈们呢已经总结好了许多的常见的索引失效场景,因此,大家在考虑索引失效的时候,可以想想是不是有使用导致了索引失效,以下是常见的索引失效场景:
SELECT * 进行查询; SELECT * 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;% 开头(左匹配)的 LIKE 查询比如 like '%abc';违法最左匹配原则:

本质:通过删除旧索引并创建全新索引,消除物理存储碎片化,重构B+树结构。 关键过程:
表重建机制:
MySQL创建临时空表 → 按主键顺序读取旧表数据 → 按顺序插入新表
→ 重建全量索引 → 原子性重命名表碎片化消除逻辑:
操作类型 | 语法示例 | 底层实现 | 适用场景 |
|---|---|---|---|
全表重建 | ALTER TABLE [表名] ENGINE=InnoDB; | 重建表结构+所有索引 | 碎片化严重场景 |
优化重建 | OPTIMIZE TABLE [表名]; | 等价于表重建 | 常规碎片整理 |
单索引重建 | ALTER TABLE [表名] DROP INDEX [索引名], ADD INDEX [索引名](字段); | 仅重建指定索引 | 部分索引碎片化场景 |
优点 | 缺点 |
|---|---|
1. 碎片消除最彻底,性能提升显著 | 1. 大量消耗I/O和CPU资源(数据量×2的读写操作) |
2. 重建后索引统计信息完全更新 | 2. 可能触发长时间锁表(非在线模式下全表锁) |
3. 物理存储连续,随机I/O减少50%+ | 3. 在线重建仍可能导致短暂性能波动(如MySQL 5.6+的ALGORITHM=INPLACE) |

表名_old,新表重命名为原表名。表名_old相关文件。重建索引是解决索引碎片化的终极手段,但需在"性能需求"与"业务影响"间平衡。建议建立分级策略:
ANALYZE TABLE)OPTIMIZE TABLE在线重建查询语句:
SELECT
t.TABLE_NAME,
i.INDEX_NAME,
t.DATA_LENGTH,
t.INDEX_LENGTH,
t.DATA_FREE,
ROUND(t.DATA_FREE / t.INDEX_LENGTH * 100, 2) AS fragmentation_percentage
FROM
information_schema.TABLES t
LEFT JOIN
information_schema.STATISTICS i
ON t.TABLE_SCHEMA = i.TABLE_SCHEMA
AND t.TABLE_NAME = i.TABLE_NAME
WHERE
t.TABLE_SCHEMA = 'test' --替换成自己的数据库名称
AND t.TABLE_NAME = 'cbook'--替换成自己表名称
GROUP BY
t.TABLE_NAME, i.INDEX_NAME
ORDER BY
fragmentation_percentage DESC;
核心原则:聚集索引决定表的物理存储顺序,选择不当将导致严重碎片化
NEWSEQUENTIALID()生成有序GUIDUUID_TO_BIN(UUID(), 1)生成有序UUID同时,当存在以下三种情况时,不推荐使用索引:
CREATE TABLE logs (ts TIMESTAMP PRIMARY KEY, ...)核心作用:通过预留页空间减少页分裂,控制碎片生成速度
维度 | 实现细节 |
|---|---|
参数名称 | innodb_fill_factor(全局配置,默认100) |
作用范围 | 仅影响二级索引叶子节点页的填充率,不影响数据页(聚集索引叶子页) |
实际填充逻辑 | - 设置80%时,页填充至80%容量,预留20%空间- 即使设为100,仍保留1/16页空间(约1KB) |
生效时机 | 仅在创建新索引或重建索引/表时生效,现有索引需重建才能应用新设置 |
场景分类 | 推荐值 | 适用条件 | 空间 trade-off |
|---|---|---|---|
读多写少 | 100 | - 日志表(仅追加)- 历史数据归档表 | 初始空间利用率最高 |
频繁随机插入 | 80-90 | - 社交平台消息表- 电商评论表 | 初始空间增加20%,碎片率降低50% |
行长度易增长 | 70-80 | - 包含TEXT/VARCHAR字段且常更新的表- 博客内容表 | 初始空间增加30%,页分裂减少70% |
innodb_buffer_pool_size:缓冲池大小的黄金法则核心作用:InnoDB 将表数据和索引缓存至内存的区域,所有数据读写均经过缓冲池。
服务器类型 | 建议配置比例 | 示例(64GB 物理内存) |
|---|---|---|
专用数据库服务器 | 可用内存的 60%-80% | innodb_buffer_pool_size = 40G |
共享环境服务器 | 可用内存的 50%-60% | innodb_buffer_pool_size = 32G |
⚠ 避免误区:
-- 计算缓冲池命中率(理想值 > 99%)
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';指标 | 含义 |
|---|---|
Innodb_buffer_pool_read_requests | 缓冲池逻辑读请求总数(从内存读取) |
Innodb_buffer_pool_reads | 磁盘物理读请求数(内存未命中) |
命中率公式 | 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) |

例如在我的数据库中,执行此命令,可以看到Innodb_buffer_pool_read_requests值为2604,而Innodb_buffer_pool_reads的值为367,因此命中率为1-367/2604=0.86
警报阈值:
innodb_buffer_pool_instances:缓冲池实例数的并发优化核心作用:将单一缓冲池拆分为多个独立实例,分散锁竞争(如 buffer_pool_mutex)。
场景 | 建议实例数 | 计算方式 |
|---|---|---|
缓冲池 ≥ 1GB | ≥ 8 个实例 | 实例数 = 缓冲池大小 / 1GB |
高并发读写场景 | 8-16 个实例 | 参考 CPU 核心数或 NUMA 节点数 |
超大缓冲池(40GB+) | 40-64 个实例(上限 64) | 确保每个实例 ≥ 1GB |
innodb_buffer_pool_dump* 与 load*核心作用:解决 MySQL 重启后缓冲池冷启动导致的性能骤降,通过转储和加载热页加速缓存恢复。
参数 | 作用 |
|---|---|
innodb_buffer_pool_dump_at_shutdown | 关闭时转储最热页列表至 ib_buffer_pool 文件(默认 OFF) |
innodb_buffer_pool_load_at_startup | 启动时异步加载热页列表(默认 OFF) |
innodb_buffer_pool_dump_pct | 转储页比例(默认 25%,建议 25%-50%) |
-- 查看转储/加载状态
SHOW STATUS LIKE 'Innodb_buffer_pool_dump_status';
SHOW STATUS LIKE 'Innodb_buffer_pool_load_status';
-- 手动转储/加载(无需重启)
SET GLOBAL innodb_buffer_pool_dump_now = ON;
SET GLOBAL innodb_buffer_pool_load_now = ON;3. 缓冲池健康检查清单
缓冲池大小 / 实例数 ≥ 1GB?ib_buffer_pool 文件是否存在?当然我们除了对数据库参数进行调优,也可以通过提升系统硬件(更快的IO、更多的内存):带宽、CPU、硬盘等
这里博主只简单介绍一些可选方案,具体内容后续可能会更新,或再出一篇博客专门讲解
关于这个问题,我们更多能做的或许就是对架构与设计优化
优化方向 | 读写分离 | 热点分片 | 队列削峰 | 混合引擎 |
|---|---|---|---|---|
适用场景 | 读多写少 | 单行高频更新 | 瞬时流量突发 | 海量数据写入分析 |
复杂度 | 中 | 高 | 中 | 极高 |
成本 | 硬件成本增加 | 开发成本增加 | 中间件运维成本 | 多引擎运维成本 |
性能提升 | 读性能提升300%+ | 写性能提升50%+ | 峰值QPS提升10倍+ | 写入TPS达百万级 |
同时,实在不行,我们只有祭出我们的终极大招:
核心原则:无通用策略,需结合业务增长预期与技术瓶颈动态调整。
#mermaid-svg-9VmGCj7Rwdeoggeq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-9VmGCj7Rwdeoggeq .error-icon{fill:#552222;}#mermaid-svg-9VmGCj7Rwdeoggeq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9VmGCj7Rwdeoggeq .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-9VmGCj7Rwdeoggeq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9VmGCj7Rwdeoggeq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9VmGCj7Rwdeoggeq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9VmGCj7Rwdeoggeq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9VmGCj7Rwdeoggeq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9VmGCj7Rwdeoggeq .marker.cross{stroke:#333333;}#mermaid-svg-9VmGCj7Rwdeoggeq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9VmGCj7Rwdeoggeq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9VmGCj7Rwdeoggeq .cluster-label text{fill:#333;}#mermaid-svg-9VmGCj7Rwdeoggeq .cluster-label span{color:#333;}#mermaid-svg-9VmGCj7Rwdeoggeq .label text,#mermaid-svg-9VmGCj7Rwdeoggeq span{fill:#333;color:#333;}#mermaid-svg-9VmGCj7Rwdeoggeq .node rect,#mermaid-svg-9VmGCj7Rwdeoggeq .node circle,#mermaid-svg-9VmGCj7Rwdeoggeq .node ellipse,#mermaid-svg-9VmGCj7Rwdeoggeq .node polygon,#mermaid-svg-9VmGCj7Rwdeoggeq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9VmGCj7Rwdeoggeq .node .label{text-align:center;}#mermaid-svg-9VmGCj7Rwdeoggeq .node.clickable{cursor:pointer;}#mermaid-svg-9VmGCj7Rwdeoggeq .arrowheadPath{fill:#333333;}#mermaid-svg-9VmGCj7Rwdeoggeq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9VmGCj7Rwdeoggeq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9VmGCj7Rwdeoggeq .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-9VmGCj7Rwdeoggeq .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-9VmGCj7Rwdeoggeq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9VmGCj7Rwdeoggeq .cluster text{fill:#333;}#mermaid-svg-9VmGCj7Rwdeoggeq .cluster span{color:#333;}#mermaid-svg-9VmGCj7Rwdeoggeq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9VmGCj7Rwdeoggeq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
是
是
否
否
单表数据量/并发是否瓶颈?
分表能否解决?
仅分表
分库+分表
持续监控
指标 | 分表阈值 | 分库阈值 |
|---|---|---|
单表数据量 | >500万行 | >5000万行 |
单库QPS | >2000 | >10000 |
磁盘I/O利用率 | >70% | >90% |
核心价值:拆分单表数据量,提升查询效率,但无法突破单库I/O瓶颈。
垂直分表(字段拆分)
将热数据字段与冷数据字段分离:
-- 原表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
phone VARCHAR(20),
address TEXT,
create_time DATETIME,
profile TEXT -- 冷数据字段
);
-- 拆分为热表与冷表
CREATE TABLE users_hot (id, name, phone, create_time);
CREATE TABLE users_cold (id, address, profile);水平分表(行拆分)
按时间分表(如每月一个表):
CREATE TABLE orders_202506 (like orders);
CREATE TABLE orders_202507 (like orders);按ID取模分表:
-- 10张表
CREATE TABLE orders_0 (like orders);
CREATE TABLE orders_1 (like orders);
-- ... 直到orders_9核心价值:将数据分散到多个数据库实例,提升整体并发能力与资源利用率。
策略 | 分片规则 | 适用场景 | 实现复杂度 |
|---|---|---|---|
垂直分库 | 按业务模块拆分(如用户库、订单库) | 业务解耦、模块间低关联 | 低 |
水平分库 | 按ID取模、哈希等拆分 | 高并发、海量数据场景 | 高 |
混合分库 | 垂直+水平结合 | 复杂业务架构 | 极高 |
分库规则:
def get_db_id(user_id):
return user_id % 4 # 4个数据库实例分表规则:
def get_table_id(user_id):
return user_id % 10 # 每个库10张表表结构:
-- 库1中的表
CREATE TABLE users_0 (id, name, ...);
CREATE TABLE users_1 (id, name, ...);
-- 库2中的表同理方案 | 解决问题 | 成本 | 技术复杂度 |
|---|---|---|---|
分表 | 单表查询慢 | 硬件成本低 | 中 |
分库分表 | 单库并发瓶颈 | 硬件成本高 | 高 |
读写分离 | 读多写少场景 | 中等硬件成本 | 中 |
混合引擎 | 海量写入分析 | 多引擎运维成本 | 极高 |
这篇博客就到此结束,我们下次再见!!!!