回表查询:为什么需要回表

回表查询:为什么需要回表

什么是回表

回表(Back to Table)是指通过二级索引查到主键值后,再通过主键到聚簇索引中查找完整数据行的过程。

sequenceDiagram
    participant Q as 查询: name='张三'
    participant S as 二级索引 idx_name
    participant P as 聚簇索引 PRIMARY

    Q->>S: 查找 name='张三'
    S-->>Q: 找到 id=10
    Note over S,Q:  第一次B+树查找

    Q->>P: 通过 id=10 找完整数据
    P-->>Q: 返回 id=10 的完整行
    Note over P,Q:  第二次B+树查找(回表)

    Note over Q:  总共2B+树查找

回表的具体过程

示例表

CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    email VARCHAR(100),
    INDEX idx_name(name)
) ENGINE=InnoDB;

触发回表的查询

-- 需要回表
SELECT * FROM user WHERE name = '张三';

执行过程:

graph TD
    subgraph 第一步[第一步:二级索引查找]
        A1[idx_name B+] --> A2[非叶子节点: a-z]
        A2 --> A3[叶子节点: name='张三'  id=10]
    end

    subgraph 第二步[第二步:回表]
        B1[拿到主键 id=10] --> B2[聚簇索引 B+]
        B2 --> B3[叶子节点: id=10  整行数据]
        B3 --> B4[id, name, age, email 全部字段]
    end

特别关键:回表不是从二级索引直接跳到数据行的!而是先拿到主键,再用主键查一次聚簇索引。

为什么需要回表

根本原因:二级索引不保存完整数据行,只保存索引列 + 主键值。

-- idx_name 叶子节点的内容
[name='张三', id=10]     -- ✅ age 和 email 不在这里
[name='李四', id=20]
[name='王五', id=30]

设计原因:
1. 避免数据冗余:如果每个二级索引都存副本,数据冗余十倍不止
2. 保证一致性:数据只有一份(在聚簇索引中),修改时只需改一处
3. 节省空间:二级索引只存关键信息,占用很少空间

何时不需要回表

覆盖索引场景

-- 查询的字段全部在二级索引中
SELECT name FROM user WHERE name = '张三';
-- idx_name 叶子节点有 name 和 id
-- 而查询只需要 name,所以不需要回表!

-- 查询 id 和 name 也不需要回表
SELECT id, name FROM user WHERE name = '张三';
-- idx_name 叶子节点有 name 和 id,足以满足查询需求

主键查询

-- 直接通过聚簇索引查询,不存在"回表"
SELECT * FROM user WHERE id = 10;

回表的性能代价

-- 大量回表的场景
SELECT * FROM user WHERE name LIKE '张%';
-- 假设有 1000 个姓"张"的用户
-- 二级索引定位:3次IO
-- 回表:1000 × 3次IO = 3000次IO
graph TD
    A[大范围二级索引查询] --> B[二级索引找到N个主键]
    B --> C{是否覆盖索引?}
    C -->|| D[只需要3IO 🎉]
    C -->|| E[需要回表N次<br/>N × 3IO]

    E --> F{数据是否连续?}
    F -->|| G[随机IO较少<br/>预读优化]
    F -->|| H[大量随机IO<br/>性能瓶颈]

随机 IO vs 顺序 IO

-- 回表本质是随机IO:
SELECT * FROM user WHERE name IN ('张三', '李四', '王五');
-- 二级索引找到 id=10, id=500, id=3000
-- 回表时:读id=10的页,跳去读id=500的页,再跳去读id=3000的页
-- 都是随机IO ⚠️

-- 对比聚簇索引的范围查询:
SELECT * FROM user WHERE id BETWEEN 10 AND 15;
-- 顺序读取相邻数据页
-- 连续IO,速度更快 ✅

如何减少回表

方法一:覆盖索引

-- 创建能够"覆盖"查询需求的联合索引
-- 查询经常需要 WHERE name 查 age
CREATE INDEX idx_name_age ON user(name, age);

-- 现在这个查询就不用回表了
SELECT name, age FROM user WHERE name = '张三';
-- idx_name_age 叶子节点存了 name + age + id
-- 所有需要的数据都在索引里 ✅

方法二:延迟关联(先查主键再关联)

-- ❌ 直接回表
SELECT * FROM user WHERE name LIKE '张%' LIMIT 10;

-- ✅ 延迟关联:先取主键,再用主键取数据
SELECT * FROM user
INNER JOIN (
    SELECT id FROM user WHERE name LIKE '张%' LIMIT 10
) AS tmp ON user.id = tmp.id;
-- 子查询用覆盖索引(id在索引里)无需回表
-- 外层用聚簇索引查10条,只需10次IO

方法三:设计更合适的主键

主键越小,二级索引越小,回表时聚簇索引也越矮(扇出更高)。

面试要点

  • 回表原因:二级索引叶子节点只有主键,没有完整数据
  • 回表过程:二级索引拿到主键 → 主键回聚簇索引取数据
  • 性能代价:每次回表都是一次 B+树查找(3-4次IO)
  • 覆盖索引:查询字段全在二级索引中时不需要回表
  • 优化建议:合理设计覆盖索引、用延迟关联减少回表

一句话总结:回表就像”查了目录知道页码,再翻到那一页看内容”——目录只告诉你页码,想看内容就得翻过去。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容