第三章:聚簇与非聚簇索引 — 数据的物理地图
Published: Thu Feb 05 2026 | Modified: Fri Feb 06 2026 , 2 minutes reading.
1. 定义
聚簇索引 (Clustered Index):一种索引结构,其中叶子节点直接存储了整行数据。一张表只能有一个聚簇索引(通常是主键),因为数据只能以一种物理顺序存储。
非聚簇索引 (Non-Clustered Index)(又称二级索引 Secondary Index):叶子节点不存储行数据,而是存储指向数据的指针。在 MySQL InnoDB 中,这个指针是主键值;在 MyISAM 中它通常是物理文件偏移量,而 PostgreSQL 则使用 Tuple ID (CTID)。
2. 技术深度:回表查询 (Double Lookup)
在 InnoDB 中,如果你通过二级索引查找数据,实际上会发生两次 B+Tree 查找。
- 第一跳:在二级索引树中查找,找到叶子节点存储的主键值(如 ID=55)。
- 第二跳(回表):拿着 ID=55 到聚簇索引树中查找,获取完整的行数据。
这种“回表”会产生额外的随机 I/O,是查询性能杀手。
3. 可视化:InnoDB 索引结构对比
flowchart TD
subgraph Secondary [二级索引 (按 Name)]
IndexLeaf[叶子节点: Key='Alice', PK=101]
end
subgraph Clustered [聚簇索引 (按 PK)]
DataLeaf[叶子节点: PK=101, RowData={Alice, 25, NYC}]
end
Query[查询: SELECT * FROM users WHERE Name='Alice']
Query -->|1. 查找 Name| Secondary
Secondary --"获得 PK=101"--> Query
Query -->|2. 回表查找 PK| Clustered
Clustered -->|返回行数据| Query4. 真实案例:Uber 的 Postgres 到 MySQL 迁移 (2013-2016)
背景:Uber 早期大量使用 PostgreSQL。Postgres 默认使用“堆表 (Heap Table)”模型,所有的索引(包括主键)都是二级索引,指向 Heap 中的 CTID(行物理位置)。
挑战:写入放大 (Write Amplification)
- Postgres 使用 MVCC,更新一行数据通常会生成一个新的行版本(新的 CTID)。
- 这意味着所有指向该行的索引都需要更新其 CTID 指针。
- 对于像 Uber 这样拥有数十个索引的大表,更新一次用户状态可能导致十几次索引写入。
技术选型:Uber 迁移到了 MySQL (InnoDB)。
- InnoDB 优势:在 InnoDB 中,二级索引指向主键值。当行数据更新位置(或页分裂)时,只要主键不变,二级索引就不需要修改。
- 这极大降低了更新操作的 I/O 成本,尤其是在从库复制(Replication)场景下。
注意:Postgres 后续通过 HOT (Heap-Only Tuples) 技术部分缓解了此问题,但 Uber 的案例仍是理解索引架构差异的经典教材。
5. 深度优化与纵深防御
A. 覆盖索引 (Covering Index)
消除回表的终极手段。
- 技巧:如果查询是
SELECT age FROM users WHERE name = 'Alice',可以创建一个联合索引(name, age)。 - 效果:数据在二级索引树的叶子节点中已经存在(
Key='Alice', age=25, PK=101),引擎直接返回,无需回表。
B. 主键的选择
- 短小:由于所有二级索引都包含主键,主键越长(如 UUID),二级索引就越膨胀。
- 静态:主键一旦确立,绝不应更新。更新主键意味着要重写聚簇索引和所有二级索引。
C. 索引选择性 (Selectivity)
- 不要为低选择性的列(如
Gender:男/女)建立单列非聚簇索引。优化器会认为全表扫描比“通过索引查找一半的主键再回表”更快,从而忽略该索引。
