目录

MySQL-12 常见问题定位

1. 问题排查通用流程

告警/反馈
    ↓
1. 确认影响范围(哪个服务?什么操作?多少用户?)
    ↓
2. 查看监控(QPS、延迟、连接数、CPU/内存/磁盘 I/O)
    ↓
3. 查看错误日志(/var/log/mysql/error.log)
    ↓
4. SHOW PROCESSLIST / SHOW ENGINE INNODB STATUS
    ↓
5. 针对具体问题深入排查
    ↓
6. 解决问题 + 记录根因
    ↓
7. 复盘改进(监控完善/代码优化/配置调整)

1.1 常用状态命令速查

-- 服务器整体状态
SHOW GLOBAL STATUS LIKE 'Threads%';
SHOW GLOBAL STATUS LIKE 'Connections';
SHOW GLOBAL STATUS LIKE 'Questions';
SHOW GLOBAL STATUS LIKE 'Slow_queries';
SHOW GLOBAL STATUS LIKE 'Com_%';  -- 各操作计数

-- 连接状态
SHOW PROCESSLIST;
SHOW FULL PROCESSLIST;  -- 显示完整 SQL

-- InnoDB 状态(最详细)
SHOW ENGINE INNODB STATUS\G

-- 表锁等待
SHOW STATUS LIKE 'Table_locks%';

-- InnoDB 行锁
SHOW STATUS LIKE 'Innodb_row_lock%';

2. 慢查询排查

2.1 发现慢查询

-- 方法1:慢查询日志(事后分析)
-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';

-- 动态开启
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;

-- 方法2:SHOW PROCESSLIST(实时)
-- 找 Time 列大的 Query
SELECT * FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep'
  AND TIME > 5
ORDER BY TIME DESC;

-- 方法3:performance_schema(历史统计)
SELECT
  DIGEST_TEXT,
  COUNT_STAR         AS exec_count,
  ROUND(AVG_TIMER_WAIT / 1e12, 3) AS avg_sec,
  ROUND(MAX_TIMER_WAIT / 1e12, 3) AS max_sec,
  SUM_ROWS_EXAMINED  AS total_rows_examined
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'myapp'
ORDER BY avg_sec DESC
LIMIT 20;

2.2 分析慢 SQL

-- 1. EXPLAIN 分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at DESC LIMIT 10;

-- 2. EXPLAIN ANALYZE(实际执行,MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT ...;

-- 3. 查看 Optimizer Trace(深度分析)
SET optimizer_trace = 'enabled=on';
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM information_schema.OPTIMIZER_TRACE\G
SET optimizer_trace = 'enabled=off';

-- 4. 查看 Profile(详细执行时间分解)
SET profiling = 1;
SELECT * FROM orders WHERE user_id = 1;
SHOW PROFILES;          -- 显示所有查询
SHOW PROFILE FOR QUERY 1;  -- 显示第1条查询的详细时间
-- 关注:Sending data 时间长 → 扫描行数多
--       Creating tmp table → GROUP BY/ORDER BY 用了临时表
--       Sorting result → filesort

2.3 常见慢查询原因和解决

-- 原因1:全表扫描(无索引)
-- EXPLAIN type = ALL
-- 解决:建索引
ALTER TABLE orders ADD INDEX idx_user_id (user_id);

-- 原因2:索引选择错误
-- EXPLAIN possible_keys 有多个,key 选了错误的
-- 解决:强制使用正确索引
SELECT * FROM orders FORCE INDEX (idx_user_id) WHERE user_id = 1;
-- 或更新统计信息
ANALYZE TABLE orders;

-- 原因3:深分页
-- LIMIT 1000000, 20 很慢
-- 解决:游标分页 + 延迟关联(见优化篇)

-- 原因4:filesort(无法利用索引排序)
-- Extra: Using filesort
-- 解决:调整索引,让 ORDER BY 列在索引中

-- 原因5:大量回表
-- type = ref,rows 很多,Rows_examined >> Rows_sent
-- 解决:覆盖索引

-- 原因6:数据量大的 IN 子查询
-- 解决:改为 JOIN 或分批查询

3. 连接数耗尽

3.1 症状

ERROR 1040 (HY000): Too many connections

3.2 排查

-- 当前连接情况
SELECT
  USER,
  HOST,
  DB,
  COMMAND,
  TIME,
  STATE,
  INFO
FROM information_schema.PROCESSLIST
ORDER BY TIME DESC;

-- 按用户/主机/状态统计
SELECT USER, HOST, COMMAND, COUNT(*) AS cnt
FROM information_schema.PROCESSLIST
GROUP BY USER, HOST, COMMAND
ORDER BY cnt DESC;

-- 找长时间 Sleep 的连接(很可能是连接池未释放)
SELECT ID, USER, HOST, TIME
FROM information_schema.PROCESSLIST
WHERE COMMAND = 'Sleep' AND TIME > 300
ORDER BY TIME DESC;

-- 查看最大连接数和当前连接数
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';  -- 历史最大值

3.3 临时缓解

-- 临时提升 max_connections(需要 SUPER 权限)
-- 注意:需要保留 1 个超级用户的连接位置
SET GLOBAL max_connections = 1200;

-- Kill 掉长时间 Sleep 的连接
-- 先生成 KILL 语句
SELECT CONCAT('KILL ', ID, ';')
FROM information_schema.PROCESSLIST
WHERE COMMAND = 'Sleep' AND TIME > 600;

-- 批量执行(在 bash 中)
mysql -u root -p -e "
SELECT CONCAT('KILL ', ID, ';')
FROM information_schema.PROCESSLIST
WHERE COMMAND = 'Sleep' AND TIME > 600
" | mysql -u root -p

3.4 根本解决

1. 检查应用层连接池配置
   - MaxOpenConns 是否合理
   - MaxIdleConns 是否设置
   - ConnMaxLifetime < MySQL wait_timeout

2. 检查是否有连接泄漏
   - 事务未提交/回滚
   - defer db.Close() 未执行
   - 循环中未关闭 rows

3. 考虑 ProxySQL 连接池
   - 前端:应用 → ProxySQL(少量连接)
   - 后端:ProxySQL → MySQL(连接复用)

4. 业务高峰期的限流/熔断

3.5 Go 连接泄漏检查

// ❌ 常见连接泄漏:rows 未关闭
func leakExample(db *sql.DB) {
    rows, err := db.Query("SELECT * FROM users")
    if err != nil {
        return
    }
    // 忘记 rows.Close()!循环结束后连接不会释放

    for rows.Next() {
        // ...
        if someCondition {
            return  // 提前返回,rows 未关闭!
        }
    }
}

// ✅ 正确:使用 defer
func correctExample(db *sql.DB) {
    rows, err := db.Query("SELECT * FROM users")
    if err != nil {
        return
    }
    defer rows.Close()  // 确保一定关闭

    for rows.Next() {
        // ...
    }
    // 检查 rows.Err()
    if err = rows.Err(); err != nil {
        log.Error(err)
    }
}

4. 死锁排查

4.1 查看死锁信息

-- 查看最近一次死锁
SHOW ENGINE INNODB STATUS\G
-- 找 LATEST DETECTED DEADLOCK 部分

-- 开启死锁日志(将死锁信息写入错误日志)
SET GLOBAL innodb_print_all_deadlocks = ON;

4.2 死锁日志分析

LATEST DETECTED DEADLOCK
------------------------
2024-01-15 10:30:00 0x7f...

*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 0 sec starting index read
MySQL thread id 100, OS thread handle ..., query id 200
UPDATE orders SET status=1 WHERE id=1     ← 事务1正在执行的SQL

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 10 n bits 72 index PRIMARY of table `myapp`.`orders`
trx id 12345 lock_mode X locks rec but not gap waiting    ← 等待 orders.id=1 的 X 锁

*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 0 sec starting index read
MySQL thread id 101, OS thread handle ..., query id 201
UPDATE orders SET status=2 WHERE id=2     ← 事务2正在执行的SQL

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY of table `myapp`.`orders`
trx id 12346 lock_mode X locks rec but not gap    ← 持有 orders.id=1 的 X 锁

*** WE ROLL BACK TRANSACTION (1)           ← 回滚了事务1(代价小)

4.3 分析死锁原因

从死锁日志可以看出:

  1. 哪两个事务发生了死锁
  2. 它们分别持有什么锁
  3. 它们在等待什么锁
  4. 发生死锁的表和索引

常见死锁场景:

场景1:两个事务以不同顺序更新同两行
场景2:UPDATE 导致的范围锁和插入意向锁冲突
场景3:外键检查导致的锁链
场景4:事务在 SELECT FOR UPDATE 后 INSERT,和其他事务的 Gap Lock 冲突

5. 主从复制异常

5.1 复制停止排查

-- 查看从库状态
SHOW SLAVE STATUS\G

-- 关键字段:
-- Slave_IO_Running: Yes/No      ← IO 线程状态
-- Slave_SQL_Running: Yes/No     ← SQL 线程状态
-- Last_IO_Error:                ← IO 线程错误
-- Last_SQL_Error:               ← SQL 线程错误
-- Seconds_Behind_Master:        ← 延迟秒数(NULL 表示复制停止)
-- Exec_Master_Log_Pos:          ← 已执行到主库 Binlog 的位置
-- Relay_Log_Space:              ← Relay Log 占用空间

5.2 常见复制错误处理

错误1:主键冲突(1062)

-- 场景:从库上已有数据,主库又插入了同样的主键
-- Last_SQL_Error: Error 'Duplicate entry '1' for key 'PRIMARY'' on query

-- 方法1:跳过这个错误(谨慎!可能导致数据不一致)
SET GLOBAL sql_slave_skip_counter = 1;  -- 跳过 1 个事件
START SLAVE;

-- 方法2:GTID 模式下跳过(推荐)
-- 找到出错的 GTID(在 Last_SQL_Error 中)
STOP SLAVE;
SET GTID_NEXT = 'error_gtid_here';
BEGIN; COMMIT;  -- 注入一个空事务,占用这个 GTID
SET GTID_NEXT = 'AUTOMATIC';
START SLAVE;

-- 方法3:重建从库(最干净,适合数据差异大时)

错误2:表不存在(1146)

-- 主库有某张表,从库没有
-- 解决:在从库手动创建表,或从主库同步

错误3:网络断开,Binlog 已删除

-- 主库的 Binlog 已经被清理,从库无法继续同步
-- 解决:重建从库(全量备份 + 重新配置)

5.3 主从延迟排查

-- 查看当前延迟
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 延迟秒数

-- 找延迟原因
-- 1. 大事务(查看 Relay_Log_Space 增长速度)
-- 2. SQL 线程是否卡在某个慢 SQL
SHOW SLAVE STATUS\G  -- 看 Exec_Master_Log_Pos 是否在推进

-- 3. 从库是否有慢查询
SHOW PROCESSLIST;  -- 看 SQL Thread 正在执行什么

-- 4. 并行复制配置
SHOW VARIABLES LIKE 'slave_parallel%';

6. 磁盘空间问题

6.1 磁盘空间占用分析

# 查看 MySQL 数据目录大小
du -sh /var/lib/mysql/*  | sort -rh | head -20
du -sh /var/lib/mysql/*.ibd | sort -rh | head -10

# 查看各目录大小
df -h  # 查看磁盘挂载情况
-- 查看各数据库大小
SELECT
  TABLE_SCHEMA AS db,
  ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mb,
  ROUND(SUM(DATA_LENGTH) / 1024 / 1024, 2) AS data_mb,
  ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) AS index_mb,
  ROUND(SUM(DATA_FREE) / 1024 / 1024, 2) AS free_mb  -- 碎片
FROM information_schema.TABLES
GROUP BY TABLE_SCHEMA
ORDER BY total_mb DESC;

-- 查看表大小
SELECT
  TABLE_NAME,
  ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mb,
  ROUND(DATA_FREE / 1024 / 1024, 2) AS free_mb,
  TABLE_ROWS
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'myapp'
ORDER BY total_mb DESC
LIMIT 20;

6.2 释放磁盘空间

-- 方法1:删除旧数据(如日志表、归档数据)
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 1000;
-- 分批删除!避免长事务

-- 方法2:OPTIMIZE TABLE(重建表,回收碎片)
OPTIMIZE TABLE orders;
-- 等价于 ALTER TABLE orders ENGINE=InnoDB;
-- 注意:会锁表,大表用 pt-online-schema-change 替代

-- 方法3:清理 Binlog
PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);
-- 注意:确保从库已经消费完这些 Binlog 后再删!

-- 方法4:清理 Undo 表空间(MySQL 8.0)
-- 查看 Undo 空间大小
SELECT NAME, ROUND(FILE_SIZE/1024/1024, 2) AS size_mb
FROM information_schema.INNODB_TABLESPACES
WHERE NAME LIKE '%undo%';

-- 触发 Undo 空间收缩
SET GLOBAL innodb_undo_log_truncate = ON;
-- 等待 purge 线程清理后,自动收缩

6.3 磁盘满了怎么办(紧急处理)

# 1. 立即检查什么文件最大
du -sh /var/lib/mysql/* | sort -rh | head -10

# 2. 如果是 Binlog 太多
# 确认从库状态后删除
mysql -u root -p -e "SHOW SLAVE STATUS\G" --slave-host  # 查从库
mysql -u root -p -e "PURGE BINARY LOGS BEFORE NOW() - INTERVAL 1 DAY;"

# 3. 如果是临时文件 /tmp
ls -lh /tmp/
# 可以重启 MySQL 清理(谨慎!)

# 4. 如果是 ibtmp1(临时表空间)增长
# 重启 MySQL 会自动重置 ibtmp1
# 根因:有大查询产生了很多临时表(GROUP BY/DISTINCT 数据量大)
SHOW PROCESSLIST;  # 找到并 KILL 掉问题查询

# 5. 临时腾出空间
# 压缩旧日志
gzip /var/log/mysql/slow.log.old

7. OOM 内存溢出

7.1 症状

MySQL 进程被 OOM Killer 杀死:
dmesg | grep -i "out of memory"
# 或
grep "Out of memory" /var/log/syslog

7.2 排查

-- 查看 MySQL 内存使用
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';

-- performance_schema 内存使用详情
SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED / 1024 / 1024 AS current_mb
FROM performance_schema.memory_summary_global_by_event_name
ORDER BY current_mb DESC
LIMIT 20;

7.3 常见原因

1. innodb_buffer_pool_size 设置过大
   → 适当减小,留给 OS 足够内存

2. 大量并发连接 × 连接级别内存
   → 减少 max_connections
   → 减小 sort_buffer_size / join_buffer_size

3. 大查询产生大临时表
   → SHOW PROCESSLIST 找到并 KILL
   → 设置 tmp_table_size 和 max_heap_table_size

4. 内存碎片(长时间运行)
   → 定期重启(非紧急情况)
   → 使用 jemalloc 替换 glibc malloc

8. 数据误删恢复

8.1 快速止损

-- 发现误操作后,立刻:
-- 1. 记录当前时间点
SELECT NOW();

-- 2. 如果误操作的事务还未提交,立刻 ROLLBACK
ROLLBACK;

-- 3. 如果已提交,准备基于 Binlog 恢复
-- 千万不要继续写入数据(可能覆盖可恢复的数据)

8.2 Binlog 恢复步骤

# 场景:2024-01-15 14:30 误执行了 DELETE FROM orders WHERE user_id = 100

# 1. 找到最近的全量备份
# 假设:backup_20240115_000000.sql (当天凌晨备份)

# 2. 恢复全量备份到临时数据库
mysql -u root -p temp_restore < backup_20240115_000000.sql

# 3. 找到误操作的 Binlog 位置
mysqlbinlog mysql-bin.000001 \
  --start-datetime='2024-01-15 00:00:00' \
  --stop-datetime='2024-01-15 14:29:00' \  # 误操作前一分钟
  > binlog_before_delete.sql

# 4. 应用 Binlog 到临时数据库
mysql -u root -p temp_restore < binlog_before_delete.sql

# 5. 从临时数据库中提取被删数据
mysql -u root -p temp_restore -e "
  SELECT * FROM orders WHERE user_id = 100
" > recovered_orders.sql

# 6. 将数据导入生产数据库
mysql -u root -p production < recovered_orders.sql

8.3 延迟从库(最佳保护)

-- 配置延迟 1 小时的从库(延迟复制)
CHANGE MASTER TO MASTER_DELAY = 3600;  -- 延迟 3600 秒

-- 误删后,立即暂停从库,让它停留在误操作前的时间点
STOP SLAVE SQL_THREAD;

-- 然后从从库恢复数据
-- 这是最快的恢复方式!

9. MySQL 崩溃恢复

9.1 查看崩溃原因

# 查看错误日志
tail -100 /var/log/mysql/error.log

# 查看系统日志
dmesg | tail -50
grep mysql /var/log/syslog | tail -50

# 常见崩溃原因:
# - InnoDB: Assertion failure(Bug 或数据损坏)
# - Signal 11(段错误,可能是 Bug 或内存问题)
# - OOM Killer 杀进程
# - 磁盘满
# - 文件权限问题

9.2 InnoDB 崩溃恢复

# 正常启动(InnoDB 自动重放 Redo Log 恢复)
mysqld --user=mysql

# 如果正常启动失败,尝试强制恢复模式
# innodb_force_recovery 从 1 到 6 逐步增大
# 数值越大,越能启动,但越多数据可能丢失

# 在 my.cnf 中设置
[mysqld]
innodb_force_recovery = 1  # 先从 1 开始

# 各级别含义:
# 1: 忽略损坏的 page(SRV_FORCE_IGNORE_CORRUPT)
# 2: 禁止主线程(不刷脏页)
# 3: 不执行事务 rollback
# 4: 不计算统计信息
# 5: 不检查 undo 日志一致性
# 6: 不 rollback forward(最危险)

# 能启动后,立刻导出数据
mysqldump -u root -p --all-databases > emergency_backup.sql

# 然后重建实例

9.3 表损坏修复

-- 检查表是否损坏
CHECK TABLE orders;
CHECK TABLE orders EXTENDED;  -- 更全面

-- 修复(MyISAM)
REPAIR TABLE orders;

-- 修复(InnoDB,通过重建)
ALTER TABLE orders ENGINE = InnoDB;
-- 或
mysqldump -u root -p myapp orders > orders_backup.sql
DROP TABLE orders;
mysql -u root -p myapp < orders_backup.sql

10. 性能抖动排查

10.1 定期刷脏页导致抖动

症状:MySQL 每隔几秒出现响应慢高峰(周期性抖动)

原因:InnoDB 定期将 Buffer Pool 中的脏页刷到磁盘
     如果脏页比例过高,会触发大量写 I/O
-- 查看脏页比例
SELECT
  ROUND(VARIABLE_VALUE * 100 / @@innodb_buffer_pool_size, 2) AS dirty_pct
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_bytes_dirty';

-- 优化配置
innodb_io_capacity = 4000      -- 告知 InnoDB 磁盘 IOPS 能力(SSD 调大)
innodb_io_capacity_max = 8000  -- 紧急情况下的最大 IOPS
innodb_max_dirty_pages_pct = 75  -- 脏页比例超过 75% 开始积极刷盘

10.2 Redo Log 写满导致抖动

症状:写操作突然变慢(毫秒级变秒级)

原因:Redo Log 写满,必须等待 Checkpoint 推进(即刷脏页到磁盘)
-- 查看 Redo Log 使用率
SHOW ENGINE INNODB STATUS\G
-- 找 LOG 部分:
-- Log sequence number vs Last checkpoint at 的差值
-- 差值 / innodb_log_file_size × innodb_log_files_in_group

-- 解决:增大 Redo Log 大小
innodb_redo_log_capacity = 4G  -- MySQL 8.0.30+
-- 或
innodb_log_file_size = 1G
innodb_log_files_in_group = 2

10.3 MDL 锁等待导致抖动

-- 症状:某段时间所有查询都慢,之后恢复正常

-- 排查:
SELECT * FROM sys.schema_table_lock_waits;

-- 找到持有 MDL 写锁的进程(ALTER TABLE)
SELECT * FROM performance_schema.metadata_locks
WHERE LOCK_TYPE = 'EXCLUSIVE';

-- Kill 掉
KILL QUERY {thread_id};

11. 常用诊断 SQL

-- ==========================================
-- 系统概览
-- ==========================================

-- QPS(每秒查询数)
SELECT ROUND(
  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Queries') /
  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Uptime'),
  2
) AS QPS;

-- TPS(每秒事务数)
SELECT ROUND(
  ((SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Com_commit') +
   (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Com_rollback')) /
  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Uptime'),
  2
) AS TPS;

-- Buffer Pool 命中率(应 > 99%)
SELECT
  ROUND(
    (1 - (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_reads') /
         (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Innodb_buffer_pool_read_requests')
    ) * 100, 4
  ) AS buffer_pool_hit_rate_pct;

-- ==========================================
-- 锁相关
-- ==========================================

-- 当前等待锁的事务
SELECT
  r.trx_id     AS waiting_trx,
  r.trx_query  AS waiting_query,
  b.trx_id     AS blocking_trx,
  b.trx_query  AS blocking_query,
  b.trx_mysql_thread_id AS blocking_thread
FROM information_schema.INNODB_LOCK_WAITS w
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id;

-- 长事务(超过 60 秒)
SELECT
  trx_id,
  trx_started,
  TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
  trx_mysql_thread_id AS thread_id,
  trx_rows_modified,
  LEFT(trx_query, 100) AS query
FROM information_schema.INNODB_TRX
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started;

-- ==========================================
-- 表空间
-- ==========================================

-- 表碎片率高的表(Data_free / (Data_length + Index_length))
SELECT
  TABLE_NAME,
  ROUND(DATA_LENGTH / 1024 / 1024, 2)  AS data_mb,
  ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb,
  ROUND(DATA_FREE / 1024 / 1024, 2)    AS free_mb,
  ROUND(DATA_FREE / (DATA_LENGTH + INDEX_LENGTH) * 100, 2) AS frag_pct
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'myapp'
  AND DATA_FREE > 0
ORDER BY frag_pct DESC
LIMIT 20;

-- ==========================================
-- 复制
-- ==========================================

-- 主从延迟告警(延迟超过 30 秒)
-- 在从库执行
SELECT
  IF(TIMESTAMPDIFF(SECOND, MIN(trx_started), NOW()) > 30,
     'ALERT: Replication lag detected',
     'OK') AS status
FROM information_schema.INNODB_TRX;
-- 更准确的方式是看 SHOW SLAVE STATUS 的 Seconds_Behind_Master

小结

故障排查的黄金原则:

1. 先止损,再排查
   - 发现问题先评估影响,必要时限流/降级/切流量

2. 保留现场
   - 记录时间点、错误信息、PROCESSLIST 快照
   - 不要急于重启(可能丢失现场信息)

3. 分层排查
   - 应用层(代码/SQL 是否有问题)
   - MySQL 层(连接/锁/慢查询)
   - OS 层(CPU/内存/I/O/网络)
   - 硬件层(磁盘/内存)

4. 记录复盘
   - 每次故障都写故障报告
   - 改进监控告警,让类似问题早发现