后端开发中,SQL 慢查询是性能问题的头号杀手。90% 的性能问题都能通过合理的索引解决。这篇文章从实战出发,帮你搞懂 MySQL 索引的原理和优化方法。
一、为什么索引能提速
没有索引时,MySQL 要逐行扫描整张表才能找到数据(全表扫描)。有索引后,通过 B+Tree 结构直接定位到目标数据。
| 数据量 | 无索引(全表扫描) | 有索引(B+Tree) |
|---|---|---|
| 1万行 | ~10ms | <1ms |
| 100万行 | ~500ms | ~1ms |
| 1000万行 | ~5秒 | ~2ms |
二、索引类型
sql
-- 1. 主键索引(自动创建)
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT
);
-- 2. 普通索引
CREATE INDEX idx_name ON user(name);
-- 3. 唯一索引
CREATE UNIQUE INDEX idx_email ON user(email);
-- 4. 联合索引
CREATE INDEX idx_name_age ON user(name, age);
-- 5. 全文索引(用于大文本搜索)
CREATE FULLTEXT INDEX idx_content ON article(content);
三、最左前缀原则
联合索引 (name, age, city) 相当于创建了三个索引:
sql
-- ✅ 用到索引
WHERE name = '张三'
WHERE name = '张三' AND age = 25
WHERE name = '张三' AND age = 25 AND city = '郑州'
-- ❌ 用不到索引
WHERE age = 25
WHERE city = '郑州'
WHERE age = 25 AND city = '郑州'
核心: 联合索引从最左列开始匹配,跳过任何一列,后面的列就失效了。
四、常见索引失效场景
1. 对索引列做了运算
sql
-- ❌ 失效
SELECT * FROM user WHERE age + 1 = 20;
-- ✅ 有效
SELECT * FROM user WHERE age = 19;
2. 使用了 LIKE 前置模糊匹配
sql
-- ✅ 有效
SELECT * FROM user WHERE name LIKE '张%';
-- ❌ 失效
SELECT * FROM user WHERE name LIKE '%三';
SELECT * FROM user WHERE name LIKE '%三%';
3. 使用了函数
sql
-- ❌ 失效
SELECT * FROM user WHERE SUBSTR(name, 1, 1) = '张';
-- ✅ 有效
SELECT * FROM user WHERE name LIKE '张%';
4. 类型不一致
sql
-- 假设 phone 是 VARCHAR 类型
-- ❌ 失效(隐式类型转换)
SELECT * FROM user WHERE phone = 13800008888;
-- ✅ 有效
SELECT * FROM user WHERE phone = '13800008888';
五、通过 EXPLAIN 分析慢查询
sql
EXPLAIN SELECT * FROM user WHERE name = '张三'\G
重点看这几列:
| 列 | 好 | 坏 |
|---|---|---|
| type | const, ref, range | ALL(全表扫描) |
| rows | 越小越好 | 几十万就要注意 |
| Extra | Using index | Using filesort(需要优化) |
| possible_keys | 有值 | null(没用到索引) |
实战分析
sql
-- 先创建一个模拟表
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64),
user_id BIGINT,
status TINYINT,
amount DECIMAL(10,2),
created_at DATETIME
);
-- 插入10万条测试数据
-- (省略插入语句,实际可以用存储过程)
-- 检查慢查询
EXPLAIN SELECT * FROM orders WHERE status = 1;
-- type: ALL, rows: 100000 → 全表扫描,需要优化
-- 加索引后
CREATE INDEX idx_status ON orders(status);
EXPLAIN SELECT * FROM orders WHERE status = 1;
-- type: ref, rows: 50000 → 用了索引,扫描行数减半
六、优化实战场景
场景1:分页查询太慢
sql
-- 慢:OFFSET 越大越慢
SELECT * FROM orders ORDER BY id LIMIT 10000, 20;
-- 快:用上一页的最大 ID 做条件
SELECT * FROM orders WHERE id > 10000 ORDER BY id LIMIT 20;
| 方式 | 100页 | 1000页 | 10000页 |
|---|---|---|---|
| OFFSET | ~30ms | ~200ms | ~2s |
| ID条件 | ~5ms | ~5ms | ~5ms |
场景2:排序导致文件排序
sql
-- 需要建立 (status, created_at) 联合索引
-- 避免 Using filesort
SELECT * FROM orders
WHERE status = 1
ORDER BY created_at DESC
LIMIT 10;
CREATE INDEX idx_status_create ON orders(status, created_at);
场景3:分组统计优化
sql
-- 给 group by 的列加索引
SELECT user_id, COUNT(*) FROM orders
GROUP BY user_id;
CREATE INDEX idx_user_id ON orders(user_id);
七、索引设计原则
1. 不是越多越好
一张表的索引数量建议控制在 5个以内。索引太多会导致:
- 插入/更新变慢(每次都要维护索引)
- 占用磁盘空间
- 查询优化器可能选错索引
2. 高区分度的列才适合建索引
sql
-- 区分度差(只有0和1两个值),索引意义不大
-- 索引扫描也要扫一半数据,不如全表扫描
CREATE INDEX idx_status ON orders(status);
-- 区分度高(每个值都不同),索引效果最好
CREATE INDEX idx_order_no ON orders(order_no);
3. 优先给 Where 和 Join 的列建索引
sql
-- WHERE 条件列
SELECT * FROM user WHERE name = '张三';
-- JOIN 关联列
SELECT * FROM order o
LEFT JOIN user u ON o.user_id = u.id; -- user.id 要有索引
总结
索引优化是后端开发的核心技能,记住三条:
- 联合索引遵循最左前缀原则 ------ 把最常查询的列放最左边
- 用 EXPLAIN 分析慢查询 ------ 重点关注 type 和 rows
- 不是越多越好 ------ 一张表 5 个以内索引足矣
建议在日常开发中,每个 SQL 都养成用 EXPLAIN 看一眼的习惯。
如果对你有帮助,欢迎点赞、评论、关注【张老师技术栈】,持续分享 Java/Python/爬虫 实战干货。