MySQL 慢查询怎么排查?从日志到索引优化完整流程

AI 概述
本文详解MySQL慢查询完整排查与优化方案,适配5.7、8.0版本。先梳理索引、SQL写法、表结构、数据库配置等各类致慢根因,讲解开启、分析慢查询日志的方法,通过EXPLAIN解读执行计划,排查全表扫描、索引失效、深度分页、JOIN低效等问题。同时介绍锁、连接、缓存等数据库状态排查方式,给出索引设计、SQL编写规范及Buffer Pool等参数优化策略,并总结标准化排查流程与日常规避规范。
目录
文章目录隐藏
  1. 适用场景
  2. MySQL 查询慢的常见根因
  3. 第一步:开启慢查询日志
  4. 第二步:用 EXPLAIN 分析执行计划
  5. 第三步:识别常见索引失效场景
  6. 第四步:深度分页问题
  7. 第五步:多表 JOIN 优化
  8. 第六步:查看 MySQL 实时状态和进程
  9. 第七步:索引设计与优化实战
  10. 第八步:配置层面的优化
  11. 第九步:完整排查流程总结
  12. 总结

MySQL 慢查询怎么排查?从日志到索引优化完整流程

MySQL 慢查询是影响业务响应速度的最常见根因。业务高峰期一次看似简单的 SELECT 查询,可能拖慢整个系统——前端页面加载转圈、API 超时、后台任务堆积、数据库连接池耗尽。更要命的是,慢查询往往不是单一问题,而是索引缺失、表结构不合理、SQL 写法糟糕、统计信息过时、硬件资源不足等多重因素叠加的结果。初期只是偶尔卡顿,后期随着数据量增长演变成全面崩溃。

这篇文章面向初中级 MySQL 运维和后端开发工程师,从一个典型的”页面加载慢”投诉出发,系统讲解 MySQL 慢查询的完整排查流程:先开启慢查询日志抓出现存的慢查询,再用 EXPLAIN 分析执行计划,找到索引问题和全表扫描,再结合 MySQL 8.0 的新特性做优化,最后给出索引设计规范和 SQL 编写规范。所有内容基于 MySQL 5.7 和 MySQL 8.0 两个主流版本,部分特性差异会做说明。

适用场景

  • 业务接口响应时间突然变长,怀疑是数据库查询慢
  • 监控显示 QPS 正常但 DB CPU 使用率持续偏高
  • 慢查询日志文件快速增长,磁盘空间告警
  • 需要优化历史遗留 SQL,消除全表扫描
  • 新功能上线前做 SQL 审核,发现潜在性能问题
  • 主从复制延迟持续偏高,怀疑是慢查询阻塞了复制线程
  • 分库分表前评估哪些大表需要优先拆分

MySQL 查询慢的常见根因

在动手排查之前,先理解可能导致查询慢的原因有哪些:

索引层面:

  • 缺少 WHERE 条件列上的索引,导致全表扫描
  • 索引失效:函数/运算在索引列上、类型转换、LIKE 前缀通配符
  • 索引选择不当:MySQL 选错了索引(索引统计信息不准)
  • 联合索引顺序不对,最左前缀原则不满足

SQL 写法层面:

  • SELECT * 读取全部列,没有利用覆盖索引
  • 多表 JOIN 时没有给小表加驱动限制
  • 子查询嵌套过深或用 IN (SELECT …) 导致全表
  • OR 条件导致索引失效
  • 分页查询 OFFSET 过大(深度分页问题)
  • 缺少 LIMIT 限制返回行数

表结构层面

  • 表过大(单表超过千万行且无分区)
  • 字段类型选择不当(VARCHAR(255) 存大文本、TEXT 字段参与排序)
  • 没有合理使用分区表
  • 字段冗余设计不合理导致更新阻塞

数据库配置层面:

  • Buffer Pool 设置太小,热数据无法全部缓存
  • 脏页刷新策略不合理导致磁盘 IO 突增
  • 并发连接数设置过高导致上下文切换
  • 临时表和排序缓冲区不足

硬件和系统层面:

  • 磁盘 IO 性能不足(SSD vs HDD)
  • 内存不足导致 swap
  • CPU 核心数不足
  • 网络延迟(跨机房部署)

第一步:开启慢查询日志

慢查询日志是排查的起点。如果还没有开启,先开启它。

1.1 确认慢查询日志是否开启

-- 查看慢查询相关配置
SHOWVARIABLESLIKE'slow_query%';
SHOWVARIABLESLIKE'long_query_time';
SHOWVARIABLESLIKE'log_output';

-- 输出示例:
-- slow_query_log                | ON
-- slow_query_log_file          | /var/lib/mysql/mysql-slow.log
-- long_query_time              | 2.000000   (超过 2 秒的查询才记录)
-- log_output                   | FILE

long_query_time 默认是 10 秒,对生产环境来说太宽松了。建议设置为 1 秒,有能力的话设置为 0.5 秒或更低,但要注意开启后日志量会增加。

1.2 临时开启慢查询日志(不重启)

-- 将超过 1 秒的查询记录到慢查询日志
SETGLOBAL slow_query_log = 'ON';
SETGLOBAL long_query_time = 1;
SETGLOBAL slow_query_log_file = '/var/lib/mysql/mysql-slow.log';

-- 同时开启记录未使用索引的查询(注意:MySQL 8.0 中这个参数被移除)
-- MySQL 5.7 中可以开启
SETGLOBAL log_queries_not_using_indexes = 'ON';

-- 确认修改生效
SHOWVARIABLESLIKE'slow_query%';
SHOWVARIABLESLIKE'long_query_time';

注意:以上修改在 MySQL 重启后会丢失。如果要永久生效,需要修改配置文件。

1.3 永久开启慢查询日志

# 编辑 MySQL 配置文件(根据安装方式不同,配置文件位置不同)
# RPM 安装:/etc/my.cnf
# Docker 安装:挂载的配置文件
# 使用 mysqld --verbose --help | grep -A 10 'Default options' 查找

sudo vi /etc/my.cnf

# 在 [mysqld] 段添加或修改以下内容:
[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
log_output = FILE

# 保存后重启 MySQL(需要确认影响范围)
sudo systemctl restart mysqld

⚠️ 风险提醒:重启 MySQL 会断开所有现有连接,生产环境需要:

  1. 确认业务是否有重连机制
  2. 确认是否有长事务未提交(重启前最好确认)
  3. 通知相关业务方
  4. 准备好回滚方案(将原配置文件备份)

1.4 用 pt-query-digest 分析慢查询日志

pt-query-digest 是 Percona Toolkit 中的工具,比直接查看日志文件效率高得多。它能聚合相似查询、排序出最慢的查询、展示查询频率和响应时间分布。

# 如果没有安装 Percona Toolkit,先安装
# CentOS/RHEL:
sudo yum install percona-toolkit -y
# Debian/Ubuntu:
sudo apt-get install percona-toolkit -y

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

# 输出结构:
#   1) 查询的指纹(将参数泛化后的标准化 SQL)
#   2) 查询时间统计:Response time、Calls、R/M/V
#   3) SQL 文本本身
#   4) 执行计划(如果有 EXPLAIN 输出)

# 只看前 10 个最慢的查询
pt-query-digest --limit 10 /var/lib/mysql/mysql-slow.log

# 只看某个时间范围后的查询
pt-query-digest --since '2026-04-29 10:00:00' /var/lib/mysql/mysql-slow.log

# 排除某个用户查询
pt-query-digest --filter '\$event->{user} =~ /^(?!repl_user)/' /var/lib/mysql/mysql-slow.log

pt-query-digest 输出的第一部分是最关键的——按响应时间排序的查询列表。第一条就是当前最需要优化的查询。

第二步:用 EXPLAIN 分析执行计划

找到慢查询后,用 EXPLAIN 分析它的执行计划。执行计划是 MySQL 优化器的决策结果,告诉你 MySQL 准备怎么执行这条查询。

2.1 基本 EXPLAIN 用法

-- 在 MySQL 客户端中执行
EXPLAIN <your_query>;

-- MySQL 8.0 支持 EXPLAIN ANALYZE,能实际执行并测量真实运行时间和行数
EXPLAINANALYZE <your_query>;

-- 完整输出包含 12 个字段
-- id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra

2.2 逐字段解读执行计划

type 字段(最重要的字段之一):

type 字段描述了 MySQL 如何找到数据行,从好到坏排序:

type 值 含义 优化建议
system 表只有一行(系统表) 最优,无需关注
const 通过主键或唯一索引一次找到 最优,查询已经最优
eq_ref JOIN 时通过主键或唯一索引关联 正常
ref 通过普通索引等值匹配 可接受,注意索引选择性
range 索引范围扫描 正常,注意范围大小
index 全索引扫描 性能差,通常需要优化
ALL 全表扫描 严重问题,必须优化

如果 type 是 ALL,说明这条查询在走全表扫描,是慢查询的最常见根因。

rows 字段:MySQL 优化器预估需要扫描的行数。这个数字越大,性能通常越差。但注意这是预估,不一定准确(统计信息过期时会不准)。

Extra 字段:包含大量优化决策信息,需要重点关注的内容:

Using filesort    -- 使用了文件排序(外部排序),非常慢,需要优化
Using temporary   -- 创建了临时表,很慢,需要优化
Using index      -- 利用了覆盖索引,性能好
Using index condition -- 利用了索引下推
Using where      -- 在存储引擎层过滤数据

常见有问题的 Extra 组合:

  • Using filesort:ORDER BY 的列没有索引,或索引顺序和 ORDER BY 不一致。
  • Using temporary:DISTINCT、GROUP BY、UNION 等操作需要临时表。
  • Using filesort + Using temporary:同时出现,说明既需要排序又需要临时表,最糟糕的情况。

2.3 实际案例分析

-- 原始慢查询
EXPLAINSELECT * FROM orders WHERE user_id = 12345ORDERBY created_at DESCLIMIT10;

-- 输出:
-- id: 1
-- select_type: SIMPLE
-- table: orders
-- type: ALL              <-- 全表扫描!这是问题
-- possible_keys: NULL    <-- 没有可用索引
-- key: NULL
-- rows: 1523872         <-- 扫描了 150 万行
-- filtered: 100.00
-- Extra: Using where; Using filesort  <-- 还用了文件排序

-- 优化后(添加索引后)
EXPLAINSELECT * FROM orders WHERE user_id = 12345ORDERBY created_at DESCLIMIT10;

-- 输出:
-- id: 1
-- select_type: SIMPLE
-- table: orders
-- type: ref              <-- 使用了索引查找
-- possible_keys: idx_user_id_created  <-- 可用索引
-- key: idx_user_id_created  <-- 实际使用的索引
-- key_len: 4             <-- 索引长度
-- ref: const
-- rows: 10               <-- 只扫描了 10 行
-- filtered: 100.00
-- Extra: Backward index scan; Using where  <-- 倒序扫描索引,Using filesort 消失

2.4 联合索引的索引列顺序判断

-- 如果查询条件是 WHERE a = 1 AND b = 2 AND c > 3 ORDER BY d
-- 索引顺序应该是:(a, b, c, d)
-- 因为最左前缀原则要求索引列从左到右依次使用

-- 验证索引顺序是否正确
EXPLAINSELECT * FROM orders WHERE a = 1AND b = 2AND c > 3ORDERBY d;

-- 如果 Extra 中仍然出现 Using filesort,说明索引顺序不对
-- 需要创建索引:(a, b, c, d)

第三步:识别常见索引失效场景

慢查询的另一个重灾区是索引明明存在,但因写法问题导致索引失效,优化器选择全表扫描。以下是常见场景。

3.1 在索引列上使用函数或运算

-- 索引失效示例:WHERE YEAR(created_at) = 2026
EXPLAINSELECT * FROM orders WHEREYEAR(created_at) = 2026;
-- type: ALL (全表扫描),因为 MySQL 必须对每一行计算 YEAR() 才能判断

-- 正确写法:使用范围查询
EXPLAINSELECT * FROM orders WHERE created_at >= '2026-01-01'AND created_at < '2027-01-01';
-- type: range(使用索引范围扫描)

-- 索引失效示例:WHERE price * 1.1 > 100
EXPLAINSELECT * FROM products WHERE price * 1.1 > 100;
-- 同样全表扫描

-- 正确写法:WHERE price > 100 / 1.1
EXPLAINSELECT * FROM products WHERE price > 100 / 1.1;

3.2 LIKE 前缀通配符

-- 索引失效:LIKE 以通配符开头
EXPLAINSELECT * FROMusersWHEREnameLIKE'%wang%';
-- type: ALL,全表扫描

-- 正确写法:如果必须模糊匹配,考虑全文索引(FULLTEXT)
EXPLAINSELECT * FROMusersWHEREnameLIKE'wang%';
-- type: range,使用索引

-- 如果业务确实需要 %wang% 这种匹配,考虑:
-- 1. 全文索引:ALTER TABLE users ADD FULLTEXT(name);
-- 2. 分词 + 倒排索引
-- 3. Elasticsearch

3.3 隐式类型转换

-- phone 是 VARCHAR(20) 类型的列,存储了字符串 "13800138000"
-- 如果查询时传入数字,索引失效

EXPLAINSELECT * FROMusersWHERE phone = 13800138000;
-- phone 是字符串类型,但传入的是数字,MySQL 会将字符串转换为数字再比较
-- 导致全表扫描

-- 正确写法:使用字符串字面量
EXPLAINSELECT * FROMusersWHERE phone = '13800138000';

3.4 OR 条件导致索引失效

-- OR 条件:只有当所有条件都有索引时才会使用索引
EXPLAINSELECT * FROMusersWHEREname = 'zhangsan'OR email = 'zhangsan@example.com';
-- 如果 name 有索引、email 没有索引,MySQL 仍然选择全表扫描

-- 优化写法:拆分成 UNION
EXPLAINSELECT * FROMusersWHEREname = 'zhangsan'
UNION
SELECT * FROMusersWHERE email = 'zhangsan@example.com';

-- 注意:如果 email 列的选择性很差(很多重复值),即使有索引也不该用

3.5 确认索引是否被使用

-- 查看某条查询实际使用的索引
EXPLAINSELECT * FROM orders WHEREstatus = 'completed'AND created_at > '2026-04-01';

-- possible_keys: 显示 MySQL 认为可用的索引
-- key: 显示实际使用的索引

-- 如果 possible_keys 有值但 key 是 NULL:
-- 可能是 MySQL 认为全表扫描更快(数据量小、统计信息不准)
-- 或者索引列的选择性太差(大量重复值)

-- 查看索引的选择性(基数)
SHOWINDEXFROM orders;
-- Cardinality 列显示索引列的唯一值数量
-- 如果 Cardinality 很小(接近行数),说明选择性差,不适合建索引

第四步:深度分页问题

深度分页(OFFSET 很大)是慢查询的重灾区。比如 LIMIT 100000, 10 这种查询,MySQL 需要先扫描前 100010 行,然后丢弃前 100000 行,返回最后 10 行。OFFSET 越大,浪费越多。

4.1 识别深度分页查询

-- 这种查询就是典型的深度分页
SELECT * FROM orders WHEREstatus = 'completed'ORDERBYidDESCLIMIT100000, 10;

-- pt-query-digest 输出的 Query 特征:
-- FROM orders WHERE status = 'completed' ORDER BY id DESC LIMIT ... OFFSET ...
-- Rows in set after limit: 10 (但实际扫描了 100010 行)

4.2 优化方案一:使用游标分页

利用主键 ID 的单调性,避免 OFFSET:

-- 第一页
SELECT * FROM orders WHEREstatus = 'completed'ORDERBYidDESCLIMIT10;
-- 记住最后一行的 id,比如是 987654

-- 第二页:利用上一页最后的 ID 作为起点
SELECT * FROM orders
WHEREstatus = 'completed'ANDid < 987654
ORDERBYidDESCLIMIT10;
-- 这个查询利用了主键索引,时间复杂度是 O(log N + 10),而不是 O(N)

注意:游标分页只适合有明确排序场景,且排序字段最好是单调递增/递减的(时间倒序、ID 倒序)。如果需要随机跳页,这种方式不适用。

4.3 优化方案二:延迟关联

减少回表次数:

-- 原始深度分页(慢)
SELECT * FROM orders WHEREstatus = 'completed'ORDERBYidDESCLIMIT100000, 10;

-- 优化:先通过索引只查主键,再关联回原表取其他列
SELECT o.* FROM orders o
INNERJOIN (
    SELECTidFROM orders WHEREstatus = 'completed'ORDERBYidDESCLIMIT100000, 10
) AS t ON o.id = t.id;

4.4 优化方案三:记录总页数上限

-- 对搜索结果设置最大页数限制,超过后返回空或提示用户使用更精确的搜索条件
-- 例如:限制最大 OFFSET 为 10000
SELECT * FROM orders
WHEREstatus = 'completed'AND created_at > '2026-04-01'
ORDERBYidDESC
LIMIT10OFFSET100000;

-- 如果超过 10000 页,返回友好提示或建议用户使用更精确的搜索条件

第五步:多表 JOIN 优化

多表 JOIN 是慢查询的另一大来源。JOIN 的执行顺序、驱动表选择、索引条件都会极大影响性能。

5.1 查看 JOIN 执行计划

EXPLAINSELECT u.name, o.order_no, o.amount
FROMusers u
INNERJOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'AND o.created_at > '2026-04-01';

-- id: 执行顺序,id 相同从上到下执行,id 越大优先级越高
-- select_type: SIMPLE(简单查询)、DERIVED(派生表/子查询)、UNION 等
-- table: 查询涉及的表
-- type: 关联类型
-- key: 实际使用的索引

-- 重点关注:
-- 1. 驱动表是谁(通常是第一张表)
-- 2. 是否有全表扫描(type=ALL)
-- 3. JOIN 的 key 是否正确关联

5.2 JOIN 优化原则

原则一:小表驱动大表。

MySQL 优化器通常会自动选择小表作为驱动表(数据量小的表),但不是绝对的。可以用 STRAIGHT_JOIN 强制指定驱动表:

-- 强制使用 users 作为驱动表(需要根据实际数据量判断)
SELECTSTRAIGHT_JOIN u.name, o.order_no, o.amount
FROMusers u
INNERJOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'AND o.created_at > '2026-04-01';

原则二:被驱动表的关联列必须有索引。

-- orders.user_id 如果没有索引,每次关联都是全表扫描
-- 先确认索引是否存在
SHOWINDEXFROM orders;

-- 如果不存在,创建索引
ALTERTABLE orders ADDINDEX idx_user_id (user_id);

5.3 常见 JOIN 问题分析

-- 问题一:多表 JOIN 后出现 Using filesort
EXPLAINSELECT u.name, o.order_no
FROMusers u
INNERJOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
ORDERBY o.created_at DESC;
-- 如果 o.created_at 上没有索引,排序会用到文件排序

-- 解决方案:添加联合索引
ALTERTABLE orders ADDINDEX idx_user_created (user_id, created_at);

-- 问题二:子查询导致的性能问题
EXPLAINSELECT * FROM orders
WHERE user_id IN (SELECTidFROMusersWHEREstatus = 'inactive');
-- IN (SELECT ...) 在 MySQL 5.7 及之前性能很差,会先执行外层再执行内层

-- 优化:改写为 JOIN
EXPLAINSELECT o.* FROM orders o
INNERJOINusers u ON o.user_id = u.id
WHERE u.status = 'inactive';

-- 问题三:LEFT JOIN 右表条件放错位置
SELECT u.name, o.order_no
FROMusers u
LEFTJOIN orders o ON u.id = o.user_id AND o.status = 'completed'-- 条件放在 ON
WHERE o.id ISNULL;  -- 找没有订单的用户

-- 这样做是对的,但很多人会把这个条件放到 WHERE 里:
SELECT u.name, o.order_no
FROMusers u
LEFTJOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed';  -- 错误:LEFT JOIN 条件放 WHERE 会把 LEFT 变成 INNER

第六步:查看 MySQL 实时状态和进程

有时候慢查询不是单条 SQL 的问题,而是大量并发查询把数据库打满了。需要从全局视角看数据库负载。

6.1 查看当前连接数和活跃查询

-- 查看当前连接状态
SHOWSTATUSLIKE'Threads%';
-- Threads_connected: 当前连接数
-- Threads_running: 当前正在执行的查询数(不包括等待中的)

-- 查看最大连接数配置
SHOWVARIABLESLIKE'max_connections';
-- 默认 151,建议根据实际并发需求调整

-- 查看当前所有连接
SHOWPROCESSLIST;
-- 或
SHOWFULLPROCESSLIST;

-- 输出各列含义:
-- Id: 连接 ID
-- User: 用户名
-- Host: 客户端 IP
-- db: 连接的数据库
-- Command: 当前命令(Sleep/Query/Connect 等)
-- Time: 执行时间(秒)
-- State: 状态
-- Info: SQL 内容(只显示前 100 字符)

重点关注:Command=Query 且 Time 很长(比如超过 60 秒)的查询,说明有慢查询正在执行。

6.2 查看数据库锁等待

锁等待是导致查询卡住的常见原因,特别是有大事务或长事务时。

-- 查看当前锁等待信息(MySQL 8.0)
SELECT * FROM performance_schema.data_lock_waits;

-- 查看当前的事务
SELECT * FROM information_schema.INNODB_TRX;

-- 输出各列含义:
-- trx_id: 事务 ID
-- trx_state: 事务状态(RUNNING/LOCK WAIT/ROLLING BACK)
-- trx_started: 事务开始时间
-- trx_rows_locked: 锁住的行数
-- trx_mysql_thread_id: 对应的 MySQL 连接 ID
-- trx_query: 正在执行的 SQL

-- 查看具体的锁信息
SHOWENGINEINNODBSTATUS;
-- 输出信息量很大,包含:
-- LATEST DETECTED DEADLOCK: 最近检测到的死锁
-- TRANSACTIONS: 当前所有事务和锁的状态
-- LOCK WAIT: 等待中的锁
-- ROW OPERATIONS: 当前的行操作统计

发现长时间 LOCK WAIT 的事务:

-- 查看超过 60 秒仍未完成的事务
SELECT
    trx_id,
    trx_state,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
    trx_rows_locked,
    trx_query,
    ps.user AS mysql_user,
    host_info
FROM information_schema.INNODB_TRX
JOIN performance_schema.threads AS ps ON ps.processlist_id = trx_mysql_thread_id
WHERETIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDERBY duration_sec DESC;

⚠️ 风险提醒:KILL 正在执行的事务是破坏性操作,可能导致事务回滚。如果要 KILL,先确认:

  1. 该事务是否是正常的长事务(比如数据导入)
  2. 是否已经提交(已提交的话 KILL 没有意义)
  3. 回滚本身也需要时间(大事务回滚可能更慢)

6.3 查看数据库整体性能指标

-- 查看 Buffer Pool 命中率
SHOWSTATUSLIKE'Innodb_buffer_pool_read%';
-- Innodb_buffer_pool_read_requests: 读请求数
-- Innodb_buffer_pool_reads: 从磁盘读取的次数(未命中)
-- 命中率 = 1 - (reads / read_requests),应该 > 95%

-- 计算公式(在 MySQL 客户端执行)
SELECT
    variable_value AS read_requests,
    variable_value AS disk_reads
FROM performance_schema.global_status
WHERE variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads');

-- 查看 QPS 和 TPS
SHOWSTATUSLIKE'Questions';
SHOWSTATUSLIKE'Uptime';

-- 查看慢查询数量
SHOWGLOBALSTATUSLIKE'Slow_queries';

-- 查看已经创建临时表和磁盘临时表的数量
SHOWGLOBALSTATUSLIKE'Created_tmp%';
-- Created_tmp_disk_tables 不应该太高,否则说明排序缓冲区不足

第七步:索引设计与优化实战

7.1 如何判断一个查询是否需要索引

原则:在 WHERE 条件、JOIN ON 条件、ORDER BY、GROUP BY 中出现的列,考虑建索引。

-- 高频查询:WHERE status = 'completed' AND created_at > '2026-04-01'
-- 应该建联合索引:(status, created_at)

-- 低频查询:如果某查询每天只执行几次,即使慢也不值得花太多精力优化
-- 优先优化高频查询

7.2 联合索引的最左前缀原则

联合索引 (a, b, c) 能支持的查询场景:

✅ WHERE a = 1                  -- 使用索引 (a)
✅ WHERE a = 1 AND b = 2        -- 使用索引 (a, b)
✅ WHERE a = 1 AND b = 2 AND c = 3  -- 使用完整索引 (a, b, c)
✅ WHERE a IN (1, 2)            -- 使用索引 (a)
✅ WHERE a = 1 AND c = 3        -- 使用索引 (a),但 c 部分无法利用索引
❌ WHERE b = 2                  -- 不使用索引
❌ WHERE c = 3                  -- 不使用索引
❌ WHERE b = 2 AND c = 3        -- 不使用索引

ORDER BY 同样遵循最左前缀原则:

✅ WHERE a = 1 ORDER BY b       -- 使用索引 (a, b),有序
✅ WHERE a = 1 ORDER BY b, c    -- 使用索引 (a, b, c),有序
❌ WHERE a = 1 ORDER BY c       -- 不使用索引排序,会产生 filesort

7.3 覆盖索引

覆盖索引是指一个索引包含了查询需要的所有列,这样 MySQL 不需要回表(访问主键索引),直接在索引树就能拿到结果:

-- 查询:只需要 id, user_id, order_no 三列
-- 如果这三个列都在索引中,就是覆盖索引

-- 创建覆盖索引
ALTERTABLE orders ADDINDEX idx_cover (user_id, order_no, id);

-- 验证是否使用了覆盖索引(Extra 列显示 Using index)
EXPLAINSELECT user_id, order_no, idFROM orders WHERE user_id = 123;
-- Extra: Using index  <-- 说明用了覆盖索引,不需要回表

EXPLAINSELECT * FROM orders WHERE user_id = 123;
-- Extra: 没有 Using index  <-- SELECT * 会回表取所有列

7.4 索引设计规范

规范一:避免在低选择性列上建索引

-- 例如 status 字段只有 3 个值(pending/active/completed),选择性极低
-- 对这种字段建 B-Tree 索引几乎没有意义
SELECTCOUNT(DISTINCTstatus) / COUNT(*) AS selectivity FROM orders;
-- 如果结果 < 0.01,说明选择性太低,不适合单独建索引

-- 但如果是联合索引的第一列,还是有意义的(配合其他高选择性列)

规范二:字符串前缀索引

如果字符串列很长(VARCHAR(255) 或 TEXT),直接建索引太占空间。用前缀索引:

-- 对 email 字段建立前 10 个字符的前缀索引
ALTERTABLEusersADDINDEX idx_email_prefix (email(10));

-- 前缀长度选择:让前缀的选择性接近完整列
SELECT
    COUNT(DISTINCTLEFT(email, 5)) / COUNT(*) AS prefix_5,
    COUNT(DISTINCTLEFT(email, 10)) / COUNT(*) AS prefix_10,
    COUNT(DISTINCTLEFT(email, 15)) / COUNT(*) AS prefix_15,
    COUNT(DISTINCT email) / COUNT(*) ASfull
FROMusers;
-- 选择使前缀选择性接近 full 值的最小长度

规范三:不要在频繁更新的字段上建索引

-- 每次更新 orders 表的 updated_at 字段时,索引也需要更新
-- 如果 updated_at 更新非常频繁,索引维护开销会很大
-- 评估方式:查看该字段的 UPDATE 频率和查询频率的对比

第八步:配置层面的优化

除了 SQL 和索引,MySQL 本身的一些参数也会影响查询性能。

8.1 Buffer Pool 配置

Buffer Pool 是 MySQL 最核心的缓存区域,存放表数据、索引、锁信息等。如果 Buffer Pool 不够大,热数据无法全部缓存,频繁 disk IO 导致查询变慢。

-- 查看 Buffer Pool 大小配置
SHOWVARIABLESLIKE'innodb_buffer_pool_size';
-- 默认通常很小,需要调整为物理内存的 50%-80%(留内存给 OS 和其他用途)

-- 查看 Buffer Pool 使用情况
SHOWSTATUSLIKE'Innodb_buffer_pool%';
-- Innodb_buffer_pool_pages_total: 总页数
-- Innodb_buffer_pool_pages_free: 空闲页数
-- Innodb_buffer_pool_pages_dirty: 脏页数(已修改未刷盘)
# 修改 Buffer Pool 大小(需要重启 MySQL)
# 编辑 /etc/my.cnf
sudo vi /etc/my.cnf
# 在 [mysqld] 段添加:
innodb_buffer_pool_size = 8G  # 调整为 8GB,根据实际内存和业务需求来

# 如果 MySQL 8.0,可以使用在线调整(不需要重启)
SET GLOBAL innodb_buffer_pool_size = 8589934592;

8.2 慢查询日志参数调优

# /etc/my.cnf 中的慢查询相关配置
[mysqld]
# 超过 1 秒的查询记录到慢查询日志
long_query_time = 1

# 慢查询日志文件路径
slow_query_log_file = /var/lib/mysql/mysql-slow.log

# 开启慢查询日志
slow_query_log = 1

# 记录未使用索引的查询(MySQL 5.7 有用,8.0 已移除该参数)
log_queries_not_using_indexes = 1

# 日志输出格式(建议 FILE,不要 TABLE,TABLE 会有锁问题)
log_output = FILE

# 限制慢查询日志文件大小,避免撑爆磁盘
max_slow_log_size = 100M

8.3 连接数和临时表配置

-- 查看最大连接数
SHOWVARIABLESLIKE'max_connections';
-- 默认 151,建议根据峰值并发调整
-- 注意:每个连接都占用内存,盲目加太大会 OOM

-- 查看临时表和排序缓冲区使用情况
SHOWGLOBALSTATUSLIKE'Created_tmp%';
SHOWVARIABLESLIKE'tmp_table_size';
SHOWVARIABLESLIKE'max_heap_table_size';

-- 如果 Created_tmp_disk_tables 很高,考虑增加 tmp_table_size
SETGLOBAL tmp_table_size = 256M;
SETGLOBAL max_heap_table_size = 256M;

第九步:完整排查流程总结

9.1 慢查询排查标准流程

第一步:抓取慢查询

-- 1. 确认慢查询日志开启
SHOWVARIABLESLIKE'slow_query_log';

-- 2. 如果没开启,临时开启
SETGLOBAL slow_query_log = 'ON';
SETGLOBAL long_query_time = 1;

-- 3. 用 pt-query-digest 分析(推荐)
pt-query-digest /var/lib/mysql/mysql-slow.log --limit 20

第二步:分析执行计划

-- 对最慢的查询逐条执行 EXPLAIN
EXPLAIN <slow_query>;

-- 关注:
-- type: 是否有 ALL(全表扫描)
-- rows: 预估扫描行数是否过大
-- Extra: 是否有 Using filesort、Using temporary

第三步:定位根因并优化

根因类型                  解决方案
────────────────────────────────────────────────────
type=ALL                 添加 WHERE 条件列的索引
Extra: Using filesort    添加 ORDER BY 列的索引,或调整索引顺序
Extra: Using temporary   优化 GROUP BY / DISTINCT / UNION 写法
索引失效(函数/运算)     改写 SQL,不在索引列上使用函数
深度分页(OFFSET 很大)   改用游标分页或延迟关联
JOIN 驱动表不对           加 STRAIGHT_JOIN 或加索引
Buffer Pool 命中率低     调大 innodb_buffer_pool_size

第四步:验证优化效果

-- 优化后再次 EXPLAIN,确认 type/rows/Extra 是否改善

-- 对比优化前后的查询时间
SET profiling = 1;
<your_optimized_query>;
SHOWPROFILES;
SHOW PROFILE FORQUERY <id>;

-- 检查慢查询日志中该查询是否消失

9.2 SQL 编写规范(避免慢查询)

规范一:避免 SELECT *

-- 慢:读取所有列
SELECT * FROM orders WHERE user_id = 123;

-- 快:只读取需要的列,利用覆盖索引
SELECT order_no, amount, created_at FROM orders WHERE user_id = 123;

规范二:WHERE 条件要具体

-- 慢:类型隐式转换
SELECT * FROMusersWHERE phone = 13800138000;

-- 快:使用正确的类型
SELECT * FROMusersWHERE phone = '13800138000';

规范三:分页查询使用游标

-- 慢:深度分页
SELECT * FROM orders ORDERBYidDESCLIMIT100000, 10;

-- 快:游标分页
SELECT * FROM orders WHEREid < <last_seen_id> ORDERBYidDESCLIMIT10;

规范四:批量操作分批提交

-- 慢:一次删除 100 万行(产生大事务、长时间锁)
DELETEFROMlogsWHERE created_at < '2026-01-01';

-- 快:分批删除,每次 1000 行
DELETEFROMlogsWHERE created_at < '2026-01-01'LIMIT1000;
-- 循环执行,直到删完

规范五:避免在 JOIN 前先做 WHERE 过滤

-- 慢:先 JOIN 再过滤,MySQL 可能先笛卡尔积再过滤
SELECT o.*, u.name
FROM orders o
INNERJOINusers u ON o.user_id = u.id
WHERE u.status = 'inactive';

-- 快:先在子查询中过滤,再 JOIN(让 MySQL 先处理小数据集)
SELECT o.*, u.name
FROM orders o
INNERJOIN (SELECTid, nameFROMusersWHEREstatus = 'inactive') u ON o.user_id = u.id;

总结

MySQL 慢查询的排查核心在于:先用慢查询日志抓出现存问题,再用 EXPLAIN 分析执行计划找到索引和查询写法的问题。

最重要的三个判断标准:

  • type 列是否为 ALL(全表扫描,是最需要优化的)
  • rows 预估扫描行数是否过大(超过十万行就要警惕)
  • Extra 列是否有 Using filesort 或 Using temporary(说明有排序或临时表开销)

最常见的慢查询根因:

  1. WHERE 条件列缺少索引,导致全表扫描
  2. 在索引列上使用函数或运算,导致索引失效
  3. 深度分页 OFFSET 过大
  4. SELECT * 不利用覆盖索引
  5. JOIN 被驱动表没有索引

优化优先级:

  1. 全表扫描(type=ALL)必须优先解决,几乎任何情况下都该加索引
  2. Using filesort 其次优化,通常加个索引就能消除
  3. 深度分页改写游标分页
  4. 最后考虑配置层面的优化(Buffer Pool、临时表大小)

日常预防:

  • 上线前用 EXPLAIN 审核每条新 SQL
  • 用 pt-query-digest 定期分析慢查询日志
  • 确保每个高并发查询都有合适的索引
  • 不要在低选择性列上单独建索引
  • 批量操作分批提交,避免长事务

相关阅读:

如何在 MySQL 中处理和优化长时间运行的查询?

十年运维经验 MySQL 生产 20 个核心参数调优实战指南

MySQL 9.5 性能优化终极指南:从 10s 到 10ms 的 5 个核心心法

以上关于MySQL 慢查询怎么排查?从日志到索引优化完整流程的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

33

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » MySQL 慢查询怎么排查?从日志到索引优化完整流程

发表回复