🐌 数据库慢查询速成班:让你的SQL从蜗牛变火箭!

"为什么我的查询跑了3分钟还在转圈圈?!" 😭

📖 什么是慢查询?

想象你去餐厅点餐:

  • 快查询:点了份炒饭,3分钟上桌 ✅ 😋
  • 慢查询:点了份炒饭,等了3小时还没上... ❌ 😡

数据库慢查询就是这样,一条 SQL 执行时间超出预期(通常 > 1秒),导致用户体验变差,甚至拖垮整个系统!

慢查询的危害 💀

sql 复制代码
用户视角:
  点击按钮 → ⏳转圈圈 → 😤等待 → 🤬超时 → 💔关闭页面

系统视角:
  慢SQL → 连接占用 → 连接池耗尽 → 新请求无法处理 → 雪崩 💥

🎯 第一步:如何发现慢查询?

方法 1:MySQL 慢查询日志 📝

开启慢查询日志

sql 复制代码
-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query%';

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';

-- 设置慢查询阈值(单位:秒)
SET GLOBAL long_query_time = 1;  -- 超过1秒就记录

-- 设置日志文件位置
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 记录未使用索引的查询
SET GLOBAL log_queries_not_using_indexes = 'ON';

查看慢查询日志

bash 复制代码
# 直接查看文件
tail -f /var/log/mysql/slow.log

# 使用 mysqldumpslow 分析(MySQL自带工具)
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s t:按查询时间排序
# -t 10:显示前10条

日志格式

sql 复制代码
# Time: 2024-10-24T10:30:45.123456Z
# User@Host: root[root] @ localhost []
# Query_time: 3.521045  Lock_time: 0.000123  Rows_sent: 1  Rows_examined: 1000000
SELECT * FROM users WHERE name LIKE '%张%';

解读

  • Query_time:查询总耗时(3.52秒,太慢了!)
  • Lock_time:等待锁的时间
  • Rows_sent:返回的行数(只返回1行)
  • Rows_examined:扫描的行数(扫了100万行!)

关键指标 :如果 Rows_examined >> Rows_sent,说明查询效率很低!


方法 2:使用监控工具 📊

Prometheus + Grafana

yaml 复制代码
# 监控慢查询指标
- name: mysql_slow_queries
  query: rate(mysql_global_status_slow_queries[5m])
  alert: slow_queries_high
  threshold: > 10  # 5分钟内慢查询超过10次就告警

阿里云 RDS / 腾讯云 MySQL

  • 控制台自带慢查询分析
  • 可视化图表
  • 一键优化建议

🔍 第二步:EXPLAIN - SQL 的"体检报告"

EXPLAIN 是 MySQL 提供的执行计划查看工具,相当于给 SQL 做了个全身检查。

基本用法

sql 复制代码
EXPLAIN SELECT * FROM users WHERE age = 25;

EXPLAIN 输出字段详解 🎓

字段 含义 重点关注
id 查询序号 数字越大越先执行
select_type 查询类型 SIMPLE、SUBQUERY、UNION
table 访问的表名 哪张表
type 访问类型 性能从好到坏
possible_keys 可能使用的索引 候选索引
key 实际使用的索引 NULL=没用索引
key_len 索引长度 越短越好
ref 索引的哪一列被使用 const、字段名
rows 预估扫描行数 越少越好
filtered 过滤百分比 越高越好
Extra 额外信息 重要优化线索

⭐ 最关键:type 字段(性能排行榜)

sql 复制代码
从最快到最慢:

🚀 system > const > eq_ref > ref > range > index > ALL 🐌

┌─────────┬──────────────────────────────┬────────┐
│  type   │           说明                │ 性能   │
├─────────┼──────────────────────────────┼────────┤
│ system  │ 表只有一行(系统表)          │ ⭐⭐⭐⭐⭐ │
│ const   │ 主键或唯一索引等值查询        │ ⭐⭐⭐⭐⭐ │
│ eq_ref  │ 唯一索引扫描(JOIN)          │ ⭐⭐⭐⭐  │
│ ref     │ 非唯一索引扫描                │ ⭐⭐⭐   │
│ range   │ 索引范围扫描(>、<、BETWEEN) │ ⭐⭐    │
│ index   │ 全索引扫描                    │ ⭐     │
│ ALL     │ 全表扫描(最慢!)            │ 💀     │
└─────────┴──────────────────────────────┴────────┘

示例对比

sql 复制代码
-- ❌ type = ALL(全表扫描,太慢!)
EXPLAIN SELECT * FROM users WHERE age = 25;

+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 100000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

扫描了 10 万行!


-- ✅ 添加索引后,type = ref(走索引,快!)
CREATE INDEX idx_age ON users(age);
EXPLAIN SELECT * FROM users WHERE age = 25;

+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
|  1 | SIMPLE      | users | ref  | idx_age       | idx_age | 4       | const | 120  | Using index |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+

只扫描 120 行!性能提升 800 倍!

⭐ Extra 字段(隐藏的优化线索)

Extra 值 含义 优化建议
Using index 覆盖索引,只读索引不回表 很好!
Using where 使用了 WHERE 过滤 普通
Using index condition 索引下推 好!
Using filesort ⚠️ 文件排序(慢) 优化!添加索引
Using temporary ⚠️ 使用临时表 优化!
Using join buffer ⚠️ JOIN 没用索引 加索引!
Impossible WHERE WHERE 永远为假 检查逻辑

🛠️ 第三步:索引优化 - SQL 的"导航仪"

什么是索引?

生活比喻

  • 没有索引 = 翻字典从第一页一页一页找(全表扫描)📖📖📖
  • 有索引 = 查字典的拼音目录,直接定位到对应页(索引查找)🎯

MySQL 索引类型

1️⃣ B+树索引(最常用)

css 复制代码
          [10, 20, 30]           ← 根节点(非叶子节点)
         /      |      \
   [3,7]     [15,18]   [25,28]  ← 中间节点
    /  \       /  \      /  \
  [1-5] [6-9] [10-19] [20-24] [25-30] ← 叶子节点(存实际数据)
    ↓     ↓      ↓      ↓       ↓
  [→] → [→] → [→] → [→] → [→]  ← 叶子节点用链表连接(支持范围查询)

特点

  • 多路平衡树,高度低(通常 3-4 层)
  • 只有叶子节点存数据
  • 叶子节点有序链表 → 支持范围查询

为什么用 B+ 树而不是二叉树?

css 复制代码
二叉树:                     B+树:
     10                        [10,20,30,40]
    /  \                      /   |   |   \
   5   15          VS.    [1-9] [10-19] [20-29] [30-40]
  / \  / \
 1  7 12 20              高度:2 层        高度:4 层
                         磁盘IO:2 次      磁盘IO:100+ 次

高度:4 层
磁盘IO:4 次

B+树 每个节点可以存几百个key,高度更低 → 磁盘IO更少 → 更快!


2️⃣ 哈希索引

bash 复制代码
hash("张三") = 123  → 直接定位到第123号槽位

特点

  • O(1) 查找,超快!
  • ❌ 不支持范围查询(>、<、BETWEEN)
  • ❌ 不支持排序(ORDER BY)
  • ❌ 不支持最左前缀匹配

适用场景 :精确匹配查询(如 WHERE id = 123


索引优化最佳实践 🎯

✅ 1. 为 WHERE、JOIN、ORDER BY 字段添加索引

sql 复制代码
-- ❌ 慢查询
SELECT * FROM orders WHERE user_id = 123 AND status = 'paid';
-- 全表扫描 10 万行

-- ✅ 添加联合索引
CREATE INDEX idx_user_status ON orders(user_id, status);
-- 只扫描 10 行

✅ 2. 联合索引遵循最左匹配原则

sql 复制代码
-- 创建联合索引
CREATE INDEX idx_abc ON table(a, b, c);

-- ✅ 会使用索引
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

-- ❌ 不会使用索引(跳过了 a)
WHERE b = 2
WHERE c = 3
WHERE b = 2 AND c = 3

-- ⚠️ 部分使用索引(只用到 a)
WHERE a = 1 AND c = 3  -- 用了 a,没用 c(中间跳过了 b)

比喻:联合索引像电话簿,先按姓氏排序,再按名字排序。

  • 你可以找"姓张"的所有人(用第一列)
  • 你可以找"姓张名三"的人(用前两列)
  • ❌ 你不能直接找"名字叫三"的人(跳过第一列)

✅ 3. 覆盖索引避免回表

sql 复制代码
-- ❌ 需要回表(先查索引,再查主键)
SELECT id, name, age, address FROM users WHERE age = 25;
-- 步骤:
-- 1. 在 age 索引中找到记录(得到主键 id)
-- 2. 根据 id 回表查询完整数据(address 不在索引中)

-- ✅ 覆盖索引(不需要回表)
CREATE INDEX idx_age_name_address ON users(age, name, address);
SELECT name, age, address FROM users WHERE age = 25;
-- 步骤:
-- 1. 在索引中找到记录(索引已经包含所有字段)
-- 2. 直接返回结果(不需要回表)

优势:减少一次磁盘IO,速度翻倍!


✅ 4. 避免索引失效的坑 ⚠️

场景 错误写法 正确写法
函数操作 WHERE YEAR(create_time) = 2024 WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'
类型转换 WHERE phone = 12345678(phone是varchar) WHERE phone = '12345678'
前导模糊 WHERE name LIKE '%张%' WHERE name LIKE '张%'
OR 条件 WHERE a = 1 OR b = 2(b无索引) 给b也加索引,或改用 UNION
不等于 WHERE status != 1 WHERE status IN (2,3,4)
IS NOT NULL WHERE email IS NOT NULL 尽量设计字段为 NOT NULL

原因:对索引字段进行函数计算/类型转换,MySQL无法使用索引,只能全表扫描!


✅ 5. 索引不是越多越好

反例

sql 复制代码
-- ❌ 疯狂建索引
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_age ON users(age);
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_phone ON users(phone);
CREATE INDEX idx_address ON users(address);
-- ...建了 20 个索引

问题

  1. 写入变慢:每次 INSERT/UPDATE 都要维护所有索引
  2. 占用空间:索引占用大量磁盘空间
  3. 优化器困惑:太多索引反而让优化器选错

原则

  • 单表索引数量 < 5 个
  • 根据实际查询场景建索引
  • 定期删除不使用的索引

📝 第四步:SQL 语句优化

优化技巧 1:避免 SELECT *

sql 复制代码
-- ❌ 查询所有字段(返回不需要的数据)
SELECT * FROM users WHERE id = 1;
-- 假设表有 50 个字段,总共 10KB 数据

-- ✅ 只查询需要的字段
SELECT id, name, age FROM users WHERE id = 1;
-- 只返回 3 个字段,100 字节

优势

  • 减少网络传输
  • 减少内存占用
  • 可能使用覆盖索引

优化技巧 2:分页优化

sql 复制代码
-- ❌ 深度分页慢(翻到第 10000 页)
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
-- MySQL 会扫描前 100020 行,然后丢弃前 100000 行

-- ✅ 使用子查询优化
SELECT * FROM orders 
WHERE id >= (SELECT id FROM orders ORDER BY id LIMIT 100000, 1) 
LIMIT 20;

-- ✅ 使用游标分页(记录上次最后一条ID)
SELECT * FROM orders WHERE id > 123456 ORDER BY id LIMIT 20;

优化技巧 3:批量操作

sql 复制代码
-- ❌ 逐条插入
INSERT INTO users VALUES (1, '张三');
INSERT INTO users VALUES (2, '李四');
INSERT INTO users VALUES (3, '王五');
-- 3 次网络往返 + 3 次事务提交

-- ✅ 批量插入
INSERT INTO users VALUES 
(1, '张三'),
(2, '李四'),
(3, '王五');
-- 1 次网络往返 + 1 次事务提交

性能提升:100 倍以上!


优化技巧 4:JOIN 优化

sql 复制代码
-- ❌ 没有索引的 JOIN
SELECT * FROM orders o 
JOIN users u ON o.user_id = u.id;
-- 如果 user_id 没有索引 → 嵌套循环,O(n*m)

-- ✅ 添加索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 有了索引 → 哈希连接,O(n+m)

JOIN 的注意事项

  1. JOIN 字段必须有索引
  2. 小表驱动大表(小表在前)
  3. 避免 JOIN 超过 3 张表

优化技巧 5:子查询改 JOIN

sql 复制代码
-- ❌ IN 子查询(可能很慢)
SELECT * FROM orders 
WHERE user_id IN (SELECT id FROM users WHERE level = 'VIP');

-- ✅ 改用 JOIN
SELECT o.* FROM orders o 
JOIN users u ON o.user_id = u.id 
WHERE u.level = 'VIP';

🗂️ 第五步:表结构优化

1. 选择合适的数据类型

场景 ❌ 差的选择 ✅ 好的选择 原因
存储年龄 VARCHAR(10) TINYINT UNSIGNED 1字节 vs 10字节
存储性别 VARCHAR(10) TINYINTENUM('男','女') 节省空间
存储价格 FLOAT DECIMAL(10,2) 精度问题
存储状态 VARCHAR(20) TINYINT + 常量定义 节省空间 + 性能
存储IP VARCHAR(15) INT UNSIGNED + INET_ATON() 4字节 vs 15字节

2. 字段设计原则

✅ 尽量 NOT NULL

sql 复制代码
-- ❌ 允许 NULL
CREATE TABLE users (
    name VARCHAR(50) NULL
);
-- 问题:
-- 1. NULL 需要额外的标志位存储
-- 2. 索引中 NULL 处理复杂
-- 3. COUNT(name) 会跳过 NULL,容易出错

-- ✅ 设置默认值
CREATE TABLE users (
    name VARCHAR(50) NOT NULL DEFAULT ''
);

✅ 合理使用 TEXT/BLOB

sql 复制代码
-- ❌ 把大字段和普通字段放一起
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    content TEXT,  -- 可能几MB
    author VARCHAR(50)
);

SELECT id, title, author FROM articles;  -- 也会加载 content,浪费IO

-- ✅ 拆分大字段到单独的表
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    author VARCHAR(50)
);

CREATE TABLE article_content (
    article_id INT PRIMARY KEY,
    content TEXT
);

3. 分表策略

垂直拆分(按字段)

bash 复制代码
原来:
users 表 (50个字段)
id | name | age | email | address | ... | 46个不常用字段

拆分后:
users_basic 表 (常用字段)
id | name | age | email

users_detail 表 (不常用字段)
user_id | address | ... | 其他45个字段

优势:热数据小了,缓存命中率高了!


水平拆分(按行)

sql 复制代码
-- 原来:orders 表 (1亿条记录)
orders
  ├─ 2020年订单 (1000万)
  ├─ 2021年订单 (2000万)
  ├─ 2022年订单 (3000万)
  ├─ 2023年订单 (4000万)

-- 拆分后:
orders_2020 (1000万)
orders_2021 (2000万)
orders_2022 (3000万)
orders_2023 (4000万)

优势:单表数据量小了,查询更快!


📊 第六步:硬件与配置优化

1. MySQL 配置参数

ini 复制代码
# my.cnf 或 my.ini

# 缓冲池大小(最重要!)
innodb_buffer_pool_size = 8G  # 物理内存的 70%-80%

# 日志文件大小
innodb_log_file_size = 512M

# 查询缓存(MySQL 8.0 已移除)
query_cache_size = 0  # 建议禁用

# 连接数
max_connections = 500

# 慢查询阈值
long_query_time = 1

2. 硬件选择

组件 建议 原因
CPU 多核高频 MySQL 单个查询是单线程
内存 越大越好 InnoDB 缓冲池吃内存
磁盘 SSD > HDD 随机IO性能是HDD的100倍
网络 万兆网卡 减少数据传输延迟

🎯 完整优化流程总结

sql 复制代码
┌────────────────────────────────────────┐
│  1. 发现慢查询                         │
│     - 慢查询日志 📝                    │
│     - 监控告警 📊                      │
└───────────┬────────────────────────────┘
            ↓
┌────────────────────────────────────────┐
│  2. EXPLAIN 分析                       │
│     - type = ALL? 全表扫描!⚠️        │
│     - key = NULL? 没用索引!⚠️        │
│     - rows 很大? 扫描太多行!⚠️       │
└───────────┬────────────────────────────┘
            ↓
┌────────────────────────────────────────┐
│  3. 索引优化                           │
│     - 添加索引 ✅                      │
│     - 联合索引 ✅                      │
│     - 覆盖索引 ✅                      │
└───────────┬────────────────────────────┘
            ↓
┌────────────────────────────────────────┐
│  4. SQL 重写                           │
│     - 避免 SELECT * ✅                │
│     - 分页优化 ✅                      │
│     - 批量操作 ✅                      │
└───────────┬────────────────────────────┘
            ↓
┌────────────────────────────────────────┐
│  5. 表结构优化                         │
│     - 合适的数据类型 ✅                │
│     - 字段设计 ✅                      │
│     - 分表 ✅                          │
└───────────┬────────────────────────────┘
            ↓
┌────────────────────────────────────────┐
│  6. 配置与硬件                         │
│     - MySQL 参数调优 ✅               │
│     - 硬件升级 ✅                      │
└────────────────────────────────────────┘

💡 面试加分回答模板

面试官:"你遇到过慢查询吗?怎么优化的?"

标准回答

"遇到过。印象最深的一次是订单查询接口响应时间从几百毫秒突然变成 30 秒。

定位过程

  1. 查看慢查询日志,发现一条 SQL:SELECT * FROM orders WHERE status = 'paid' AND create_time > '2024-01-01'
  2. EXPLAIN 分析:type = ALL,rows = 500万,没用索引
  3. 发现 status 和 create_time 字段都没有索引

优化方案

  1. 加联合索引CREATE INDEX idx_status_time ON orders(status, create_time)
  2. SQL 改写 :把 SELECT * 改成只查需要的 10 个字段
  3. 分页优化:用游标分页代替 OFFSET(因为有深度分页场景)

优化结果

  • 查询时间从 30秒 降到 50毫秒
  • EXPLAIN 显示 type = range,rows = 1200(索引生效)
  • 服务器 CPU 从 80% 降到 10%

总结经验

  • 开发时养成习惯,WHERE、JOIN 字段都加索引
  • 定期 Review 慢查询日志
  • 使用 EXPLAIN 验证每条重要的 SQL"

🎉 总结金句

  1. 索引是性能的基石 - 没有索引的查询就像没有导航的旅行 🧭
  2. EXPLAIN 是你的 X 光机 - 看不见的问题用它来透视 🔍
  3. rows 数量决定性能 - 扫描越少越快 🎯
  4. type = ALL 是大忌 - 全表扫描是性能杀手 💀
  5. 优化永无止境 - 但要抓主要矛盾 📈

📚 扩展阅读


最后的最后,送你一个公式 🎁:

sql 复制代码
慢查询优化 = 监控发现 + EXPLAIN 分析 + 索引优化 + SQL 改写

记住这四步,走遍天下都不怕!😎

祝你的 SQL 永远快如闪电!

复制代码
相关推荐
京东云开发者3 小时前
提供方耗时正常,调用方毛刺频频
后端
cipher3 小时前
用 Go 找预测市场的赚钱机会!
后端·go·web3
星辰h3 小时前
基于JWT的RESTful登录系统实现
前端·spring boot·后端·mysql·restful·jwt
用户68545375977693 小时前
🔍 内存泄漏侦探手册:拯救你的"健忘"程序!
后端
京东云开发者3 小时前
java小知识-ShutdownHook(优雅关闭)
后端
京东云开发者3 小时前
真实案例解析缓存大热key的致命陷阱
后端
undefinedType3 小时前
并查集(Union-Find) 文档
后端
YDS8293 小时前
苍穹外卖 —— 文件上传和菜品的CRUD
java·spring boot·后端
bcbnb3 小时前
Fiddler抓包实战教程 从安装配置到代理设置,详解Fiddler使用方法与调试技巧(HTTPHTTPS全面指南)
后端