前言
💡 痛点: EXPLAIN 看得一头雾水?索引失效频繁发生?慢查询优化全靠猜?连接池配置一塌糊涂?
🎯 解决方案: 从索引结构原理→慢查询分析→执行计划解读→索引设计规范,系统掌握 MySQL 性能优化。
#mermaid-svg-5cm3AScfaPDu5gs3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5cm3AScfaPDu5gs3 .error-icon{fill:#552222;}#mermaid-svg-5cm3AScfaPDu5gs3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5cm3AScfaPDu5gs3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5cm3AScfaPDu5gs3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5cm3AScfaPDu5gs3 .marker.cross{stroke:#333333;}#mermaid-svg-5cm3AScfaPDu5gs3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5cm3AScfaPDu5gs3 p{margin:0;}#mermaid-svg-5cm3AScfaPDu5gs3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5cm3AScfaPDu5gs3 .cluster-label text{fill:#333;}#mermaid-svg-5cm3AScfaPDu5gs3 .cluster-label span{color:#333;}#mermaid-svg-5cm3AScfaPDu5gs3 .cluster-label span p{background-color:transparent;}#mermaid-svg-5cm3AScfaPDu5gs3 .label text,#mermaid-svg-5cm3AScfaPDu5gs3 span{fill:#333;color:#333;}#mermaid-svg-5cm3AScfaPDu5gs3 .node rect,#mermaid-svg-5cm3AScfaPDu5gs3 .node circle,#mermaid-svg-5cm3AScfaPDu5gs3 .node ellipse,#mermaid-svg-5cm3AScfaPDu5gs3 .node polygon,#mermaid-svg-5cm3AScfaPDu5gs3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5cm3AScfaPDu5gs3 .rough-node .label text,#mermaid-svg-5cm3AScfaPDu5gs3 .node .label text,#mermaid-svg-5cm3AScfaPDu5gs3 .image-shape .label,#mermaid-svg-5cm3AScfaPDu5gs3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5cm3AScfaPDu5gs3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5cm3AScfaPDu5gs3 .rough-node .label,#mermaid-svg-5cm3AScfaPDu5gs3 .node .label,#mermaid-svg-5cm3AScfaPDu5gs3 .image-shape .label,#mermaid-svg-5cm3AScfaPDu5gs3 .icon-shape .label{text-align:center;}#mermaid-svg-5cm3AScfaPDu5gs3 .node.clickable{cursor:pointer;}#mermaid-svg-5cm3AScfaPDu5gs3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5cm3AScfaPDu5gs3 .arrowheadPath{fill:#333333;}#mermaid-svg-5cm3AScfaPDu5gs3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5cm3AScfaPDu5gs3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5cm3AScfaPDu5gs3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5cm3AScfaPDu5gs3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5cm3AScfaPDu5gs3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5cm3AScfaPDu5gs3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5cm3AScfaPDu5gs3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5cm3AScfaPDu5gs3 .cluster text{fill:#333;}#mermaid-svg-5cm3AScfaPDu5gs3 .cluster span{color:#333;}#mermaid-svg-5cm3AScfaPDu5gs3 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-5cm3AScfaPDu5gs3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5cm3AScfaPDu5gs3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5cm3AScfaPDu5gs3 .icon-shape,#mermaid-svg-5cm3AScfaPDu5gs3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5cm3AScfaPDu5gs3 .icon-shape p,#mermaid-svg-5cm3AScfaPDu5gs3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5cm3AScfaPDu5gs3 .icon-shape .label rect,#mermaid-svg-5cm3AScfaPDu5gs3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5cm3AScfaPDu5gs3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5cm3AScfaPDu5gs3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5cm3AScfaPDu5gs3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 优化流程
慢查询日志
EXPLAIN 分析
索引检查
SQL 重写
新增/删除索引
性能验证
索引结构
InnoDB 索引
聚簇索引
主键=B+树,叶子存整行数据
辅助索引
主键=B+树,叶子存主键值
叶子节点1
完整数据行
叶子节点2
完整数据行
Page 16KB
Page 16KB
MySQL 8.0 新特性速览:
| 特性 | 说明 | 性能影响 |
|---|---|---|
| Instant ADD COLUMN | 秒级添加字段(不锁表) | DDL 不阻塞写入 |
| 原子 DDL | DDL 操作可回滚 | 数据安全提升 |
| 窗口函数 | ROW_NUMBER / RANK / LAG | SQL 简化,避免自关联 |
| CTE | WITH 子句,递归查询 | 代码可读性大幅提升 |
| Hash Join | 大表关联优化(替代 BNL) | JOIN 性能提升 10x |
| 不可见索引 | INVISIBLE 索引(灰度验证) |
线上安全调优 |
| 降序索引 | idx(a DESC, b ASC) |
避免 filesort |
一、InnoDB 索引结构原理
1.1 B+ 树 vs B 树 vs 二叉树
#mermaid-svg-Hff9BhiDMzokqrbY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Hff9BhiDMzokqrbY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Hff9BhiDMzokqrbY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Hff9BhiDMzokqrbY .error-icon{fill:#552222;}#mermaid-svg-Hff9BhiDMzokqrbY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Hff9BhiDMzokqrbY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Hff9BhiDMzokqrbY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Hff9BhiDMzokqrbY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Hff9BhiDMzokqrbY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Hff9BhiDMzokqrbY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Hff9BhiDMzokqrbY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Hff9BhiDMzokqrbY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Hff9BhiDMzokqrbY .marker.cross{stroke:#333333;}#mermaid-svg-Hff9BhiDMzokqrbY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Hff9BhiDMzokqrbY p{margin:0;}#mermaid-svg-Hff9BhiDMzokqrbY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Hff9BhiDMzokqrbY .cluster-label text{fill:#333;}#mermaid-svg-Hff9BhiDMzokqrbY .cluster-label span{color:#333;}#mermaid-svg-Hff9BhiDMzokqrbY .cluster-label span p{background-color:transparent;}#mermaid-svg-Hff9BhiDMzokqrbY .label text,#mermaid-svg-Hff9BhiDMzokqrbY span{fill:#333;color:#333;}#mermaid-svg-Hff9BhiDMzokqrbY .node rect,#mermaid-svg-Hff9BhiDMzokqrbY .node circle,#mermaid-svg-Hff9BhiDMzokqrbY .node ellipse,#mermaid-svg-Hff9BhiDMzokqrbY .node polygon,#mermaid-svg-Hff9BhiDMzokqrbY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Hff9BhiDMzokqrbY .rough-node .label text,#mermaid-svg-Hff9BhiDMzokqrbY .node .label text,#mermaid-svg-Hff9BhiDMzokqrbY .image-shape .label,#mermaid-svg-Hff9BhiDMzokqrbY .icon-shape .label{text-anchor:middle;}#mermaid-svg-Hff9BhiDMzokqrbY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Hff9BhiDMzokqrbY .rough-node .label,#mermaid-svg-Hff9BhiDMzokqrbY .node .label,#mermaid-svg-Hff9BhiDMzokqrbY .image-shape .label,#mermaid-svg-Hff9BhiDMzokqrbY .icon-shape .label{text-align:center;}#mermaid-svg-Hff9BhiDMzokqrbY .node.clickable{cursor:pointer;}#mermaid-svg-Hff9BhiDMzokqrbY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Hff9BhiDMzokqrbY .arrowheadPath{fill:#333333;}#mermaid-svg-Hff9BhiDMzokqrbY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Hff9BhiDMzokqrbY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Hff9BhiDMzokqrbY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hff9BhiDMzokqrbY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Hff9BhiDMzokqrbY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hff9BhiDMzokqrbY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Hff9BhiDMzokqrbY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Hff9BhiDMzokqrbY .cluster text{fill:#333;}#mermaid-svg-Hff9BhiDMzokqrbY .cluster span{color:#333;}#mermaid-svg-Hff9BhiDMzokqrbY 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-Hff9BhiDMzokqrbY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Hff9BhiDMzokqrbY rect.text{fill:none;stroke-width:0;}#mermaid-svg-Hff9BhiDMzokqrbY .icon-shape,#mermaid-svg-Hff9BhiDMzokqrbY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hff9BhiDMzokqrbY .icon-shape p,#mermaid-svg-Hff9BhiDMzokqrbY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Hff9BhiDMzokqrbY .icon-shape .label rect,#mermaid-svg-Hff9BhiDMzokqrbY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hff9BhiDMzokqrbY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Hff9BhiDMzokqrbY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Hff9BhiDMzokqrbY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} B树 vs B+树区别
B树: 所有节点存数据
B+树: 仅叶子存数据
内部节点只存索引
B+树结构
root节点
内部节点A
30
内部节点B
60
叶子15,20,30
叶子40,45,50
叶子60,70,80
叶子90,95,100
Page 16KB
Page 16KB
sql
-- ===== B+树 深度计算 =====
-- 一棵 3 层 B+树能存多少数据?
-- InnoDB Page 大小:16KB
-- 假设主键为 BIGINT:8字节
-- 每行数据估算:1KB(含 VARCHAR 平均长度)
-- 每页可存指针数 = 16KB / 8B = 2048 个
-- 每页可存数据行 = 16KB / 1KB = 16 行
-- 根节点:2048 个指针 → 指向 2048 个内部节点
-- 每个内部节点:2048 个指针 → 指向 2048 个叶子
-- 叶子层:2048 × 2048 × 16 = 67,108,864 行 ≈ 6700 万行!
-- 结论:3 层 B+树足以支撑千万级数据,且树高稳定为 3
-- 任何查询只需 3 次磁盘 I/O(根 → 内部 → 叶子)
-- ===== 聚簇索引 vs 辅助索引 =====
-- 聚簇索引(Clustered Index)
-- 特点:叶节点存储完整数据行,表只能有一个聚簇索引
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY, -- 主键 → 聚簇索引
customer_id BIGINT,
amount DECIMAL(10,2),
created_at DATETIME
);
-- 查询:SELECT * FROM orders WHERE order_id = 100;
-- 执行:聚簇索引树搜索 → 直接命中数据行(覆盖索引,无需回表)
-- 辅助索引(Secondary Index)
-- 特点:叶节点存储索引列 + 主键值
CREATE INDEX idx_customer ON orders(customer_id);
-- 查询:SELECT * FROM orders WHERE customer_id = 999;
-- 执行:辅助索引树搜索(idx_customer)→ 获得主键 → 回表查询聚簇索引 → 获取完整行
-- 回表:select * 会导致回表(需访问聚簇索引获取完整数据)
-- 覆盖索引优化(无需回表)
SELECT customer_id, order_id FROM orders WHERE customer_id = 999;
-- 执行:idx_customer 叶子已包含 select 所需字段,无需回表
1.2 联合索引与最左前缀原则
sql
-- ===== 联合索引结构 =====
-- 创建联合索引
CREATE INDEX idx_user_status_time ON users(status, type, created_at);
-- 索引结构(B+树,按建索引的顺序排序):
-- 第1层:status (ENUM: 0=禁用, 1=正常, 2=冻结)
-- 第2层:type (INT: 1=免费, 2=付费, 3=企业)
-- 第3层:created_at (DATETIME)
-- 等值 + 范围组合:
-- ✅ SELECT * FROM users WHERE status = 1;
-- → 只用 status 列,走索引
--
-- ✅ SELECT * FROM users WHERE status = 1 AND type = 2;
-- → 走 status + type,索引有效
--
-- ✅ SELECT * FROM users WHERE status = 1 AND type = 2 AND created_at > '2024-01-01';
-- → 全部走索引(最左前缀 + 范围 on created_at)
--
-- ✅ SELECT * FROM users WHERE status = 1 AND created_at > '2024-01-01';
-- → 走 status(碰到 type 列时断了),created_at 无法利用索引
--
-- ❌ SELECT * FROM users WHERE type = 2;
-- → 不走索引(最左前缀不满足,跳过 status)
--
-- ❌ SELECT * FROM users WHERE created_at > '2024-01-01';
-- → 不走索引(最左列 status 完全跳过)
-- ===== 索引列顺序选择 =====
-- 原则:区分度高的列放前面
-- status(3种) vs type(3种) vs created_at(亿级)
-- ❌ 错误顺序:created_at 区分度最高,却在最后
CREATE INDEX idx_bad ON users(status, type, created_at);
-- ✅ 正确顺序:区分度高的 created_at 放最后
CREATE INDEX idx_good ON users(created_at, type, status);
-- ===== Index Condition Pushdown(ICP)=====
-- MySQL 5.6+ 支持 ICP
-- 将 WHERE 条件下推到索引层面,减少回表次数
EXPLAIN SELECT * FROM users
WHERE status = 1
AND type IN (2, 3)
AND nickname LIKE '张%';
-- 无 ICP:先通过 (status, type) 找到主键,逐一回表,再过滤 nickname
-- 有 ICP:直接在索引中过滤 nickname,无需回表
二、慢查询分析与诊断
2.1 慢查询日志配置
sql
-- ===== 慢查询日志配置 =====
-- 查看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_output';
-- 临时开启(重启失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SET GLOBAL log_output = 'TABLE,FILE'; -- 同时输出到表和文件
SET GLOBAL slow_query_log_file = '/var/lib/mysql/mysql-slow.log';
-- 永久配置(my.cnf / my.ini)
-- [mysqld]
-- slow_query_log = 1
-- slow_query_log_file = /var/lib/mysql/mysql-slow.log
-- long_query_time = 1
-- log_queries_not_using_indexes = 1 -- 记录未使用索引的查询
-- ===== mysql.slow_log 表(MySQL 8.0)=====
-- MySQL 8.0 将慢查询记录到 slow_log 表(system_time_zone 支持)
SELECT
start_time,
query_time,
lock_time,
rows_sent,
rows_examined,
db,
sql_text
FROM mysql.slow_log
ORDER BY query_time DESC
LIMIT 10;
-- ===== pt-query-digest 分析慢查询 =====
-- pt-query-digest 是 Percona Toolkit 工具,分析慢查询日志
-- 安装
-- yum install percona-toolkit # CentOS
-- apt install percona-toolkit # Ubuntu
-- 分析慢查询日志
-- pt-query-digest /var/lib/mysql/mysql-slow.log
-- 分析结果示例:
-- # Profile
-- # Rank Query ID Response time Calls R/Call Item
-- # ==== =========== =============== ===== ======= ====
-- # 1 0x1234... 5.2341 45.2% 500 0.0105 SELECT orders
-- # 2 0x5678... 2.1234 18.3% 200 0.0106 UPDATE users
-- 输出优化建议:
-- Query_time > 0.5s: 考虑添加索引
-- Rows_examined > 10×Rows_sent: 考虑覆盖索引
2.2 performance_schema 诊断
sql
-- ===== 开启 performance_schema =====
SHOW VARIABLES LIKE 'performance_schema';
-- 默认 ON,无需配置
-- ===== 监控 SQL 执行统计 =====
-- 按 SQL 文本分组统计(耗时TOP)
SELECT
DIGEST_TEXT AS sql_query,
COUNT_STAR AS exec_count,
SUM_TIMER_WAIT / 1000000000000 AS total_time_sec,
AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec,
SUM_ROWS_EXAMINED AS rows_scanned,
SUM_ROWS_SENT AS rows_sent,
SUM_SORT_MERGE_PASSES AS sort_merge_passes,
SUM_SORT_ROWS AS rows_sorted,
SUM_NO_INDEX_USED AS no_index_count,
SUM_NO_GOOD_INDEX_USED AS bad_index_count
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;
-- ===== 监控表级 I/O =====
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
COUNT_FETCH AS fetch_count,
SUM_NUMBER_OF_BYTES_READ AS bytes_read,
COUNT_INSERT AS insert_count,
COUNT_UPDATE AS update_count,
COUNT_DELETE AS delete_count
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema')
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- ===== 监控索引使用情况 =====
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
COUNT_FETCH,
COUNT_INSERT,
COUNT_UPDATE,
COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'shop'
ORDER BY COUNT_FETCH DESC;
-- 辅助定位:哪些索引从未被使用(可安全删除)
三、EXPLAIN 执行计划详解
3.1 各字段含义
sql
-- ===== EXPLAIN ANALYZE(MySQL 8.0)=====
EXPLAIN ANALYZE
SELECT u.id, u.username, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
GROUP BY u.id
ORDER BY order_count DESC
LIMIT 10;
-- 输出示例:
-- -> Limit: 10 row(s) (cost=1234.45 rows=10) (actual time=45.67..45.89 rows=10 loops=1)
-- -> Sort: <temporary>.<filesort>, with temporary table using <temporary>.<sort_key>
-- -> Table scan on <auto_distinct_key> using index idx_status on u (cost=...)
-- -> Nested loop left join (cost=...)
-- -> Index lookup on u using idx_status (status=1) (actual...)
-- -> Index range scan on o using idx_user_id over (u.id) (actual...)
-- 关键信息:
-- cost=1234.45:优化器估算成本
-- actual time=45.67..45.89:实际执行时间范围
-- rows=10 loops=1:返回10行,循环1次
-- Table scan:全表扫描(注意)
-- Index lookup:索引查找(高效)
-- ===== type 字段(访问类型,从优到劣)=====
-- system:表只有一行(系统表)
-- const:主键/唯一索引等值查询,最多匹配一行
EXPLAIN SELECT * FROM users WHERE id = 1; -- const
-- eq_ref:关联查询,使用主键或唯一索引
EXPLAIN SELECT * FROM orders o, users u WHERE o.user_id = u.id; -- eq_ref
-- ref:普通索引等值查询
EXPLAIN SELECT * FROM orders WHERE user_id = 999; -- ref
-- range:索引范围扫描
EXPLAIN SELECT * FROM orders WHERE id > 100 AND id < 200; -- range
-- index:全索引扫描(比全表扫描好,但不如 range)
EXPLAIN SELECT id, username FROM users; -- index (覆盖索引)
-- ALL:全表扫描(最差,需优化)
EXPLAIN SELECT * FROM users WHERE username = 'zhangsan'; -- ALL
-- ===== key_len 计算 =====
-- 估算索引使用程度
EXPLAIN SELECT * FROM users WHERE status = 1 AND type = 2;
-- key_len = status(1字节 + NULL标志1字节) + type(4字节 + NULL标志1字节) = 7B
-- 如果 key_len < 索引总长度,说明只用了部分索引
-- ===== rows 字段 =====
-- 估算需要扫描的行数(不是返回行数)
-- rows 越大,扫描成本越高
EXPLAIN SELECT * FROM orders WHERE status = 0; -- status=0 占95%,rows 很大
-- ===== Extra 字段(关键优化提示)=====
-- Using filesort:需要额外排序(内存或磁盘),尽量避免
EXPLAIN SELECT * FROM orders ORDER BY created_at DESC; -- Using filesort
-- Using temporary:使用临时表,尽量避免
EXPLAIN SELECT username, COUNT(*) FROM orders GROUP BY username; -- Using temporary
-- Using index:覆盖索引,无需回表
EXPLAIN SELECT id, status FROM orders WHERE status = 1; -- Using index
-- Using index condition:索引条件下推(ICP)
EXPLAIN SELECT * FROM orders WHERE status = 1 AND amount > 100; -- Using index condition
-- Using where:服务层额外过滤
EXPLAIN SELECT * FROM orders WHERE amount > 100; -- Using where
-- Using join buffer (Block Nested Loop):BNL 大表关联,性能差
EXPLAIN SELECT * FROM orders, users WHERE orders.user_id = users.id; -- BNL
-- Using MRR:Multi-Range Read,优化随机 I/O
EXPLAIN SELECT * FROM orders WHERE id IN (1, 5, 10);
3.2 常见优化案例
sql
-- ===== 案例1: 深分页优化 =====
-- ❌ 深分页(越翻越慢)
SELECT * FROM orders
WHERE status = 1
ORDER BY id DESC
LIMIT 1000000, 10;
-- 分析:LIMIT offset 越大,MySQL 需要跳过越多行
-- 解决1: 游标分页(利用主键连续性)
SELECT * FROM orders
WHERE status = 1 AND id < 1000000
ORDER BY id DESC
LIMIT 10;
-- 上一次查询最后一条 id=1000000,本次用 id < 1000000
-- 解决2: 延迟关联(利用覆盖索引回表)
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE status = 1
ORDER BY id DESC
LIMIT 1000000, 10
) AS t ON o.id = t.id;
-- ===== 案例2: COUNT(*) 优化 =====
-- ❌ 全表 COUNT(慢)
SELECT COUNT(*) FROM orders WHERE status = 1;
-- 方案1: 添加条件索引(快速定位)
CREATE INDEX idx_status ON orders(status);
-- COUNT(*) 只扫描索引树,不扫描数据
-- 方案2: 缓存计数(Redis)
-- 写操作时同步更新 Redis 计数器
-- 方案3: 近似 COUNT(数据量要求不精确时)
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'shop' AND TABLE_NAME = 'orders';
-- ===== 案例3: 字符串索引优化 =====
-- ❌ 前缀 LIKE 导致索引失效
SELECT * FROM users WHERE phone LIKE '%138%'; -- 前后都有%,索引失效
-- 方案1: 使用覆盖索引 + 全文索引
CREATE FULLTEXT INDEX ft_phone ON users(phone);
SELECT * FROM users WHERE MATCH(phone) AGAINST('138');
-- 方案2: 反向索引(适合后缀查询)
-- phone 存储为反向字符串 + 触发器维护
-- 方案3: Elasticsearch(适合亿级数据)
四、索引设计规范
4.1 索引创建原则
sql
-- ===== 索引创建决策树 =====
/*
创建索引
│
┌─────── 字段类型 ───────┐
│ │
主键/唯一 ├─ 等值查询 ├─ 排序/分组 ├─ 范围查询
│ │ 频繁 │ 频繁 │ 频繁
▼ ▼ ▼ ▼
聚簇索引 B+树索引 B+树索引 B+树索引
│ │ (配合DESC) │
区分度>20% 联合索引+排序 最左前缀+范围放最后
*/
-- ===== 高效索引设计案例 =====
-- 场景1: 用户表,按 status + type 查询,按 created_at 排序
-- 查询:WHERE status = ? AND type = ? ORDER BY created_at DESC
-- ❌ 错误设计
CREATE INDEX idx_bad ON users(status, created_at, type);
-- 问题:排序字段在中间,查询时无法同时利用索引排序
-- ✅ 正确设计(排序放最后,且符合最左前缀)
CREATE INDEX idx_good ON users(status, type, created_at DESC);
-- 场景2: 订单表,支持 customer_id + status + 时间范围查询
-- 查询:WHERE customer_id = ? AND status IN (1,2) AND created_at BETWEEN ? AND ?
-- ✅ 联合索引
CREATE INDEX idx_orders_lookup ON orders(customer_id, status, created_at);
-- 场景3: SELECT * 的索引失效场景
-- 查询:SELECT * FROM orders WHERE user_id = ? AND status = 1
-- 如果 user_id 有索引,但 status 无索引,且返回所有列
-- → 会回表,性能差
-- ✅ 覆盖索引优化
CREATE INDEX idx_cover ON orders(user_id, status);
-- 如果 select 只查 user_id, status, id 三列,全部在索引中,无需回表
-- ===== 索引下推 ICP 示例 =====
-- 查询:SELECT * FROM orders WHERE user_id = 1 AND status = 1 AND amount > 100
-- 无 ICP(MySQL 5.5):
-- 1. 在 (user_id) 索引树找到 user_id=1 的所有记录(假设1000条)
-- 2. 逐一回表,检查 status=1 AND amount>100
-- → 回表 1000 次
-- 有 ICP(MySQL 5.6+):
-- 1. 在 (user_id, status, amount) 索引树找到 user_id=1 的所有记录
-- 2. 索引层直接过滤 status=1 AND amount>100
-- → 只回表符合条件的那几十条
-- → 减少回表次数
-- ===== 不可见索引(灰度验证)=====
-- 创建不可见索引(不影响查询)
CREATE INDEX idx_test ON orders(customer_id) INVISIBLE;
-- 验证查询是否走索引
EXPLAIN SELECT * FROM orders WHERE customer_id = 1;
-- Extra: Using index condition (如果走了不可见索引)
-- 线上验证:确认性能提升后,使索引可见
ALTER INDEX idx_test VISIBLE;
-- 验证后确认有问题,使索引不可见(不影响服务)
ALTER INDEX idx_test INVISIBLE;
-- 最终删除(确认无误后)
DROP INDEX idx_test ON orders;
4.2 索引失效场景
sql
-- ===== 索引失效清单 =====
-- 1. 函数/运算:索引列参与计算
SELECT * FROM orders WHERE YEAR(created_at) = 2024; -- ❌
SELECT * FROM orders WHERE created_at >= '2024-01-01'; -- ✅
-- 2. 类型转换:字符串列用数字查询
EXPLAIN SELECT * FROM users WHERE phone = 13812345678; -- ❌(隐式转换)
EXPLAIN SELECT * FROM users WHERE phone = '13812345678'; -- ✅
-- 3. 前缀通配符:LIKE '%xxx'
SELECT * FROM users WHERE name LIKE '%张三'; -- ❌
SELECT * FROM users WHERE name LIKE '张%'; -- ✅(后缀通配符可以用索引)
-- 4. OR 混用:非索引列 OR
SELECT * FROM users WHERE id = 1 OR email = 'x@x.com'; -- ❌(email无索引)
-- 改用 UNION
SELECT * FROM users WHERE id = 1
UNION
SELECT * FROM users WHERE email = 'x@x.com';
-- 5. NOT / != / <> / IS NOT NULL
SELECT * FROM users WHERE status != 1; -- ❌(范围查询,索引部分有效)
SELECT * FROM users WHERE status IS NOT NULL; -- ❌
-- 6. 联合索引跳过最左列
CREATE INDEX idx ON users(a, b, c);
SELECT * FROM users WHERE b = 1; -- ❌(跳过 a)
-- 7. MySQL 优化器判断:数据量太小(全表扫描更快)
SELECT * FROM users WHERE status = 1; -- ❌(status=1 占95%数据)
-- 解决:FORCE INDEX 或添加其他过滤条件
-- ===== optimizer_switch 调优 =====
-- 查看优化器开关
SHOW VARIABLES LIKE 'optimizer_switch';
-- 关闭 ICP(特定场景)
SET SESSION optimizer_switch = 'index_condition_pushdown=off';
-- 开启 MRR(随机 I/O 优化)
SET SESSION optimizer_switch = 'mrr_cost_based=on';
-- 使用 HASH JOIN(MySQL 8.0,默认 ON)
SET SESSION optimizer_switch = 'hash_join=on';
五、InnoDB 锁机制
5.1 锁类型与兼容矩阵
sql
-- ===== InnoDB 锁类型 =====
-- 共享锁(S锁):允许事务读取行
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;
-- 排他锁(X锁):允许事务读取或更新行
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
UPDATE orders SET amount = 100 WHERE id = 1; -- 自动加 X 锁
-- ===== 锁兼容矩阵 =====
/*
S锁 X锁
S锁 ✅ ❌
X锁 ❌ ❌
*/
-- ===== 记录锁(Record Lock)=====
-- 对索引记录加锁
BEGIN;
SELECT * FROM orders WHERE id = 5 LOCK IN SHARE MODE; -- 锁定 id=5 的记录
-- 锁范围:id=5 这一行
-- ===== 间隙锁(Gap Lock)=====
-- 锁定索引记录之间的间隙(防止幻读)
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 10 AND 20 LOCK IN SHARE MODE;
-- 锁定 (5, 10) 和 (20, 30) 之间的间隙,防止插入 id=15 的新记录
-- 目的:防止幻读(同一事务两次查询结果不一致)
-- ===== Next-Key Lock(记录锁 + 间隙锁)=====
-- 默认 RR 隔离级别使用 Next-Key Lock
BEGIN;
SELECT * FROM orders WHERE id >= 10 AND id <= 20 LOCK IN SHARE MODE;
-- 锁定:id=10, id=20, 以及 (10,20) 之间的间隙
-- ===== 意向锁(Intention Lock)=====
-- 表级锁,表示事务有意向对某行加锁
-- 意向共享锁(IS):事务打算给行加 S 锁
-- 意向排他锁(IX):事务打算给行加 X 锁
-- 锁兼容矩阵:
/*
IS IX S X
IS ✅ ✅ ✅ ❌
IX ✅ ✅ ❌ ❌
S ✅ ❌ ✅ ❌
X ❌ ❌ ❌ ❌
*/
-- ===== 死锁检测 =====
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- 默认 ON:启用死锁检测(主动检测并回滚一个事务)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 默认 50 秒:锁等待超时(被动超时回滚)
-- ===== 模拟死锁 =====
-- T1:
BEGIN;
SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- 锁 id=1
UPDATE orders SET amount = 200 WHERE id = 2; -- 等待 id=2
-- T2:
BEGIN;
SELECT * FROM orders WHERE id = 2 FOR UPDATE; -- 锁 id=2
UPDATE orders SET amount = 300 WHERE id = 1; -- 等待 id=1,触发死锁!
-- MySQL 自动选择一个事务回滚
-- ERROR 1213 (40001): Deadlock found when trying to get lock
-- ===== 锁等待排查 =====
-- 查看当前锁等待
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.INNODB_LOCK_WAITS w
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
-- 查看当前所有锁
SELECT * FROM information_schema.INNODB_LOCKS;
-- 查看当前事务
SELECT * FROM information_schema.INNODB_TRX\G;
5.2 事务隔离级别
sql
-- ===== 隔离级别对比 =====
-- 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION transaction_isolation = 'REPEATABLE-READ'; -- MySQL 默认
-- ===== 四种隔离级别 =====
-- 1. READ UNCOMMITTED(读未提交)
-- 脏读:可能读到其他事务未提交的数据
-- 2. READ COMMITTED(读已提交)
-- 不可重复读:同一事务两次查询结果可能不同(其他事务已提交)
-- 3. REPEATABLE READ(可重复读)------ MySQL 默认
-- 幻读:同一事务两次查询结果可能不同(其他事务插入了新行)
-- InnoDB: 使用 MVCC + Gap Lock 解决幻读
-- 4. SERIALIZABLE(串行化)
-- 完全串行执行,性能最差
-- ===== MVCC 原理 =====
-- 每一行数据有两个隐藏列:
-- DB_TRX_ID:最近修改的事务ID
-- DB_ROLL_PTR:指向 undo log 的指针
-- Read View(快照)结构:
-- m_ids:活跃事务ID列表
-- min_trx_id:最小活跃事务ID
-- max_trx_id:创建 Read View 时最大事务ID
-- creator_trx_id:当前事务ID
-- 可见性判断规则:
-- 1. DB_TRX_ID = creator_trx_id:自己的修改,可见
-- 2. DB_TRX_ID < min_trx_id:已提交,可见
-- 3. DB_TRX_ID >= max_trx_id:自己的修改之后发生的,不可见
-- 4. DB_TRX_ID in m_ids:在活跃列表中,不可见
-- RC vs RR 区别:
-- RC:每次 SELECT 创建新 Read View
-- RR:第一次 SELECT 创建 Read View,后续复用
-- ===== RC 隔离级别下的快照读 vs 当前读 =====
-- 快照读:读取快照,不加锁
SELECT * FROM orders; -- 快照读
-- 当前读:读取最新数据,加锁
SELECT * FROM orders LOCK IN SHARE MODE; -- 加 S 锁
SELECT * FROM orders FOR UPDATE; -- 加 X 锁
INSERT / UPDATE / DELETE -- 当前读
六、SQL 优化技巧
6.1 JOIN 优化
sql
-- ===== 小表驱动大表 =====
-- MySQL JOIN 算法:
-- NL(Nested Loop Join):小表驱动大表,循环次数 = 小表行数
-- BNL(Block Nested Loop):无索引时,大表放入 join buffer
-- Hash Join:MySQL 8.0+,大表等值关联优化
-- ✅ 小表在前
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 1; -- users 有 status=1 索引
-- 分析:users 先过滤,剩下 1000 行,orders 用主键关联快
-- ===== 避免 SELECT * =====
-- ❌ SELECT *,回表次数多
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id;
-- ✅ 只查需要的列
SELECT o.id, o.amount, o.created_at, u.username
FROM orders o
INNER JOIN users u ON o.user_id = u.id;
-- ===== 关联条件加索引 =====
-- orders.user_id 加索引(被关联字段)
CREATE INDEX idx_orders_user ON orders(user_id);
-- users.id 已有主键索引,无需额外建
-- ===== 多表 JOIN 顺序 =====
-- 原则:先过滤,后关联
-- ❌ 低效
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id
WHERE u.status = 1; -- users 过滤条件在 WHERE
-- ✅ 优化:子查询先过滤
SELECT * FROM (
SELECT id FROM users WHERE status = 1
) u
INNER JOIN orders o ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id;
-- ===== UNION vs UNION ALL =====
-- UNION:去重,会对合并结果排序
SELECT username FROM users WHERE status = 1
UNION
SELECT username FROM admins WHERE status = 1;
-- UNION ALL:不去重,不排序,性能更好
SELECT username FROM users WHERE status = 1
UNION ALL
SELECT username FROM admins WHERE status = 1;
-- ===== 批量插入优化 =====
-- ❌ 逐条插入(1000次 I/O)
INSERT INTO orders (user_id, amount) VALUES (1, 100);
INSERT INTO orders (user_id, amount) VALUES (2, 200);
...
-- ✅ 批量插入(1次 I/O)
INSERT INTO orders (user_id, amount) VALUES
(1, 100), (2, 200), (3, 300), (4, 400), (5, 500);
-- ✅ LOAD DATA(最快,文件导入)
LOAD DATA INFILE '/tmp/orders.csv'
INTO TABLE orders
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(user_id, amount, created_at);
-- ===== 千万级数据分页 =====
-- 分页查询优化:延迟关联
SELECT o.*, u.username FROM orders o
INNER JOIN (
SELECT id, username FROM users WHERE status = 1
) u ON o.user_id = u.id
ORDER BY o.created_at DESC
LIMIT 1000000, 10;
6.2 分区表与分库分表
sql
-- ===== 分区表(MySQL 原生)=====
-- 按时间分区(适合日志/订单)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
created_at DATETIME
) PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- 按月分区
CREATE TABLE logs (
id BIGINT PRIMARY KEY,
level VARCHAR(20),
message TEXT,
created_at DATETIME
) PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p2024_01 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p2024_02 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- ===== 分区裁剪(Partition Pruning)=====
-- 查询自动跳过不相关的分区
EXPLAIN SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-03-31';
-- Extra: Using partition (p2024) only(只扫描 p2024 分区)
-- ===== 分库分表(ShardingSphere)=====
-- ShardingSphere-JDBC 配置示例
-- application.yml
-- sharding:
-- tables:
-- orders:
-- actual-data-nodes: ds_${0..1}.orders_${0..15}
-- table-strategy:
-- standard:
-- sharding-column: user_id
-- sharding-algorithm-name: orders_mod
-- key-generate-strategy:
-- column: id
-- key-generator-name: snowflake
-- binding-tables:
-- - orders
七、配置优化与运维
7.1 核心参数调优
ini
# ===== my.cnf / my.ini 核心参数 =====
[mysqld]
# === 连接配置 ===
max_connections = 2000 # 最大连接数(默认151,根据内存估算:max_connections × 4MB)
wait_timeout = 600 # 空闲连接超时(秒)
interactive_timeout = 600 # 交互式连接超时
thread_cache_size = 64 # 线程缓存大小(一般设置为 max_connections 的 10%)
# === 缓冲池配置(InnoDB)===
innodb_buffer_pool_size = 12G # 缓冲池大小(建议为物理内存的 60-80%)
innodb_buffer_pool_instances = 4 # 缓冲池分区数(减少锁竞争,建议等于 CPU 核数)
innodb_buffer_pool_load_at_startup = ON # 启动时加载热点数据
innodb_buffer_pool_dump_at_shutdown = ON # 关闭时保存热点数据
# === 日志配置 ===
innodb_log_file_size = 2G # 日志文件大小(太大恢复慢,太小频繁切换)
innodb_log_buffer_size = 64M # 日志缓冲区
innodb_flush_log_at_trx_commit = 1 # 事务提交刷盘策略(1=安全,2=性能,0=最快)
# 1(默认):每次提交刷盘,最安全
# 2:提交到 OS 缓存,OS 负责刷盘
# 0:每秒刷盘,可能丢失 1 秒数据
# === I/O 配置 ===
innodb_file_per_table = ON # 每个表独立表空间(5.6+ 默认 ON)
innodb_flush_method = O_DIRECT # 绕过 OS 文件缓存,直接刷盘
innodb_io_capacity = 2000 # I/O 能力(SSD 设置 2000+,HDD 设置 200)
innodb_io_capacity_max = 4000 # 最大 I/O 能力
innodb_read_io_threads = 8 # 读 I/O 线程数
innodb_write_io_threads = 8 # 写 I/O 线程数
# === 临时表与排序 ===
tmp_table_size = 256M # 内存临时表大小
max_heap_table_size = 256M # MEMORY 表最大大小
sort_buffer_size = 4M # 排序缓冲区
join_buffer_size = 4M # JOIN 缓冲区
read_buffer_size = 2M # 顺序读缓冲区
# === 慢查询 ===
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/mysql-slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
7.2 监控与巡检
sql
-- ===== 日常巡检 SQL =====
-- 1. 连接数使用情况
SHOW STATUS LIKE 'Threads_connected'; -- 当前连接数
SHOW STATUS LIKE 'Max_used_connections'; -- 历史最大连接数
SHOW STATUS LIKE 'Aborted_connects'; -- 失败连接数
-- 2. 查询缓存命中率(MySQL 8.0 已移除)
-- SHOW STATUS LIKE 'Qcache%'; -- 8.0 已无此功能
-- 3. 缓冲池状态
SHOW ENGINE INNODB STATUS\G
-- 关键信息:
-- Buffer pool size: 786432 pages → 缓冲池总页数
-- Free pages: 1024 → 空闲页数(应接近 0,说明充分利用)
-- Database pages: 775308 → 已用页数
-- Modified db pages: 0 → 脏页数(太多会拖慢 checkpoint)
-- 4. 事务与锁
SHOW ENGINE INNODB STATUS\G
-- 查看当前锁等待、死锁信息
-- Trx id: 12345 → 事务ID
-- Lock wait: 3 → 等待锁数量
-- 5. 主从延迟
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 0 → 无延迟
-- Seconds_Behind_Master: 30 → 延迟 30 秒,需排查
-- 6. 慢查询 Top 10
SELECT
DIGEST_TEXT,
COUNT_STAR,
SUM_TIMER_WAIT / 1000000000000 AS total_sec,
AVG_TIMER_WAIT / 1000000000000 AS avg_sec,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- ===== pt-stalk 自动化采集 =====
-- Percona Toolkit 的 pt-stalk 可在故障时自动采集
-- yum install percona-toolkit
-- pt-stalk --daemonize -- --user=root --password=xxx --iterations=10 --interval=1 --sleep=60
-- 触发条件:Query_time > 10s
-- 采集内容:processlist, vars, status, vars, logs, vars
八、总结
技术全景
| 层 | 核心概念 | 关键点 |
|---|---|---|
| B+树结构 | 聚簇/辅助索引 | 16KB Page,3 层撑千万数据 |
| 最左前缀 | 联合索引顺序 | 区分度高的列放后面 |
| 慢查询 | pt-query-digest | 耗时 Top SQL 定位 |
| EXPLAIN | type/key/Extra | const/eq_ref/ref > range > ALL |
| 索引设计 | 覆盖索引 + ICP | 减少回表次数 |
| 锁机制 | Gap Lock + Next-Key | RR 级别防幻读 |
| MVCC | Read View | RC/RR 隔离级别区别 |
| SQL 优化 | 小表驱动大表 | UNION ALL vs UNION |
| 分区表 | Partition Pruning | 减少扫描范围 |
最佳实践
| 实践 | 说明 |
|---|---|
| 覆盖索引 | select 字段全在索引中,避免回表 |
| 最左前缀 | 联合索引严格按照顺序使用 |
| ICP | 索引条件下推,减少回表 |
| 小表驱动 | 小表放 JOIN 左边,减少 NL 循环 |
| 深分页 | 游标分页或延迟关联 |
| COUNT | 用条件索引或 Redis 缓存 |
| 隔离级别 | 读多写少用 RC,财务系统用 RR |
| 缓冲池 | 设置为物理内存 60-80%,多实例分区 |
本文涵盖 MySQL 8.0 性能优化完整知识:B+树索引原理、慢查询诊断、EXPLAIN 执行计划解读、索引设计规范、InnoDB 锁机制、事务隔离级别与 MVCC、SQL 优化技巧、分区表、生产配置参数与运维巡检。