城南图书馆的管理员小陈,守着十二面墙的借阅卡。每张卡对应一本书,记着书名、作者姓氏、出版年份和书架位置。读者报一个需求,小陈就得到卡片墙里翻。一开始卡片乱挂,找一本《MySQL 修炼之道》要翻几百张,读者排起长队;把卡片按书名排好后,报书名的查询瞬间完成,可读者又改问“姓王的人 2007 年出版的书”。他这才意识到:卡片怎么排、按哪些字段排、谁在前谁在后,决定了找书到底快不快。数据库里的索引选择性,就是卡片对目标的区分能力;联合索引设计,则是给卡片设计一套既实用又省力的多级排序规则。这篇文章,就跟着小陈一起把这面卡片墙重新排一遍——学会用选择性这把尺子,为高频查询设计出高性价比的联合索引。

岗位关联

掌握选择性计算与最左前缀设计原则,你就能从“会建索引”升级为“会设计索引”,让复杂查询从秒级降到毫秒级——这是后端开发与 DBA 进阶的核心技能。

目录
  1. 可视化演示
  2. 选择性概念
  3. 选择性计算
  4. 联合索引结构
  5. 最左前缀原则
  6. 字段顺序策略
  7. 覆盖索引优化
  8. 设计权衡
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示索引选择性计算、联合索引B+树最左前缀匹配及列顺序设计策略对查询效率的影响。

本节配有交互动效(进入视口后自动加载,约 3KB)。

索引选择性 选择性=区分度/总行数越高越好 邮箱列95%区分度适合做索引 性别列仅2值选择性极低不推荐

选择性概念

小陈第一次优化卡片墙时,把卡片按「书名」排。每本书名不同,读者报一个书名,他顺着字母一卡定位,几乎不用看第二张。后来读者改问“有没有姓王的作者写的书”,他只能按「作者姓氏」翻:姓王的有 40 张卡,还是要翻一阵。他发现,同样是排卡片,有些信息一张卡就能区分到底,有些信息会把好多张卡归在一起。

这个“区分到底”的能力,在数据库里叫索引选择性。它回答一个问题:当查询以这一列为条件时,索引能把它快速缩小到多小的范围。定义很朴素:这一列有多少种不同的取值,占整张表行数的比例。取值种类越多、重复越少,选择性越高;反之,一列若只有两三种取值,选择性就低,过滤效果形同虚设。

小陈很快就明白:书名卡的区分能力接近满分,而作者姓氏卡只有一半。他决定把“区分能力强”的字段作为排序的第一依据——但这只是第一步,因为读者往往要同时报出多个条件。选择性本身不决定一切,但它决定了每一刀下去能砍掉多少无关卡片。

S=DN=COUNT(DISTINCT x)COUNT(∗)\displaystyle S = \frac{D}{N} = \frac{\text{COUNT}(\text{DISTINCT}\ x)}{\text{COUNT}(*)}
选择性 $S$ 越接近 1,说明这一列取值越独特,查找时候选行越少,索引过滤能力越强;越接近 0,说明大量行共享同一个值,过滤效果越差。
  1. 分母 N(总卡片数)$N = \text{COUNT}(*)$,统计整张表有多少行记录。在小陈的例子里,它就是卡片墙上的卡片总数。
    N=COUNT(∗)N = \text{COUNT}(*)
  2. 分子 D(不同取值数)$D = \text{COUNT}(\text{DISTINCT}\ x)$,数一数列 $x$ 一共出现过多少种不同的值。书名列几乎一本书一个值,$D$ 接近 $N$;作者姓氏列只有几个姓,$D$ 很小。
    D=COUNT(DISTINCT x)D = \text{COUNT}(\text{DISTINCT}\ x)
  3. 比值 S(区分能力)$S = D / N$。$S$ 越大,每个取值对应的行数越少,一次定位越准。
    S=DNS = \frac{D}{N}

选择性计算

为了让决策不靠感觉,小陈做了一张统计表。他数了数:12 张演示卡片里,作者姓氏只有 5 种(曹、李、王、张、赵),每种姓大约 2 张卡;而出版年份有 10 种,每种大约 1 张卡。显然年份这一列区分能力更强。在数据库里,这个“数一数”的过程就是一条 SQL 统计,把每列的不同取值数除以总行数,就得到该列的选择性。

把统计结果贴在卡片墙旁边,小陈每次设计新的排列规则前,都会先算一算每列的区分度。这个习惯,就是索引设计的第一性原理:先量化,再决策。数值一出来,哪个字段该当“第一排序级”,答案已经写在了墙上。

S姓氏=COUNT(DISTINCT 姓氏)COUNT(∗)=512≈0.42,S年份=COUNT(DISTINCT 年份)COUNT(∗)=1012≈0.83\displaystyle S_{\text{姓氏}} = \frac{\text{COUNT}(\text{DISTINCT}\ \text{姓氏})}{\text{COUNT}(*)} = \frac{5}{12} \approx 0.42, \qquad S_{\text{年份}} = \frac{\text{COUNT}(\text{DISTINCT}\ \text{年份})}{\text{COUNT}(*)} = \frac{10}{12} \approx 0.83
姓氏列选择性 0.42,年份列 0.83。0.83 > 0.42,说明年份列过滤后候选集更小。业界经验:选择性高于 0.3 才值得单独建索引,低于 0.1 基本不用考虑。
  1. 统计姓氏的区分度把 12 行里的姓氏去重,得到 5 种,用 COUNT(DISTINCT 姓氏) 一条 SQL 即可完成。
    COUNT(DISTINCT 姓氏)=5\text{COUNT}(\text{DISTINCT}\ \text{姓氏}) = 5
  2. 统计年份的区分度年份去重后得到 10 种,比姓氏多一倍。
    COUNT(DISTINCT 年份)=10\text{COUNT}(\text{DISTINCT}\ \text{年份}) = 10
  3. 分别除以总行数$5 / 12 \approx 0.42$,$10 / 12 \approx 0.83$,数值越高代表过滤后候选集越小。
    512≈0.42,1012≈0.83\frac{5}{12} \approx 0.42, \qquad \frac{10}{12} \approx 0.83

联合索引结构

单一条件优化完,读者开始组合查询:“我要姓王的、2007 年出版的书”。小陈如果把卡片按姓氏排,姓王仍有 3 张卡,还得在这 3 张里翻年份。他决定改造卡片墙:把「作者姓氏」和「出版年份」做成两级排序——第一级按姓氏,第二级在同一个姓氏内部按年份从小到大排。这样报出“王 + 2007”,他先跳到姓王区间,再在区间内二分定位年份,两步合成一步。

这就是联合索引(复合索引)。它不是一个“包含多个列”的普通记号,而是把所有列拼成一个组合键,按照 $(A, B)$ 的顺序整体排序。B+ 树里任何相邻的组合键都满足:先比 $A$,$A$ 相同再比 $B$。这个“天然有序”的特性,让联合索引不仅能过滤,还能直接支撑 ORDER BY 与范围查询。

小陈的卡片墙从此有了“区—排—架”三级坐标,找书快了一倍。但新问题来了:两级排序确实好,读者却可能跳过第一级直接问“有没有 2007 年出版的书”,这时候这套卡片墙还管用吗?答案,藏在最左前缀原则里。

SAB=COUNT(DISTINCT (A,B))COUNT(∗),SAB≥max⁡(SA,SB)\displaystyle S_{AB} = \frac{\text{COUNT}(\text{DISTINCT}\ (A, B))}{\text{COUNT}(*)}, \qquad S_{AB} \ge \max(S_A, S_B)
组合后的 $(A, B)$ 对几乎不会完全重复,所以联合索引的选择性通常不低于任何单列——组合唯一性越高,整棵索引树的分支定位越准。
  1. 组合键去重计数把 $(A, B)$ 两列当作一个整体去重,数出有多少种不同的组合,它记录的是“两级排序到底产生了多少个互不相同的坐标”。
    COUNT(DISTINCT (A,B))\text{COUNT}(\text{DISTINCT}\ (A, B))
  2. 除以总行数得到组合选择性组合越独特,$S_{AB}$ 越接近 1。
    SAB=COUNT(DISTINCT (A,B))COUNT(∗)S_{AB} = \frac{\text{COUNT}(\text{DISTINCT}\ (A, B))}{\text{COUNT}(*)}
  3. 与单列选择性的关系组合键包含了 A 的全部信息,它至少和 A 一样独特,所以 $S_{AB} \ge S_A$ 且 $S_{AB} \ge S_B$。
    SAB≥max⁡(SA,SB)S_{AB} \ge \max(S_A, S_B)

最左前缀原则

小陈试了一周发现问题:【姓氏+年份】排好的卡片,读者只报年份时,他完全不知道从哪一段开始翻——因为“年份”是第二排序级,必须知道第一个姓才能进入对的区间。读者只问年份,他就得把 12 张卡全看一遍。这就是联合索引最核心的约束——最左前缀匹配。

反过来,前缀还能复用:有了 $(A,B,C)$,查询只带 $A$ 或只带 $A,B$ 也能命中同一棵索引树,所以不需要再为这些前缀单独建索引。小陈不怪读者,他开始思考另一件事:如果读者最常问的是“年份+姓氏”,当初把卡片按 (年份,姓氏) 排是不是更好?这便引出了字段顺序的策略问题。

可用前缀={(A), (A,B), (A,B,C)},不可用={(B), (B,C), (C)}\displaystyle \text{可用前缀} = \{(A),\ (A,B),\ (A,B,C)\}, \qquad \text{不可用} = \{(B),\ (B,C),\ (C)\}
联合索引 $(A, B, C)$ 只能命中查询条件里出现 $(A)$、$(A,B)$、$(A,B,C)$ 三种连续前缀的查询;只要条件不是从 $A$ 开始,索引的有序性就无法发挥。
  1. 前缀 = 从左往右连续的列联合索引匹配严格从第一列开始,可以使用前 1 列、前 2 列或全部列,这对应查询条件里提供了前几个等值列。
    {(A), (A,B), (A,B,C)}\{(A),\ (A,B),\ (A,B,C)\}
  2. 跳列 = 失去有序性跳过 $A$ 直接用 $B$ 或 $C$,等价于在一本先按姓氏后按年份的电话簿里只按年份查找,只能全量扫描。
    {(B), (B,C), (C)}∉可用前缀\{(B),\ (B,C),\ (C)\} \notin \text{可用前缀}

字段顺序策略

小陈拿两组实验对比:按 (姓氏,年份) 排,报出“姓王”后还剩 3 张卡要翻;按 (年份,姓氏) 排,报出“2007 年”后只剩 1 张卡。原因是年份选择性 0.83 远高于姓氏的 0.42。结论很直观:高选择性的字段放前面,第一刀就能砍掉更多卡片。数据库里同理——联合索引 $(A, B)$ 对 WHERE A = ? AND B = ? 的过滤分两步:先按 $A$ 定位候选,再按 $B$ 缩小,所以 $A$ 的选择性越高,第一步的候选越小,整体越高效。

小陈还定下另一条经验:等值优先于范围。= 条件能精确跳转,>、<、BETWEEN 一旦开始,后面的列就无法再用于索引内的精确定位。所以他把等值列排在前、范围列排在后,两条经验合起来,就是联合索引字段顺序的黄金法则。

若 SA≥SB,则 (A,B) 通常优于 (B,A),N命中=NDA×DB\displaystyle \text{若 } S_A \ge S_B \text{,则 } (A, B) \text{ 通常优于 } (B, A), \qquad N_{\text{命中}} = \frac{N}{D_A \times D_B}
两列独立时,等值过滤后的候选行数约等于总行数除以两列取值数的乘积。所以把取值种类多的列放在第一位,能保证每一步筛掉的基数最大。
  1. 第一步候选数WHERE A = ? 命中约 $N / D_A$ 行;$D_A$ 越大(选择性越高),第一刀砍掉越多。
    N第一步=NDAN_{\text{第一步}} = \frac{N}{D_A}
  2. 两步后的候选数继续用 $B$ 过滤后约剩 $N / (D_A \times D_B)$ 行。
    N最终=NDA×DBN_{\text{最终}} = \frac{N}{D_A \times D_B}
  3. 顺序判断依据把选择性大的列放在 $D_A$ 的位置,第一步基数就小,整体更省扫描。
    若 SA≥SB,则 (A,B) 通常优于 (B,A)\text{若 } S_A \ge S_B \text{,则 } (A, B) \text{ 通常优于 } (B, A)

覆盖索引优化

有的读者只问“这本书在哪个区”,有的读者还要“看下作者和年份”。小陈发现,如果卡片背面顺手记上书的年份,那么回答“有没有 2007 年的书”时,他看卡片就知道答案了,根本不用走到书架去翻实体书——省下了一趟“回架”的路。这个“不用回书架”的优化,在数据库里叫覆盖索引。

与覆盖索引搭配的还有索引下推(ICP):把 WHERE 里能判断的条件推到存储引擎的索引层提前过滤,减少回表行数,相当于小陈在卡片墙前就替读者排除掉不符合年份的卡片,而不必先跑一趟书架再回来。小陈尝到了“用索引空间换查询时间”的甜头,也意识到——凡是好事都有代价。

Extra回表=N命中×C回表 → 覆盖索引 0\displaystyle \text{Extra}_{\text{回表}} = N_{\text{命中}} \times C_{\text{回表}} \ \xrightarrow{\ \text{覆盖索引}\ } 0
回表,就是根据索引找到主键后再回主表查整行,属于随机 I/O,非常昂贵。当索引里已经包含查询需要的所有列时,只扫索引页就能给结果,回表次数归零。
  1. 命中行数 N$N_{\text{命中}}$ 是索引过滤后剩余的行数,也是回表的次数。
    N命中=ND组合N_{\text{命中}} = \frac{N}{D_{\text{组合}}}
  2. 单次回表成本$C_{\text{回表}}$ 是一次按主键随机读数据的成本,远高于顺序读索引页。
    C回表≫C索引页扫描C_{\text{回表}} \gg C_{\text{索引页扫描}}
  3. 总回表代价命中越少、回表次数越少越好;覆盖索引直接把该项清零。
    Extra回表=N命中×C回表⟶0\text{Extra}_{\text{回表}} = N_{\text{命中}} \times C_{\text{回表}} \longrightarrow 0

设计权衡

小陈的卡片墙越来越多:按书名排、按姓氏+年份排、按年份+姓氏排。读者找书快了,但他发现每天上架新书时,要把每张新卡写进每一面墙,写卡工作量成倍增加。偶尔还会因为哪面墙没同步,出现“卡片找得到、书架找不到”的尴尬。这正是索引设计的核心权衡:读优化与写放大。

半年后,小陈的卡片墙稳定成了三面,每面都服务一批高频问题,新书上架节奏也恢复了。他总结出挂在墙上的三条法则:选择性决定过滤强度、最左前缀决定能服务哪些查询、真实负载决定到底建几个索引。这面墙,就是数据库索引设计最好的寓言。

C写入=C行+∑i=1KC索引i,K=索引个数\displaystyle C_{\text{写入}} = C_{\text{行}} + \sum_{i=1}^{K} C_{\text{索引}_i}, \qquad K = \text{索引个数}
写一行数据的总代价 = 写主表行的代价 + 每个二级索引各自的更新代价。索引越多,写放越大;这就是为什么“宁精勿滥”。
  1. 主表写入成本$C_{\text{行}}$ 是写入主表一条记录本身的开销,无论如何都省不掉。
    C行C_{\text{行}}
  2. 每个索引的更新成本每维护一个索引都要付出 $C_{\text{索引}}$,可能触发页分裂、排序调整。
    ∑i=1KC索引i\sum_{i=1}^{K} C_{\text{索引}_i}
  3. 写放大的含义索引个数 $K$ 越大,写入越慢。应通过前缀复用让一个索引服务多个查询,控制冗余索引。
    索引越少、前缀复用越多,写入越轻\text{索引越少、前缀复用越多,写入越轻}

AI 应用场景

电商订单中心查询优化

阶段:数据存储与检索优化

解决的问题:订单表几千万行,订单列表页需要 WHERE user_id = ? AND status = ? ORDER BY created_at DESC。没有联合索引时,user_id 过滤后仍可能剩几千行参与排序,接口经常超时。AI 分析慢查询日志后,按选择性计算:user_id 选择性接近 1、status 仅 0.2、created_at 接近 1,最终设计联合索引 (user_id, created_at) 并让 status 走索引下推,一个索引同时服务“只看某用户”和“某用户某状态”两类查询。

具体案例:某电商大促后慢查询 TOP1 是订单列表页,SQL 扫描 80 万行耗时 1.8 秒。DBA 按“高选择性在前、等值优先范围”重排字段为 (user_id, created_at, status),扫描行数骤降到 200 行,耗时降为 8 毫秒,P99 从 2.4 秒降到 60 毫秒。

日志检索平台查询提速

阶段:数据管道与监控告警优化

解决的问题:日志表每天新增数亿行,检索条件是 level = 'ERROR' AND app_id = ? AND ts > ?。level 只有 5 个取值、选择性低,直觉上不该放第一列;但 90% 的查询都带 level 等值条件,将其放在首位配合 app_id 等值与 ts 范围,恰好契合最左前缀,且 ERROR 判断在索引层下推完成,大幅减少回表。

具体案例:某监控平台用 AI 聚类历史查询模式,发现高频查询集中在“级别 + 应用 + 时间区间”三元组,于是引导建 (level, app_id, ts) 联合索引,重放压测后 P99 延迟从 620 毫秒降到 95 毫秒,索引数量反而从 9 个减到 4 个。

用户画像特征宽表设计

阶段:特征存储与训练数据准备

解决的问题:特征宽表有 500 列,AI 模型训练要按 (user_id, feature_date) 拉取全量特征。性别、城市等标签列单独看选择性极低,每列建索引会带来巨大写放大。正确做法是先计算多列组合选择性,确认 (user_id, feature_date) 组合几乎唯一后,只建这一个联合索引。

具体案例:某推荐系统特征表,开发先用 Python 脚本对 10 万行样本统计各列选择性与组合选择性,确认组合值为 1.0 后落地联合索引,特征回刷耗时下降 70%,写入吞吐提升 3 倍。

Python 演示

python81 行
# -*- coding: utf-8 -*-"""模拟索引选择性与联合索引的最左前缀匹配"""import random  def selectivity(values):    """计算某列的选择性:不同取值数 / 总行数"""    n = len(values)    if n == 0:        return 0.0    d = len(set(values))    return round(d / n, 4)  def simulate_records(num=2000):    """生成图书借阅记录,凑出两两不同的 (author, year) 组合"""    surnames = ['曹', '李', '王', '张', '赵', '刘', '陈', '杨', '黄', '周',                '吴', '徐', '孙', '马', '朱', '胡', '郭', '何', '林', '高']    years = list(range(1950, 2050))    records = []    seen = set()    while len(records) < num:        pair = (random.choice(surnames), random.choice(years))        if pair in seen:            continue        seen.add(pair)        records.append({            'author': pair[0],            'year': pair[1],            'title': 'book_%d' % len(records)        })    return records  def build_composite_index(records, keys):    """模拟联合索引:组合键 -> 行号列表"""    index = {}    for pos, rec in enumerate(records):        key = tuple(rec[k] for k in keys)        index.setdefault(key, []).append(pos)    return index  def leftmost_match(index, keys, conditions):    """最左前缀匹配:条件从左到右,None 表示该列不限"""    matched = []    for key, positions in index.items():        ok = True        for i, cond in enumerate(conditions):            if cond is not None and key[i] != cond:                ok = False                break        if ok:            matched.extend(positions)    return sorted(matched)  def main():    random.seed(42)    records = simulate_records(2000)    a_sel = selectivity([r['author'] for r in records])    y_sel = selectivity([r['year'] for r in records])    pair_sel = selectivity([(r['author'], r['year']) for r in records])    print('总行数 N =', len(records))    print('author 列选择性 =', a_sel)    print('year 列选择性 =', y_sel)    print('author+year 组合选择性 =', pair_sel)    idx_ab = build_composite_index(records, ['author', 'year'])    rows_a = leftmost_match(idx_ab, ['author', 'year'], ['王', None])    sample = None    for rec in records:        if rec['author'] == '王':            sample = rec['year']            break    rows_ab = leftmost_match(idx_ab, ['author', 'year'], ['王', sample])    print('仅按 author 匹配 =', len(rows_a), '行')    print('王 + %d 年 匹配 = %d 行' % (sample, len(rows_ab)))  if __name__ == '__main__':    main()
目的:用小数据模拟数据库表与联合索引,计算单列和组合选择性,并演示最左前缀匹配如何影响命中行数。
  1. d = len(set(values))用集合去重得到这一列有多少种不同取值,即选择性公式的分子。
  2. return round(d / n, 4)不同取值数除以总行数,得到 0 到 1 之间的选择性。
  3. pair = (random.choice(surnames), random.choice(years))一次生成一条记录的两个索引列。
  4. if pair in seen: continue保证 (author, year) 组合不重复,对应组合选择性为 1。
  5. key = tuple(rec[k] for k in keys)把多列拼成一个组合键,模拟联合索引的一条索引项。
  6. index.setdefault(key, []).append(pos)相同组合键挂上多个行号,等价于索引叶子页里的行指针。
  7. if cond is not None and key[i] != cond:从最左列开始逐个比对条件,一列不满足则整个键不命中。
  8. rows_ab = leftmost_match(idx_ab, ['author', 'year'], ['王', sample])用完整 (A, B) 前缀精确命中目标行。

常见误区

误区我以为只要把选择性最高的列放在联合索引第一位,这个索引就一定最高效,哪怕这列从不出现在 WHERE 条件里。

正解选择性高只代表“单卡定位能力强”,还要看真实查询负载。一个高选择性列若从不出现在查询谓词中,放在前面只会白白消耗维护成本;反过来,低选择性但高频过滤的列(如 is_deleted)配合高频查询仍然值得设计。小陈不会因为“年份”卡最有区分度就把所有卡都按年份排——他先问读者最常怎么问。

为何常见:只盯着“区分度数字大小”这一单一指标,忽略了查询频率与访问模式才是索引设计的出发点和落脚点。

误区我建了 `(a, b, c)` 联合索引,那遇到 `WHERE b = 1` 或 `WHERE c = 2` 应该也能加速吧?毕竟索引里“包含”这些列。

正解最左前缀原则明确:(a, b, c) 只保证以 a 开头的连续前缀可用,即 (a)、(a,b)、(a,b,c)。跳过 a 直接查 b 或 c,索引的有序链无从发挥,只能全表扫描。就像卡片按“姓氏+年份”排好,只报年份时小陈不知道从哪一段开始翻。

为何常见:把“建了索引”错误地等价于“所有列都被索引”,忽略了 B+ 树从左到右的有序依赖关系。

误区`WHERE a = 1 AND b > 10` 里 `b` 是范围,能筛掉更多行,所以把 `b` 放前面建 `(b, a)` 更合理。

正解范围条件(>、<、BETWEEN)一旦开始,其后的列无法再在索引内精确定位。因此等值列应放在范围列之前:(a, b) 中 a 先精确定位,b 在缩小的区间内做范围扫描;若建 (b, a),b 的范围先扫出一大片,a 只能在区间内逐行判断,效率差很多。

为何常见:凭直觉认为“能筛掉更多行的条件放前面”,但没有理解 B+ 树对等值与范围两种谓词的匹配机制完全不同。

误区既然要建索引,就把 SELECT 里出现的所有列都塞进联合索引,一次覆盖所有查询,岂不完美。

正解每个索引都有写放大与存储成本:每次 INSERT、UPDATE、DELETE 都要同步维护每个 B+ 树,页分裂和排序都会拖慢写入。盲目堆列会制造巨大冗余索引。正确的做法是为高频查询加“恰好够用”的列,并通过前缀复用一个索引服务多个查询。

为何常见:只盯着读路径的收益,忽略了写路径与维护成本,这正是小陈“卡片墙越来越多、上架越来越慢”的教训。

误区性别列只有两个取值,选择性才 0.5,建索引肯定没用,直接放弃。

正解低选择性列单独用效果差,但在联合索引里作第二列或与高选择性列组合依然有用;当查询条件能配合覆盖索引时,低选择性列也可能把扫描范围从全表缩小到一小段。关键看“实际需要扫描的行数”,而非选择性数字本身。小陈的“年份”列覆盖了查询后,连低选择性的 level 也敢放到联合索引首位了。

为何常见:把“选择性低”绝对化,忽略了查询返回行数、索引下推、覆盖索引等综合效果对性能的决定作用。

面试题

Q1. 索引选择性 S = D / N 中,D 表示什么?

单选题 · easy

  • A.表中总行数
  • B.该列不同取值数← 正确答案
  • C.该列重复值个数
  • D.索引叶子页数量

答案:B。D 是 COUNT(DISTINCT 列) 的结果,表示这一列有多少种不同的取值;N 才是总行数。就像小陈数“姓氏卡”有几种姓,而不是数卡的总数。A 是分母 N 的含义,C、D 与公式无关。

Q2. 选择性越接近哪个数时,索引的过滤效果越强?

单选题 · easy

  • A.0(所有值都相同)
  • B.1(每个值都独特)← 正确答案
  • C.-1(完全不相关)
  • D.100(值域非常大)

答案:B。S = D / N,当每个值都独特时 D ≈ N,S 接近 1,索引一步定位;S 接近 0 表示大量行共享同一个值,过滤后候选集依然庞大。就像书名卡每张指向一本书,而“教材类别”卡一翻就是几百张。

Q3. 联合索引 (a, b) 能充分利用索引的查询是?

单选题 · mid

  • A.WHERE b = 5(只过滤 b)
  • B.WHERE a = 1 AND b = 2← 正确答案
  • C.WHERE b > 5 AND b < 10
  • D.WHERE a > 1 AND b = 2

答案:B。B 同时提供 a、b 两个等值条件,完整命中联合索引。A、C 跳过了最左列 a 直接过滤 b,无法利用索引有序性;D 中 a 是范围条件,范围开始后 b = 2 无法再精确定位,只能作 residual 过滤。

Q4. 设计联合索引字段顺序时,第一原则是什么?

单选题 · mid

  • A.高选择性字段放在最前← 正确答案
  • B.低选择性字段放在最前
  • C.按字段名长度排序
  • D.按建表时的先后排序

答案:A。高选择性字段放前面,第一步就能砍掉最多的候选行。小陈的实验里 (年份,姓氏) 优于 (姓氏,年份),正是因为年份选择性 0.83 高于姓氏 0.42。B 会放大中间结果,C、D 与查询效率无关。

Q5. 关于“回表”,下列说法正确的是?

单选题 · mid

  • A.通过索引找到主键后回主表取整行← 正确答案
  • B.查询执行失败后自动重试
  • C.存储引擎重启后的崩溃恢复
  • D.把改变的数据刷回磁盘

答案:A。回表是二级索引的固有代价:索引叶子只存索引键和主键,要取其他列必须按主键再读一次数据行,属于随机 I/O。小陈“看完卡片还要跑一趟书架”就是回表。B、C、D 描述的是重试、恢复与刷盘,与索引查找无关。

Q6. 联合索引 (a, b, c),下列哪些查询可命中该索引?

多选题 · hard

  • A.WHERE a = 1(只带 a 条件)← 正确答案
  • B.WHERE b = 2(只带 b 条件)
  • C.WHERE a = 1 AND b = 2← 正确答案
  • D.WHERE b = 2 AND c = 3

答案:AC。A 使用前缀 (a),C 使用前缀 (a, b),都在可用前缀集合内。B 和 D 都跳过了最左列 a,无法进入索引的有序区间,只能全表扫描,违反最左前缀原则。

Q7. 某列选择性仅为 0.05,下列描述正确的是?

单选题 · mid

  • A.能快速定位到唯一一行
  • B.能过滤掉绝大部分记录
  • C.过滤后候选集依然很大← 正确答案
  • D.该列无法创建任何索引

答案:C。选择性 0.05 意味着平均每个取值对应 20 行记录,过滤后候选集依然庞大,索引的“精确制导”作用大打折扣。就像只有两种姓的卡片墙,报出姓氏后还要在一堆卡里继续翻。D 是常识性错误,低选择性列同样可以建索引,只是单独用效果差。

Q8. 下列哪些手段能有效减少回表带来的开销?

多选题 · hard

  • A.使用覆盖索引直接取数← 正确答案
  • B.索引下推在索引层过滤← 正确答案
  • C.把所有二级索引全部删除
  • D.强制查询走全表扫描

答案:AB。覆盖索引让查询所需列都在索引页中,回表次数归零;索引下推把 WHERE 条件提前到存储引擎的索引层判断,减少回表行数。C、D 删除索引或强行走全表,只会让扫描行数爆炸,回表问题不仅没解决反而更糟。

Q9. 一张 1 万行的表,某列有 2500 种不同取值,其选择性约为?

单选题 · easy

  • A.0.25← 正确答案
  • B.0.75
  • C.2.50
  • D.4.00

答案:A。S = 2500 ÷ 10000 = 0.25。选择性取值恒在 0 到 1 之间,所以 C、D 直接排除;0.75 是把除数和被除数弄反了(7500 才是 0.75)。这个数值说明该列平均 4 行共享一个取值,过滤能力中等偏弱。

Q10. 联合索引 (a, b) 中 a 选择性 0.9、b 选择性 0.5,下列说法正确的是?

多选题 · extreme

  • A.通常 (a, b) 优于 (b, a)← 正确答案
  • B.仅查 b 也能命中 (a, b)
  • C.组合选择性不低于 a← 正确答案
  • D.高频查 b 时宜用 (b, a)← 正确答案

答案:ACD。高选择性在前是通用法则,A 正确;组合键包含 a 的全部信息,S_ab ≥ S_a,C 正确;若真实负载大量按 b 单独过滤,D 把 b 放首位以贴合最左前缀,正确。B 违反最左前缀,单独查 b 无法命中 (a, b),错误。这道题提醒我们:选择性要结合真实查询模式一起看。