数据库运维6 min read

MySQL 慢查询深度分析与优化实战

慢查询:数据库性能的头号杀手

一条慢 SQL 足以拖垮整个数据库。在 MySQL 优化中,慢查询分析是最直接、回报最高的工作。本文将建立一套从发现到优化、从优化到验证的完整方法论。

慢查询日志配置

-- 查看当前配置
SHOW VARIABLES LIKE '%slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
SET GLOBAL long_query_time = 1;           -- 超过1秒记录
SET GLOBAL log_queries_not_using_indexes = ON;  -- 记录未使用索引的查询
SET GLOBAL log_slow_admin_statements = ON;      -- 记录管理语句

-- 持久化(my.cnf)
-- slow_query_log = 1
-- slow_query_log_file = /var/log/mysql/slow-query.log
-- long_query_time = 1
-- log_queries_not_using_indexes = 1

pt-query-digest 分析工具

Percona Toolkit 中的 pt-query-digest 是慢查询分析的标配工具:

# 安装 Percona Toolkit
yum install -y percona-toolkit

# 分析慢查询日志
pt-query-digest /var/log/mysql/slow-query.log > slow_report.txt

# 实时监控(从 processlist)
pt-query-digest --processlist h=localhost,u=root --interval=1

# 分析指定时间范围
pt-query-digest \
  --since '2025-06-18 00:00:00' \
  --until '2025-06-18 23:59:59' \
  /var/log/mysql/slow-query.log

关键输出指标:

指标 含义 关注点
Response time 总响应时间 占比越大越需要优化
Calls 执行次数 高频查询即使单次快也值得优化
Rows examine 扫描行数 扫描越多越糟糕
Rows sent 返回行数 examine/send 比值是关键

EXPLAIN 完全解读

EXPLAIN FORMAT=JSON
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'pending'
  AND o.created_at > '2025-01-01'
ORDER BY o.created_at DESC
LIMIT 20;

JSON 格式输出字段解读:

{
  "query_block": {
    "select_id": 1,
    "cost_info": { "query_cost": "1258.50" },
    "ordering_operation": {
      "using_filesort": false,          // ✅ 使用了索引排序
      "table": {
        "table_name": "orders",
        "access_type": "range",         // range = 范围扫描,优于 ALL 但不如 ref
        "possible_keys": ["idx_status_created"],
        "key": "idx_status_created",    // 实际使用的索引
        "key_length": "9",
        "rows_examined_per_scan": 500,
        "filtered": 100,
        "using_index": false            // false = 需要回表
      }
    }
  }
}

关键字段优先级

性能从优到劣:
system > const > eq_ref > ref > range > index > ALL
  1       1      很好      好     一般     差    灾难

常见慢查询模式与优化

模式1:隐式类型转换

-- ❌ 慢:phone 是 VARCHAR,传入数字导致全表扫描
SELECT * FROM users WHERE phone = 13912345678;

-- ✅ 修复:使用正确类型
SELECT * FROM users WHERE phone = '13912345678';

模式2:函数包裹索引列

-- ❌ 慢:DATE() 函数导致无法使用索引
SELECT * FROM orders WHERE DATE(created_at) = '2025-06-18';

-- ✅ 修复:用范围查询替代
SELECT * FROM orders
WHERE created_at >= '2025-06-18 00:00:00'
  AND created_at < '2025-06-19 00:00:00';

模式3:前置模糊匹配

-- ❌ 慢:LIKE '%keyword' 无法使用索引
SELECT * FROM articles WHERE title LIKE '%MySQL优化%';

-- ✅ 方案A:全文索引
ALTER TABLE articles ADD FULLTEXT(title, content);
SELECT * FROM articles WHERE MATCH(title, content) AGAINST('MySQL优化');

-- ✅ 方案B:Elasticsearch 等外部搜索引擎

模式4:大偏移量分页

-- ❌ 慢:OFFSET 100000 需要扫描前 100020 行
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;

-- ✅ 修复:基于游标的分页(记住上一页最后一条的 id)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;

-- 如果必须用 OFFSET,加一个子查询减少回表
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 20 OFFSET 100000) tmp
ON o.id = tmp.id;

模式5:OR 条件

-- ❌ 慢:OR 可能导致全表扫描
SELECT * FROM users WHERE email = 'test@test.com' OR phone = '13912345678';

-- ✅ 修复:UNION 让每个子查询分别走索引
SELECT * FROM users WHERE email = 'test@test.com'
UNION
SELECT * FROM users WHERE phone = '13912345678';

索引优化原则

-- 联合索引的"最左前缀"原则
-- 索引: (status, created_at, user_id)

-- ✅ 可以使用索引
WHERE status = 'pending'
WHERE status = 'pending' AND created_at > '2025-01-01'
WHERE status = 'pending' AND created_at > '2025-01-01' AND user_id = 100

-- ❌ 不能使用索引(跳过了最左列 status)
WHERE created_at > '2025-01-01'
WHERE user_id = 100

-- ⚠️ 部分使用(范围查询后的列无法使用)
WHERE status = 'pending' AND created_at > '2025-01-01' AND user_id = 100
--                                      ↑ 范围查询    ↑ 此列索引失效

覆盖索引

-- ❌ 需要回表(Using index condition)
SELECT * FROM orders WHERE status = 'pending';

-- ✅ 覆盖索引(Using index,Extra 中出现)
-- 索引: (status, created_at, user_id)
SELECT status, created_at, user_id FROM orders WHERE status = 'pending';

验证优化效果

-- 开启 profiling
SET profiling = 1;

-- 执行优化前后的查询
SELECT ... ;

-- 查看执行耗时明细
SHOW PROFILES;
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;

-- 或使用 EXPLAIN ANALYZE (MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'pending';

黄金法则:优化之前先量化。没有基准数据的优化是盲目的,无法证明优化是否有效。

分享:

相关文章