数据库索引怎么加才真变快:三个判断与一次验证

作者:程序员白大力 · 2026-09-29 18:40
数据库索引怎么加才真变快:三个判断与一次验证

网站上线一段时间,慢查询告警一响,很多人的第一反应是「给查询列加个索引」。加完之后结果分三种:有的立竿见影,有的纹丝不动,有的查询没快、写入反而变慢。问题不在索引本身,而在索引不是开关,是一种有明确使用前提的数据结构——查询形态不符合,它就装睡。这篇文章把「什么时候该加、列怎么排、加了为什么不走、怎么验证」理成一套可复用的判断流程。

一、加之前:先过三个判断

判断一:这列真的会被高频过滤吗。 索引的价值是把全表扫描变成定点查找,只有经常出现在 WHERE、JOIN、ORDER BY 里的列才值得考虑。躺在表里从来不过滤的列,加了纯属浪费。

判断二:这列的选择性够高吗。 选择性可以粗略理解为「不重复值的数量 ÷ 总行数」。手机号、邮箱、订单号接近 1,是索引的黄金列;「状态」「类型」这类只有两三个取值的列,建了索引大概率还是要扫掉大半张表,收益有限。低选择性不是绝对禁区,但有条件:比如只查询「已发布」文章,可以给索引加过滤条件,只索引符合条件的行——Django 的 models.Index 支持 condition 参数,官方文档称之为条件索引。注意这种写法只有 PostgreSQL 和 SQLite 支持,MySQL 后端用不了。

判断三:这列是不是已经有索引了。 用 Django 的项目尤其要过这一关:主键、unique=True 字段、外键默认都自动带索引。Django 官方文档明确写了 ForeignKey 会自动建索引,想关掉得显式传 db_index=False——反过来手动再建一遍就是重复。

二、复合索引:列顺序决定一切

单列判断完,轮到复合索引。核心规则一条:最左前缀。索引 (a, b, c) 能服务按 a、(a, b)、(a, b, c) 过滤的查询,不能服务只按 b 或 (b, c) 过滤的查询。B-tree 先按第一列排序,第一列相同才轮到第二列——跳过姓氏直接按名查电话本,只能一页页翻。

列顺序的通用排法:等值列在前,范围列在后,排序列垫底。查询一旦遇到范围条件(>、<、BETWEEN),后面的列就不再参与索引过滤,只能退化为逐行比对。

以 Django 为例,查询 filter(author=...).filter(status=...).order_by("-created") 对应的索引应该这么建:

class Article(models.Model):
    status = models.CharField(max_length=20)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(
                fields=["author", "status", "created"],
                name="article_author_status_created_idx",
            ),
        ]

author 和 status 都是等值条件,排前面;created 做排序,放最后,排序也能直接吃索引、省掉一次 filesort。另外 Django 官方文档建议尽量用 Meta.indexes 而不是字段上的 db_index=True——前者功能更全,支持复合索引和条件索引。

还有一个常见的反模式值得点一下:查询形态一多,就给每种查询单独建一个索引。复合索引是可以复用的——(a, b) 的索引天然覆盖只有 a 条件的查询,所以遇到新查询先想想能不能扩展或重排现有索引,而不是平添一个。用 MySQL 的还可以查 sys.schema_unused_indexes,把长期没人用的索引清掉。

三、加了为什么不走:三个高频失效场景

场景一:对索引列套函数或做运算。 WHERE DATE(created) = '2026-01-01' 基本等于放弃索引——索引里存的是 created 的原始值,套上函数后比较对象变了,B-tree 无处下叉。改成 created__range=(起点, 终点) 的范围写法,索引就能接上。

场景二:隐式类型转换。 MySQL 官方文档原文说得很清楚:字符串列与数字比较时无法使用该列的索引,因为有太多不同的字符串都能转换成同一个数字。典型翻车是 varchar 类型的手机号列拿整数去等值查询。查询值的类型要和列类型对齐。

场景三:范围条件夹在中间。 索引建成了 (status, created, author),查询却是 status=? AND created>? AND author=?——created 是范围条件,author 就吃不到了。这不是索引失效,是列顺序建错了,回到第二节重排。

同类问题还有前缀匹配:LIKE '关键词%' 能走索引,LIKE '%关键词%' 就走不了,原理和最左前缀一样——索引按列值从左往右排,开头不定就只能逐行看。

四、加完必须验证:一条命令的事

「应该会用上」不算数,得看执行计划。Django 2.1 起 QuerySet 自带 explain(),除 Oracle 外所有内置数据库后端都支持:

print(Article.objects.filter(author=1, status="published").explain())
# SQLite 输出示例:
# SEARCH article USING INDEX article_author_status_created_idx
#     (author_id=? AND status=?)

SQLite 的输出里,SEARCH ... USING INDEX 说明走了索引,SCAN TABLE 说明在扫全表——后者出现就回到第二节检查列顺序。PostgreSQL 还可以加 explain(analyze=True) 看到真实执行耗时,比估算更直观。

结尾

索引这件事可以收成一句话:先看查询形态,再掂量选择性,列顺序等值在前范围在后,最后 explain 验证。四步里最容易被跳过的是最后一步——验证只花几秒,却能避免一个永远不生效还白白拖慢写入的索引。别忘了索引的本质是拿写入开销换读取速度,每个索引都要在每次 INSERT 和 UPDATE 时同步维护,只加不清理的表会越写越沉。

参考文章

  • 《MySQL Reference Manual: Multiple-Column Indexes》
  • 《MySQL Reference Manual: Type Conversion in Expression Evaluation》
  • 《Django Documentation: Model field reference》
  • 《Django Documentation: QuerySet API reference》
  • 《Queryset.explain() released in Django 2.1》
本文由 AI 辅助生成并经人工审核发布,内容仅供参考。