想象你刚被任命为一座百年图书馆的新任馆长。馆里藏书上百万册,可前任留下的“找书系统”只是一卷手写藏书清单——没有目录、没有分类,管理员找一本书只能一个书架一个书架地翻。读者要《三体》,管理员走了十分钟;读者要“所有刘慈欣的书”,管理员直接崩溃。 你决定改革:建一套目录卡片系统。每本书做一张卡片,记录编号、书名、作者、存放位置,按顺序排列。找书先查卡片,再按编号直奔书架。但很快你发现,卡片柜不是越多越好——每多一套都要占空间、要维护,新书上架时还得同步更新。什么书该做卡片?卡片怎么排?做几层?哪些该取消?这些决策,正是数据库索引设计原则要回答的问题。 数据库世界里,图书管理员叫“查询优化器”,藏书清单叫“表”,目录卡片叫“索引”。这篇文章带你跟着馆长一步步学索引设计:从基本概念、聚簇索引、复合索引,到失效场景、优化实践,最后掌握代价平衡。

岗位关联

掌握索引设计原则,你就能像资深DBA一样用最小的存储代价换取最大的查询提速,这是后端、数据与AI工程岗位写出高性能系统的核心基本功。

目录
  1. 可视化演示
  2. 索引基本概念:目录卡片的诞生
  3. 聚簇索引设计:决定书架顺序的总目录
  4. 复合索引原则:多层级联卡片柜
  5. 失效场景识别:让卡片柜失灵的四颗雷
  6. 优化实践策略:馆长的季度体检单
  7. 代价平衡艺术:不做贪心的馆长
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

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

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

索引基本概念:目录卡片的诞生

陈馆长接手图书馆后做的第一件事,是统计管理员的找书时间。他发现没有目录时,找一本指定的书平均要翻 8 分钟,找“某个作者的全部作品”更是灾难。于是他给每本书做了一张卡片,记录编号、书名、作者和书架位置,并按书名拼音排序。从此,找书变成“翻开卡片柜 → 定位书名 → 直奔书位”三步。这就是数据库索引的雏形:索引是独立于数据本身的一种“检索目录”。

在数据库里,表就是书库,一行数据就是一本“书”。当你要执行 SELECT * FROM books WHERE title = '三体' 时,如果没有索引,数据库只能从第一行开始逐行比对标题,直到最后一行。这种查找方式叫全表扫描,也就是管理员逐本翻阅书架的全过程。要理解索引的价值,需要先看清两种查找方式的复杂度差距。

全表扫描与索引查找的时间复杂度可分别记为:

有了目录卡片,陈馆长把平均找书时间从 8 分钟压缩到 30 秒。但真正的高手会问:这套卡片系统会带来什么新麻烦?这正是后面几节要解决的问题。

Tscan=O(n)Tindex=O(log⁡n)\displaystyle T_{\text{scan}} = O(n) \qquad T_{\text{index}} = O(\log n)
意思是:数据行数从 1 万涨到 100 万时,全表扫描的耗时也涨 100 倍;而索引查找的耗时只增加约 2 倍($\log_2 1000000 \approx 20$ 次比较)。
  1. n 代表表的行数$n$ 是表中的数据行数,行数越多,全表扫描要访问的“书”就越多。
  2. 全表扫描的线性代价$T_{\text{scan}} = O(n)$ 表示从头到尾逐行读,耗时与行数严格成正比,等价于管理员一本一本地翻。
  3. 索引查找的对数代价$T_{\text{index}} = O(\log n)$ 表示 B+ 树索引每层都能排除一半数据,约 $\log_2 n$ 次比较就能定位,类似查字典时“翻到中间,比较后再翻一半”。
  4. 常数开销的提醒索引查找本身也有读取索引页、回表取整行的固定开销,所以行数很少时索引未必更快。

聚簇索引设计:决定书架顺序的总目录

陈馆长很快发现,馆里最重要的一套卡片不是“书名卡”,而是“编号卡”——它不但记录了每本书的位置,还规定了书在书架上的物理摆放顺序。新买的书按编号从小到大插到架上,整座图书馆的书都沿着编号连续排列。这套“编号卡”就是数据库里的聚簇索引。

在 InnoDB 中,聚簇索引就是主键构成的 B+ 树,树的叶子节点直接保存整行数据。换句话说,数据的物理顺序由主键决定。这意味着主键的选择直接影响写入性能和空间利用率。如果主键是自增整型,新行总是追加到 B+ 树最右侧,只需要在最后一页写入;如果主键是 UUID,新行的主键大小随机,数据可能被插到树的中间,原有页被“撑爆”后就要发生页分裂——把一半数据搬到新页,再调整指针。

主键顺序与否带来的插入代价差异可以写成:

陈馆长把编号卡从“按字母随机编号”改成“按入库顺序编号”后,管理员上架新书再也不用挪动半面墙的书了。这个道理落到数据库上,就是一句铁律:主键优先选自增整型,让聚簇索引保持顺序写入。

Tinsert={O(1),主键自增,追加到末尾页O(n),主键随机,频繁页分裂搬移\displaystyle T_{\text{insert}} = \begin{cases} O(1), & \text{主键自增,追加到末尾页} \\[4pt] O(n), & \text{主键随机,频繁页分裂搬移} \end{cases}
条件分支表达式:上面一行是自增主键的理想插入,下面一行是随机主键的最坏情况。
  1. 自增主键的追加写入当主键严格递增,新记录永远落在 B+ 树最右叶子页,$O(1)$ 即可完成,耗时稳定。
  2. 随机主键的页分裂UUID 等随机主键会插入树的中部,如果目标页已满,需要把该页一半记录移到新页,并更新父节点指针。
  3. 分裂的级联代价频繁页分裂还产生大量物理碎片和空闲空间,进一步放大为 $O(n)$ 级别,写放大严重。

复合索引原则:多层级联卡片柜

单靠书名卡还不够,读者经常问“刘慈欣写的、2010 年后出版的书”。陈馆长又做了一套“作者 + 出版年”的联排卡片:先按作者姓氏排,同一作者内部再按年份排。这套卡片就是复合索引,由多个列共同组成排序键。

复合索引 (a, b, c) 的本质是:先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。因此它隐含地支持了 (a)、(a, b)、(a, b, c) 三个前缀匹配场景,却不能支持跳过 a 直接查 b 或 c 的查询。这就是最左前缀原则。设计列顺序时,通常把等值条件列放前面、范围条件列放后面,把区分度高或查询频率高的列放最左。此外,如果查询所需列全部包含在索引中,就形成覆盖索引,连“回表取整行”都省了;MySQL 5.6+ 还支持索引下推,在引擎层先用部分条件过滤,减少传给服务层的行数。

复合索引可用性的数学表达:

这套“三层卡片柜”给了陈馆长启发:卡片柜每一层的顺序都服务于一种查找路径,列顺序定生死。设计复合索引,本质上是在回答“读者的查询模式长什么样”,而不是拍脑袋堆列。

(a1,a2,…,ak) ⇒ 可匹配前缀 (a1,a2,…,aj),j≤k\displaystyle (a_1, a_2, \ldots, a_k) \ \Rightarrow\ \text{可匹配前缀 } (a_1, a_2, \ldots, a_j), \quad j \le k
也就是说,复合索引能服务的查询条件必须是从最左列开始的一段连续列,长度 j 可以小于等于 k,但绝不能跳过中间的列。
  1. 复合索引的排序键$(a_1, a_2, \ldots, a_k)$ 表示一个由 k 列构成的排序键,B+ 树按照这个顺序依次比较。
  2. 前缀可匹配查询条件若包含 $a_1, a_2, \ldots, a_j$($j \le k$),就能用上索引的这 j 列。
  3. 范围条件截断如果第 $j$ 列是范围查询(如 <、>、BETWEEN),则第 $j+1$ 列及之后无法继续用于过滤和排序,因为后面的列在范围值内不再有序。

失效场景识别:让卡片柜失灵的四颗雷

有一回,读者递来一张字条,上面的书名全是大写字母拼成的缩写。管理员拿着字条去对“书名拼音卡片”,怎么都对不上——因为索引卡是按原书名排序的,而不是按变形后的书名。陈馆长恍然大悟:只要查询条件对索引列做了某种“变形”,B+ 树的有序性就失效了。

最常见的四颗雷,第一是函数运算:在 WHERE 里写 WHERE YEAR(create_time) = 2024,优化器无法对 YEAR(create_time) 这棵“表达式树”做二分定位,只能全表扫。第二是隐式类型转换:字符串列与数字比较时,数据库会把列隐式转成数字,等于对列套了函数。第三是前导模糊匹配:LIKE '%张' 无法利用字母序定位,只能逐行判断。第四是范围条件中断:复合索引中,一旦某列使用了范围条件,它后面的列就无法继续参与索引定位。

函数变形导致索引失效,可以用等号两端的变换来理解:

陈馆长把这条经验贴在了办公室墙上:“查询条件写得像查字典,索引才像字典一样帮你;写成了对联字谜,就只能一页页翻全书。”

f(col)=v ⇒ 索引失效col=f−1(v) ⇒ 可走索引\displaystyle f(\text{col}) = v \ \Rightarrow\ \text{索引失效} \qquad \text{col} = f^{-1}(v) \ \Rightarrow\ \text{可走索引}
关键在“变形”发生在哪一侧:对列做函数,索引有序性被破坏;对常数做函数,列保持原样,索引照常生效。
  1. 左侧变形失效$f(\text{col}) = v$ 表示对索引列施加函数,B+ 树节点里存的是原始列值,无法直接比较 $f(\text{col})$,查询优化器被迫放弃索引。
  2. 右侧变形保留$\text{col} = f^{-1}(v)$ 表示把函数挪到常数侧,列保持原始形式,索引树仍可按原值定位。
  3. 统一思路任何破坏“索引列原始值有序性”的写法——函数、隐式 CAST、前导通配符、范围后的列——都会让卡片柜哑火。

优化实践策略:馆长的季度体检单

图书馆运转半年后,陈馆长开始做“卡片柜体检”。他发现两件事:一是有些卡片柜半年都没人翻过,还占着最好的位置;二是有些读者专挑长书名来查,完整书名索引又大又慢。他据此总结了一套实操策略。

第一招是前缀索引:对很长的文本列(如书名、URL、身份证号),只用前 N 个字符建索引,可以大幅压缩索引体积。但前缀不能拍脑袋选,要用“选择性”来衡量。第二招是冗余索引清理:(a, b) 索引已经隐含了 (a) 索引,若还单独建 (a) 就是冗余;用 SHOW INDEX 和慢查询日志找出从未被使用的索引并删除。第三招是慢查询分析:开启慢查询日志,定期捞取执行时间超阈值的 SQL,反推缺失的索引或错误的写法。

前缀索引是否值得建,看选择性公式:

陈馆长的体检单逻辑,对应到 DBA 日常就是“先采集慢查询,再分析索引使用情况,最后用选择性指标决定增减”。这套循环不是做一次就完,而是要随着业务查询模式的变化持续复检。

S=DprefixDfull\displaystyle S = \frac{D_{\text{prefix}}}{D_{\text{full}}}
S 表示前缀区分度:S 越接近 1,说明前缀越能代表整列,索引的过滤效果好,值得建。
  1. 全列去重数$D_{\text{full}}$ 表示该列完整值的不同取值个数,是选择性对比的基准。
  2. 前缀去重数$D_{\text{prefix}}$ 表示取前 N 个字符后仍然不同的取值个数,体现前缀的区分能力。
  3. 比值逼近 1 才划算$S$ 越接近 1,前缀信息损失越少;实践中一般要求 $S > 0.9$ 才使用前缀索引,否则区分度太差,过滤不了多少行。

代价平衡艺术:不做贪心的馆长

体检单让陈馆长领悟到最重要的道理:卡片柜不是越多越好。柜子本身占空间、耗木材,管理员每次上架新书都要同步更新所有柜子。馆里曾经同时有 12 套卡片目录,结果查询是快了,可每周上架 2000 本新书时,管理员得加班到凌晨——这就是写入放大。

数据库世界里,每个二级索引都是一棵独立的 B+ 树。读查询也许能受益,但每一次 INSERT、UPDATE、DELETE 都要同时维护全部索引树。索引越多,写入越慢,存储开销也越大。更微妙的是低选择性列:像“性别”这种只有两三个取值的列,索引查找后仍要返回接近一半的行,优化器往往觉得“不如直接全表扫”。索引艺术的核心,是把宝贵的存储和写入预算,留给真正高频、高区分度的查询。

写入代价与索引数量的关系可以建模为:

陈馆长最后把 12 套目录砍到 4 套:只保留读者最常用的书名卡、作者卡、编号卡和一套复合检索卡。查询依然飞快,管理员也不再加班。索引设计的终极原则,不是“越多越好”,而是“精准匹配查询模式,同时把写入和存储代价控制在可接受范围”——这正是索引设计原则对工程师最核心的启示。

Twrite=O(W×Nindex)\displaystyle T_{\text{write}} = O\bigl(W \times N_{\text{index}}\bigr)
写入耗时等于写入次数乘以索引个数:索引每多一个,每次写入就多一份维护成本。
  1. 写入次数 W$W$ 表示一段时间内执行的增删改操作数量,是写入负载的度量。
  2. 索引数量 $N_{\text{index}}$索引每多一棵 B+ 树,写操作就要分散到多棵树中同步更新。
  3. 乘积的放大效应二者相乘意味着索引数量翻倍,写入代价线性翻倍;如果 W 本身很大,多建几个索引就可能压垮写入性能。

AI 应用场景

电商订单管理的复合索引

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

解决的问题:订单表上亿行,用户端“查我的历史订单”接口极慢。传统做法是给每个条件列各建一个单列索引,但 WHERE 同时用 user_id 和 create_time 时,数据库只能选一个索引再用另一条件回表过滤,依然有大量随机 IO。

具体案例:某电商把 (user_id, create_time) 设为复合索引,用户详情页查询走最左前缀,P99 延迟从 2.1 秒降到 28 毫秒;再把高频统计字段组合成覆盖索引 (user_id, status, amount),让订单状态汇总完全不回表,报表任务耗时下降 80%。

日志平台的时间聚簇与写入平衡

阶段:数据写入管道与存储设计

解决的问题:AI 训练与推理平台每天产生数十亿条日志,如果给日志表的每个字段都建索引,写入吞吐会被索引维护彻底拖垮。核心矛盾是“查询要快”和“写入要顶得住”之间的平衡。

具体案例:某日志平台以时间戳自增主键构建聚簇索引,让日志按时间顺序落盘,另按时间维度做分区裁剪,只保留一个“业务 ID”二级索引。写入吞吐提升 4 倍,按小时范围查日志时通过分区裁剪加聚簇顺序读,查询依旧毫秒级。

推荐系统用户特征表的覆盖索引

阶段:特征存储与在线召回

解决的问题:推荐服务每个请求要实时读取用户的多维特征(标签、兴趣分、最近行为统计)。若每次查询都回表取整行,延迟高且浪费 IO;索引列若设计不当,特征拼接查询频繁触发全表扫描。

具体案例:某在线课程推荐平台为 (user_id, tag, score) 建覆盖索引,特征查询全部命中索引页,P99 延迟从 120ms 降到 15ms;针对超长标签字段采用前缀索引,索引体积缩小 70%,加载进内存后命中率显著提升。

Python 演示

python42 行
class SimpleIndex:    """模拟数据库索引的两种查找方式"""     def __init__(self, records):        self.records = records          # 数据行列表,模拟表        self.index = {}                 # 索引字典:键 -> 行号列表     def build_index(self, key_func):        """为某个字段建立索引,类似图书管理员做卡片目录"""        self.index.clear()        for i, row in enumerate(self.records):            k = key_func(row)            self.index.setdefault(k, []).append(i)     def scan_find(self, key, key_func):        """全表扫描:从头到尾逐行比对"""        steps = 0        result = []        for i, row in enumerate(self.records):            steps += 1            if key_func(row) == key:                result.append(row)        return result, steps     def index_find(self, key):        """索引查找:按索引卡直接定位行号桶"""        steps = 1        ids = self.index.get(key, [])        result = [self.records[i] for i in ids]        return result, steps  if __name__ == '__main__':    records = [{'order_id': i, 'user_id': i % 100, 'amount': i * 3}               for i in range(1000)]    db = SimpleIndex(records)    db.build_index(lambda r: r['user_id'])     res1, steps1 = db.scan_find(42, lambda r: r['user_id'])    res2, steps2 = db.index_find(42)    print(f'全表扫描: {steps1} 步, 找到 {len(res1)} 条')    print(f'索引查找: {steps2} 步, 找到 {len(res2)} 条')
目的:用 1000 条订单记录模拟“全表扫描”与“哈希索引查找”的步数差异,直观展示索引为何能大幅加速查询。
  1. self.index.setdefault(k, []).append(i)为每个键建立一个“行号桶”,对应图书管理员往卡片柜里插卡片。
  2. for i, row in enumerate(self.records)全表扫描的本质:无论找什么都得从头到尾逐行枚举。
  3. ids = self.index.get(key, [])索引查找通过键直接命中桶,一步拿到所有行号,等价于查卡片后直奔书位。
  4. 'amount': i * 3模拟业务表里的普通字段,与索引无关,仅让示例更贴近真实订单表。
  5. db.build_index(lambda r: r['user_id'])为 user_id 字段建立索引,索引键即“卡片排序依据”。

常见误区

误区我只要把所有查询可能用到的列都建上索引,数据库就一定会快。索引又不影响业务逻辑,多多益善,反正机器内存大。

正解索引是典型的“空间换时间”。每个索引都要占磁盘,每次增删改都要同步维护所有索引树,写入放大严重。一张表若有 10 个索引,一次 INSERT 就要更新 10 棵 B+ 树。就像图书馆为每一本书同时做 10 套目录卡——查起来快,但每次新书上架管理员都要加班到深夜。索引应该精而准,而不是多而全。

为何常见:初学时只看到索引“加速查询”的一面,忽略了写入与存储的代价;小数据量下问题不明显,数据量一大立刻暴露。

误区UUID 全局唯一、生成不依赖数据库、还能防止遍历猜 id,比自增主键“高端”多了,选它当主键肯定没错。

正解InnoDB 聚簇索引按主键物理排列数据。UUID 随机无序,新行会插到 B+ 树中间,频繁触发页分裂和页重组,产生碎片的写入延迟可能放大一个数量级。自增整型主键让新行追加到末尾,几乎不发生页分裂。若必须在分布式环境用 UUID,建议换成“雪花 ID”这类趋势递增的 64 位整型。

为何常见:UUID 在“生成唯一键”这个维度很优秀,很容易让人忽略聚簇索引对物理排列顺序的刚性依赖。

误区只要 WHERE 条件里的列都在复合索引 `(a, b, c)` 中,无论顺序和位置如何,索引都能全部用上。

正解最左前缀决定一切:必须以 a 开头连续匹配;一旦某列用了范围条件,其后的列全部失效。WHERE b = 1 AND c = 1 完全用不上;WHERE a = 1 AND c = 1 只有 a 用上索引。这就像三层卡片柜必须从最上层开始一层层翻,跳层和从中间翻都没法对号。

为何常见:把复合索引理解成了“多个单列索引的简单叠加”,没有意识到 B+ 树的层级排序结构对列顺序的强依赖。

误区`WHERE YEAR(create_time) = 2024` 或 `WHERE name LIKE '%张%'` 都是很常见的写法,数据库引擎肯定能自动优化走索引。

正解对索引列施加函数或运算后,B+ 树的有序性被“变形值”打破,优化器无法二分定位,只能退化为全表扫描。正确做法是把函数转移到常量侧,例如 create_time >= '2024-01-01' AND create_time < '2025-01-01',让索引列保持原始形态。前导通配符 LIKE '%xx' 同理,会直接让索引失效。

为何常见:SQL 写起来顺手就用了函数式条件,不知道 B+ 树需要“原始列值”才能计算比较区间。

面试题

Q1. 在没有任何索引的情况下,查找一条记录的时间复杂度是?

单选题 · easy

  • A.O(1),固定次数直接命中
  • B.O(log n),按索引逐层缩小
  • C.O(n),需要从头遍历全部行← 正确答案
  • D.O(n²),还需要排序合并

答案:C。全表扫描就是逐行比对,最坏情况要看完全部 n 行,因此是 O(n)。B 是索引查找的复杂度;A 是哈希主键点查的理想情况;D 是某些连接排序算法的复杂度。回想图书馆没有目录时,管理员只能一个书架一个书架逐本翻。

Q2. 数据库索引的主要作用是什么?

单选题 · easy

  • A.减少数据的磁盘备份次数
  • B.加快查询时定位数据行← 正确答案
  • C.直接压缩表占用的空间
  • D.让所有查询都无需加锁

答案:B。索引是额外维护的检索目录,目的是把“逐行扫描”变成“按序跳跃”,从而加快定位数据行。它不会减少备份、不压缩数据,也与并发锁机制无关。就像图书馆目录卡片只帮你快速找到书位,并不会让书变薄或让书架变干净。

Q3. InnoDB 聚簇索引下,主键应优先选择哪种?

单选题 · mid

  • A.随机 UUID 字符串
  • B.业务编号可随时修改
  • C.自增整型业务无关值← 正确答案
  • D.包含时间的复合主键

答案:C。自增顺序主键让新行总插入 B+ 树最右叶子,避免页分裂;UUID 随机插入会造成频繁页分裂和物理碎片,写入性能明显变差。比喻:新书上架按编号顺序排在末尾只需走几步,按随机编号插进去就要挪动半个书架。业务编号易变更、复合主键过长都不适合做主键。

Q4. 复合索引 (a, b, c) 遵循最左前缀,下列查询一定能完整用上该索引的是?

单选题 · mid

  • A.WHERE c = 1,不含 a b
  • B.WHERE b = 1,不含 a
  • C.WHERE a = 1 且 c = 1 无 b
  • D.WHERE a = 1 且 b = 1 且 c = 1← 正确答案

答案:D。最左前缀要求从最左列 a 开始连续匹配。D 含 a、b、c 的全部等值条件,索引三列全部生效;C 缺 b,索引只能用到 a;A、B 没有从 a 开始,索引完全用不上。类比三层卡片柜,必须从最上层开始逐层翻开,跳层则无法定位。

Q5. 下列写法中,哪个会导致该列索引失效?

单选题 · mid

  • A.WHERE age >= 18 普通范围
  • B.WHERE name LIKE '张%' 前缀匹配
  • C.WHERE YEAR(ctime) = 2024 函数套列← 正确答案
  • D.WHERE status IN (1, 2, 3) IN 列表

答案:C。对索引列套函数 YEAR(ctime),B+ 树的有序性被破坏,优化器无法二分定位,只能全表扫描后逐行计算。A 是正常范围查询、B 是前缀匹配、D 的 IN 列表相当于多个等值点查,都能走索引。类比:管理员把书名改成大写缩写,再去按拼音排序的卡片柜里找,当然对不上号。

Q6. 覆盖索引(covering index)最大的收益是什么?

单选题 · mid

  • A.减少回表取数据行的随机 IO← 正确答案
  • B.让索引不再占用额外存储
  • C.免去主键本身的创建过程
  • D.让写入操作完全跳过索引

答案:A。普通索引查到主键后还要回聚簇索引取整行(回表),产生大量随机 IO。覆盖索引让查询所需的列全部包含在索引页中,无需回表即可返回结果。B 错在索引必然占用空间,C 错在主键天然存在,D 错在每次写入仍需维护索引树。

Q7. 某列有 10000 个不同值,取前 10 个字符后仅剩 200 个不同值,该前缀索引的选择性是多少?

单选题 · hard

  • A.0.002,约等于千分之二
  • B.0.020,即两百分之一← 正确答案
  • C.0.200,即五分之一
  • D.0.980,接近全部保留

答案:B。选择性 = 前缀去重数 / 全列去重数 = 200 / 10000 = 0.02,即 2%。0.002 是 20/10000,0.2 是 2000/10000,0.98 则是 9800/10000。选择性越接近 1 越适合建前缀索引;本例 0.02 区分度很差,说明前缀长度过短,需要增加长度。

Q8. 索引下推(Index Condition Pushdown)的作用是什么?

单选题 · hard

  • A.把条件判断下推到索引层过滤← 正确答案
  • B.把整张表直接搬到内存中
  • C.让优化器自动增加所需索引
  • D.将范围查询转为等值查询

答案:A。MySQL 5.6+ 的索引下推让存储引擎在扫描索引时先用 WHERE 中的部分条件过滤,减少回表行数和服务层处理量。B 是缓冲池/内存表的行为,C 是索引建议工具,D 不是 ICP 的功能。类比:管理员在卡片柜前就排除掉不符合主题的卡片,而不是抱出一摞书再慢慢挑。

Q9. 以下哪些情况适合为列创建索引?

多选题 · mid

  • A.频繁出现在 WHERE 等值条件中← 正确答案
  • B.某列取值只有男女两种
  • C.经常用于 ORDER BY 排序的列← 正确答案
  • D.一张只有几十行的配置表

答案:AC。A 的高频等值查询和 C 的排序列都能从索引中明显获益。B 选择性太低,索引可能返回一半行,不如全表扫描;D 表行数极少,全表扫描比读索引页更快,建索引纯属浪费。类比:只有两类书的分类,管理员一眼就能数完,不需要再做卡片柜。

Q10. 关于复合索引 (a, b, c),下列说法正确的有?

多选题 · hard

  • A.相当于建了 (a)、(a,b)、(a,b,c) 三个索引← 正确答案
  • B.只要含 c 的查询就能用上全部索引
  • C.含 a 和 b 范围时 c 无法用于过滤← 正确答案
  • D.与 (b, a, c) 索引效果完全相同

答案:AC。A 是最左前缀的经典推论:复合索引隐含支持所有前缀索引。C 正确:a 等值让 b 可用于范围,但 b 范围之后 c 的排序信息被破坏,无法继续过滤。B 错在必须从 a 开始匹配;D 错在列顺序决定 B+ 树排序结构,(b, a, c) 要求查询从 b 开始。类比:三层卡片柜每层排序方式不同,决定了它只能支持某几种“逐层查找”路径。