从前有座藏书百万的图书馆。书库里的书按索书号(主键)整整齐齐排着,每本书的正文都“长”在书架上——这就是 MySQL 的聚簇索引。读者查资料,先在前台翻目录卡片柜:每张卡片只记几个关键标注,外加一句“该书在第几排第几格”。拿着卡片进书库把书抱出来翻看,这个动作在数据库里叫回表。新来的图书管理员小M发现,很多读者问的问题,卡片上早就印着答案。比如“《数据库》作者是谁”,卡片正面就写着,何必进书库?她于是整理出一批“答案随身卡”——这就是覆盖索引。更高明的是,她还学会了进书库前先站在前台,把所有不相关的卡片提前抽掉,只抱真正需要的书——这招正是索引下推(ICP)。这篇笔记,就是小M的管理心得。

岗位关联

覆盖索引与索引下推是 SQL 调优的“读心术”:学会后你能一眼看穿 EXPLAIN 里的 Using index 与 Using index condition,把高频慢查询从秒级压到毫秒级,成为后端团队里最懂索引的人。

目录
  1. 可视化演示
  2. 回表问题根源
  3. 覆盖索引原理
  4. 覆盖索引设计
  5. 索引下推原理
  6. 触发与失效场景
  7. 性能收益量化
  8. 联合索引实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

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

覆盖索引:零回表查询 查询所需字段全部包含在索引中,无需回表访问聚集索引 Extra字段显示Using index即为覆盖索引生效标志 联合索引(city,age,name)可覆盖SELECT city,age,name查询 覆盖索引消除回表,查询仅扫描索引树

回表问题根源

把小M的图书馆放大一万倍来看:书库的每一排书架都贴着索书号(主键),书架上每一本书就是数据库里的一行完整记录。InnoDB 把“主键+整行数据”捆在一起存放,这套结构叫聚簇索引——书和位置永远不分离,主键就是那本书唯一的索书号。

前台那一人多高的目录卡片柜,则是二级索引。每张卡片(索引项)只记录两样东西:你关心的关键词(索引列的值),以及“这本书的索书号是多少”(主键值)。卡片本身很轻,缺点是信息不全——想知道书里第47页写了什么,就必须拿着索书号走进书库把书抱出来翻。

这个“进书库取书”的动作就是回表。SQL 写成 SELECT * FROM t WHERE name='张三',先顺着 name 二级索引找到符合条件的卡片,再拿着每张卡片上的主键去聚簇索引取完整行。每取一行,就发生一次随机 IO:机械硬盘时代是一次磁盘寻道,即便在 SSD 上,随机读也比顺序读慢一个数量级。命中的行数越多,这笔账就越吓人。

小M看着卡片柜上厚厚的借阅单发愁:查一次“北京”的书目,几百本书就要跑几百趟书库。她意识到,每次跑腿都是一次随机 IO——这就是回表问题的根源。只要能把“跑腿”变成“在家翻卡片”,就能省下大把时间。

C=Tidx+N×TrowC = T_{\text{idx}} + N \times T_{\text{row}}
这条公式告诉我们:一次二级索引查询的总开销 = 索引扫描成本 + 命中的行数 × 单次回表成本。行数 N 是放大器,只要它能变小,总开销就会线性下降。
  1. 总代价 C一次二级索引查询的总时间开销,等于索引扫描与回表两段之和。
    C=Tidx+N×TrowC = T_{\text{idx}} + N \times T_{\text{row}}
  2. 索引扫描 T_idx只在二级索引(目录卡片)里顺序扫描匹配项的时间,通常很快。
    TidxT_{\text{idx}}
  3. 回表行数 NWHERE 条件在二级索引中命中的记录数,每条都要去书库取一次。
    N×TrowN \times T_{\text{row}}
  4. 单次回表耗时 T_row按主键做一次随机 IO,去聚簇索引取一行数据的时间。

覆盖索引原理

有一天,小M发现图书证的背面印着一张“热门问题速查表”:书名、作者、出版年份、在哪个区域——十来个高频问题的答案全在这张小卡上。读者问“《数据库》出版年份是哪年”,小M瞄一眼速查表就能回答,完全不用进书库。

这张“速查表”就是覆盖索引。当 SELECT 要查询的所有列,恰好全部出现在某个二级索引的索引项中时,MySQL 只需要扫描二级索引,直接从索引叶子节点取数据返回,根本不用拿着主键去聚簇索引回表。EXPLAIN 里这一招的标志是 Using index。

判断是否覆盖,就看“查询列集合”与“索引列集合”的关系。比如联合索引 (name, age, city),查询是 SELECT name, age FROM t WHERE city='北京':city 条件用得上索引,name 和 age 又都在索引里,那么这次查询只需要遍历二级索引,零回表。注意 SELECT * 永远无法覆盖——星号意味着要全部列,而任何索引都不可能恰好装下整张表的列。

小M把速查表越做越全,读者问得最多的问题几乎都能当场答出。同样,你的数据库如果给高频查询配一张“答案速查卡”——覆盖索引,查询引擎也能潇洒地摊摊手说:答案就在索引里,书库(聚簇索引)根本不用去。

{c1,c2,…,ck}⊆I\{c_1, c_2, \ldots, c_k\} \subseteq I
“查询所需的每一列”都属于“索引拥有的列”,覆盖索引就成立了。空手出门,满载而归,全程不踏进书库一步。
  1. 查询列集合SELECT 要返回的每一列,加上 WHERE 里参与条件判定的列,组成一个集合。
    {c1,c2,…,ck}\{c_1, c_2, \ldots, c_k\}
  2. 索引列集合 I某个二级索引上所有列的集合。
    II
  3. 子集关系查询列全部落在索引列范围内,该索引就成为覆盖索引。
    ⊆\subseteq

覆盖索引设计

速查表也不能无限加——每加一列,卡片就要做大一圈。小M把最常被问到的问题排了优先级:先放“姓名”,再放“年龄”,最后补上“城市”。这就像设计联合索引:列的顺序,决定了哪些查询能享受“免跑腿”待遇。

联合索引遵循最左前缀原则:索引 (a,b,c) 能高效匹配 (a)、(a,b)、(a,b,c) 三种查询,但用不上以 (b) 或 (c) 开头的查询。MySQL 的索引是复合键字典序排列,跳过最左列等于撕掉了字典的部首索引,后面的字根本没法查。设计覆盖索引时,要把等值条件的列放最前,范围条件居中,最后放“只是为了拿答案”的覆盖列。

假设高频查询是 WHERE city=? AND age>? ORDER BY create_time,返回 name。一个合理的联合索引可以是 (city, age, create_time, name):city 承担前导等值匹配,age 做范围过滤,create_time 避免额外排序,name 作为冗余列保证覆盖。注意,冗余列不是越多越好——索引膨胀会让写入和更新变慢,每一次 INSERT/UPDATE 都要同步改索引,就像速查卡每改一个数据就要重印一版。

就像小M先问清“读者最常问什么”再排版速查表,设计覆盖索引也必须从真实 SQL 出发:分析高频查询的 WHERE 列、排序列、返回列,按等值→范围→排序→覆盖的顺序排布,才能让索引既是“导航仪”又是“答案卡”,而不是一本没人翻的厚词典。

(a,b,c)⇒{(a),(a,b),(a,b,c)},nprefix=3(a, b, c) \Rightarrow \{(a), (a, b), (a, b, c)\}, \quad n_{\text{prefix}} = 3
三列联合索引恰好有三个可用前缀:从最左开始连续取一列、两列或三列。跳列查询如 (a,c) 无法完整走索引,这是设计时最容易踩的坑。
  1. 联合索引 (a,b,c)一个包含三个列的二级索引,按 a、b、c 依次排序存储。
    (a,b,c)(a, b, c)
  2. 前缀组合能高效利用该索引的查询,必须从最左列开始按序连续取列。
    {(a),(a,b),(a,b,c)}\{(a), (a, b), (a, b, c)\}
  3. 可用前缀数n 列联合索引恰好有 n 个可用前缀,与总列数相等。
    nprefix=3n_{\text{prefix}} = 3

索引下推原理

借阅台前排起了长队。小M发现,读者总是先抱着一摞书走到台前,才发现“哎,我要的是 2020 年以后的版本”——白跑一趟。于是她在书库门口加了道岗:拿着今天的预约单,先翻卡片核验版本年份,不符合的直接从单子上划掉,再进库取书。

这道岗就是索引下推(Index Condition Pushdown,简称 ICP)。没有 ICP 时,MySQL 的存储引擎层把二级索引命中的每一条记录都回表取出整行,再交给 Server 层逐一判断剩余 WHERE 条件。有了 ICP,存储引擎在扫描二级索引时,直接把那些“索引里已有列”能判定的条件一并过滤掉,只有真正可能命中的行才回表。

最典型的场景是联合索引与范围条件组合:索引 (a,b),查询 WHERE a=? AND b>10 AND c='x'。c 不在索引里无法在索引层判断,但 a 和 b 都能。没有 ICP:a 条件定位后,所有 b>10 的记录(可能几千行)全部回表,再在 Server 层过滤 c。有 ICP:存储引擎扫描时发现 b<=10 的直接丢弃,只有通过的行才回表,数量可能缩小到十分之一。

小M的岗哨把每小时从书库进出的搬运量砍掉了七成。数据库也是这样:ICP 把“先搬出来再扔”变成“先在门口扔再搬”,每个被挡在门外的条件,都是一次省下的随机 IO。

Nback=∣σcond(Sidx)∣N_{\text{back}} = \left| \sigma_{\text{cond}}(S_{\text{idx}}) \right|
回表次数不再是“索引命中的候选行数”,而是“经过索引层过滤后的剩余行数”。过滤得越狠,回表越少,随机 IO 越省。
  1. 索引扫描结果集 S_idx二级索引扫描后得到的候选记录集合。
    SidxS_{\text{idx}}
  2. 索引层过滤 σ_cond把可下推的谓词下放到存储引擎,在扫描时进行选择。
    σcond\sigma_{\text{cond}}
  3. 回表次数 N_back过滤后集合的大小,才是真正需要回表的次数。
    Nback=∣⋅∣N_{\text{back}} = \left| \cdot \right|

触发与失效场景

岗哨也不是万能的——读者要是递过来一张字迹潦草、写满函数运算的纸条,哨兵只能无奈摇头:“这我没法在门口核验,麻烦您进去自己翻吧。”覆盖索引和 ICP,各自也有自己的“先决条件”。

覆盖索引的触发条件有三条:一是查询列必须全部包含在索引中;二是遵循最左前缀,WHERE 条件用得上该索引;三是查询不会突破索引的边界,比如 SELECT * 就永远无法覆盖。ICP 的触发条件更拧巴:它要求查询走二级索引(聚簇索引主键查询不适用),WHERE 中有“索引里能判断但无法完全用索引定位”的条件,引擎必须是 InnoDB,且索引列不能被函数包裹。

EXPLAIN 是判断触发与否的照妖镜:看到 Using index 说明命中覆盖索引;看到 Using index condition 说明 ICP 生效;两者同时出现,说明这个查询又覆盖又下推,是优等生。而下面这些情况 ICP 会静默失效:MyISAM 表、全表扫描、条件列不在索引中、索引列套了函数。优化器遇到这些,会悄悄退回“先回表再过滤”的老路,EXPLAIN 里看不到任何提示。

小M在岗哨旁贴了张“不能核验清单”:看不清的纸条不收、需要临时算的不收。数据员们看了 EXPLAIN 的 Using index condition,就像看到了那张清单——知道岗哨挡不下哪些请求,也就知道该回去把哪张速查卡(联合索引)重新排一排。

ICP生效: InnoDB∧二级索引∧(下推列∈I)∧¬f(col)\text{ICP生效}:\ \text{InnoDB} \wedge \text{二级索引} \wedge (\text{下推列} \in I) \wedge \neg f(\text{col})
一个逻辑表达式,把 ICP 生效的四道闸门串起来:引擎是 InnoDB、走的是二级索引、下推的列在索引里、索引列没被函数改头换面。任意一道闸门关上,ICP 就静默退出。
  1. 引擎条件ICP 只在 InnoDB 存储引擎上实现,MyISAM 不支持。
    InnoDB\text{InnoDB}
  2. 索引类型必须是二级索引扫描,聚簇索引主键查找没有“回表”可言。
    二级索引\text{二级索引}
  3. 下推列归属被下推的条件列必须是索引里的列,否则存储引擎无从判断。
    (下推列∈I)(\text{下推列} \in I)
  4. 非函数索引列上套了函数或运算,索引本身有序性被破坏,无法下推。
    ¬f(col)\neg f(\text{col})

性能收益量化

几个月后,馆长让小M汇报这两招省了多少人力。她翻出借阅记录:以前查一次“北京 + 2020 年后”的书目,平均要跑 216 趟书库;现在只要跑 32 趟。数字摆出来,全馆为之惊叹。

数据库的账本是一样的。一次回表 = 一次聚簇索引的随机 IO。InnoDB 的 B+ 树数据页默认 16KB,随机 IO 的成本集中在寻道与页加载上,单次耗时约几十微秒到几毫秒。把回表次数从 N 降到小 n,省下的时间近乎线性。覆盖索引更狠——直接把回表次数降到 0,查询全程在索引的紧凑页面上完成。

在 EXPLAIN 里量化对比收益:先用没有优化索引的版本跑一遍,看 rows 列预估的扫描行数;加上覆盖/ICP 索引后,rows 大减,type 从 ref 变成 index 或 const,Extra 出现 Using index。经验数据:一个命中几百行的范围查询,开启 ICP 后回表次数往往能下降一个数量级;而在线上流量放大下,数据库 IO 从 70% 降到 20% 并不罕见。

馆长把那张“216→32“的数据表贴在了前台。从此图书馆定下规矩:一切新查询先查目录卡片能否直接作答,再决定要不要进书库。你的团队也可以定下同样的规矩——每次写 SQL 前用 EXPLAIN 照一照,让”回表次数”这个指标像账本一样透明,优化就不再是玄学。

G=1−NafterNbeforeG = 1 - \frac{N_{\text{after}}}{N_{\text{before}}}
性能收益 = 1 − 优化后回表次数 / 优化前回表次数。回表从 100 次降到 10 次,收益就是 90%。
  1. 优化前后回表次数分别统计使用索引优化前后的回表次数。
    Nbefore, NafterN_{\text{before}},\ N_{\text{after}}
  2. 剩余比例优化后回表次数占优化前的比例。
    NafterNbefore\frac{N_{\text{after}}}{N_{\text{before}}}
  3. 总收益 G1 减去剩余比例,就是省下的回表开销占比。
    G=1−NafterNbeforeG = 1 - \frac{N_{\text{after}}}{N_{\text{before}}}

联合索引实战

小M把这段时间的经验总结成了一本《选址手册》:先看读者最常问哪几类问题(高频 SQL),再决定速查卡上放哪些字段、什么顺序。图书馆从此不再盲目增加卡片。

手册翻译成数据库语言,就是联合索引设计五步法:第一步,收集高频查询,把它们按 WHERE、GROUP BY、ORDER BY、SELECT 拆解;第二步,把等值匹配的列放索引最前面;第三步,范围条件列紧跟其后;第四步,排序/分组列继续排,让索引直接给出有序数据;第五步,在索引尾部追加“只为了覆盖”的列,让 SELECT 少回表。

一个订单系统的实战例子。高频查询:SELECT order_id, status, amount FROM orders WHERE user_id=? AND status=? ORDER BY create_time DESC LIMIT 20。五步法产出索引 (user_id, status, create_time, amount):user_id 和 status 做快速过滤,create_time 避免排序,amount 是覆盖列。注意别贪心:每多一个联合索引,INSERT/UPDATE 时就要同步维护,写放大是覆盖索引的隐性账单;低基数列、超长文本列不值得进索引,就像速查卡不会印整整一页的废话。

如今小M的《选址手册》成了馆内新员工的必修教材。你手里的数据库也是如此:索引设计不是堆数量,而是像排卡片一样按“过滤→排序→覆盖”的顺序精心布局。会了这一手,下次线上慢查询爆出,别人还在手忙脚乱地 EXPLAIN,你已经笑着把索引方案填好了。

I=(aeq→brange→corder→dcover)I = (a_{\text{eq}} \to b_{\text{range}} \to c_{\text{order}} \to d_{\text{cover}})
索引列的顺序公式:等值列 → 范围列 → 排序列 → 覆盖列。按这个顺序排布联合索引,既快又省回表。
  1. 等值列 aWHERE 中用 = 匹配的列,放最前,选择性越高越好。
    aeqa_{\text{eq}}
  2. 范围列 b用 >、<、BETWEEN 的列,紧随其后参与过滤。
    brangeb_{\text{range}}
  3. 排序列 cORDER BY/GROUP BY 列,放入索引可省掉 filesort。
    corderc_{\text{order}}
  4. 覆盖列 dSELECT 要返回但不参与过滤的列,放最后保覆盖。
    dcoverd_{\text{cover}}

AI 应用场景

电商订单中心的高频列表接口

阶段:OLTP 查询优化(在线服务侧,也是 AI 推理前特征读取的必经之路)

解决的问题:订单列表页每次请求要查用户最近 N 笔订单,普通索引命中几百行就回表几百次,接口 RT 飙到 800ms。而覆盖索引让查询只走二级索引叶子节点,回表清零;ICP 再把 status 等条件在存储引擎层提前过滤,只对少数行回表。这套手法在 AI 场景同样适用——推荐系统在线推理前批量拉取用户特征宽表,命中覆盖索引能显著降低特征服务的 P99 延迟。

具体案例:某电商平台订单中心日均 PV 3000 万,EXPLAIN 显示 Extra=Using index condition,开启 ICP 后回表次数从 500+ 降到 30,数据库 IO 压力下降 60%,接口 RT 从 800ms 降到 35ms。

推荐系统召回层的候选过滤

阶段:在线推理阶段的候选检索与过滤

解决的问题:推荐系统要按“用户标签等值 + 活跃度范围”快速召回候选用户,旧实现全量回表后过滤,P99 延迟超 100ms。索引下推把活跃度过滤下沉到存储引擎,只有通过的行才回表;再配合覆盖索引直接返回 uid + score 两个字段,二次索引扫完即得,连回表都免了。

具体案例:某内容平台候选池 5000 万用户,优化后召回耗时从 120ms 降到 12ms,吞吐提升 10 倍,GPU 推理的喂数速度再也不拖后腿。

特征宽表物化刷新加速

阶段:离线训练数据的批量加工(ETL)

解决的问题:为训练集物化特征宽表时,经常按维度键高频读取明细行。覆盖索引让扫描只走索引块,聚合任务不再反复随机读聚簇索引,整批 ETL 耗时大幅缩短。

具体案例:某数据团队把维表关联 SQL 的二级索引改为覆盖索引后,1 小时的批任务压缩到 18 分钟,特征新鲜度从 T+1 升级到 T+0。

Python 演示

python75 行
# -*- coding: utf-8 -*-"""覆盖索引与索引下推(ICP)查询开销模拟器"""import random  class Table:    def __init__(self, rows):        self.rows = rows        self.io_count = 0     def build_index(self, key_cols):        """构建二级索引: 多列键 -> 记录ID列表"""        self.index_map = {}        for rid, row in enumerate(self.rows):            key = tuple(row[c] for c in key_cols)            self.index_map.setdefault(key, []).append(rid)     def fetch_row(self, rid):        """回表: 按主键去聚簇索引取整行, 计1次随机IO"""        self.io_count += 1        return self.rows[rid]     def collect_rids(self, city):        """只扫描索引, 收集city匹配的所有记录ID(未回表)"""        rids = []        for (c, _age), ids in self.index_map.items():            if c == city:                rids.extend(ids)        return rids     def query_without_icp(self, city, age_lo):        rids = self.collect_rids(city)        result = []        for rid in rids:            row = self.fetch_row(rid)      # 全部回表            if row["age"] > age_lo:        # 到Server层才过滤                result.append(row)        return result     def query_with_icp(self, city, age_lo):        result = []        for (c, age), ids in self.index_map.items():            if c != city:                continue            if age <= age_lo:              # 存储引擎层先过滤                continue            for rid in ids:                result.append(self.fetch_row(rid))  # 只回表通过的        return result  def main():    random.seed(2024)    n = 20000    rows = [        {"city": random.choice(["bj", "sh", "gz"]),         "age": random.randint(18, 60)}        for _ in range(n)    ]    t = Table(rows)    t.build_index(["city", "age"])     t.io_count = 0    t.query_without_icp("bj", 30)    before = t.io_count    t.io_count = 0    t.query_with_icp("bj", 30)    after = t.io_count    print("未开启ICP: 回表IO =", before)    print("开启ICP:   回表IO =", after)    print("节省比例:  %.1f%%" % ((before - after) / before * 100))  if __name__ == "__main__":    main()
目的:用 2 万行模拟数据对比“未开启 ICP”与“开启 ICP”两种查询模式的回表 IO 次数,直观呈现索引下推的收益。
  1. key = tuple(row[c] for c in key_cols)用多个列拼成联合索引的复合键,模拟 (city, age) 二级索引。
  2. self.io_count += 1每次回表计一次随机 IO,IO 次数就是性能的体温计。
  3. def collect_rids只扫描索引收集候选 ID,模拟“前台翻卡片”阶段,此时还没踏入书库。
  4. if age <= age_lo: continue索引层先过滤年龄,不满足的直接跳过,这是 ICP 的核心动作。
  5. self.fetch_row(rid)只有通过索引层过滤的行才真正回表取数。
  6. print("节省比例: ...")用前后对比量化收益,让优化效果看得见。

常见误区

误区“既然覆盖索引能免回表,那我干脆把整张表的字段全加到一个联合索引里,以后所有查询都覆盖了!”

正解覆盖索引不是“字段越多越好”。索引本质是冗余的排序结构,每加一个字段,B+ 树节点变大、层数可能变高,写入时的排序维护成本直线上升。正确做法是只把高频查询真正用到的列放进去,用“查询列 ⊆ 索引列”作为设计依据,而不是“全表字段 ⊆ 索引列”。

为何常见:初学者只看到“覆盖免回表”的好处,忽略了索引的写放大和维护成本,把优化目标误以为“越大越全越好”。

误区“我给 (a,b,c) 建了联合索引,那 WHERE b=? 和 WHERE c=? 的查询应该也很快吧?”

正解联合索引遵循最左前缀原则,(a,b,c) 只能高效匹配 (a)、(a,b)、(a,b,c)。跳过 a 直接查 b,索引无法定位,会退化为全索引扫描甚至全表扫描。设计时要把使用频率最高的等值列放在最左,而不是按字母顺序或建表顺序排。

为何常见:把“联合索引”类比成多个独立索引的简单叠加,忽略了 B+ 树复合键的字典序排序特性。

误区“EXPLAIN 结果里有 Using index,是不是就说明索引下推(ICP)生效了?”

正解两者是不同机制:Using index 表示覆盖索引,查询在索引层就拿到全部数据,回表次数为 0;Using index condition 才表示 ICP 生效,即存储引擎在索引扫描时做了下推过滤。一个查询可能同时出现两者,但看到任何单独一个都要能分清。

为何常见:名字相似、都出现在 Extra 列,且都代表“索引帮了大忙”,容易被混为一谈。

误区“ICP 能在索引层过滤条件,那我干脆把所有 WHERE 条件都丢给索引判断,不用管列在不在索引里。”

正解ICP 只能下推“索引中存在的列”的条件。MySQL 优化器会严格检查:条件列必须出现在当前使用的二级索引中,且不能对索引列套函数或表达式,引擎还得是 InnoDB。如果条件列不在索引里,优化器只能放弃下推,老老实实回表后过滤。

为何常见:把“下推”想象成万能过滤器,忽略了存储引擎接触不到索引之外的列这一硬约束。

面试题

Q1. 二级索引查询中,出现“回表”的根本原因是?

单选题 · easy

  • A.索引叶子存主键还需取整行← 正确答案
  • B.查询语句没有使用任何索引
  • C.表里数据量实在过于庞大
  • D.数据库连接本身发生了超时

答案:A。二级索引叶子节点只存“索引列+主键”,要拿完整行必须凭主键回聚簇索引再查一次,这就是回表。B 没索引是全表扫描而非回表;C 和 D 与回表机制无关。好比目录卡片只写了索书号,想看书还得进书库取。

Q2. 覆盖索引最核心的特征是?

单选题 · easy

  • A.索引包含查询所需全部列← 正确答案
  • B.索引覆盖整张表所有行
  • C.索引里存了所有数据库
  • D.索引把数据备份到内存

答案:A。覆盖索引指查询的列集合(SELECT + WHERE 涉及的列)全部落在某个索引列集合内,查询只需扫描索引即可。B、C、D 都是对“覆盖”字面的错误联想:覆盖的是“列”,不是“行”“库”“内存”。

Q3. 联合索引 (a,b,c) 能高效匹配哪些前缀查询?

单选题 · mid

  • A.前缀组合(a)(a,b)(a,b,c)全组合← 正确答案
  • B.(b)(c)(b,c)中间列组合
  • C.(a,c)(b,c)(c,a)跳跃组合
  • D.(a)(b)(c)单列各自组合

答案:A。最左前缀原则要求从第一列开始连续取列,(a,c) 跳过了 b,无法用索引定位;B、C、D 都违反了最左前缀。这和查字典一样:必须先查部首,才能按笔画翻到对应页。

Q4. 索引下推(ICP)的过滤动作发生在哪里?

单选题 · mid

  • A.客户端应用层过滤
  • B.存储引擎层索引扫描时← 正确答案
  • C.Server层回表之后
  • D.优化器生成计划时

答案:B。ICP 把可判断的条件下推到存储引擎,引擎在扫描二级索引时就过滤掉不满足的行,减少回表。C 是未开启 ICP 时的老流程;A、D 与数据读取路径无关。就像岗哨在书库门口拦人,而不是等读者把书搬回家再检查。

Q5. 下列哪些场景适合用覆盖索引优化?

多选题 · mid

  • A.高频查询只返回固定少数列← 正确答案
  • B.查询用SELECT *返回整行
  • C.过滤条件配合少量结果列← 正确答案
  • D.表写多读少频繁更新列

答案:AC。覆盖索引要求查询列少且都在索引里,A、C 满足。SELECT * 想覆盖必须把所有列都塞进索引,索引会巨大;D 的写多场景,覆盖索引反而加重写放大,得不偿失。

Q6. 下列哪种查询条件组合最可能触发索引下推?

单选题 · hard

  • A.等值查询直接返回主键
  • B.前缀模糊匹配加额外等值条件← 正确答案
  • C.聚合函数统计大于某值
  • D.主键范围删除大量记录

答案:B。ICP 的经典场景是二级索引上存在“无法用于定位但仍可判断”的条件。联合索引 (name, age) 上 name LIKE '张%' 只能定位前缀范围,age 无法参与索引定位却能在索引层过滤,于是被下推。A、D 是主键查询不适用 ICP;C 的聚合通常走覆盖扫描,下推意义不大。

Q7. EXPLAIN 的 Extra 列出现 Using index condition 表示?

单选题 · mid

  • A.查询已经用上覆盖索引
  • B.索引下推正在生效← 正确答案
  • C.索引损坏需要立即重建
  • D.查询未使用任何索引

答案:B。Using index condition 是 ICP 生效的标志,表示存储引擎在索引扫描阶段做了过滤。覆盖索引的标志是 Using index;能区分两者是面试高频考点。记住:condition 是“在索引上做条件判断”,index 是“索引就是答案本身”。

Q8. 下列哪些情况会导致 ICP 不生效?

多选题 · hard

  • A.存储引擎是MyISAM不支持← 正确答案
  • B.查询做全表扫描无索引候选← 正确答案
  • C.条件列不在索引中← 正确答案
  • D.索引列用函数包裹← 正确答案

答案:ABCD。ICP 依赖 InnoDB 的二级索引扫描实现,MyISAM 不支持;全表扫描无索引可下推;条件列不在索引里,存储引擎无从判断;函数破坏了索引列原始值的有序性,无法在索引层比较。四项全部成立,缺一不可。

Q9. 覆盖索引比普通索引直接减少的开销是?

单选题 · hard

  • A.二级索引的扫描范围
  • B.聚簇索引的随机IO← 正确答案
  • C.排序需要的临时内存
  • D.连接查询的驱动次数

答案:B。覆盖索引让查询完全不必回表,聚簇索引的随机 IO 降到 0。A 中扫描范围由 WHERE 决定,覆盖并不改变范围;C 靠索引排序解决;D 与连接顺序有关。就像速查表直接看答案,省下了进书库取书这一趟路。

Q10. 写 SQL 时最有利于命中覆盖索引的习惯是?

单选题 · easy

  • A.一律使用SELECT *省事
  • B.只SELECT实际需要的列← 正确答案
  • C.把条件全部转成子查询
  • D.给每个字段单独建索引

答案:B。列越少越容易全部落入索引,覆盖命中率越高。SELECT * 几乎不可能覆盖;C 增加复杂度且不改变列集合;D 的单字段索引无法覆盖多列查询。记住口诀:SELECT 少拿列,索引好覆盖。