Mybatis 回表 索引下推

一、回表(Table Return / Lookup)

1. 核心定义

回表是 InnoDB 中二级索引查询的补充逻辑:二级索引的叶子节点仅存储「索引列值 + 聚簇索引键值(主键 / 隐藏 rowid)」,若查询需要的字段超出二级索引列范围,需通过聚簇索引键值到聚簇索引中查找完整行数据,这个 “从二级索引跳转到聚簇索引查全量数据” 的过程就是回表。

2. 核心背景(InnoDB 索引结构)

InnoDB 有两类索引:
  • 聚簇索引:主键索引,叶子节点直接存储整行数据(表数据本身就是聚簇索引);
  • 二级索引:普通索引 / 唯一索引,叶子节点只存 “索引列值 + 聚簇索引键值”(不存整行数据)。

3. 回表的完整流程(举例)

sql
-- 表结构:主键id(聚簇索引),二级索引idx_name(name)
CREATE TABLE user (
  id INT PRIMARY KEY, -- 聚簇索引
  name VARCHAR(10),
  age INT,
  KEY idx_name (name) -- 二级索引
);

-- 查询1:无需回表(覆盖索引)
SELECT name FROM user WHERE name = '张三';
-- 逻辑:仅需扫描idx_name的叶子节点(已包含name),直接返回,无回表

-- 查询2:需要回表
SELECT id, name, age FROM user WHERE name = '张三';
-- 逻辑:
-- 1. 扫描idx_name二级索引,找到name='张三'的叶子节点,获取对应的主键id;
-- 2. 用id到聚簇索引中查找,获取age字段(整行数据);
-- 3. 合并结果返回 → 步骤2就是回表
 

4. 回表的影响与优化

  • 性能损耗:回表需要两次索引扫描(二级索引 + 聚簇索引),若数据量大 / 回表次数多,会增加磁盘 IO;
  • 优化手段:使用「覆盖索引」(查询字段全部包含在二级索引中),避免回表。例如:
    sql
    -- 建组合索引,包含name+age,查询字段被索引覆盖,无需回表
    ALTER TABLE user ADD KEY idx_name_age (name, age);
    SELECT name, age FROM user WHERE name = '张三'; -- 无回表

二、索引下推(Index Condition Pushdown,ICP)

1. 核心定义

索引下推是 MySQL 5.6 + 引入的查询优化机制:在二级索引扫描阶段,将原本由 “服务器层” 执行的过滤条件(如age > 20)下推到 “存储引擎层” 执行,提前过滤掉不符合条件的记录,减少回表的次数和数据传输量。

2. 无 ICP vs 有 ICP 的流程对比(举例)

sql
-- 表结构:主键id,二级索引idx_name (name),字段age无索引
SELECT * FROM user WHERE name LIKE '张%' AND age > 20;
 
无 ICP(MySQL 5.5 及以前)有 ICP(MySQL 5.6+)
1. 扫描 idx_name,找到所有 name 以 “张” 开头的记录,获取主键 id;
 
2. 逐个回表,获取整行数据;
 
3. 服务器层过滤 age > 20 的记录;
 
→ 所有 “张开头” 的记录都要回表,即使 age 不满足
1. 扫描 idx_name,找到 name 以 “张” 开头的记录;
 
2. 存储引擎层先判断 age > 20(若二级索引包含 age 则直接判断,否则无法下推);
 
3. 仅对 age > 20 的记录,获取主键 id 并回表;
 
→ 提前过滤,减少回表次数

3. 索引下推的关键规则

  • 适用场景:仅针对二级索引(聚簇索引无需下推,本身存整行数据),且仅支持「等值 / 范围 /like(前缀匹配)」等能在索引上执行的条件;
  • 限制条件:
    1. 条件字段必须包含在当前扫描的二级索引中(否则存储引擎无法获取字段值,无法下推);
    2. 不支持子查询 / 函数运算后的条件(如DATE_FORMAT(create_time) = '2025-01-01'无法下推);
    3. 不支持主键索引(聚簇索引)的查询;
  • 典型案例:
    sql
    -- 案例1:可下推(age在组合索引中)
    ALTER TABLE user ADD KEY idx_name_age (name, age);
    SELECT * FROM user WHERE name LIKE '张%' AND age > 20; -- age条件下推到引擎层
    
    -- 案例2:不可下推(age不在idx_name索引中)
    SELECT * FROM user WHERE name LIKE '张%' AND age > 20; -- 无组合索引,age字段无法在引擎层获取,只能服务器层过滤

4. 如何验证索引下推是否生效

通过EXPLAIN查看执行计划,若Extra列包含Using index condition,说明索引下推已生效:
sql
EXPLAIN SELECT * FROM user WHERE name LIKE '张%' AND age > 20;
-- Extra: Using index condition → ICP生效

三、回表与索引下推的关联

  1. 索引下推的核心目标:减少回表次数 —— 通过提前过滤无效记录,避免对不符合条件的记录执行回表操作,降低 IO 开销;
  2. 覆盖索引的特殊情况:若查询使用覆盖索引(无回表),索引下推无意义(无需回表,过滤在引擎层 / 服务器层无本质差异);
  3. 性能优先级:覆盖索引(避免回表) > 索引下推(减少回表) > 无优化(全量回表)。

四、总结

概念核心逻辑作用优化手段
回表 二级索引查不全字段,需通过聚簇索引键值查整行数据 完成全字段查询(但耗性能) 建覆盖索引,避免回表
索引下推 过滤条件下推到存储引擎层,提前过滤无效记录 减少回表次数,提升查询效率 建组合索引包含过滤字段,触发 ICP
简单记:
  • 回表是 “查索引不够,要查全表数据” 的动作;
  • 索引下推是 “能在索引层过滤的,就别带回表后再过滤” 的优化。

---- 

如下这段话对吗??

唯一索引和普通索引:查询:没区别,都是从索引树查询

修改:唯一索引更慢,因为要读取数据到内存,并判断是否冲突

-

部分正确,但存在关键细节偏差—— 查询并非 “完全没区别”,修改的慢因也不止 “读取数据 + 冲突判断”,以下从「查询、修改」两个维度拆解精准逻辑,先给核心结论,再补细节:

一、核心结论(先纠正 / 补充)

维度描述正确事实
查询 没区别,都是从索引树查询 「等值查询 + 无回表」场景基本无区别;「范围查询 / 排序 / 回表」场景有细微差异(唯一索引略优);且唯一索引避免了 “索引下推 / 优化器的额外判断”
修改 唯一索引更慢,因为要读取数据到内存,并判断是否冲突 慢的核心是「唯一性校验」,但不止 “读取数据 + 冲突判断”:
 
1. 需加更严格的锁(如 next-key 锁);
 
2. 校验需遍历唯一索引树(可能跨页);
 
3. 无法利用 “change buffer”(普通索引可利用,大幅提升写入性能)

二、查询维度:看似无区别,实则有细微差异

1. 基本场景:等值查询(无回表)→ 几乎无区别

无论是唯一索引(UNIQUE)还是普通索引(KEY),等值查询(如 WHERE id = 100)的核心逻辑都是:
  • 从索引根节点→叶子节点,定位到匹配的索引记录;
  • 唯一索引:找到第一条匹配记录后直接终止遍历(因为唯一性保证无重复);
  • 普通索引:找到第一条匹配记录后,需继续遍历到下一条不匹配的记录(确认是否有重复);
👉 差异:普通索引多了 “确认下一条” 的微小操作,但 InnoDB 索引叶子节点是有序链表,这个操作耗时可忽略,业务层面感知不到区别。

2. 特殊场景:唯一索引略优

  • 范围查询 / 排序:唯一索引因无重复值,优化器无需考虑 “去重 / 重复值排序”,执行计划更简洁(但差异依然极小);
  • 回表场景:唯一索引定位到记录后,回表的聚簇索引键值唯一,无需额外判断;普通索引无此优势,但同样无感知差异;
  • 索引下推(ICP):唯一索引无需 ICP 的额外过滤(本身无重复),普通索引可能触发 ICP,但仅极端场景有差异。

3. 关键例外:查询性能的 “隐性差异”

若普通索引存在大量重复值(如 gender 字段,只有 0/1),查询 WHERE gender = 0 时:
  • 普通索引需扫描所有匹配的叶子节点(可能数万条);
  • 若该字段建唯一索引(不可能,因为重复值无法建),但如果是 “唯一索引 + 组合字段”(如 UNIQUE (gender, id)),则能快速定位,此时差异显著。

三、修改维度:唯一索引更慢的核心原因(不止冲突判断)

修改操作(INSERT/UPDATE/DELETE)的性能差异是核心,唯一索引慢的原因有 3 层,且 “change buffer” 是最关键因素:

1. 核心原因 1:无法利用「change buffer」(最大性能损耗)

  • change buffer 是什么:InnoDB 的优化机制 —— 当修改非聚簇索引(普通索引)时,若目标数据页不在内存中,不会立即加载磁盘页,而是将修改记录到 change buffer,后续读取该页时再合并更新,大幅减少磁盘 IO;
  • 唯一索引的限制:唯一性校验必须 “实时确认数据是否存在”,因此无法使用 change buffer,无论数据页是否在内存,都必须加载磁盘页到内存做校验,磁盘 IO 开销远大于普通索引。
👉 举例:插入 10 万条数据到普通索引字段 → 若数据页不在内存,只需写 change buffer(内存操作,毫秒级);插入到唯一索引字段 → 需逐页加载磁盘(秒级甚至分钟级),性能差距可达百倍。

2. 核心原因 2:唯一性冲突校验(你提到的点,但需补充)

  • 插入 / 更新唯一索引字段时,需遍历唯一索引树,确认无重复值:
    • 内存中校验:若目标页在内存,直接判断(耗时短);
    • 磁盘中校验:若目标页不在内存,需先加载到内存(耗时长);
  • 普通索引无此校验步骤,只需将修改写入索引树即可。

3. 核心原因 3:锁机制更严格

  • 唯一索引的修改会触发「next-key 锁」(防止幻读 + 唯一性冲突),锁范围更大,且持有时间更长;
  • 普通索引的修改(非唯一)锁范围更小,甚至可通过快照读避免锁等待;
  • 极端场景下,唯一索引的锁竞争会导致修改操作阻塞,进一步降低性能。

四、举例验证(直观感知差异)

例 1:插入性能对比(核心差异)

sql
-- 表结构
CREATE TABLE t1 (
  id INT,
  uid INT UNIQUE, -- 唯一索引
  name VARCHAR(10),
  KEY idx_name (name) -- 普通索引
);

-- 插入10万条数据(uid无重复,name大量重复)
-- 插入uid(唯一索引):需逐页加载磁盘校验,耗时~5秒
INSERT INTO t1 (id, uid, name) SELECT n, n, 'test' FROM numbers LIMIT 100000;

-- 插入name(普通索引):利用change buffer,无需加载磁盘,耗时~0.5秒
INSERT INTO t1 (id, uid, name) SELECT n+100000, n+100000, 'test' FROM numbers LIMIT 100000;
 

例 2:更新性能对比

sql
-- 更新唯一索引(uid):需校验唯一性+无change buffer,耗时~2秒
UPDATE t1 SET uid = uid + 200000 WHERE id < 50000;

-- 更新普通索引(name):利用change buffer,耗时~0.2秒
UPDATE t1 SET name = 'test2' WHERE id < 50000;

五、总结:关键认知

  1. 查询:99% 的业务场景下,唯一索引和普通索引的查询性能无感知差异;唯一索引仅在 “终止遍历” 环节略优,但可忽略;
  2. 修改:唯一索引性能显著低于普通索引,核心是「无法利用 change buffer」+「唯一性校验」+「锁更严格」,而非仅 “读取数据 + 冲突判断”;
  3. 选型建议:
    • 需保证字段唯一性(如用户 ID、订单号)→ 必须建唯一索引(性能损耗是必要代价);
    • 无需唯一性的字段(如姓名、性别)→ 建普通索引,优先利用 change buffer 提升写入性能;
    • 避免 “为了查询性能” 给非唯一字段建唯一索引(无意义,反而牺牲写入性能)。
补充:MySQL 8.0 对唯一索引的校验有小幅优化(如批量插入的批量校验),但核心性能差异依然存在

 -------------

change buffer 是内存结构(Buffer Pool 约 50% 空间可分配给它,由 innodb_change_buffer_max_size 控制),它的设计目标就是 “避免为了修改索引而频繁加载磁盘页到内存”—— 只有索引页不在内存时,这个 “内存缓存修改” 的价值才体现。

补充:change buffer 的 merge 时机(内存→磁盘)

change buffer 里的修改不会一直存在于内存,会在以下场景自动合并到磁盘:
  1. 当后续查询加载了该索引页到内存时(触发 “按需 merge”);
  2. 后台线程定期 merge(默认每秒 / 每 10 秒,可配置);
  3. 数据库空闲时;
  4. 数据库关闭 / 崩溃恢复时;
  5. change buffer 空间不足时。

总结(一句话理清)

change buffer 是 InnoDB 内存(Buffer Pool)中的一块区域,仅当要修改的普通索引页不在内存时,才会把索引修改操作缓存到这里;若索引页已在内存,则直接修改内存页,无需走 change buffer。
 

---

posted @ 2025-12-23 14:52  野鹤闲人  阅读(1)  评论(0)    收藏  举报