目录

MySQL-12 常见问题定位

MySQL 常见问题定位


目录

  1. 问题排查通用流程
  2. 慢查询排查
  3. 连接数耗尽
  4. 死锁排查
  5. 主从复制异常
  6. 磁盘空间问题
  7. OOM 内存溢出
  8. 数据误删恢复
  9. MySQL 崩溃恢复
  10. 性能抖动排查
  11. 常用诊断 SQL

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. 记录复盘
   - 每次故障都写故障报告
   - 改进监控告警,让类似问题早发现