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

📅 2026/6/17 6:58:53
32-慢查询排查全流程(下)-索引优化实战与最左前缀原则
文章目录你的接口为什么慢下——索引优化实战什么列该建索引、最左前缀原则与联合索引导入语1 ~ BTree 与最左前缀原则——联合索引的生死线1.1 BTree 的数据排列1.2 最左前缀原则1.3 为什么不建四个单列索引而行不通2 ~ Django 中建索引——db_index 与 Meta.indexes2.1 单列索引2.2 联合索引3 ~ 覆盖索引——为什么 SELECT * 是索引的敌人3.1 什么是覆盖索引3.2 Django 层面如何利用覆盖索引4 ~ 索引选择性——不是每列都该建索引4.1 选择性计算4.2 在 Django 中检查选择性5 ~ 真实案例——多条件搜索接口 3 秒到 90ms思考 总结结尾你的接口为什么慢下——索引优化实战什么列该建索引、最左前缀原则与联合索引文章简介上篇用 Debug Toolbar 抓到了慢查询用 EXPLAIN 读懂了执行计划。下篇进入索引实战——不是给所有列都加索引而是精准找出对业务最有用的那几列。从 BTree 的排列规则讲起推导出最左前缀原则的完整含义——为什么(a, b, c)联合索引在WHERE b 1时根本用不上。覆盖索引Using index为什么比普通索引快、为什么SELECT *让索引优化效果打折、以及 Djangodb_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 ~ BTree 与最左前缀原则——联合索引的生死线1.1 BTree 的数据排列MySQL InnoDB 引擎的索引底层是 BTree大部分情况下。联合索引的内部排列是先按第一列排序再按第二列排序——后面的列依赖于前面的列。一个联合索引(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_dateSELECT*FROMbookWHEREauthor鲁迅ANDcategory文学ANDpub_date2020-01-01;MySQL 的优化器通常只会选择一个索引——而不是三个各自用一段。它选了idx_author后另外两个列的过滤条件无法用到索引只能回表读取数据后逐行检查。联合索引(author, category, pub_date)能一次性定位到数据——三列都参与索引扫描。2 ~ Django 中建索引——db_index与Meta.indexes2.1 单列索引classBook(models.Model):authormodels.CharField(max_length100,db_indexTrue)# 单列索引titlemodels.CharField(max_length200)2.2 联合索引classBorrowRecord(models.Model):usermodels.ForeignKey(User,on_deletemodels.CASCADE)bookmodels.ForeignKey(Book,on_deletemodels.CASCADE)borrowed_atmodels.DateTimeField()is_returnedmodels.BooleanField(defaultFalse)classMeta:indexes[models.Index(fields[user,is_returned,borrowed_at]),# 用于某用户借了哪些未还的书的查询]Django 迁移会自动生成CREATE INDEXCREATEINDEXborrow_user_returnedONborrowrecord(user_id,is_returned,borrowed_at);3 ~ 覆盖索引——为什么SELECT *是索引的敌人3.1 什么是覆盖索引当一个索引包含了查询需要的所有列MySQL 可以直接从索引返回结果不需要再去访问数据行本身。这就是覆盖索引——EXPLAIN 里Extra列会显示Using index。-- 有联合索引 (user_id, borrowed_at)-- 查询只用到这两列SELECTuser_id,borrowed_atFROMborrowrecordWHEREuser_id42;-- Extra: Using index ← 覆盖索引——只扫描索引没碰数据行但如果SELECT *-- 有同样的索引但查询用到了索引之外的列如 book_id、is_returned 等SELECT*FROMborrowrecordWHEREuser_id42;-- Extra: Using index condition ← 用了索引定位但要回表读取其他列SELECT * 让覆盖索引优化失效——每次都多一次回表操作。3.2 Django 层面如何利用覆盖索引# ❌ 回表查询——ORM 默认 SELECT *recordsBorrowRecord.objects.filter(user_id42)# ✅ 覆盖索引——只选索引中包含的列recordsBorrowRecord.objects.filter(user_id42).values_list(user_id,borrowed_at)4 ~ 索引选择性——不是每列都该建索引4.1 选择性计算索引选择性 不重复的值数 ÷ 总行数。选择性越接近 1索引效果越好。-- 性别列只有 男 女 两个值1 百万行 → 选择性 0.000002——极差-- 用户 ID1 百万行中 1 百万个唯一值 → 选择性 1.0——完美索引列-- 订单状态pending/paid/shipped/cancelled 四个值选择性 0.000004——还是太差选择性低于 10-15% 的列不建议建单独索引。MySQL 优化器可能直接忽略它选择全表扫描。4.2 在 Django 中检查选择性fromdjango.db.modelsimportCount# 查看某列的不重复值数占比totalBorrowRecord.objects.count()distinct_statusesBorrowRecord.objects.values(is_returned).distinct().count()selectivitydistinct_statuses/totalprint(f选择性:{selectivity:.4f})# is_returned 只有 True/False → 选择性极低5 ~ 真实案例——多条件搜索接口 3 秒到 90ms2021 年那个多条件搜索——原始没有联合索引SELECT*FROMbookWHEREauthor鲁迅ANDcategory文学ANDpub_date2020-01-01;-- EXPLAIN: typeref, keyidx_author, rows5000, ExtraUsing where选了idx_author索引category和pub_date的过滤在回表后逐行判断。索引只起到了缩小集合的作用没起到过滤的作用。建联合索引后ALTERTABLEbookADDINDEXidx_multi(author,category,pub_date);SELECT*FROMbookWHEREauthor鲁迅ANDcategory文学ANDpub_date2020-01-01;-- EXPLAIN: keyidx_multi, rows120, ExtraUsing index condition扫描行数从 5000 降到 120响应从 3 秒降到 90ms。核心不是多加索引而是把多个条件组合成一个联合索引。思考 总结索引优化的关键公式最左前缀原则——联合索引的列从最左开始匹配不能跳级。WHERE 只用到 b 时无法用(a, b, c)索引。覆盖索引比普通索引快——因为它不需要回表。能不用SELECT *就不用。索引选择性决定列是否值得建索引。低于 10-15% 的列不建单独索引。联合索引的顺序最重要。把选择性高、查询频率高的列放最左边。范围查询列放最后。结尾慢查询排查上下篇完结。下篇进入内存排查——Python 内存泄漏的排查工具箱。源码骑士 — 源码级拆解从底层看透技术关注跟博主一起从源码视角深耕底层原理❤️点赞让优质内容被更多人看见⭐收藏核心知识点存好随用随查评论分享你的经验或疑问一起交流一键四连别忘了给博主一键四连️寄语一个正确的联合索引胜过四个单列索引。结语索引不是越多越好——是越精准越值钱。EXPLAIN 联合索引 覆盖索引三板斧就能解决 90% 的慢查询。一键四连