想象你走进一座巨大的图书馆——它有上百层、每层几十个书架,藏书千万册。如果管理员每次找书都顺着书架一本本翻,这座图书馆根本无法运转。所以图书馆在入口放了一本“分类导览册”:想找《红楼梦》,先翻到“文学区”,再定位到“中国古典小说”书架,3 步之内就能站到正确的书架前。MySQL 里的 B+树索引,就是数据库这座图书馆的“分类导览册”。 这本导览册有个精妙设计:它不存书的正文,只存“路标”——每一层只告诉你下一层往哪走,真正的书全部整整齐齐排在最后一层“书架”上,而且同一层书架之间有通道相连,可以从第一本一路扫到最后一本。正因如此,索引既快又能做范围查询。 但索引不是万能的。按名字查一本书,导览册能快速带你到位;可如果导览册上没印书的封面信息,你还得亲手走到书架前翻开书——这就是“回表”。聪明的做法是让导览册直接印上你关心的字段,这就是“覆盖索引”。如果导览册同时按“分类→作者→书名”三级编排,想查“所有鲁迅的书”只需用到前两级……这恰恰是联合索引“最左匹配”的由来。今天就借着这座图书馆,把 B+树索引与覆盖索引一次讲清。

岗位关联

理解 B+树索引的底层逻辑,是写出高效 SQL 的分水岭——让你从“会写查询”升级到“会设计索引”,在面试和业务调优中都能直击要点。

目录
  1. 可视化演示
  2. B+树结构
  3. 选B+树原因
  4. 索引类型
  5. 覆盖索引
  6. 最左匹配
  7. 索引下推
  8. 页分裂影响
  9. 索引设计实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示B+树索引结构、查找路径、覆盖索引免回表与回表查询的完整流程,交互切换不同模式观察索引查找与数据检索的差异。

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

B+树索引结构 根节点 叶子节点 覆盖索引 O(log n) 点击步骤切换模式

B+树结构

你走进图书馆,门口服务台放着一本《全馆导览册》。它不像藏书目录那样抄一遍每本书的内容,而是按“楼层→区域→书架”逐层指引:第一页只有几个大字“文学区 / 科学区 / 历史区”,翻到“文学区”这一章,出现“小说 / 散文 / 诗歌”的子路标,再往下才是成排的具体书架号。数据库里的 B+树索引,就是这本层层递进的导览册。

与技术对应:B+树是一棵多叉平衡树,非叶子节点只保存“键值 + 指向子节点的指针”,相当于路标;叶子节点才保存真实数据,相当于书架上排列的书。最妙的是所有叶子节点通过指针串成一条链表——图书馆同一层书架之间有通道相连,读者可以从第一本一路扫到最后一本。

这带来一个惊人效果:由于每个节点能容纳上千个孩子(InnoDB 默认页 16KB,一个键加指针约 14 字节,扇出轻松破千),三层 B+树就能索引上千亿条记录。真实数据库表数据上亿,树高通常只有 3~4 层,查询任何一条记录都只需走这么几级。

所以哪怕藏书千万册,读者永远只需要翻 3 次导览页;数据库也如此,无论表多大,走主键索引查一条记录,磁盘 IO 次数恒定且极低。

N≈bh=10003=109N \approx b^{h} = 1000^{3} = 10^{9}
树能索引的记录总数约等于“每个节点孩子数”的“层数”次方。扇出 1000、高 3 层,足够存放十亿条记录,也就是读者最多翻 3 次导览页就能站到正确的书架前。
  1. 扇出 b每个节点最多能指引多少个孩子,类似导览册一页能印多少个分类路标(约 1000)。
    b≈1000b \approx 1000
  2. 树高 h从根节点到叶子节点需要经过的层数,导览册从总目录翻到书架页码要翻几页。
    h=3h = 3
  3. 总记录数 N整棵 B+树能索引的记录上限,对应图书馆的藏书量上限。
    N=bhN = b^{h}
  4. 实例换算1000 的 3 次方等于 10 亿,这就是“三层即十亿”的由来。
    10003=1091000^{3} = 10^{9}

选B+树原因

有了导览册,为什么不用更简单的方案?比如把全馆书单打在一张超长纸上(线性扫描),或者请一位记性极好的门卫,你问他《红楼梦》在哪,他 0.1 秒报出位置(哈希表)。超长纸查“文学区所有书”要顺着纸从头扫到尾;门卫只会精确报位置,你问他“文艺复兴时期所有书”,他就懵了。

数据库选 B+树而非其他结构,本质是磁盘 IO 太贵。机械硬盘一次随机读约 10ms,而内存访问约 100ns,差五个数量级。如果把数据组织成二叉树,2 亿条记录需要约 28 层,意味着 28 次随机磁盘 IO,慢到无法接受;B+树扇出上千,同样数据 3 层即可。

相比 B 树,B+树非叶子节点不存数据,同样页能容纳更多路标,树更矮;且叶子链表天然支持范围查询与排序扫描。相比哈希索引,B+树能处理“大于、小于、BETWEEN、LIKE 前缀”这类范围条件。数据库读多写少、查询复杂,B+树是综合最优解。

图书馆最终选择了“层层路标的导览册”而非“记性超好的门卫”,因为它既能精确指路,又能按区域翻页扫描——这正是数据库查询最常见的两种姿势:点查与范围查。

T=O(log⁡bN) 次磁盘 IOT = O(\log_{b}N) \text{ 次磁盘 IO}
查询耗时正比于以扇出 b 为底、记录总数 N 的对数;对 10 亿条记录来说就是 3 次随机读,而不是 28 次。
  1. T完成一次点查所需的磁盘 IO 次数,也就是读者从入口走到目标书架翻导览的次数。
    TT
  2. 对数项以 b 为底 N 的对数,表示树的高度;b 越大树越矮,IO 越少。
    log⁡bN\log_{b}N
  3. 代入实例b=1000、N=10^9 时高度为 3,即 3 次随机磁盘 IO。
    log⁡1000109=3\log_{1000}10^{9}=3

索引类型

同样是查找,图书馆有两种组织方式。聚簇索引像“书脊上直接印着编号、书就按编号排满书架”——找到编号就是找到了书,书和编号不可分离。非聚簇索引则像服务台那盒独立索引卡,每张卡只写“书名→书架号”,你要先摸到卡片,再走到书架把书抽出来。

InnoDB 表必然有一个聚簇索引:若定义了主键,主键就是聚簇索引,其叶子节点存整行数据;没有主键则选第一个非空唯一索引;再没有则隐藏生成 rowid。除聚簇索引外,其他索引都叫二级索引(非聚簇索引),叶子节点只存索引列和主键值。

因此用二级索引查询通常要两步:第一步走二级索引拿到主键,第二步拿着主键回聚簇索引取整行——这就是“回表”。回表一次就是一次随机磁盘 IO。如果查询的列恰好都在二级索引里,第二步可以省略,这就是下一节的主角。

聚簇索引是“书与编号一体”的自助书架,二级索引是“先抄卡片再取书”的索引卡抽屉——懂得二者区别,才能理解为什么覆盖索引能省掉一半路程。

R=N−KR = N - K
回表次数 R 等于查询命中的总行数 N 减去索引页直接能提供的行数 K。当 K=N 时 R=0,即无需回表。
  1. NWHERE 条件在二级索引上命中的记录总数,对应导览卡筛出的候选书架数量。
    NN
  2. K二级索引页本身就能返回的列数量(行),也就是卡上已印好的信息。
    KK
  3. R还需要回聚簇索引取数据的次数,每次对应一次额外磁盘随机读。
    R=N−KR = N - K

覆盖索引

假如导览卡上除了“书架号”,还把“书名、作者、出版年”都印了出来,而你想查的恰好就是这三项——那就不必走到书架前翻开书了,站在服务台抄卡片即可。这张“印得全”的索引卡就是覆盖索引。

覆盖索引不是一种独立的索引类型,而是一种使用状态:当一条 SELECT 语句要返回的所有列都包含在某个二级索引的键列中时,InnoDB 可以只扫描索引页就完成查询,完全不需要回表。比如有索引 (name, age),执行 SELECT name, age FROM t WHERE name='alice',引擎在索引页上就拿到了全部答案。

把高频查询涉及的列加入索引键,就能把查询从“索引加回表”降级为“纯索引扫描”。代价是索引体积变大、写入变慢。因此实践中一般只把 2~3 个高频列放进覆盖索引,而不是把整张表都塞进去。EXPLAIN 中 Extra 出现 "Using index" 即为覆盖索引生效。

覆盖索引的智慧在于“让导览卡多说一点,读者就少跑一点”——但卡片也不是越厚越好,这背后永远是查询加速与写入成本的平衡。

C⊆I⇒R=0C \subseteq I \Rightarrow R = 0
当查询需要的字段集合 C 被索引字段集合 I 完全包含时,回表次数 R 必然为 0,查询只需访问索引页。
  1. 集合 CSELECT 子句需要返回的列集合,可理解为读者抄在便签上的信息需求。
    C={name, age}C = \{\text{name, age}\}
  2. 集合 I当前索引包含的键列集合,也就是导览卡上印的全部字段。
    I={name, age}I = \{\text{name, age}\}
  3. 子集关系C 完全包含在 I 内,说明卡上的信息已覆盖需求。
    C⊆IC \subseteq I
  4. 回表数为 0所有需求都在索引页满足,无需再走聚簇索引。
    R=0R = 0

最左匹配

导览册按“大类→中类→小类”三级编排,读者报出“文学区→小说→《红楼梦》”三级信息,管理员 3 步定位;只报“小说→《红楼梦》”而漏掉大类,管理员就只能一页页翻遍整本册子;报“《红楼梦》→小说→文学区”顺序颠倒,优化器贴心地把顺序理顺后照样能用。

联合索引 (a,b,c) 的定位能力遵循最左前缀原则:只有从第一列开始、连续若干列出现在查询条件中,索引才能用于精确定位。可用的前缀组合有 (a)、(a,b)、(a,b,c)。如果跳过 a 直接查 b、c,索引无法用于定位。此外,遇到范围条件(>、<、BETWEEN、LIKE 前缀)后,后续列也无法继续精确定位。

MySQL 8.0 的优化器会主动调整 WHERE 中条件的顺序(如把 b=2 AND a=1 重写为 a=1 AND b=2),所以“顺序写反”通常不影响;但“缺失最左列”和“范围中断”是硬伤。设计联合索引时,应把等值查询列放前面、范围查询列放后面,最大化可用前缀。

用联合索引就像向管理员报三级分类,务必从第一级开始报;最左匹配不是玄学,而是“导览册逐级翻页”这个物理动作的数学表达。

L=max⁡{k∣a1,…,ak∈WHERE}L = \max\left\{k \mid a_1,\dots,a_k \in \text{WHERE}\right\}
联合索引可用的前缀长度 L,等于“从第一列开始连续出现在 WHERE 条件中的最大列数”。L=0 表示索引无法用于定位。
  1. 第 i 列 a_i联合索引的第 i 列,对应导览册第 i 级分类(大类、中类、小类)。
    a1=文学区, a2=小说, a3=书名a_1 = \text{文学区},\ a_2 = \text{小说},\ a_3 = \text{书名}
  2. 连续出现条件中必须包含从第 1 列开始的连续列,中间缺一列就断档。
    a1, a2∈WHERE, a3∉WHERE⇒L=2a_1,\ a_2 \in \text{WHERE},\ a_3 \notin \text{WHERE} \Rightarrow L=2
  3. L 的含义可用前缀长度,L 越大,索引圈定的范围越精确。
    L=max⁡{k∣a1,…,ak∈WHERE}L = \max\{k \mid a_1,\dots,a_k \in \text{WHERE}\}

索引下推

老式图书馆里,管理员按“文学区→鲁迅”筛出一百个书架号,把一百张便签塞给你,让你自己去每个书架确认哪本出版于 1930 年以前——你在一百个书架间来回穿梭。新式图书馆把“出版年份”也列在导览卡上,管理员在服务台先划掉不符合年份的,只给你二十张便签。这就是索引下推(ICP)。

MySQL 5.6 引入 Index Condition Pushdown。没有 ICP 时,存储引擎遍历二级索引拿到每行的主键后立即回表,把完整行交给 Server 层再做 WHERE 过滤。有了 ICP,存储引擎在遍历索引的过程中,直接用索引里已有的列判断部分 WHERE 条件,不满足的直接跳过,减少了回表次数。

ICP 对联合索引尤其有效:比如索引 (name, age),查询 WHERE name LIKE '张%' AND age>30,name 的 LIKE 前缀能定位,age 的条件无法用于定位;但没有 ICP 时,所有“张”开头的人都要回表后再过滤年龄。开启 ICP 后,age 在索引页上就被过滤掉,回表量大幅下降。EXPLAIN 中 Extra 出现 "Using index condition" 即 ICP 生效。

让最了解索引卡的人(存储引擎)在服务台多筛一轮,读者跑的路自然就少了——索引下推就是这么朴素的“前置过滤”智慧。

Nafter=(1−f)×NbeforeN_{\text{after}} = (1 - f) \times N_{\text{before}}
下推后回表次数等于 1 减去索引层过滤比例 f,再乘以下推前的回表次数。过滤比例越高,节省越多。
  1. N_before不使用 ICP 时,需要回表的行数(所有通过前缀定位的行)。
    Nbefore=100N_{\text{before}} = 100
  2. 过滤比例 f索引层能直接过滤掉的行数占比,比如 80% 的行因 age 条件被划掉。
    f=0.8f = 0.8
  3. N_after启用 ICP 后实际回表的行数,只剩 20 行需要取书。
    Nafter=(1−0.8)×100=20N_{\text{after}} = (1-0.8)\times100 = 20

页分裂影响

图书馆的书架按编号严丝合缝排满。新采购的书要插进“文学区”中间的空缺,管理员发现这个书架已经满了,只好新搬来一个书架,把原有书架一半的书挪过去,再在楼层导览图上新增一条“书架 3B”的路标。挪书期间,读者经过这片区域总要绕几步——这就是页分裂。

B+树以页(默认 16KB)为存储单位,页满后插入新键就要分裂:分配一个新页,把原页约一半的键记录迁移过去,再在父节点插入新的路标键。若父节点也满了,分裂会向上递归,甚至导致树高增加一层。

页分裂的代价是实打实的写入放大:一次逻辑插入可能牵动两次物理写和一次父节点更新。并且分裂后两页利用率都只有约 50%,后续插入若再次触发分裂,会产生大量碎片页。实践中用自增主键让插入集中在最右页,可以显著降低分裂频率;而随机主键(如 UUID)会导致插入散布全树,分裂频繁。

自增主键像“只在最后一排书架加书”,顺序紧凑极少搬架;随机主键则像“随机往每个塞满的书架里塞书”,管理员天天都在腾架子。

usplit≈ufull2u_{\text{split}} \approx \frac{u_{\text{full}}}{2}
页分裂后,每个新页的平均利用率约为分裂前的一半。若分裂前页几乎写满(利用率接近 100%),分裂后两页各约 50%。
  1. u_full分裂前数据页的填充率,接近 1 表示书架已排满。
    ufull≈1u_{\text{full}} \approx 1
  2. u_split分裂后单个页的填充率,原书被分成两架。
    usplit≈0.5u_{\text{split}} \approx 0.5
  3. 利用率减半一半空间暂时空置,等待后续新书填充,写放大与碎片由此而来。
    usplit=ufull2u_{\text{split}} = \frac{u_{\text{full}}}{2}

索引设计实践

优秀的图书馆管理员不会给每本书都印一张专属索引卡,而是做三件事:统计哪些查询最频繁(高频优先)、检查卡片厚度是否失控(控制冗余)、定期抽查管理员翻卡速度(EXPLAIN 体检)。

落到数据库设计上,就是三条原则:第一,为高频查询建覆盖索引,让 SELECT 列尽量落在索引里;第二,避免冗余索引,比如已有索引 (a,b) 时,再建 (a) 就高度冗余,因为 (a,b) 的前缀已覆盖 (a);第三,用 EXPLAIN 检查 type、key、rows、Extra,判断索引是否真正生效。

选择索引列时还要看选择性——某列不同值的数量占总行数的比例。主键的选择性是 1,性别列的选择性可能只有百万分之二,低选择性列放联合索引前列意义不大。实践中联合索引的设计公式是“过滤强度大的等值列前置 + 范围列后置 + 附加覆盖列收尾”,并注意 WHERE 与 ORDER BY 的配合避免 filesort。

数据库索引设计没有银弹,只有“用导览册思维做统计、做取舍、做体检”——把每一次查询都想象成读者翻导览页的次数,优化方向自然清晰。

S=DNS = \frac{D}{N}
索引列的选择性 S 等于该列不同值数量 D 除以总行数 N。S 越接近 1,该列过滤效果越好,越适合放在联合索引前面。
  1. 不同值数量 D索引列不同取值的个数,主键每行不同所以 D=N。
    D=107 (主键)D = 10^{7}\ \text{(主键)}
  2. 总行数 N表的总行数,对应图书馆总藏书量。
    N=107N = 10^{7}
  3. 实例对比主键选择性=1,性别列选择性=2/1000 万≈0.0000002,高下立判。
    S=2107≈0.0000002S = \frac{2}{10^{7}} \approx 0.0000002

AI 应用场景

慢查询分析与覆盖索引改造

阶段:数据查询优化

解决的问题:传统方式是 DBA 凭经验加索引,常常加错列或用不上;AI 辅助工具通过分析慢查询日志和索引使用统计,自动推荐覆盖索引组合。

具体案例:某电商平台 MySQL 频繁出现全表扫描,平均查询耗时 380ms。使用性能分析工具扫描慢查询,识别出高频模式 WHERE user_id=? AND status=? ORDER BY created_at DESC,创建联合索引 (user_id, status, created_at),查询降到 6ms,且 EXPLAIN 显示 Using index。

订单列表深分页优化

阶段:推理阶段查询优化

解决的问题:LIMIT 100000, 20 这类深分页,传统方式要扫描并丢弃前面 10 万行;用覆盖索引先取主键再回表,只访问需要的 20 行。

具体案例:BI 报表系统导出百万订单时,页深越翻越慢。使用覆盖索引 (status, created_at, id) 先查出目标页的 id 列表,再 JOIN 原表取完整行,翻页时间从 4 秒降到 80ms。

自动化索引推荐系统

阶段:规则引擎与统计分析

解决的问题:人工设计索引依赖经验,字段顺序错了索引就失效;工具基于查询日志统计最左匹配命中率,自动生成联合索引建议。

具体案例:开源工具 pt-index-usage 或云数据库智能调优功能,扫描审计日志,按查询模式聚类,输出“建议创建 idx(tenant_id, biz_type, created_at)”这类报告,并估算节省的回表 IO 次数。

Python 演示

python44 行
class MiniDb:    """迷你数据库:模拟聚簇索引与二级索引的IO差异"""     def __init__(self, rows):        self.clustered = {r["id"]: r for r in rows}        self.idx_name = {r["name"]: r["id"] for r in rows}        self.cover_name_age = {r["name"]: (r["name"], r["age"]) for r in rows}        self.io = 0     def _page(self):        self.io += 1     def q_noncover(self, name):        self._page()                      # 读二级索引页        pk = self.idx_name.get(name)        if pk is None:            return None        self._page()                      # 回表:再读聚簇索引页        row = self.clustered[pk]        return (row["name"], row["age"])     def q_cover(self, name):        self._page()                      # 只读覆盖索引页        return self.cover_name_age.get(name)     def compare(self, name):        self.io = 0        a = self.q_noncover(name)        io_before = self.io        self.io = 0        b = self.q_cover(name)        io_after = self.io        return a, b, io_before, io_after  rows = [    {"id": 1, "name": "alice", "age": 30},    {"id": 2, "name": "bob", "age": 25},    {"id": 3, "name": "carol", "age": 28},]db = MiniDb(rows)a, b, io1, io2 = db.compare("alice")print("非覆盖结果:", a, "IO次数:", io1)print("覆盖结果:", b, "IO次数:", io2)
目的:用一个迷你数据库模拟 InnoDB 的聚簇索引与二级索引行为,对比“普通二级索引+回表”和“覆盖索引”两种查询的 IO 次数差异。
  1. self.clustered = {r["id"]: r for r in rows}模拟聚簇索引,主键直接映射整行数据,相当于“书脊即编号”。
  2. self.idx_name = {r["name"]: r["id"] for r in rows}模拟普通二级索引,只存“名字→主键”,相当于只写书架号的索引卡。
  3. self.cover_name_age = {r["name"]: (r["name"], r["age"]) for r in rows}模拟覆盖索引,卡上直接印了查询需要的 name 和 age 两列。
  4. pk = self.idx_name.get(name)走二级索引先拿到主键,这是回表前的第一步。
  5. row = self.clustered[pk]回表动作:拿着主键回聚簇索引取整行,对应绕道书架抽书。
  6. self._page()每次读页 IO 计数,直观看到非覆盖查询多了一次回表 IO。
  7. db.compare("alice")对比同一查询在两种索引策略下的 IO 次数差异。

常见误区

误区“反正查询慢就加索引,索引多了查询总该快了吧?我把常用字段都建上索引,表也不大,应该没坏处。”

正解索引是典型的“空间换时间”,每一棵 B+树都会占用磁盘、拖慢 INSERT/UPDATE/DELETE——每写一行要同步维护所有索引树,页分裂还会放大写入。索引就像图书馆的导览册,书架上每新增一本书,服务台所有导览卡都要补一条,卡片太多抄写就慢。

为何常见:查询变慢的痛点太直观,容易忽略写入侧和维护成本;且小表上索引代价不明显,误导人以为免费。

误区“联合索引是 (a,b,c),那我查询只带了 a 和 b,不带 c,索引应该用不上吧?”

正解最左前缀指“从第一列开始连续取前缀”,(a,b) 是完全合法的前缀,只有 (a,c) 或 (b,c) 这种跳过中间列的才残缺。所谓“最左”强调的是不能跳过第一列,而不是必须用全所有列。

为何常见:名字里的“最左”容易让人联想到“必须左到右全部用上”,加上网上流传的简化口诀,容易误解。

误区“覆盖索引既然能免回表,那我建一个包含全部列的大索引,查询全走索引不是最快?”

正解索引列越多,页能容纳的路标越少,树高可能上升、缓存命中率下降;写入时每列都要维护,页分裂概率大增。真正做法是只为高频查询的固定少数列建覆盖索引,普通查询仍靠聚簇索引取整行。

为何常见:覆盖索引的“零回表”效果太诱人,容易忽视它同样有体积和写入成本,导致反向优化。

误区“回表不就是多查一次主键吗?数据库很快的,多一次应该没啥。”

正解回表是随机磁盘 IO,一次约 10ms 量级;若二级索引命中了 1 万行,就要回表 1 万次,累计可达上百秒。页大小 16KB 一次 IO 通常只能取回一行,无法批量。所以覆盖索引和 ICP 的优化空间往往比想象中大得多。

为何常见:人脑对“一次”不敏感,但数据库里“一次”是每行一次;没有把行数乘进去的直觉。

面试题

Q1. B+树非叶子节点主要存储什么?

单选题 · easy

  • A.完整的数据行
  • B.键值和子节点指针← 正确答案
  • C.表结构定义信息
  • D.数据行的物理备份

答案:B。非叶子节点只存“路标”——键值和指向子节点的指针,相当于导览册的目录页;真正数据都在叶子节点。A 是聚簇索引叶子的职责,C、D 与索引页无关。

Q2. B+树叶子节点之间的链表结构主要服务于什么?

单选题 · easy

  • A.树形父子引用
  • B.双向循环链表
  • C.范围查询顺序扫描← 正确答案
  • D.哈希冲突链

答案:C。叶子链表让数据库可以从第一本“书”一路扫到最后一本,范围查询和排序扫描因此高效。A 描述的是层级关系,B 说双向不准确(通常单链),D 是哈希结构。

Q3. 覆盖索引能避免的最核心开销是什么?

单选题 · easy

  • A.日志写入开销
  • B.回表查询开销← 正确答案
  • C.事务锁等待开销
  • D.连接线程切换开销

答案:B。覆盖索引让查询所需列全部在索引页上,自然无需回聚簇索引取整行。回表是随机磁盘 IO,代价远高于日志顺序写,也因此成为覆盖索引优化的核心目标。

Q4. 联合索引建立在 (a,b,c) 上,查询 WHERE a=1 AND c=3 时,正确的说法是?

单选题 · mid

  • A.a 和 c 都能定位
  • B.只有 a 能用于定位← 正确答案
  • C.只有 c 能用于定位
  • D.整个索引完全失效

答案:B。最左匹配要求从第一列开始的连续前缀。WHERE 含 a 和 c,但少了 b,c 无法用于精确定位;只有 a 能参与定位。这就像导览册报了大类和小类却漏了中类,小类无法直接定位。

Q5. 扇出为 1000、高度为 3 的 B+树大约能索引多少条记录?

单选题 · mid

  • A.十万条级别
  • B.百万条级别
  • C.十亿条级别← 正确答案
  • D.万亿条级别

答案:C。容量 N = b^h = 1000^3 = 10^9,即十亿条。这就是“三层即十亿”的由来——哪怕数据量大到千亿,树高通常也不超过 4 层,查询 IO 次数几乎恒定。

Q6. InnoDB 聚簇索引的叶子节点存放的是什么?

单选题 · mid

  • A.主键值的完整副本
  • B.该行的全部字段数据← 正确答案
  • C.指向数据文件的指针
  • D.所有二级索引的汇总

答案:B。聚簇索引的叶子就是数据行本身,找到主键就拿到了整行,对应“书脊即书”。二级索引叶子只存主键值,需要回表才能拿到完整行。

Q7. 关于 MySQL 索引下推(ICP)的正确说法是?

单选题 · hard

  • A.将排序下推到应用层
  • B.遍历索引时过滤部分WHERE条件← 正确答案
  • C.把整表数据缓存到内存
  • D.仅适用于覆盖索引的场景

答案:B。ICP 让存储引擎在遍历二级索引时,直接用索引列判断部分 WHERE 条件,不满足的记录直接跳过,减少回表。它不要求覆盖索引,Extra 显示 "Using index condition"。就像管理员在服务台先筛掉不合适的便签再让你跑腿。

Q8. 联合索引建立在 (a,b,c) 上,下列哪些查询组合能完整吃到最左前缀?

多选题 · mid

  • A.条件包含 a 和 b← 正确答案
  • B.条件包含 a 和 c
  • C.条件包含 b 和 c
  • D.条件只包含 a← 正确答案

答案:AD。完整吃到前缀要求从第一列开始连续取列。A 取 a、b 两列是合法前缀;D 只取 a 是单列前缀也完整;B 跳过了 b,c 无法定位;C 跳过了 a,整个索引无法用于定位。最左匹配的本质是逐级翻导览册,不能跳级。

Q9. 以下哪些场景适合使用覆盖索引?

多选题 · mid

  • A.高频查询只取固定列← 正确答案
  • B.查询必须返回所有列
  • C.报表固定列组合统计← 正确答案
  • D.写入频繁且列动态变化

答案:AC。覆盖索引要求 SELECT 列全部落在索引键中,适合“固定少数列 + 高频查询”的场景。SELECT * 需要全部列,覆盖索引会变成巨大宽索引,本末倒置;写入频繁且列动态变化时,维护大索引的代价远超收益。

Q10. 关于 B+树页分裂的影响,正确的有哪些?

多选题 · hard

  • A.页满插入会触发分裂← 正确答案
  • B.分裂后利用率近似减半← 正确答案
  • C.顺序主键可减少分裂← 正确答案
  • D.页分裂只影响查询速度

答案:ABC。页满后插入新键必然触发分裂,分裂后两页各约 50% 利用率,产生写入放大和碎片,因此同时影响写性能、空间利用率和缓存命中率,并非只影响查询。自增主键让插入集中在最右页,显著降低分裂频率,对应“只在最后一排书架加书”。