Skip to content

聚簇索引:InnoDB 的「独家秘密」

你有没有想过这个问题:

当 MySQL 执行 SELECT * FROM users WHERE id = 1 时,它是怎么找到完整数据的?

答案就藏在聚簇索引里。


什么是聚簇索引?

聚簇索引(Clustered Index)的核心特点是:索引即数据,数据即索引

InnoDB 中,主键索引就是聚簇索引。索引的叶子节点存储的不是主键值,而是完整的行数据。

聚簇索引结构:

                    [主键=1 | 主键=10 | 主键=20]
                   /          |            \
            ┌─────────┐  ┌─────────┐  ┌─────────┐
            │ id=1   │  │ id=10   │  │ id=20   │
            │ name   │  │ name    │  │ name    │
            │ age    │  │ age     │  │ age     │
            │ email  │  │ email   │  │ email   │
            │ ...    │  │ ...     │  │ ...     │
            └─────────┘  └─────────┘  └─────────┘

看起来和普通索引没什么区别对吧?但关键在于:这些数据页本身就是 B+ 树的叶子节点。

换句话说,聚簇索引的 B+ 树就是整张表


聚簇索引 vs 非聚簇索引

对比一下两者的区别:

对比项聚簇索引非聚簇索引(辅助索引)
叶子节点存什么完整行数据主键值
数量每表只能有 1 个可以有多个
查询是否回表否(直接返回数据)是(需要回表查主键)
插入性能受主键顺序影响影响较小
主键选择影响整体性能-

聚簇索引查询过程

java
// 聚簇索引:直接返回数据
public Row queryByClusteredIndex(Page rootPage, long id) {
    // 从根节点定位到叶子节点
    Page leafPage = locateLeafPage(rootPage, id);

    // 在叶子节点中找到数据行
    Row row = findRowInPage(leafPage, id);

    // 直接返回,No 回表!
    return row;
}

非聚簇索引查询过程

java
// 非聚簇索引:需要回表
public Row queryBySecondaryIndex(Page idxRootPage, String name, Page clusteredRootPage) {
    // 1. 从辅助索引定位到主键
    Page idxLeafPage = locateLeafPage(idxRootPage, name);
    long primaryKey = findPrimaryKey(idxLeafPage, name);

    // 2. 用主键回表查询完整数据
    Page clusteredLeafPage = locateLeafPage(clusteredRootPage, primaryKey);
    Row row = findRowInPage(clusteredLeafPage, primaryKey);

    // 两步完成,这就是「回表」
    return row;
}

主键选择的重要性

因为聚簇索引的顺序就是数据的物理存储顺序,所以主键的选择直接影响表性能

场景一:自增主键(推荐)

sql
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,  -- 自增主键
    order_no VARCHAR(32),
    amount DECIMAL(10,2),
    created_at DATETIME
);

插入数据时,新记录总是追加到末尾,顺序写入,性能最优

java
// 自增主键的插入过程
public void insertOrder(Connection conn, Order order) {
    // 1. 主键自动递增,无需指定
    order.setId(null);

    // 2. 插入到聚簇索引的末尾(顺序写入)
    String sql = "INSERT INTO orders (order_no, amount) VALUES (?, ?)";
    // 执行插入...
}

场景二:UUID 主键(不推荐)

sql
CREATE TABLE orders (
    id VARCHAR(36) PRIMARY KEY,  -- UUID 主键
    order_no VARCHAR(32),
    amount DECIMAL(10,2)
);

UUID 是无序的,每次插入都可能在 B+ 树中间某个位置。页分裂和随机 I/O 是性能杀手。

java
// UUID 主键的插入问题
public void insertWithUUID() {
    // UUID 示例:550e8400-e29b-41d4-a716-446655440000
    // 每次插入,数据可能在 B+ 树的任意位置
    // 1. 查找插入位置
    // 2. 如果页满了,触发页分裂
    // 3. 随机 I/O 写入

    // 性能差距:顺序写入 vs 随机写入,可能差 10-100 倍
}

场景三:业务主键(复合主键)

有些场景必须用业务主键,比如用户表用身份证号作为主键。

sql
CREATE TABLE users (
    id_card VARCHAR(18) PRIMARY KEY,  -- 身份证号(业务主键)
    name VARCHAR(50),
    phone VARCHAR(11)
);

如果业务上确实需要业务主键,可以再增加一个自增 id 作为聚簇索引,把业务主键降级为唯一索引:

sql
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,  -- 聚簇索引
    id_card VARCHAR(18) UNIQUE,           -- 唯一索引
    name VARCHAR(50),
    phone VARCHAR(11)
);

聚簇索引的适用场景

适合用聚簇索引的场景

  1. 主键查询:直接定位到完整数据,无回表
  2. 范围查询:数据顺序存储,范围扫描效率高
  3. 排序查询:物理顺序即索引顺序,无需额外排序
  4. 高并发插入:自增主键,顺序追加

不适合用聚簇索引的场景

  1. UUID 作为主键:频繁页分裂,性能下降
  2. 主键更新频繁:更新主键会导致数据移动
  3. 随机插入场景:无法利用顺序写入的优势
  4. 需要频繁修改的列作为主键:主键变化影响所有索引

面试场景模拟

面试官: 聚簇索引和非聚簇索引的区别是什么?

你: 聚簇索引的叶子节点存储完整行数据,非聚簇索引只存主键值。所以用主键查询可以直接返回数据,但用非聚簇索引查询需要先找到主键,再回表查完整数据。

面试官: 如果表没有主键呢?

你: 如果没有显式主键,InnoDB 会选择第一个唯一非空索引作为聚簇索引。如果也没有唯一索引,InnoDB 会生成一个隐藏的 6 字节 ROW_ID 作为主键。

面试官: 聚簇索引的优缺点是什么?

你: 优点是主键查询快、范围查询效率高、排序效率高。缺点是主键更新代价大、插入性能依赖主键顺序、辅助索引查询需要回表。


一句话总结

聚簇索引是 InnoDB 的核心机制:主键即索引,索引即数据,数据的物理顺序由主键决定。

选择合适的主键类型(推荐自增),是表设计的第一步。

基于 VitePress 构建