vault backup: 2026-05-21 19:31:28

This commit is contained in:
hhs
2026-05-21 19:31:28 +08:00
parent 531d4b7d8c
commit f9d21f0026
9 changed files with 1290 additions and 78 deletions
@@ -42,6 +42,7 @@ SHOW VARIABLES LIKE 'long_query_time';
> - 不要盲目设置极低的 `long_query_time`(如 0.1s),否则会产生大量噪声。建议从 **1~5 秒**开始逐步下调。
> - 开启 `log_queries_not_using_indexes` 会让所有无索引查询都被记录,即使它们只需要 0.01s。**谨慎启用**,最好配合 `min_examined_row_limit` 过滤无害查询。
> - `TABLE` 模式会往 `mysql.slow_log` 写数据,需要评估写入开销。如果已有 FILE 模式,优先选 FILE。
> - DDL 语句(`ALTER TABLE`、`CREATE INDEX`)默认不记录到慢查询日志。如有需要可开启 `log_slow_admin_statements = 1`,排查大表 DDL 阻塞。
### min_examined_row_limit
@@ -72,6 +73,9 @@ WHERE o.status = 'pending' AND o.amount > 100
ORDER BY o.created_at DESC LIMIT 20;
```
> [!NOTE] `SET timestamp=1715848245;` 是什么?
> 这行不是用户执行的 SQL,而是 MySQL **自动注入**的时间锚点。它的作用是:当你用 `mysql < slow.log` 回放这条慢查询时,MySQL 会用这个 Unix 时间戳作为 `NOW()` 的基准,确保时间相关函数(`NOW()`、`CURDATE()` 等)在回放时的值与原始执行时一致。**不影响执行计划,仅供回放使用。**
### 关键字段含义
| 字段 | 含义 | 关注点 |
@@ -92,13 +96,17 @@ ORDER BY o.created_at DESC LIMIT 20;
#### Lock_time 专项解读
```
Lock_time 占 Query_time 的比例 诊断方向
──────────────────────────────────────────
< 1% 正常,无锁问题
1% ~ 10% 轻度竞争,可接受
> 10% 需要排查:是否有长事务或未命中索引
> 50% 紧急:大概率死锁或锁升级
`Lock_time` 占 `Query_time` 的比例越大,说明查询在"等锁"而非"执行"上浪费了越多时间。下面的诊断路径帮你快速判断严重程度:
```mermaid
flowchart LR
A["计算 Lock_time / Query_time"] --> B{"< 1% ?"}
B -- "是" --> C["🟢 正常,无锁问题"]
B -- "否" --> D{"< 10% ?"}
D -- "是" --> E["🟡 轻度竞争,可接受"]
D -- "否" --> F{"< 50% ?"}
F -- "是" --> G["🟠 需排查:长事务 or 未命中索引"]
F -- "否" --> H["🔴 紧急:大概率死锁或锁升级"]
```
## mysqldumpslow 内置工具
@@ -215,17 +223,80 @@ pt-query-digest --since 30m --filter '$event->{qt} > 1000000' /var/log/mysql/slo
-- MySQL 5.7+ 启用 performance_schema
SET GLOBAL performance_schema = ON;
-- 清理已有数据(可选)
-- 清理已有数据(可选,重置统计基线)
TRUNCATE performance_schema.events_statements_summary_by_digest;
```
-- 等业务跑一会儿后,用 pt-query-digest 直接分析
等业务跑一段时间后,用 `pt-query-digest` 直接从 Performance Schema 提取分析:
```bash
# 方式一:基于 processlist 实时采样
pt-query-digest --processlist D=localhost,U=root \
--no-report /var/log/mysql/slow.log
-- 或者直接用下面的方式从 Performance Schema 提取
# 方式二:直接从 digest 表提取
pt-query-digest --type processlist D=host:port,user,password
```
## SHOW PROFILE(单条 SQL 微观分析)
`pt-query-digest` 和 `mysqldumpslow` 解决的是"哪些 SQL 慢"的**宏观**问题;而当你已经定位到一条具体的慢 SQL,想进一步搞清楚**它的时间花在哪个阶段**时,就需要 `SHOW PROFILE`。
> [!TIP] EXPLAIN vs SHOW PROFILE
> - **EXPLAIN**:告诉你查询**将会**怎么执行(执行计划),是"预言家"
> - **SHOW PROFILE**:告诉你查询**已经**怎么执行(各阶段耗时),是"回放仪"
> 两者互补:先 EXPLAIN 看计划是否合理,再 PROFILE 看执行时到底慢在哪。
```sql
-- 开启当前会话的 profiling
SET profiling = 1;
-- 执行你的目标 SQL
SELECT o.id, o.status, o.amount
FROM orders o
WHERE o.user_id = 100 AND o.status = 'pending'
ORDER BY o.created_at DESC LIMIT 20;
-- 查看最近一次查询的各阶段耗时
SHOW PROFILE;
-- 更详细:查看 CPU 和 Block IO
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;
```
典型输出:
```
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000089 |
| checking permissions | 0.000012 |
| Opening tables | 0.000035 |
| init | 0.000025 |
| System lock | 0.000015 |
| optimizing | 0.000018 |
| statistics | 0.000042 |
| preparing | 0.000020 |
| executing | 0.000008 |
| Sending data | 0.452300 | ← ⚠️ 99% 的时间在这里
| end | 0.000012 |
| query end | 0.000008 |
+----------------------+----------+
```
> [!NOTE] 如何解读各阶段
> | 阶段 | 含义 | 如果占比过高意味着什么 |
> |------|------|----------------------|
> | **Sending data** | 扫描行、返回结果集 | 索引不佳或结果集过大 |
> | **Sorting result** | 排序操作 | 缺少 ORDER BY 对应索引 |
> | **Creating tmp table** | 创建临时表(内存/磁盘) | GROUP BY / DISTINCT 无法走索引 |
> | **System lock** | 等待表锁 | 存在锁竞争 |
> | **statistics** | 优化器统计信息收集 | 统计信息过旧,需 `ANALYZE TABLE` |
> [!WARNING] 版本兼容性
> `SHOW PROFILE` 在 MySQL 8.0 中已被标记为**废弃**(deprecated),官方推荐迁移到 `performance_schema.events_statements_*` 表。但在 MySQL 5.7 以及快速排查场景下,它依然是最轻便的选择。
## 常见慢查询反模式
即使有索引,SQL 写法不当也会导致索引失效。以下是生产中最常见的几种反模式:
@@ -236,13 +307,13 @@ pt-query-digest --type processlist D=host:port,user,password
| **隐式类型转换** | `WHERE phone = 13800138000` (phone 是 VARCHAR) | 字符型字段与数字比较触发隐式转换,索引失效 | 传参类型与列类型一致 |
| **OR 条件未覆盖** | `WHERE idx_col = 1 OR other_col = 2` | 只有第一个字段有索引 | 拆分 UNION ALL 或用 BITMAP |
| **函数包裹列名** | `WHERE YEAR(created_at) = 2025` | 对列计算函数导致索引失效 | 改为范围扫描:`created_at >= '2025-01-01' AND created_at < '2026-01-01'` |
| **NOT IN / <>** | `WHERE id NOT IN (SELECT id FROM ...)`)` | 子查询难以走索引 | 改写为 LEFT JOIN ... IS NULL |
| **NOT IN / <>** | `WHERE id NOT IN (SELECT id FROM ...)` | 子查询难以走索引 | 改写为 LEFT JOIN ... IS NULL |
| **SELECT \*** | `SELECT * FROM orders WHERE ...` | 回表额外开销 + 网络传输浪费 | 只查需要的列 |
> [!QUESTION] 为什么 `SELECT *` 会拖慢查询?
> 有两个原因:
> 1. **回表**:如果用的是二级索引(非聚簇),`SELECT *` 需要拿着二级索引的值去聚簇索引回表取出所有列数据;而如果只 SELECT 了索引中包含的列,则可以直接"覆盖索引"取数,无需回表。
> 2. **带宽浪费**:即使通过聚簇索引取数,多取的每列也会占用更多内存缓冲和网路传输时间。在高频查询场景下,每次省 2KB,一万次就是 20MB 的额外开销。
> 2. **带宽浪费**:即使通过聚簇索引取数,多取的每列也会占用更多内存缓冲和网络传输时间。在高频查询场景下,每次省 2KB,一万次就是 20MB 的额外开销。
## 慢查询优化工作流
@@ -322,15 +393,23 @@ flowchart LR
end
subgraph "SQL 改写"
B1["LIKE '%xxx' → FULLTEXT"] --> B3["验证:rows↓, time↓"]
B2["YEAR(col) → range 扫描"] --> B3
B3["NOT IN → LEFT JOIN IS NULL"] --> B3
B1["LIKE '%xxx' → FULLTEXT"]
B2["YEAR(col) → range 扫描"]
B3["NOT IN → LEFT JOIN IS NULL"]
B4["验证:rows↓, time↓"]
B1 --> B4
B2 --> B4
B3 --> B4
end
subgraph "架构类优化"
C1["深分页 OFFSET → 延迟关联"] --> C3["验证:P95 latency ↓"]
C2["大事务拆小"] --> C3
C3["读写分离 / 缓存"] --> C3
C1["深分页 OFFSET → 延迟关联"]
C2["大事务拆小"]
C3["读写分离 / 缓存"]
C4["验证:P95 latency ↓"]
C1 --> C4
C2 --> C4
C3 --> C4
end
```
@@ -356,6 +435,169 @@ flowchart LR
> ```
> 将此查询放入定时任务(如每 5 分钟),用 Grafana 或 Prometheus 做可视化告警。
## 日志轮转与清理策略
慢查询日志是持续增长的——高流量系统一天可以产生数 GB 的慢日志。没有轮转策略,磁盘会被撑满,甚至影响 MySQL 正常写入。
> [!WARNING] 真实案例
> 曾有团队开启了慢查询日志但忘了清理,3 个月后磁盘 100%,MySQL 被迫停止服务。日志文件本身变成了"生产事故"。
### 方案一:MySQL 原生轮转
```bash
# 手动轮转(适合简单场景)
mv /var/log/mysql/slow.log /var/log/mysql/slow.log.$(date +%Y%m%d)
mysql -e "FLUSH SLOW LOGS;" # 让 MySQL 重新打开新的 slow.log
```
### 方案二:logrotate(推荐)
```bash
# /etc/logrotate.d/mysql-slow
/var/log/mysql/slow.log {
daily # 每天轮转
rotate 7 # 保留最近 7 份
compress # gzip 压缩旧日志
missingok # 文件不存在时不报错
notifempty # 空文件不轮转
create 640 mysql mysql
postrotate
mysql -e "FLUSH SLOW LOGS;"
endscript
}
```
> [!TIP] 搭配 pt-query-digest 做"先分析、再清理"
> ```bash
> # 在 logrotate 的 prerotate 中先分析,再轮转
> prerotate
> pt-query-digest /var/log/mysql/slow.log > /var/log/mysql/slow_report_$(date +%Y%m%d).txt
> endscript
> ```
> 这样每天轮转前自动保存一份分析报告,既不丢数据,又控制了磁盘占用。
## 实战 Walkthrough:从慢日志到上线优化
> [!NOTE] 场景设定
> 电商系统,用户反馈"订单列表页加载慢"。DBA 介入排查。
### Step 1 — 采集慢日志
```bash
# 确认慢日志已开启
mysql -e "SHOW VARIABLES LIKE 'slow_query_log%';"
# 用 pt-query-digest 分析最近 1 小时
pt-query-digest --since 1h /var/log/mysql/slow.log > /tmp/slow_report.txt
```
### Step 2 — 定位 Top 1 瓶颈
报告 Profile 区域显示:
```
Rank Query ID Response time Calls R/Call Item
==== ============== ============== ===== ====== ===================
1 0xABCD1234 1850.5s 62.3% 3200 0.58s SELECT orders JOIN users
```
> [!TIP] 关键数字解读
> **3200 次调用 × 0.58s = 1850s 总耗时**——高频 + 中等单次耗时,累积起来就是最大的性能杀手。
对应的 SQL 指纹:
```sql
SELECT o.id, o.status, o.amount, o.created_at, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.user_id = ? AND o.status = ?
ORDER BY o.created_at DESC
LIMIT 20 OFFSET ?;
```
### Step 3 — EXPLAIN 诊断
```sql
EXPLAIN FORMAT=JSON
SELECT o.id, o.status, o.amount, o.created_at, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.user_id = 100 AND o.status = 'pending'
ORDER BY o.created_at DESC
LIMIT 20 OFFSET 5000;
```
关键输出(简化):
```json
{
"table": "o",
"access_type": "ALL",
"key": null,
"rows": 520000,
"filtered": 10.0,
"Extra": "Using where; Using filesort"
}
```
> [!QUESTION] 诊断结论是什么?
> 三个致命信号:
> 1. **`type=ALL`** — 全表扫描,52 万行逐行检查
> 2. **`key=null`** — 没有任何索引被使用
> 3. **`OFFSET 5000`** — 深分页,即使有索引也会丢弃前 5000 行
### Step 4 — 优化方案
**问题 A:缺少索引** — 添加联合索引:
```sql
-- 覆盖 WHERE + ORDER BY + SELECT 列(覆盖索引)
ALTER TABLE orders ADD INDEX idx_user_status_created
(user_id, status, created_at, amount);
```
**问题 B:深分页** — 改用"延迟关联":
```sql
-- 改写前:OFFSET 5000 需要扫描并丢弃 5000 行
SELECT ... FROM orders WHERE user_id = ? AND status = ?
ORDER BY created_at DESC LIMIT 20 OFFSET 5000;
-- 改写后:先用覆盖索引定位主键,再回表取数据
SELECT o.id, o.status, o.amount, o.created_at, u.username
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.id IN (
SELECT id FROM orders
WHERE user_id = 100 AND status = 'pending'
ORDER BY created_at DESC
LIMIT 20 OFFSET 5000
)
ORDER BY o.created_at DESC;
```
### Step 5 — 验证效果
优化前后的 `EXPLAIN` 对比:
| 指标 | 优化前 | 优化后 |
|------|--------|--------|
| **access_type** | ALL | range |
| **key** | null | idx_user_status_created |
| **rows** | 520,000 | 20 |
| **Extra** | Using filesort | Using index condition |
```bash
# 重新采集慢日志确认
pt-query-digest --since 1h /var/log/mysql/slow.log
# → 该 SQL 指纹已不在 Top 10 中 ✅
```
> [!TIP] 上线 Checklist
> - [ ] 在测试环境验证 EXPLAIN 输出符合预期
> - [ ] 确认新索引不会影响写入性能(`ALTER TABLE` 可用 `pt-online-schema-change` 避免锁表)
> - [ ] 接入 Performance Schema 定时采集,设置告警阈值(如 Top SQL 单次 P95 > 2s 触发告警)
## 关联笔记
- [[hhs/MySQL/03-索引与查询优化/15-EXPLAIN 完全指南]] — EXPLAIN 各字段的详细解读与执行计划分析