文章目录
- 你的接口为什么慢?(下)------索引优化实战:什么列该建索引、最左前缀原则与联合索引
-
- 导入语
- [1 ~> B+Tree 与最左前缀原则------联合索引的生死线](#1 ~> B+Tree 与最左前缀原则——联合索引的生死线)
-
- [1.1 B+Tree 的数据排列](#1.1 B+Tree 的数据排列)
- [1.2 最左前缀原则](#1.2 最左前缀原则)
- [1.3 为什么不建四个单列索引而行不通](#1.3 为什么不建四个单列索引而行不通)
- [2 ~> Django 中建索引------`db_index` 与 `Meta.indexes`](#2 ~> Django 中建索引——
db_index与Meta.indexes) -
- [2.1 单列索引](#2.1 单列索引)
- [2.2 联合索引](#2.2 联合索引)
- [3 ~> 覆盖索引------为什么 `SELECT *` 是索引的敌人](#3 ~> 覆盖索引——为什么
SELECT *是索引的敌人) -
- [3.1 什么是覆盖索引](#3.1 什么是覆盖索引)
- [3.2 Django 层面如何利用覆盖索引](#3.2 Django 层面如何利用覆盖索引)
- [4 ~> 索引选择性------不是每列都该建索引](#4 ~> 索引选择性——不是每列都该建索引)
-
- [4.1 选择性计算](#4.1 选择性计算)
- [4.2 在 Django 中检查选择性](#4.2 在 Django 中检查选择性)
- [5 ~> 真实案例------多条件搜索接口 3 秒到 90ms](#5 ~> 真实案例——多条件搜索接口 3 秒到 90ms)
- [思考 && 总结](#思考 && 总结)
- 结尾
你的接口为什么慢?(下)------索引优化实战:什么列该建索引、最左前缀原则与联合索引
📖 文章简介: 上篇用 Debug Toolbar 抓到了慢查询,用 EXPLAIN 读懂了执行计划。下篇进入索引实战------不是"给所有列都加索引",而是精准找出对业务最有用的那几列。从 B+Tree 的排列规则讲起,推导出最左前缀原则的完整含义------为什么 (a, b, c) 联合索引在 WHERE b = 1 时根本用不上。覆盖索引(Using index)为什么比普通索引快、为什么 SELECT * 让索引优化效果打折、以及 Django db_index 和 Meta.indexes 的单列与联合索引建法。配有一个真实案例------一个多条件搜索接口从 3 秒降到 90ms,核心是建了一个正确的联合索引而不是四个单列索引。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇我们学会了用 Debug Toolbar 抓查询、用 EXPLAIN 读计划。这个星期我们真正动手------给表加索引。但"加索引"三个字说起来容易,落实到具体的列上就模糊了:该加哪几列?单列索引还是联合索引?联合索引的列顺序怎么排?
2021 年我优化过一个多条件搜索接口------按"作者名 + 图书分类 + 出版日期范围"三个条件组合查询。最初建了三个单列索引------两个有用到一个被忽略。后来换成 (author_id, category_id, pub_date) 的联合索引------同样三个条件的查询,响应时间从 3 秒降到 90ms。区别就在于------你懂不懂最左前缀原则。
1 ~> B+Tree 与最左前缀原则------联合索引的生死线
1.1 B+Tree 的数据排列
MySQL InnoDB 引擎的索引底层是 B+Tree(大部分情况下)。联合索引的内部排列是先按第一列排序,再按第二列排序------后面的列依赖于前面的列。一个联合索引 (a, b, c) 的内部数据排列:
索引树节点(按 a → b → c 排序):
(1, 2, 3), (1, 2, 5), (1, 4, 1), (2, 1, 3), (2, 3, 1), (2, 3, 7) ...
1.2 最左前缀原则
联合索引在匹配时从最左列开始------不能跳级。
| 你的 WHERE 子句 | 索引 (a, b, c) 能用吗 |
用什么 |
|---|---|---|
WHERE a = 1 |
✅ 能用 | 用 a 列 |
WHERE a = 1 AND b = 2 |
✅ 能用 | 用 a 和 b 列 |
WHERE a = 1 AND b = 2 AND c = 3 |
✅ 全能用 | a、b、c 都参与 |
WHERE b = 2 |
❌ 不能 | 索引被跳过------无法定位 |
WHERE c = 3 |
❌ 不能 | 索引被跳过 |
WHERE a = 1 AND c = 3 |
⚠️ 只用 a | c 被跳过了,索引停在 a |
简单记:联合索引像"一排书架------每层按 a 字母排序,同 a 下再按 b 排序。你要找 b 一样的东西,必须先从 a 进去,没从 a 进来的话无法定位。
1.3 为什么不建四个单列索引而行不通
sql
-- 单列索引各有建一个:idx_author、idx_category、idx_pub_date
SELECT * FROM book
WHERE author = "鲁迅" AND category = "文学" AND pub_date >= "2020-01-01";
MySQL 的优化器通常只会选择一个索引------而不是"三个各自用一段"。它选了 idx_author 后,另外两个列的过滤条件无法用到索引,只能回表读取数据后逐行检查。联合索引 (author, category, pub_date) 能一次性定位到数据------三列都参与索引扫描。
2 ~> Django 中建索引------db_index 与 Meta.indexes
2.1 单列索引
python
class Book(models.Model):
author = models.CharField(max_length=100, db_index=True) # 单列索引
title = models.CharField(max_length=200)
2.2 联合索引
python
class BorrowRecord(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
book = models.ForeignKey(Book, on_delete=models.CASCADE)
borrowed_at = models.DateTimeField()
is_returned = models.BooleanField(default=False)
class Meta:
indexes = [
models.Index(fields=["user", "is_returned", "borrowed_at"]),
# 用于"某用户借了哪些未还的书"的查询
]
Django 迁移会自动生成 CREATE INDEX:
sql
CREATE INDEX borrow_user_returned ON borrowrecord (user_id, is_returned, borrowed_at);
3 ~> 覆盖索引------为什么 SELECT * 是索引的敌人
3.1 什么是覆盖索引
当一个索引包含了查询需要的所有列 ,MySQL 可以直接从索引返回结果,不需要再去访问数据行本身。这就是覆盖索引------EXPLAIN 里 Extra 列会显示 Using index。
sql
-- 有联合索引 (user_id, borrowed_at)
-- 查询只用到这两列
SELECT user_id, borrowed_at FROM borrowrecord WHERE user_id = 42;
-- Extra: Using index ← 覆盖索引------只扫描索引,没碰数据行
但如果 SELECT *:
sql
-- 有同样的索引,但查询用到了索引之外的列(如 book_id、is_returned 等)
SELECT * FROM borrowrecord WHERE user_id = 42;
-- Extra: Using index condition ← 用了索引定位,但要回表读取其他列
SELECT * 让覆盖索引优化失效------每次都多一次"回表"操作。
3.2 Django 层面如何利用覆盖索引
python
# ❌ 回表查询------ORM 默认 SELECT *
records = BorrowRecord.objects.filter(user_id=42)
# ✅ 覆盖索引------只选索引中包含的列
records = BorrowRecord.objects.filter(user_id=42).values_list("user_id", "borrowed_at")
4 ~> 索引选择性------不是每列都该建索引
4.1 选择性计算
索引选择性 = 不重复的值数 ÷ 总行数。选择性越接近 1,索引效果越好。
sql
-- 性别列:只有 "男" "女" 两个值,1 百万行 → 选择性 0.000002------极差
-- 用户 ID:1 百万行中 1 百万个唯一值 → 选择性 1.0------完美索引列
-- 订单状态:"pending/paid/shipped/cancelled" 四个值,选择性 0.000004------还是太差
选择性低于 10-15% 的列不建议建单独索引。 MySQL 优化器可能直接忽略它,选择全表扫描。
4.2 在 Django 中检查选择性
python
from django.db.models import Count
# 查看某列的不重复值数占比
total = BorrowRecord.objects.count()
distinct_statuses = BorrowRecord.objects.values("is_returned").distinct().count()
selectivity = distinct_statuses / total
print(f"选择性: {selectivity:.4f}") # is_returned 只有 True/False → 选择性极低
5 ~> 真实案例------多条件搜索接口 3 秒到 90ms
2021 年那个多条件搜索------原始没有联合索引:
sql
SELECT * FROM book WHERE author = "鲁迅" AND category = "文学"
AND pub_date >= "2020-01-01";
-- EXPLAIN: type=ref, key=idx_author, rows=5000, Extra=Using where
选了 idx_author 索引,category 和 pub_date 的过滤在回表后逐行判断。索引只起到了"缩小集合"的作用,没起到"过滤"的作用。
建联合索引后:
sql
ALTER TABLE book ADD INDEX idx_multi (author, category, pub_date);
SELECT * FROM book WHERE author = "鲁迅" AND category = "文学"
AND pub_date >= "2020-01-01";
-- EXPLAIN: key=idx_multi, rows=120, Extra=Using index condition
扫描行数从 5000 降到 120,响应从 3 秒降到 90ms。核心不是"多加索引",而是"把多个条件组合成一个联合索引"。
思考 && 总结
索引优化的关键公式:
- 最左前缀原则 ------联合索引的列从最左开始匹配,不能跳级。WHERE 只用到 b 时无法用
(a, b, c)索引。 - 覆盖索引比普通索引快 ------因为它不需要回表。能不用
SELECT *就不用。 - 索引选择性决定列是否值得建索引。 低于 10-15% 的列不建单独索引。
- 联合索引的顺序最重要。 把选择性高、查询频率高的列放最左边。范围查询列放最后。
结尾
慢查询排查上下篇完结。下篇进入内存排查------Python 内存泄漏的排查工具箱。
源码骑士 --- 源码级拆解,从底层看透技术
👀 关注:跟博主一起从源码视角深耕底层原理
❤️ 点赞:让优质内容被更多人看见
⭐ 收藏:核心知识点存好,随用随查
💬 评论:分享你的经验或疑问,一起交流
🔄 一键四连:别忘了给博主一键四连!
🗡️ 寄语:一个正确的联合索引胜过四个单列索引。
结语:索引不是越多越好------是越精准越值钱。EXPLAIN + 联合索引 + 覆盖索引,三板斧就能解决 90% 的慢查询。一键四连!