32-慢查询排查全流程(下)-索引优化实战与最左前缀原则

你的接口为什么慢?(下)——索引优化实战:什么列该建索引、最左前缀原则与联合索引

📖 文章简介: 上篇用 Debug Toolbar 抓到了慢查询,用 EXPLAIN 读懂了执行计划。下篇进入索引实战——不是"给所有列都加索引",而是精准找出对业务最有用的那几列。从 B+Tree 的排列规则讲起,推导出最左前缀原则的完整含义——为什么 (a, b, c) 联合索引在 WHERE b = 1 时根本用不上。覆盖索引(Using index)为什么比普通索引快、为什么 SELECT * 让索引优化效果打折、以及 Django db_indexMeta.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⚠️ 只用 ac 被跳过了,索引停在 a

简单记:联合索引像"一排书架——每层按 a 字母排序,同 a 下再按 b 排序。你要找 b 一样的东西,必须先从 a 进去,没从 a 进来的话无法定位。

1.3 为什么不建四个单列索引而行不通

-- 单列索引各有建一个: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_indexMeta.indexes

2.1 单列索引

class Book(models.Model):
    author = models.CharField(max_length=100, db_index=True)   # 单列索引
    title = models.CharField(max_length=200)

2.2 联合索引

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

CREATE INDEX borrow_user_returned ON borrowrecord (user_id, is_returned, borrowed_at);

3 ~> 覆盖索引——为什么 SELECT * 是索引的敌人

3.1 什么是覆盖索引

当一个索引包含了查询需要的所有列,MySQL 可以直接从索引返回结果,不需要再去访问数据行本身。这就是覆盖索引——EXPLAIN 里 Extra 列会显示 Using index

-- 有联合索引 (user_id, borrowed_at)
-- 查询只用到这两列
SELECT user_id, borrowed_at FROM borrowrecord WHERE user_id = 42;
-- Extra: Using index  ← 覆盖索引——只扫描索引,没碰数据行

但如果 SELECT *

-- 有同样的索引,但查询用到了索引之外的列(如 book_id、is_returned 等)
SELECT * FROM borrowrecord WHERE user_id = 42;
-- Extra: Using index condition  ← 用了索引定位,但要回表读取其他列

SELECT * 让覆盖索引优化失效——每次都多一次"回表"操作。

3.2 Django 层面如何利用覆盖索引

# ❌ 回表查询——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,索引效果越好。

-- 性别列:只有 "男" "女" 两个值,1 百万行 → 选择性 0.000002——极差
-- 用户 ID:1 百万行中 1 百万个唯一值 → 选择性 1.0——完美索引列
-- 订单状态:"pending/paid/shipped/cancelled" 四个值,选择性 0.000004——还是太差

选择性低于 10-15% 的列不建议建单独索引。 MySQL 优化器可能直接忽略它,选择全表扫描。

4.2 在 Django 中检查选择性

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 年那个多条件搜索——原始没有联合索引:

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 索引,categorypub_date 的过滤在回表后逐行判断。索引只起到了"缩小集合"的作用,没起到"过滤"的作用。

建联合索引后:

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。核心不是"多加索引",而是"把多个条件组合成一个联合索引"。


思考 && 总结

索引优化的关键公式:

  1. 最左前缀原则——联合索引的列从最左开始匹配,不能跳级。WHERE 只用到 b 时无法用 (a, b, c) 索引。
  2. 覆盖索引比普通索引快——因为它不需要回表。能不用 SELECT * 就不用。
  3. 索引选择性决定列是否值得建索引。 低于 10-15% 的列不建单独索引。
  4. 联合索引的顺序最重要。 把选择性高、查询频率高的列放最左边。范围查询列放最后。

结尾

慢查询排查上下篇完结。下篇进入内存排查——Python 内存泄漏的排查工具箱。

源码骑士 — 源码级拆解,从底层看透技术

👀 关注:跟博主一起从源码视角深耕底层原理

❤️ 点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬 评论:分享你的经验或疑问,一起交流

🔄 一键四连:别忘了给博主一键四连!

🗡️ 寄语:一个正确的联合索引胜过四个单列索引。

结语:索引不是越多越好——是越精准越值钱。EXPLAIN + 联合索引 + 覆盖索引,三板斧就能解决 90% 的慢查询。一键四连!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值