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

文章目录

  • 你的接口为什么慢?(下)------索引优化实战:什么列该建索引、最左前缀原则与联合索引
    • 导入语
    • [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_indexMeta.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_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 ⚠️ 只用 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_indexMeta.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 索引,categorypub_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。核心不是"多加索引",而是"把多个条件组合成一个联合索引"。


思考 && 总结

索引优化的关键公式:

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

结尾

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

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

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

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

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

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

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

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

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

相关推荐
闵孚龙1 小时前
《PyTorch 深度修炼》Dataset 和 DataLoader:数据如何喂给模型
人工智能·pytorch·python
goldenrolan2 小时前
A公司物料替代测试系统 v1.7:从需求到 exe/apk 的 AI 辅助全链路实践
android·自动化测试·软件测试·python·ai
菜板春2 小时前
jupyter入门-手册-特征探索
python·jupyter
Metaphor6922 小时前
使用 Python 将 PDF 转换为 HTML
python·pdf·html
极光代码工作室2 小时前
基于数据仓库的电商数据分析平台
大数据·hadoop·python·spark·数据可视化
开发小能手-roy2 小时前
StringBuilder vs StringBuffer:2024年还需要线程安全字符串吗?
开发语言·python·安全
AC赳赳老秦3 小时前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw
2601_954706493 小时前
云手机技术详解+Python实战调用|2026高稳云手机平台推荐
开发语言·python·智能手机
chushiyunen3 小时前
java中的路径处理、左右斜杠
java·开发语言·python