This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/17-聚簇索引与二级索引.md
T
2026-05-17 00:06:11 +08:00

7.2 KiB
Raw Blame History

tags, create time
tags create time
MySQL
聚簇索引
二级索引
Covering Index
回表
2026-05-16 00:00

聚簇索引 vs 二级索引

概述

InnoDB 使用 B+ Tree 组织所有索引,但根据「索引叶子节点存什么」的不同,分为两类:聚簇索引和二级索引。理解这两者的差异,是你设计高效查询和执行计划的起点。

[!QUESTION] 思考 假设一张用户表按 id 排序存储在磁盘上——
现在要查 WHERE email = 'alice@test.com',数据库需要做什么?
(提示:email 不是存储排序的依据。)

带着这个问题往下看。

聚簇索引(Clustered Index)

聚簇索引就是数据本身。InnoDB 表中只能有一个聚簇索引。

聚簇索引的形成规则

优先级 条件 说明
① PRIMARY KEY 如果有显式 PK,它自动成为聚簇索引
② UNIQUE + NOT NULL 没有 PK 但有唯一非空列,用它
③ 隐藏 row_id 都没有时,InnoDB 自动生成隐藏的 6-byte row_id

[!WARNING] 隐藏的 row_id 是个坑 如果你的表既没 PK 也没有 UNIQUE NOT NULL 列,InnoDB 会自动生成隐藏主键。这时:

  • 你自定义的任何索引都会变成二级索引
  • 二级索引回表时需要额外跳转 → 回表代价更高
  • 外键引用不可靠(ID 对用户透明)

结论:每张表必须有显式 PRIMARY KEY。

聚簇索引的特性

flowchart TD
    A["按主键排序存储数据"] --> B["数据页紧凑排列"]
    B --> C["连续主键 = 顺序写入"]
    C --> D["最小化页分裂"]
    D --> E["最佳写入性能"]
    
    A --> F["范围查询极快<br/>WHERE id BETWEEN 100 AND 200<br/>只需扫描一段连续的叶子页"]
    F --> G["顺序 I/O 而非随机 I/O"]
    
    style E fill:#00D866,color:#fff
    style G fill:#00B6BC,color:#fff

二级索引(Secondary Index)

除了聚簇索引外的所有索引都是二级索引。叶子节点存储的是:索引列的值 + 主键值。

flowchart TD
    subgraph "二级索引页<br/>idx_status_created = (status, created_at)"
        direction LR
        R1["status=1 \| created_at=01 → PK=5"]
        R2["status=1 \| created_at=02 → PK=12"]
        R3["status=1 \| created_at=03 → PK=18"]
        R4["status=0 \| created_at=01 → PK=3"]
        R5["status=0 \| created_at=04 → PK=25"]
    end
    
    style R1 fill:#E8DFF5,color:#333
    style R3 fill:#00B6BC,color:#fff

[!NOTE] 关键观察

  • 每行都包含 索引列值(用于匹配查询条件)和 主键值(用于回表定位完整行数据)
  • 二级索引本身是 B+ Tree,按索引列排序;叶子层每一行指向聚簇索引中的对应数据
  • 如果只需要索引中的列(如 SELECT status WHERE status=1),就 无需回表——这就是 Covering Index

回表的代价

sequenceDiagram
    participant App as 应用层
    participant SI as 二级索引(idx_email)
    participant CI as 聚簇索引(PK=id)
    participant DB as 数据盘

    App->>SI: 查找 email='alice@test.com'
    SI-->>App: 找到 → id=12345
    
    App->>CI: 用 id=12345 查找
    CI->>DB: 定位叶子页 (随机 IO #1)
    DB-->>CI: 返回完整行
    
    CI-->>App: 返回完整行

    Note over SI,DB: 至少 2 次 IO:1次二级索引 + 1次回表

减少回表的策略

-- ❌ 差:Covering Index 未命中,需要回表拿 name 字段
SELECT name, email FROM users WHERE email = 'alice@test.com';
-- idx_email 能匹配 WHERE,但 name 不在索引里 → 需要回表

-- ✅ 好:覆盖索引,无需回表
ALTER TABLE users ADD INDEX idx_email_name (email, name);
SELECT name, email FROM users WHERE email = 'alice@test.com';
-- EXPLAIN Extra: Using index ← 完美!

[!SUCCESS] Covering Index 的黄金法则 把 SELECT 中的列 + WHERE 中的列放到同一个联合索引中,就能实现 Index Only Scan。

  • 适合:高频查询、固定列选择
  • 不适合:SELECT *(永远无法覆盖)、列变化频繁的查询

回表 vs 索引下推(ICP)

MySQL 5.6 引入的 Index Condition Pushdown 优化了部分回表场景。

-- 假设联合索引 idx_name_status = (name, status)
-- 查询:WHERE name LIKE '张%' AND status = 1

-- ❌ 无 ICP:所有匹配的 name 都要回表查 status
SELECT * FROM users WHERE name LIKE '张%' AND status = 1;
-- 步骤:1) 找到所有 '张%' 的行  2) 逐条回表查 status  3) 过滤

-- ✅ 有 ICP:在二级索引中就先过滤 status
-- 引擎层直接读二级索引页,提取 name 和 status,先判断 status=1
-- 只有满足条件的才回表
-- 减少了大量无效回表

-- EXPLAIN 验证
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND status = 1\G
-- Extra 显示: Using index condition
flowchart TD
    N["无 ICP"] --> A["查到 1000 条 '张%' 的记录"]
    A --> B["1000 次回表检查 status"]
    B --> C["最终只有 10 条符合"]

    Y["有 ICP"] --> D["在索引中预检 status"]
    D --> E["1000 条中筛出 10 条"]
    E --> F["仅 10 次回表"]

    style B fill:#EE5A24,color:#fff
    style F fill:#00D866,color:#fff

两种索引的空间对比

graph TB
    subgraph Clustered["聚簇索引 — 100 万行"]
        CI["每页约 200 行  |  16KB / 80 bytes"]
        CI2["总页数 ≈ 5000 页"]
    end
    
    subgraph Secondary["二级索引 — idx_email VARCHAR(255)"]
        SI["每页约 80 行  |  16KB / 200 bytes"]
        SI2["总页数 ≈ 12500 页"]
    end
    
    CI --> CI2
    SI --> SI2
    
    style CI2 fill:#00B6BC,color:#fff
    style SI2 fill:#C44569,color:#fff

[!NOTE] 二级索引通常比聚簇索引大得多 因为二级索引存的是「索引列 + 主键」,而聚簇索引存的是「整行」。如果索引列很长(如 VARCHAR(255)),二级索引会膨胀得很厉害。这也是为什么长字符串列做索引时要限制长度:(name(50))。

两种索引的对比总结

维度 聚簇索引 (Clustered) 二级索引 (Secondary)
数量 每张表仅一个 可以有多个
叶子节点存储 整行数据 索引列值 + 主键值
形成依据 PK / 唯一非空列 / 隐藏 row_id CREATE INDEX 或 UNIQUE KEY
范围查询 极快(顺序扫描连续页) 需要回表,代价高
覆盖索引 天然覆盖(本身就是数据) 仅当所需列都在索引中时生效
空间占用 基准大小 通常更大(多一列主键 + 膨胀风险)
写入代价 插入可能触发页分裂 更新索引列需改索引 + 回表改数据

[!SUMMARY] 核心记忆点

  1. 聚簇索引 = 数据本身,一张表只能有一个
  2. 二级索引 = 「索引列 + 主键」,查数据要回表
  3. 能用 Covering Index 的场景永远优于回表
  4. ICP 是 MySQL 5.6 对回表的温和优化——能省则省

关联笔记