一、概述
1.1 背景介绍
在互联网高并发场景下,数据库性能往往是整个系统的关键瓶颈。一条执行超过1秒的慢查询,在业务高峰期可能直接导致服务瘫痪。作为最流行的关系型数据库,MySQL的性能优化是每位后端开发和DBA必须掌握的核心技能。
然而,网络上流传着大量关于MySQL优化的"军规"和"潜规则",其中既有正确经验,也不乏以讹传讹的误解。本文从实战角度出发,深入解析15个常见的SQL优化误区,揭示其背后原理,帮助开发者真正理解适用场景。
1.2 技术特点
- 索引原理:基于B+Tree数据结构,理解其特性才能有效运用索引
- 查询优化器:MySQL查询优化器自动选择执行计划,但并非总是最优
- 执行计划分析:EXPLAIN是优化基础,需掌握每个字段含义
- 成本模型:优化器基于成本估算选择执行计划,了解模型才能理解决策逻辑
1.3 适用场景
- 线上出现慢查询告警,需要快速定位和优化
- 新功能上线前的SQL审核和性能优化
- 数据库整体性能调优和架构改进
- 面试中关于MySQL优化的深度技术考察
1.4 环境要求
| 组件 |
版本要求 |
说明 |
| MySQL |
5.7+ / 8.0+ |
本文示例基于MySQL 8.0 |
| 操作系统 |
Linux主流发行版 |
生产环境推荐CentOS 7+ / Ubuntu 20.04+ |
| 内存 |
8GB+ |
根据数据量调整buffer pool配置 |
二、15个SQL优化误区解析
2.1 误区一:SELECT * 是性能杀手
◆ 传统观点
“永远不要使用SELECT *,必须明确指定字段”
◆ 真相剖析
-- 传统认为"错误"的写法
SELECT * FROM users WHERE id = 1;
-- 传统认为"正确"的写法
SELECT id, name, email FROM users WHERE id = 1;
实际情况:
- 如果查询走主键索引且需要大部分字段,SELECT * 性能损失极小
- 核心问题在于:存在覆盖索引时,SELECT * 会导致回表操作
-- 假设有索引 idx_name_email (name, email)
-- 此查询可走覆盖索引,无需回表
SELECT name, email FROM users WHERE name = 'zhangsan';
-- 此查询必须回表,因*包含索引未覆盖字段
SELECT * FROM users WHERE name = 'zhangsan';
验证方法:
EXPLAIN SELECT name, email FROM users WHERE name = 'zhangsan';
-- Extra: Using index (覆盖索引)
EXPLAIN SELECT * FROM users WHERE name = 'zhangsan';
-- Extra: NULL (需要回表)
结论:SELECT * 的真正问题在于可能无法利用覆盖索引,而非数据传输量。
2.2 误区二:索引越多越好
◆ 传统观点
“为所有可能查询字段创建索引”
◆ 真相剖析
-- 错误示范:过度创建索引
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_age ON users(age);
CREATE INDEX idx_city ON users(city);
CREATE INDEX idx_gender ON users(gender);
CREATE INDEX idx_created_at ON users(created_at);
索引的代价:
- 写入性能下降:INSERT/UPDATE/DELETE需维护所有索引
- 存储空间增加:每个索引都需要额外存储
- 优化器选择困难:索引过多可能导致选错索引
正确做法:基于实际查询模式设计索引
-- 分析真实查询需求
-- 查询1: WHERE name = ? AND age > ?
-- 查询2: WHERE city = ? ORDER BY created_at DESC
-- 查询3: WHERE gender = ? AND city = ?
-- 针对性创建联合索引
CREATE INDEX idx_name_age ON users(name, age);
CREATE INDEX idx_city_created ON users(city, created_at);
CREATE INDEX idx_gender_city ON users(gender, city);
监控无用索引:
-- MySQL 8.0+ 查看索引使用统计
SELECT
object_schema,
object_name,
index_name,
count_star AS use_count
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'your_database'
ORDER BY use_count ASC;
2.3 误区三:联合索引必须遵循最左前缀
◆ 传统观点
“联合索引必须从最左字段开始使用,否则失效”
◆ 真相剖析
-- 假设联合索引 idx_a_b_c (a, b, c)
-- 传统认知:
WHERE a = 1 -- 能用索引 ✓
WHERE a = 1 AND b = 2 -- 能用索引 ✓
WHERE a = 1 AND b = 2 AND c = 3 -- 能用索引 ✓
WHERE b = 2 -- 不能用索引 ✗
WHERE b = 2 AND c = 3 -- 不能用索引 ✗
WHERE a = 1 AND c = 3 -- 只能用到a字段
MySQL 8.0的索引跳跃扫描:
-- MySQL 8.0.13+ 支持Index Skip Scan
-- 即使不满足最左前缀,特定场景下仍可使用索引
-- 假设gender只有M/F两个值,索引为idx_gender_age (gender, age)
SELECT * FROM users WHERE age = 25;
-- MySQL 8.0可能自动转换为:
SELECT * FROM users WHERE gender = 'M' AND age = 25
UNION ALL
SELECT * FROM users WHERE gender = 'F' AND age = 25;
验证Index Skip Scan:
EXPLAIN SELECT * FROM users WHERE age = 25;
-- Extra: Using index for skip scan
结论:最左前缀原则仍然重要,但MySQL 8.0+在特定条件下可以突破此限制。
2.4 误区四:索引列使用函数必然失效
◆ 传统观点
“在索引列上使用函数会导致索引失效”
◆ 真相剖析
-- 传统认为会导致索引失效的写法
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
SELECT * FROM users WHERE UPPER(email) = 'TEST@EXAMPLE.COM';
MySQL 8.0的函数索引:
-- MySQL 8.0.13+ 支持函数索引(表达式索引)
CREATE INDEX idx_year ON orders ((YEAR(created_at)));
CREATE INDEX idx_upper_email ON users ((UPPER(email)));
-- 现在这些查询可以使用索引
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
SELECT * FROM users WHERE UPPER(email) = 'TEST@EXAMPLE.COM';
传统优化方式仍然有效:
-- 范围查询替代函数调用
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01';
2.5 误区五:NOT IN和!=无法使用索引
◆ 传统观点
“使用NOT IN、!=或<>会导致全表扫描”
◆ 真相剖析
-- 测试表结构
CREATE TABLE test_data (
id INT PRIMARY KEY,
status TINYINT,
INDEX idx_status (status)
);
-- 插入测试数据:status有0,1,2,3,4五个值,各20万条,共100万条
实际测试:
-- 排除数据量较小时,仍可能使用索引
EXPLAIN SELECT * FROM test_data WHERE status != 0;
-- 可能走索引,取决于优化器成本估算
EXPLAIN SELECT * FROM test_data WHERE status NOT IN (0, 1);
-- 可能转换为status IN (2, 3, 4)的范围扫描
关键因素是选择性:
-- 如果status=0占90%,status!=0返回10%数据
-- 优化器可能选择使用索引
-- 如果status=0占10%,status!=0返回90%数据
-- 优化器选择全表扫描,成本更低
结论:NOT IN和!=并非绝对无法使用索引,取决于数据分布和优化器成本估算。
2.6 误区六:JOIN必须小表驱动大表
◆ 传统观点
“JOIN时要用小表驱动大表,FROM后的是驱动表”
◆ 真相剖析
-- 传统认为的"小表驱动大表"
SELECT * FROM small_table s
JOIN big_table b ON s.id = b.small_id;
真相:
- MySQL优化器自动选择驱动表,不按SQL书写顺序
- 可通过STRAIGHT_JOIN强制指定连接顺序
-- 查看实际连接顺序
EXPLAIN SELECT * FROM small_table s
JOIN big_table b ON s.id = b.small_id;
-- 强制指定连接顺序
SELECT * FROM small_table s
STRAIGHT_JOIN big_table b ON s.id = b.small_id;
影响JOIN性能的关键因素:
-- 1. 被驱动表连接字段是否有索引
-- 好:big_table.small_id有索引
-- 差:big_table.small_id无索引
-- 2. Join Buffer大小配置
SHOW VARIABLES LIKE 'join_buffer_size';
-- 3. 能否使用Block Nested Loop或Hash Join
-- MySQL 8.0.18+ 支持Hash Join
EXPLAIN FORMAT=TREE SELECT * FROM t1 JOIN t2 ON t1.a = t2.a;
2.7 误区七:LIMIT 1总能优化查询
◆ 传统观点
“确定只需一条数据时,加LIMIT 1可提升性能”
◆ 真相剖析
-- 场景1:主键/唯一索引查询
SELECT * FROM users WHERE id = 1 LIMIT 1;
-- LIMIT 1无意义,主键查询本就返回一条
-- 场景2:普通索引查询,确实需要一条
SELECT * FROM users WHERE status = 1 LIMIT 1;
-- LIMIT 1有意义,找到第一条就返回
-- 场景3:排序后取第一条
SELECT * FROM users WHERE status = 1 ORDER BY created_at DESC LIMIT 1;
-- 有索引(status, created_at)时效率高
-- 无合适索引时仍需排序全部数据
关键点:LIMIT 1的优化效果取决于能否"提前终止"
-- 能提前终止的情况
EXPLAIN SELECT * FROM users WHERE status = 1 LIMIT 1;
-- 找到一条就返回
-- 不能提前终止的情况
EXPLAIN SELECT * FROM users WHERE status = 1 ORDER BY created_at DESC LIMIT 1;
-- 无合适索引时需要先排序所有符合条件数据
2.8 误区八:EXISTS总是优于IN
◆ 传统观点
“大表用EXISTS,小表用IN”
◆ 真相剖析
-- IN写法
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 1);
-- EXISTS写法
SELECT * FROM orders o
WHERE EXISTS (
SELECT 1 FROM users u
WHERE u.id = o.user_id AND u.status = 1
);
MySQL 5.6+的优化:
-- MySQL 5.6+ 自动将某些IN子查询转换为semi-join
EXPLAIN SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 1);
-- 查看select_type是否为SIMPLE(说明已转换为JOIN)
实际选择依据:
- 子查询结果集小:IN可能更好(可利用索引)
- 子查询结果集大:EXISTS可能更好(提前终止)
- 现代MySQL:优化器通常能正确选择
-- 最佳实践:改写为JOIN
SELECT DISTINCT o.*
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 1;
2.9 误区九:OR条件导致索引失效
◆ 传统观点
“OR条件会导致索引失效,应改写为UNION”
◆ 真相剖析
-- 情况1:同一字段的OR
SELECT * FROM users WHERE status = 1 OR status = 2;
-- 优化器转换为status IN (1, 2),可使用索引
-- 情况2:不同字段的OR,且都有索引
SELECT * FROM users WHERE name = 'zhangsan' OR email = 'test@test.com';
-- MySQL可能使用Index Merge优化
Index Merge优化:
EXPLAIN SELECT * FROM users
WHERE name = 'zhangsan' OR email = 'test@test.com';
-- type: index_merge
-- Extra: Using union(idx_name,idx_email); Using where
OR确实导致问题的场景:
-- 当其中一个条件无索引时
SELECT * FROM users WHERE name = 'zhangsan' OR age > 25;
-- 如果age无索引,可能导致全表扫描
2.10 误区十:COUNT(*)比COUNT(1)慢
◆ 传统观点
“COUNT(1)比COUNT()快,因COUNT()读取所有列”
◆ 真相剖析
-- 这三个查询在InnoDB中性能几乎相同
SELECT COUNT(*) FROM users;
SELECT COUNT(1) FROM users;
SELECT COUNT(id) FROM users; -- id是主键
原因:
- MySQL优化器对COUNT(*)和COUNT(1)处理完全相同
- InnoDB引擎选择最小索引统计行数
- COUNT(*)不读取任何列值
官方文档说明:
"InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference."
真正的性能差异在COUNT(column):
-- COUNT(column)排除NULL值,需要额外判断
SELECT COUNT(nullable_column) FROM users; -- 需判断是否为NULL
SELECT COUNT(*) FROM users; -- 直接计数,不判断
2.11 误区十一:ORDER BY必须匹配WHERE条件才能用索引
◆ 传统观点
“ORDER BY字段必须在WHERE条件中出现才能用索引”
◆ 真相剖析
-- 索引idx_status_created (status, created_at)
-- 情况1:WHERE + ORDER BY,索引有效
SELECT * FROM orders WHERE status = 1 ORDER BY created_at;
-- 走索引,无需额外排序
-- 情况2:只有ORDER BY,索引可能有效
SELECT * FROM orders ORDER BY status, created_at LIMIT 10;
-- LIMIT值小,优化器可能选择索引扫描
-- 情况3:ORDER BY顺序与索引不一致
SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC, id ASC;
-- 排序方向不一致,可能导致filesort
MySQL 8.0的降序索引:
-- MySQL 8.0+ 支持降序索引
CREATE INDEX idx_status_created_desc ON orders(status ASC, created_at DESC);
-- 现在此查询可直接使用索引
SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC;
2.12 误区十二:LIKE '%xxx%'绝对不走索引
◆ 传统观点
“LIKE以%开头的模糊查询无法使用索引”
◆ 真相剖析
-- 确实无法使用B+Tree索引
SELECT * FROM articles WHERE title LIKE '%MySQL%';
解决方案:
- 全文索引
-- 创建全文索引
ALTER TABLE articles ADD FULLTEXT INDEX ft_title (title);
-- 使用全文检索
SELECT * FROM articles
WHERE MATCH(title) AGAINST('MySQL' IN NATURAL LANGUAGE MODE);
2. **覆盖索引优化**
```sql
-- 虽然无法用于WHERE筛选,但减少回表
-- 索引idx_title_id (title, id)
SELECT id FROM articles WHERE title LIKE '%MySQL%';
-- Extra: Using where; Using index
- 搜索引擎方案
# 大量全文检索需求,建议使用专门搜索引擎
# - Elasticsearch
# - OpenSearch
# - Meilisearch
2.13 误区十三:分页查询无需特殊优化
◆ 传统观点
“LIMIT offset, size在offset大时性能下降”
◆ 真相剖析
-- 问题查询:深度分页
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- 需要扫描1000010行,只返回10行
优化方案一:基于游标的分页
-- 记住上一页最后一条记录的id
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
-- 直接定位,效率很高
优化方案二:延迟关联
-- 先查出id,再关联获取详情
SELECT * FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) t ON o.id = t.id;
-- 子查询只扫描索引,减少回表次数
优化方案三:业务限制
-- 限制最大页数,引导用户使用搜索
-- 电商网站通常只显示前100页
2.14 误区十四:只需关注SELECT的执行计划
◆ 传统观点
“只需要关注SELECT语句的执行计划”
◆ 真相剖析
-- MySQL 5.7+ 支持查看UPDATE/DELETE的执行计划
EXPLAIN UPDATE orders SET status = 2 WHERE user_id = 10086;
EXPLAIN DELETE FROM logs WHERE created_at < '2023-01-01';
常见问题:
-- 问题1:UPDATE未用索引
EXPLAIN UPDATE orders SET status = 2 WHERE DATE(created_at) = '2024-01-01';
-- 索引列使用函数,导致全表扫描
-- 问题2:DELETE大量数据
DELETE FROM logs WHERE created_at < '2023-01-01';
-- 数据量大,长时间锁表
大批量DELETE优化:
-- 分批删除,避免长时间锁表
DELIMITER //
CREATE PROCEDURE batch_delete()
BEGIN
DECLARE rows_affected INT DEFAULT 1;
WHILE rows_affected > 0 DO
DELETE FROM logs
WHERE created_at < '2023-01-01'
LIMIT 10000;
SET rows_affected = ROW_COUNT();
-- 可加入短暂延迟,减少主从延迟
DO SLEEP(0.1);
END WHILE;
END//
DELIMITER ;
CALL batch_delete();
2.15 误区十五:只为频繁查询字段建索引
◆ 传统观点
“为频繁查询的字段建索引”
◆ 真相剖析
索引选择性 = 不重复值数量 / 总行数
-- 计算字段选择性
SELECT
COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
COUNT(DISTINCT user_id) / COUNT(*) AS user_id_selectivity,
COUNT(DISTINCT order_no) / COUNT(*) AS order_no_selectivity
FROM orders;
-- 示例结果:
-- status_selectivity: 0.00001 (低选择性,只有几个值)
-- user_id_selectivity: 0.1 (中等选择性)
-- order_no_selectivity: 1.0 (高选择性,每个值都唯一)
低选择性字段建索引的问题:
-- status只有0,1,2,3,4五个值
CREATE INDEX idx_status ON orders(status);
-- 此查询即使有索引,优化器也可能选择全表扫描
SELECT * FROM orders WHERE status = 1;
-- 因status=1记录可能占20%,回表成本太高
联合索引字段顺序:
-- 高选择性字段放前面
CREATE INDEX idx_user_status ON orders(user_id, status); -- 推荐
CREATE INDEX idx_status_user ON orders(status, user_id); -- 不推荐
-- 原因:高选择性字段在前,能更快缩小范围
三、执行计划深度解读
3.1 EXPLAIN各字段含义
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
| 字段 |
含义 |
| id |
查询序号,值越大优先级越高 |
| select_type |
查询类型:SIMPLE/PRIMARY/SUBQUERY/DERIVED等 |
| table |
访问的表 |
| partitions |
匹配的分区 |
| type |
访问类型:system > const > eq_ref > ref > range > index > ALL |
| possible_keys |
可能使用的索引 |
| key |
实际使用的索引 |
| key_len |
使用的索引长度(字节) |
| ref |
与索引比较的列或常量 |
| rows |
预估扫描行数 |
| filtered |
按条件过滤后的行百分比 |
| Extra |
额外信息 |
3.2 type字段详解
-- const: 主键或唯一索引的等值查询
EXPLAIN SELECT * FROM users WHERE id = 1;
-- eq_ref: 连接查询中,被驱动表使用主键或唯一索引
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- ref: 普通索引的等值查询
EXPLAIN SELECT * FROM orders WHERE user_id = 10086;
-- range: 索引范围扫描
EXPLAIN SELECT * FROM orders WHERE created_at > '2024-01-01';
-- index: 全索引扫描
EXPLAIN SELECT user_id FROM orders;
-- ALL: 全表扫描(需要优化)
EXPLAIN SELECT * FROM orders WHERE description LIKE '%test%';
-- Using index: 覆盖索引,无需回表
-- Using where: 存储引擎返回后还需MySQL层过滤
-- Using temporary: 使用临时表(常见于GROUP BY、DISTINCT)
-- Using filesort: 文件排序(ORDER BY无法使用索引)
-- Using index condition: 索引条件下推(ICP)
-- Using join buffer: 连接缓冲区(被驱动表无法使用索引)
3.4 EXPLAIN ANALYZE(MySQL 8.0.18+)
-- 实际执行并显示真实统计
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 10086;
-- 输出包含:
-- - 实际执行时间
-- - 实际扫描行数
-- - 循环次数
四、最佳实践与注意事项
4.1 索引设计原则
◆ 设计检查清单
- 高频查询优先:为80%查询设计索引
- 考虑写入影响:索引降低写入性能
- 联合索引设计:
- 等值条件字段在前
- 范围条件字段在后
- 高选择性字段优先
- 避免冗余索引:(a,b)已包含(a)的功能
-- 冗余索引示例
CREATE INDEX idx_a ON t(a);
CREATE INDEX idx_a_b ON t(a, b);
-- idx_a是冗余的,可删除
◆ 字符串索引优化
-- 长字符串字段使用前缀索引
CREATE INDEX idx_url ON pages(url(50));
-- 计算合适的前缀长度
SELECT
COUNT(DISTINCT LEFT(url, 10)) / COUNT(*) AS len_10,
COUNT(DISTINCT LEFT(url, 20)) / COUNT(*) AS len_20,
COUNT(DISTINCT LEFT(url, 50)) / COUNT(*) AS len_50,
COUNT(DISTINCT url) / COUNT(*) AS full_len
FROM pages;
-- 选择区分度接近完整字段的最短前缀
4.2 SQL编写规范
◆ 查询优化检查清单
-- 1. 避免SELECT *
SELECT id, name, email FROM users WHERE id = 1;
-- 2. 合理使用LIMIT
SELECT * FROM logs WHERE status = 'error' LIMIT 100;
-- 3. 避免在循环中执行SQL
-- 错误示范
for user_id in user_ids:
cursor.execute("SELECT * FROM orders WHERE user_id = %s", user_id)
-- 正确示范
cursor.execute("SELECT * FROM orders WHERE user_id IN (%s)" % ','.join(user_ids));
-- 4. 使用UNION ALL代替UNION(如果不需要去重)
SELECT id FROM t1 WHERE status = 1
UNION ALL
SELECT id FROM t2 WHERE status = 1;
◆ JOIN优化
-- 1. 确保被驱动表的连接字段有索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 2. 避免过多表JOIN
-- 单个查询JOIN表数不超过3-5个
-- 3. 使用STRAIGHT_JOIN在必要时控制连接顺序
SELECT STRAIGHT_JOIN *
FROM small_table s
JOIN big_table b ON s.id = b.small_id;
4.3 监控与告警
◆ 慢查询日志配置
# my.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
◆ 慢查询分析
# 使用mysqldumpslow分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# 使用pt-query-digest分析(更详细)
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
◆ 实时监控
-- 查看当前执行的查询
SHOW PROCESSLIST;
-- 查看执行时间超过N秒的查询
SELECT * FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep' AND TIME > 5;
-- 终止慢查询
KILL QUERY <process_id>;
4.4 常见问题与解决方案
| 问题现象 |
原因分析 |
解决方案 |
| 执行计划显示ALL |
无合适索引或索引失效 |
检查索引、避免索引失效场景 |
| Using filesort |
ORDER BY无法使用索引 |
调整索引或查询条件 |
| Using temporary |
GROUP BY、DISTINCT需要临时表 |
优化查询或增加tmp_table_size |
| 有索引却不使用 |
优化器认为全表扫描成本更低 |
FORCE INDEX或优化数据分布 |
| 相同SQL时快时慢 |
数据页不在buffer pool |
增加buffer pool或优化查询 |
五、故障排查实战
5.1 案例一:生产环境突发慢查询
◆ 问题描述
告警:订单查询接口P99延迟从50ms飙升到5s
时间:业务高峰期14:00-14:30
◆ 排查步骤
-- 1. 查看当前长时间运行的查询
SELECT
id, user, host, db, command, time, state, info
FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep' AND TIME > 2
ORDER BY TIME DESC;
-- 2. 查看慢查询日志
-- 发现大量相同慢查询:
SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 20;
-- 3. 检查执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 ORDER BY created_at DESC LIMIT 20;
-- type: ALL, key: NULL, Extra: Using where; Using filesort
-- 全表扫描 + 文件排序!
-- 4. 检查索引
SHOW INDEX FROM orders;
-- 发现idx_user_id索引被误删除
◆ 解决方案
-- 紧急创建索引
CREATE INDEX idx_user_created ON orders(user_id, created_at DESC);
-- 验证
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 ORDER BY created_at DESC LIMIT 20;
-- type: ref, key: idx_user_created, Extra: Backward index scan
5.2 案例二:索引存在但不使用
◆ 问题描述
-- 表有索引idx_status (status)
-- 但此查询不走索引
SELECT * FROM orders WHERE status = 1;
◆ 排查步骤
-- 1. 检查数据分布
SELECT status, COUNT(*) AS cnt,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM orders), 2) AS pct
FROM orders
GROUP BY status;
-- 结果:status=1占60%数据
-- 优化器认为全表扫描比走索引后回表更高效
-- 2. 验证优化器决策
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE status = 1;
-- 查看cost信息
◆ 解决方案
-- 方案1:查询更具选择性的条件
SELECT * FROM orders WHERE status = 1 AND created_at > '2024-01-01';
-- 方案2:使用覆盖索引
CREATE INDEX idx_status_covering ON orders(status, id, user_id, created_at);
SELECT id, user_id, created_at FROM orders WHERE status = 1;
-- 方案3:强制使用索引(谨慎)
SELECT * FROM orders FORCE INDEX(idx_status) WHERE status = 1;
5.3 监控指标配置
# Prometheus + MySQL Exporter告警规则示例
groups:
- name: mysql_slow_query
rules:
- alert: MySQLSlowQueries
expr: rate(mysql_global_status_slow_queries[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL慢查询数量过多"
description: "实例 {{ $labels.instance }} 5分钟内慢查询数: {{ $value }}"
- alert: MySQLHighQueryTime
expr: mysql_global_status_queries / mysql_global_status_uptime > 1000
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL QPS过高"
六、总结
6.1 核心要点回顾
- SELECT *的真正问题:无法使用覆盖索引,而非数据传输量
- 索引不是越多越好:需平衡查询性能和写入性能
- 最左前缀原则有例外:MySQL 8.0的Index Skip Scan可绕过
- 函数导致索引失效可解决:MySQL 8.0支持函数索引
- NOT IN/!=未必不走索引:取决于数据分布和优化器决策
- COUNT(*)和COUNT(1)性能相同:官方确认
- OR不一定导致全表扫描:Index Merge可优化
- 深度分页必须优化:使用游标分页或延迟关联
- 索引选择性很重要:低选择性字段的索引价值有限
6.2 优化方法论
- 先测量后优化:使用EXPLAIN和EXPLAIN ANALYZE确认问题
- 关注成本模型:理解优化器决策逻辑
- 持续监控:慢查询日志 + 实时监控 + 定期巡检
- 版本升级:新版本优化器通常更智能
6.3 进阶学习方向
-
深入理解InnoDB存储引擎
- B+Tree索引结构
- Buffer Pool工作原理
- MVCC和事务隔离级别
-
查询优化器原理
-
分布式数据库方案
- 分库分表
- 读写分离
- NewSQL(TiDB、OceanBase)
6.4 参考资料
- MySQL 8.0官方文档 - Optimization
- High Performance MySQL, 4th Edition
- MySQL实战45讲 - 极客时间
- Percona官方博客
附录
A. 常用命令速查表
-- 查看表索引
SHOW INDEX FROM table_name;
-- 查看表结构
SHOW CREATE TABLE table_name;
-- 查看执行计划
EXPLAIN SELECT ...;
EXPLAIN FORMAT=JSON SELECT ...;
EXPLAIN ANALYZE SELECT ...;
-- 查看优化器重写后的SQL
SHOW WARNINGS;
-- 查看索引使用统计
SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage;
-- 强制使用/忽略索引
SELECT * FROM t FORCE INDEX(idx_name) WHERE ...;
SELECT * FROM t IGNORE INDEX(idx_name) WHERE ...;
-- 分析表统计信息
ANALYZE TABLE table_name;
-- 查看InnoDB引擎状态
SHOW ENGINE INNODB STATUS;
B. 索引类型对比
| 索引类型 |
存储引擎 |
特点 |
适用场景 |
| B+Tree |
InnoDB |
支持范围查询、排序 |
大多数OLTP场景 |
| Hash |
Memory |
只支持等值查询,速度快 |
等值查询频繁场景 |
| Full-Text |
InnoDB |
全文检索 |
文本搜索 |
| Spatial |
InnoDB |
空间索引 |
GIS数据 |
C. 术语表
| 术语 |
英文 |
解释 |
| 覆盖索引 |
Covering Index |
索引包含查询所需所有字段,无需回表 |
| 回表 |
Table Lookup |
通过二级索引找到主键后,再到主键索引查询数据 |
| 索引下推 |
Index Condition Pushdown |
将部分WHERE条件下推到存储引擎层过滤 |
| 索引合并 |
Index Merge |
使用多个索引并合并结果 |
| 文件排序 |
Filesort |
无法使用索引排序时,MySQL使用的排序算法 |
| 临时表 |
Temporary Table |
GROUP BY、DISTINCT等操作可能创建的中间表 |
| 选择性 |
Selectivity |
索引列不重复值数量与总行数的比值 |