🐌 数据库慢查询速成班:让你的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 永远快如闪电!

复制代码
相关推荐
程序员爱钓鱼21 分钟前
为什么学习 Rust?Rust 能做什么?
后端·rust
掘金者阿豪1 小时前
Docker命令太多记不住?用Portainer把容器管理变成可视化操作
后端
用户69371750013842 小时前
AI Agent 里的 Loop 到底是什么?
android·前端·后端
Rain的Java大神之路2 小时前
高并发下的抢优惠券如何设计
后端
钟智强2 小时前
AWS CloudGoat 实战:一个 SSRF 如何撬动整个云账户
后端
Ai拆代码的曹操2 小时前
@Transactional 写在 private 方法上,事务根本没生效
后端
云技纵横2 小时前
@Async 默认执行器又炸了:SimpleAsyncTaskExecutor 为什么不是线程池?
后端·面试
程序员Agions2 小时前
Fypro 新手快速上手与实战指南
后端
舒心的云朵2 小时前
用三张图片详解Asp.Net 全生命周期
后端·asp.net
卷无止境2 小时前
C++ 类型转换变迁之路:从隐式转换到 static_cast
c++·后端