MySQL 查询性能核武器

一条SQL拖垮整个数据库,这事每天都在发生。生产环境一次"慢查询屠杀":凌晨3点,核心交易表因一个漏写索引的WHERE order_date = CURDATE(),触发了全表扫描 + 文件排序,瞬间占满磁盘IO,数据库连接数飙到上限,订单服务全面瘫痪20分钟。而事后定位时发现,慢查询日志里那条SQL执行时间从正常的50ms飙升到了48秒------差了一个960倍的数量级。

慢查询不是"偶尔慢一下",而是系统性的性能定时炸弹。本文将带你从慢查询的"第一现场"出发,逐步深入EXPLAIN执行计划的内核,通过索引优化的四个层级、SQL重写的十种实战技巧、以及三个真实生产案例,彻底掌握MySQL查询优化的屠龙之术。


一、慢查询:从"开启日志"到"精准破案"

1.1 生产环境慢查询配置------别再默认关闭了

很多MySQL实例默认slow_query_log = OFF,相当于在裸奔。正确的生产配置如下:

sql

复制代码
-- 查看当前状态
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';

-- 在线开启(重启失效,建议写入my.cnf)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;          -- 捕捉超过500ms的SQL(高并发业务建议0.1~0.5)
SET GLOBAL log_queries_not_using_indexes = ON;   -- 没有索引的SQL也记录
SET GLOBAL log_throttle_queries_not_using_indexes = 10; -- 每分钟最多记录10条无索引SQL,防止日志爆炸

my.cnf 稳定配置

ini

复制代码
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
long_query_time = 0.3
log_queries_not_using_indexes = 1
log_slow_admin_statements = 1          # 记录慢的管理命令(OPTIMIZE, ANALYZE TABLE)
min_examined_row_limit = 1000          # 扫描行数超过1000才记录

1.2 慢查询分析神器:pt-query-digest

MySQL自带的mysqldumpslow功能太弱,生产环境首选Percona Toolkit的pt-query-digest

bash

复制代码
# 分析今天的慢查询日志,按查询时间降序输出前10
pt-query-digest /var/log/mysql/slow-query.log --limit 10

# 分析并输出到报告文件
pt-query-digest /var/log/mysql/slow-query.log --since '2026-06-04 00:00:00' --until '2026-06-04 23:59:59' > slow_report_0604.txt

输出报告中最关键的三个指标:

  • Response time:总响应时间占比 → 哪个SQL是最大元凶

  • Rows examine:扫描行数 vs 返回行数(扫描/返回比,理想值接近1:1)

  • Full scan:全表扫描标记

1.3 一条价值千万元的慢查询日志实例

sql

复制代码
# Time: 2026-06-04T03:15:23.123456Z
# User@Host: order_app[order_app] @ [10.0.1.100]
# Query_time: 45.832901  Lock_time: 0.000123  Rows_sent: 10  Rows_examined: 5847296
SET timestamp=1749071723;
SELECT o.order_id, o.amount, u.name, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID'
  AND DATE(o.create_time) = '2026-06-03'
  AND u.vip_level > 3
ORDER BY o.amount DESC
LIMIT 10;

问题一眼可见

  • Rows_examined: 5847296 → 扫描了584万行,只返回10行,效率极差

  • DATE(o.create_time) = ... → 对索引列使用函数,索引失效

  • LEFT JOIN + WHERE u.vip_level > 3 → 实际将LEFT JOIN变成了INNER JOIN,可能误导优化器

  • 大表orders没有合适索引,statuscreate_time分开索引无法合并

优化后该SQL从45秒降到0.08秒,QPS从35提升到2100。


二、EXPLAIN 执行计划------一眼看穿SQL的"五脏六腑"

EXPLAIN是MySQL查询优化的手术刀,每个字段都藏着关键信息。我们拆解最核心的字段。

2.1 type:连接类型(性能从好到差)

type 含义 是否可用
system 系统表,只有一行 极少见
const 主键或唯一索引等值匹配 完美
eq_ref 连接时使用主键或唯一索引 优秀
ref 非唯一索引等值匹配 良好
range 索引范围扫描(BETWEEN, >, <, IN) 可接受
index 遍历整个索引树(比全表好一点) 较差
ALL 全表扫描 绝对禁止在生产出现

原则 :线上OLTP系统,大部分查询的type至少应为range,核心查询必须是refconst

2.2 possible_keys vs key

  • possible_keys:优化器认为可能用到的索引

  • key:实际选择的索引

常见陷阱possible_keys有值但key为NULL → 优化器认为走索引不如全表扫描。原因可能是索引区分度太低(如性别字段),或统计信息过期。

sql

复制代码
-- 更新统计信息,让优化器重新评估
ANALYZE TABLE orders;

2.3 rows:优化器估算需要扫描的行数

rows不是精确值,但量级非常重要。如果估算值远大于实际返回行数,说明过滤条件严重依赖索引。

案例 :一个rows=500000LIMIT 10的查询,很可能在filesort或临时表中处理了50万行后才丢弃。

2.4 Extra:魔鬼藏在细节里

Extra 信息 含义 处理方式
Using where 使用WHERE条件过滤 正常,尽量让过滤发生在索引层
Using index 覆盖索引,不回表 最佳状态
Using index condition 索引下推(ICP) MySQL 5.6+ 好特性,减少回表
Using filesort 需要额外排序,不能利用索引顺序 必须优化,为ORDER BY字段建索引
Using temporary 使用临时表(GROUP BY或DISTINCT无索引) 高危,必须加索引
Using join buffer 连接无索引,使用缓存块 需要为连接字段加索引

一条必须看懂的EXPLAIN输出

text

复制代码
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------+
| id | select_type | table  | type       | key  | rows          | Extra                                 |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------+
| 1  | SIMPLE      | orders | ALL        | NULL | 2345678       | Using where; Using filesort            |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-----------------+

诊断 :type=ALL(全表扫描234万行)+ Using filesort → 死定了,必须重建索引。


三、索引优化:从"随便建"到"艺术级设计"

3.1 联合索引的"三最法则"

最左前缀、最高区分度、最少字段

案例 :订单表需要按statuscreate_timeuser_id查询。

sql

复制代码
-- ❌ 错误设计:区分度低的字段放前面
CREATE INDEX idx_status_time_user ON orders(status, create_time, user_id);
-- status只有'PAID','UNPAID','CANCELLED'等少数值,索引树很快遍历大量相同值

-- ✅ 正确设计:区分度高的字段优先(user_id唯一性高),其次范围查询字段放最后
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);
-- 但如果查询条件是WHERE status = 'PAID',则索引无法使用最左前缀,需要重新评估

更精确的设计方法:分析查询模式,将等值查询字段放在前面,范围查询字段(>、<、BETWEEN)放在最后。

3.2 覆盖索引:零回表的极致性能

当索引中包含查询所需的所有列时,InnoDB不需要回表,直接从索引树返回数据,性能提升巨大。

sql

复制代码
-- 查询需求:根据订单号获取订单金额和状态
SELECT order_no, amount, status FROM orders WHERE order_no = 'ORD123456';

-- 创建覆盖索引
CREATE INDEX idx_order_no_cover ON orders(order_no, amount, status);

-- EXPLAIN 显示 Using index

适用场景:高频查询字段固定、对响应极度敏感的核心接口。

3.3 索引失效的12种场景(附验证SQL)

失效场景 示例 解决
1. 索引列使用函数 WHERE DATE(create_time) = '2026-06-04' 改为范围查询 create_time BETWEEN '2026-06-04 00:00:00' AND '2026-06-04 23:59:59'
2. 隐式类型转换 WHERE phone = 13800138000(phone是varchar) 统一类型:WHERE phone = '13800138000'
3. 模糊查询左匹配 WHERE name LIKE '%张三' 业务允许改为右匹配'张三%',或使用全文索引
4. OR条件 WHERE status = 'PAID' OR amount > 1000 使用UNION合并两个索引结果,或用IN
5. 不等于(!=, <>) WHERE status != 'CANCELLED' 改为IN列举可能值
6. IS NULL / IS NOT NULL WHERE deleted_at IS NULL 避免为可空列建索引,或用默认值代替NULL
7. 联合索引跳过最左列 索引(a,b,c),WHERE b=1 重建索引顺序或额外建索引(b)
8. 范围查询后列失效 索引(a,b),WHERE a>10 AND b=1 范围列放最后
9. 使用表达式或计算 WHERE amount + 100 > 2000 移项到常量侧amount > 1900
10. 使用NOT IN / NOT EXISTS WHERE id NOT IN (1,2,3) 改为EXISTSLEFT JOIN ... IS NULL
11. 字符集不一致 表utf8mb4,连接字符集latin1 统一字符集
12. 排序字段方向不一致 索引(a ASC,b DESC),ORDER BY a ASC,b ASC 索引排序方向需与ORDER BY完全一致

3.4 索引维护:不可忽视的"熵增"

索引不是一建永逸的。随着数据更新,索引会产生碎片,导致扫描效率下降。

sql

复制代码
-- 查看索引碎片程度(information_schema)
SELECT TABLE_NAME, INDEX_NAME, STAT_NAME, STAT_VALUE
FROM mysql.innodb_index_stats 
WHERE TABLE_NAME = 'orders' AND STAT_NAME = 'n_diff_pfx%';

-- 重建索引(Online DDL,MySQL 5.6+不锁表)
ALTER TABLE orders ENGINE = InnoDB;

-- 或使用OPTIMIZE TABLE(会锁表,生产低峰期执行)
OPTIMIZE TABLE orders;

建议:每周或每月低峰期对高频更新的大表重建一次索引。


四、SQL重写实战:十条军规

4.1 避免 SELECT *,只取需要的列

sql

复制代码
-- ❌ 返回了所有字段,浪费网络和内存
SELECT * FROM orders WHERE order_id = 12345;

-- ✅ 明确列,并且可以走覆盖索引
SELECT order_id, amount, status FROM orders WHERE order_id = 12345;

4.2 分页优化:深度分页的"大坑"

sql

复制代码
-- ❌ 偏移量100万行,MySQL需要扫描并丢弃前100万行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

-- ✅ 延迟关联或记录上次ID
SELECT * FROM orders 
WHERE id > 1000000 
ORDER BY id 
LIMIT 20;

-- ✅ 或者使用子查询先获取ID
SELECT * FROM orders 
INNER JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 1000000, 20
) AS tmp USING(id);

4.3 JOIN优化:小表驱动大表,确保连接字段有索引

  • INNER JOIN:MySQL自动选择驱动表,但需保证被驱动表连接字段有索引

  • LEFT JOIN:驱动表固定为左表,左表尽量小

案例

sql

复制代码
-- ❌ 连接字段无索引,导致全表扫描
SELECT * FROM orders o 
LEFT JOIN users u ON o.user_id = u.id;  -- users.id有主键,但o.user_id无索引,orders全扫

-- ✅ 为orders.user_id建索引
CREATE INDEX idx_user_id ON orders(user_id);

4.4 使用 EXISTS 替代 IN(当子查询数据量大时)

sql

复制代码
-- 当子查询结果集很大时,IN会先构建临时表
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE vip_level = 1);

-- EXISTS 更适合:外部查询驱动,利用索引
SELECT * FROM orders o 
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.vip_level = 1);

4.5 批量操作:逐条循环改批量

java

复制代码
// ❌ 逐条更新,1000次网络往返
for (Long id : ids) {
    jdbcTemplate.update("UPDATE orders SET status = 'DONE' WHERE id = ?", id);
}

// ✅ 单条SQL批量更新,1次网络往返
jdbcTemplate.update("UPDATE orders SET status = 'DONE' WHERE id IN (?)", ids);

4.6 GROUP BY + 索引优化

如果GROUP BY的列没有索引,会生成临时表并可能产生Using filesort

sql

复制代码
-- 为group by的列加上索引,同时满足where条件
CREATE INDEX idx_status_user ON orders(status, user_id);
SELECT status, COUNT(*) FROM orders GROUP BY status;

4.7 避免在WHERE子句中对字段进行NULL判断

sql

复制代码
-- ❌
SELECT * FROM orders WHERE update_time IS NULL;

-- ✅ 改为默认值(如'1970-01-01'),并建索引

4.8 UNION 使用原则

  • UNION ALLUNION 快(不去重)

  • 对每个子查询的WHERE条件充分利用索引

4.9 使用约束代替查询后过滤

尽量在SQL层面完成数据过滤,而不是在应用层。

4.10 定期审查慢查询周报

每周分析慢查询日志趋势,发现潜在的性能退化。


五、实战案例:从30秒到0.03秒的三次蜕变

5.1 案例1:统计报表中的"全表扫描+临时表"

原始SQL(统计某天不同状态的订单总额):

sql

复制代码
SELECT status, SUM(amount) 
FROM orders 
WHERE DATE(create_time) = '2026-05-01' 
GROUP BY status;

执行计划 :type=ALL,Extra=Using where; Using temporary; Using filesort

扫描行数 :834万。

响应时间:32秒。

优化过程

  1. 消除函数:create_time BETWEEN '2026-05-01 00:00:00' AND '2026-05-01 23:59:59'

  2. 创建联合索引:(create_time, status, amount)(覆盖索引)

  3. 改写SQL,利用索引有序性

优化后

sql

复制代码
SELECT status, SUM(amount) 
FROM orders 
WHERE create_time >= '2026-05-01 00:00:00' 
  AND create_time < '2026-05-02 00:00:00'
GROUP BY status;

结果 :type=range,Extra=Using index,扫描行数=8230行,响应时间=0.09秒。

5.2 案例2:分页查询中的"延迟关联"

原始分页查询

sql

复制代码
SELECT * FROM orders WHERE status = 'PAID' ORDER BY create_time DESC LIMIT 100000, 20;

即使有(status, create_time)索引,MySQL仍需要扫描前100020行数据,offset 100000的性能灾难。

优化后

sql

复制代码
SELECT * FROM orders o
INNER JOIN (
    SELECT id FROM orders 
    WHERE status = 'PAID' 
    ORDER BY create_time DESC 
    LIMIT 100000, 20
) tmp ON o.id = tmp.id;

原理 :子查询SELECT id可以利用覆盖索引快速定位20个ID,然后主查询用主键快速回表。

响应时间:从5.2秒降到0.08秒。

5.3 案例3:JOIN顺序导致的"全表扫描噩梦"

原始SQL

sql

复制代码
SELECT o.order_no, u.name 
FROM users u 
LEFT JOIN orders o ON u.id = o.user_id 
WHERE u.registration_time > '2026-01-01'
  AND o.amount > 1000;

问题:LEFT JOIN + WHERE o.amount > 1000 将LEFT JOIN隐式转为INNER JOIN,且驱动表为users(可能很大),orders表的user_id无索引。

优化:先过滤小表,再JOIN,并为关联字段加索引。

sql

复制代码
SELECT o.order_no, u.name 
FROM (SELECT id, name FROM users WHERE registration_time > '2026-01-01') u
INNER JOIN orders o ON u.id = o.user_id 
WHERE o.amount > 1000;

创建索引:ALTER TABLE orders ADD INDEX idx_user_amount (user_id, amount);

效果:从18秒降到0.2秒。


六、慢查询治理体系:从"救火"到"免疫"

6.1 开发规范:上线前必须经过EXPLAIN审核

  • 禁止type=ALL的SQL上线

  • 禁止使用SELECT *

  • 禁止对索引列使用函数和隐式转换

  • 联合索引必须遵循最左前缀

6.2 自动化工具:pt-query-digest + 监控告警

将慢查询日志集成到Prometheus或ELK,设置阈值告警(如慢查询频率 > 10次/分钟触发报警)。

6.3 周期性复盘:每月一次SQL性能Review

挑选TOP 10慢查询,归类分析原因并优化。建立SQL性能基线和退化检测。


七、总结:查询优化的核心心法

  1. 慢查询日志是起点:没有度量就没有优化;

  2. EXPLAIN是手术刀:学会读懂type、rows、Extra,一眼定位问题;

  3. 索引设计是艺术:遵循"三最法则",覆盖索引是最强武器;

  4. SQL重写是基本功:避免函数、隐式转换、深度分页、不合理的JOIN;

  5. 治理体系是保障:规范+自动化+周期复盘,让系统具备免疫能力。

最后分享一个真实感受:很多团队花了大量时间在架构设计、中间件选型上,却不愿意花半小时分析一条慢查询。然而实际生产线上,一条索引漏建导致的数据库CPU飙高,足以摧毁所有上层的高可用设计。从今天起,把你的EXPLAIN能力练到炉火纯青,那才是真正保护系统尊严的核武器。

文末互动:你在工作中遇到过最离谱的慢查询是什么样的?欢迎在评论区分享你的踩坑经历!

相关推荐
CingSyuan1 小时前
服务器RAID阵列与硬盘状态运维指南:RAID级别、降级、踢盘、掉盘、热备盘、重建风险、硬盘指示灯与点灯流程解析
运维·服务器
八解毒剂2 小时前
查找-从二分查找到二叉排序树
数据结构·c++·算法
JP-Destiny2 小时前
docker-安装nacos
运维·docker·容器
pixelpilot12 小时前
Moldflow下载安装教程(附安装包)Moldflow 2026超详细图文安装教程
运维·服务器·其他
haven-8522 小时前
mysql索引当中的B+树,聚簇/二级索引,最左匹配,失效场景
数据库·b树·mysql
你是个什么橙2 小时前
Docker下载安装及服务
运维·docker·容器
开发者联盟league2 小时前
在ubuntu上安装harbor
linux·运维·ubuntu
jason_renyu2 小时前
MySQL数据表设计入门学习文档(基于Flask+Vue3图书馆管理系统·小白专用)
mysql·数据表设计入门学习·mysql数据库表设计学习·新手入门数据表设计
luoqice2 小时前
Windows下局域网rtsp流媒体服务器搭建-测试
服务器·windows·音视频