注册
聚集索引与普通索引的区别
专栏/技术分享/ 文章详情 /

聚集索引与普通索引的区别

DM_WJH 2026/08/21 222 3 0
摘要

导读:索引是数据库性能优化的核心,而"聚集索引 vs 普通索引"又是索引知识中最基础也最容易混淆的概念。本文不空谈理论,而是通过三个 SQL 场景的执行计划对比,一步步验证聚集索引与普通索引的底层差异。

一、为什么要写这篇文章

很多开发者在建索引时都会遇到这样的困惑:

  • 明明建了索引,为什么查询还是慢?
  • 主键查询和普通索引查询,执行路径到底有什么不同?
  • 执行计划里出现的 CSEK2SSEK2BLKUP2 都是什么意思?
  • 什么是"回表"?回表是不是一定不好?

二、 B+ 树与 ROWID概念

在 DM8 中,除位图索引、位图连接索引、全文索引外,索引数据均采用 B+ 树结构存储。

两个最基础的概念:

概念 说明
B+ 树 一种多路平衡查找树,叶子节点有序存储键值,内部节点只存键用于路由。查找、插入、删除都是 O(log n) 级别
ROWID 数据行的物理存储地址(类似文件中的"偏移量"),通过 ROWID 可以精确定位到一行数据

理解这两点后,核心问题就变成了:B+ 树的叶子节点里到底存了什么? 存整行 → 聚集索引;只存"索引列值 + 行地址" → 普通索引。这就是全部秘密。

三、聚集索引 vs 普通索引:概念先行

3.1 核心概念对比表

索引类型 含义
聚集索引<br>CLUSTER INDEX 索引叶子节点直接存储整行数据索引即表数据;表主键默认就是聚集索引
普通二级索引<br>SECONDARY INDEX 索引叶子节点只存索引列值 + 行地址(ROWID / 聚集键)不存完整行

3.2 官方定义(达梦《管理索引》)

  • 聚集索引(一级索引、主索引):按照聚集索引键构造一棵 B+ 树,表数据存储在 B+ 树叶子节点上。通过定位索引可直接在 B+ 树中找到数据。每个表有且仅有一个聚集索引。
  • 非聚集索引(二级索引、辅助索引):将二级索引列聚集索引列共同存储在 B+ 树叶子节点上。查找非聚集索引键值或聚集索引键值可直接在 B+ 树中找到;查找其他数据需回表(回到一级索引)进行二次查找。每个表可以有多个非聚集索引。

3.3 一张图看懂两种索引的叶子节点差异

聚集索引(主键 ID):
┌─────────────────────────────────────────────┐
│  B+ 树叶子节点:ID | 整行数据(所有列)       │
│  ┌──────┬───────────────────────────────┐   │
│  │ ID=2 │ NAME │ DEPARTMENT │ SALARY   │   │
│  └──────┴───────────────────────────────┘   │
│  —— 索引即数据,定位到叶子 = 拿到整行        │
└─────────────────────────────────────────────┘

普通二级索引(NAME 列):
┌─────────────────────────────────────────────┐
│  B+ 树叶子节点:NAME | 行地址(ROWID/主键)  │
│  ┌───────────┬─────────────────────────┐    │
│  │ NAME      │ ROWID(物理行地址)      │    │
│  └───────────┴─────────────────────────┘    │
│  —— 只存索引列值 + 行地址,不存完整行        │
└─────────────────────────────────────────────┘

这就是两者最本质的区别:聚集索引的叶子节点是"完整的数据行",而普通索引的叶子节点只是"索引列值 + 一个指向数据行的指针"。


四、实验环境准备

4.1 建表

-- 员工表:ID 为主键(默认即聚集索引),NAME 上建立普通二级索引 CREATE TABLE T_EMP ( ID INT PRIMARY KEY, -- 聚集索引键 NAME VARCHAR(50), DEPARTMENT VARCHAR(50), SALARY NUMERIC(10,2) );

4.2 插入测试数据

INSERT INTO T_EMP VALUES (1, 'Zhang San', 'R&D', 15000); INSERT INTO T_EMP VALUES (2, 'Li Si', 'HR', 12000); INSERT INTO T_EMP VALUES (3, 'Wang Wu', 'Finance', 18000); INSERT INTO T_EMP VALUES (4, 'Chen Liu', 'R&D', 16000); INSERT INTO T_EMP VALUES (5, 'Zhao Qi', 'Sales', 14000); COMMIT;

4.3 创建普通二级索引

-- 在 NAME 列创建普通索引(二级索引) CREATE INDEX IDX_EMP_NAME ON T_EMP(NAME);

此时表上有两个索引:

索引 类型 键列 叶子节点存储内容
主键索引 聚集索引 ID ID + 整行数据(NAME/DEPARTMENT/SALARY)
IDX_EMP_NAME 普通二级索引 NAME NAME + 行地址(ROWID/主键ID)

4.4 如何查看执行计划

EXPLAIN SELECT * FROM T_EMP WHERE ID = 2;

使用 EXPLAIN 关键字即可查看 SQL 的执行计划,无需真正执行。


五、执行计划操作符速查

先认识本文会反复出现的三个关键操作符(源自达梦《管理索引》官方文档):

操作符 名称 含义
CSEK2 聚集索引扫描 通过聚集索引键直接定位数据。因为叶子节点就是整行数据,一次定位即可返回完整行
SSEK2 二级索引扫描 通过非聚集索引键定位数据。只能拿到索引列值 + 行地址(参数 is_global(0) 表示局部索引)
BLKUP2 回表 通过二级索引找到行地址后,回到聚集索引(表)中获取其他列数据。参数 use_clu_addr(0) 表示通过 ROWID(物理行地址) 方式回表

这三个操作符的组合,恰好完整刻画了索引查找的两条路径:一条路直达(CSEK2),一条路中转(SSEK2 ± BLKUP2)


六、三个场景,验证两种索引的本质区别

场景一:走聚集索引 —— SELECT * 一步到位

SQL 与执行计划:

EXPLAIN SELECT * FROM T_EMP WHERE ID = 2;
1   #NSET2: [1, 1, 112]
2     #PRJT2: [1, 1, 112]; exp_num(7), is_atom(FALSE)
3       #CSEK2: [1, 1, 112]; scan_type(ASC), C1(T_EMP), scan_range[2,2]

执行路径分析:

WHERE ID = 2
   │
   ▼
CSEK2 聚集索引扫描(主键索引)
   │  叶子节点 = ID + 整行数据
   ▼
直接返回完整行(NAME、DEPARTMENT、SALARY 全都有)

结论:因为 ID 是聚集索引键,而聚集索引的叶子节点直接存储了整行数据(表中所有列的值),所以 SELECT * 所需的全部列在聚集索引叶子节点上都能找到——一次索引定位直接返回,无需回表

这就是"id 列含有表中所有值,所以 select * 直接走聚集"的含义:查询的列都在聚集索引上,索引即数据


场景二:走普通索引,查询主键值 —— 不回表

SQL 与执行计划:

EXPLAIN SELECT ID FROM T_EMP WHERE NAME = 'Chen Liu';
1   #NSET2: [1, 1, 64]
2     #PRJT2: [1, 1, 64]; exp_num(1), is_atom(FALSE)
3       #SSEK2: [1, 1, 64]; scan_type(ASC), S1(T_EMP), scan_range['Chen Liu','Chen Liu'], is_global(0)

执行路径分析:

WHERE NAME = 'Chen Liu'
   │
   ▼
SSEK2 二级索引扫描(IDX_EMP_NAME)
   │  叶子节点 = NAME + 行地址(ROWID/主键ID)
   ▼
要查询的列是 ID(主键)—— 恰好就在二级索引的叶子节点里!
直接返回 ID = 4,不需要回表

结论:二级索引 IDX_EMP_NAME 的叶子节点中,除了索引列 NAME,还冗余存储了主键 ID(达梦二级索引叶子节点会携带聚集索引列)。当查询列恰好是主键 ID 时,从二级索引叶子节点就能直接取到,无需回表

这在索引优化中被称为 索引覆盖(Covering Index):查询所需的全部列都包含在索引中,就不用再去访问数据行。


场景三:走普通索引,查询非索引列 —— ROWID 方式回表

SQL 与执行计划:

EXPLAIN SELECT * FROM T_EMP WHERE NAME = 'Chen Liu';
1   #NSET2: [1, 1, 96]
2     #PRJT2: [1, 1, 96]; exp_num(7), is_atom(FALSE)
3       #BLKUP2: [1, 1, 96]; S1(T_EMP); use_clu_addr(0)
4         #SSEK2: [1, 1, 96]; scan_type(ASC), S1(T_EMP), scan_range['Chen Liu','Chen Liu'], is_global(0)

执行路径分析:

WHERE NAME = 'Chen Liu',但要查 DEPARTMENT、SALARY 等非索引列
   │
   ▼
SSEK2 二级索引扫描(IDX_EMP_NAME)
   │  叶子节点只有 NAME + 行地址,没有 DEPARTMENT / SALARY
   ▼
BLKUP2 回表:通过 use_clu_addr(0) 方式
   │  用二级索引叶子节点里的 ROWID(物理行地址)回到数据行
   ▼
到聚集索引(表数据)中取出完整行,返回所有列

结论SELECT * 需要的 DEPARTMENTSALARY 等列不在二级索引叶子节点上,必须通过回表获取。这里的回表方式参数为 use_clu_addr(0),含义是:

  • 二级索引叶子节点中存储的是 ROWID(物理行地址)
  • 回表时直接通过 ROWID 精确定位到数据行,再取出其余列

注意:如果表显式创建了聚集索引(CREATE CLUSTER INDEX),二级索引叶子节点存储的将是聚集索引键,回表参数会变为 use_clu_addr(1),表示用聚集键在聚集索引 B+ 树中再查一次。本文默认表结构下,走的是 ROWID 方式(use_clu_addr(0))。


七、三个场景对比总表

对比项 场景一:走聚集索引 场景二:走二级索引不回表 场景三:走二级索引回表
SQL SELECT * WHERE ID = ? SELECT ID WHERE NAME = ? SELECT * WHERE NAME = ?
使用索引 聚集索引(主键 ID) 普通索引 IDX_EMP_NAME 普通索引 IDX_EMP_NAME
核心操作符 CSEK2 SSEK2 SSEK2 + BLKUP2
是否回表 否(索引覆盖) use_clu_addr(0) 走 ROWID)
叶子节点是否有全部所需列 是(存整行) 是(含主键列) 否(缺非索引列)
访问路径 一步直达 一步直达 两步:索引定位 + 回表取行

一句话总结

  • 聚集索引:叶子节点存整行 → 查什么列都"够用",不需要回表;
  • 普通索引:叶子节点只存"索引列 + 行地址" → 查主键(索引列)够用,不回表;查其他非索引列,必须回表

八、延伸:回表的代价与覆盖索引优化

8.1 回表为什么"贵"?

  • 场景三是两次 IO:先扫二级索引 B+ 树,再按 ROWID 回表按物理地址随机读取数据页;
  • 当命中行数很多(比如 NAME 有 1000 条重复值)时,回表会变成大量随机 IO,性能急剧下降;
  • 极端情况下,优化器甚至会放弃索引,改为全表扫描。

8.2 如何避免回表?—— 覆盖索引

把查询需要的列"塞进"索引里,让二级索引叶子节点包含所有查询列:

-- 把 DEPARTMENT 也放进索引,查询 SELECT DEPARTMENT FROM T_EMP WHERE NAME=? 时就不用回表 CREATE INDEX IDX_EMP_NAME_DEPT ON T_EMP(NAME, DEPARTMENT);
EXPLAIN SELECT DEPARTMENT FROM T_EMP WHERE NAME = 'Chen Liu';
1   #NSET2: [1, 1, 64]
2     #PRJT2: [1, 1, 64]; exp_num(1), is_atom(FALSE)
3       #SSEK2: [1, 1, 64]; scan_type(ASC), S1(T_EMP), scan_range['Chen Liu','Chen Liu'], is_global(0)

执行计划中不再出现 BLKUP2——DEPARTMENT 已包含在索引中,直接覆盖,无需回表。

8.3 优化建议

  1. 高频查询优先设计覆盖索引,把 SELECT 的列纳入复合索引;
  2. 不要盲目 SELECT *,只取需要的列,减少回表概率;
  3. 复合索引列顺序:等值字段在前,非等值字段在后;
  4. 索引不是越多越好,每个索引都会增加 DML 的维护开销。

九、达梦聚集索引管理要点(官方文档补充)

9.1 显式创建聚集索引

达梦支持通过 CREATE CLUSTER INDEX 显式指定聚集索引键(默认聚集索引键是 ROWID):

-- 按 ENAME 列物理组织表数据 CREATE CLUSTER INDEX clu_emp_name ON emp(ename);

9.2 注意事项(务必牢记)

要点 说明
每表仅一个 每个普通表有且仅有一个聚集索引,重复创建会报错
重建代价极大 新建聚集索引会重建整个表及其所有索引(包括主键索引),建议在建表时或数据量少时确定聚集索引键
删除自动还原 删除聚集索引时,会以 ROWID 为键重建聚集索引,同样要重建所有索引
ROWID 索引不可删 默认的 ROWID 聚集索引不允许删除
适用范围受限 不能用于函数索引;不能在列存储表和堆表上新建;语句不能含分区子句
与位图索引互斥 存在 CLUSTER KEY 的表不支持位图索引、位图连接索引

9.3 索引维护

  • 重建索引(消除碎片、释放空间):
SELECT SP_REBUILD_INDEX('SYSDBA', 1547892); -- 参数:模式名, 索引ID

注意:虚索引、聚集索引、水平分区子表、临时表和系统表上的索引不支持重建。

  • 删除索引
DROP INDEX IDX_EMP_NAME; -- 删除普通索引 DROP INDEX clu_emp_name; -- 删除聚集索引(会以 ROWID 重建聚集索引)
  • 查看索引定义
SELECT INDEXDEF(1547892, 0); -- 0 不带模式名前缀 SELECT INDEXDEF(1547892, 1); -- 1 带模式名前缀

十、总结

通过三个场景的执行计划对比,我们可以清晰看到达梦数据库中索引工作的底层逻辑:

  1. 聚集索引"索引即数据":叶子节点直接存储整行数据,主键默认即聚集索引。WHERE ID = ?SELECT * 通过 CSEK2 一步定位,无需回表——因为要什么列都在叶子节点上。

  2. 普通索引"只存目录":叶子节点只存"索引列值 + 行地址"。查询列恰好是主键(索引覆盖)时,SSEK2 直接返回,不回表;查询其他非索引列时,SSEK2 定位后还要通过 BLKUP2 回表,参数 use_clu_addr(0) 表明走 ROWID 物理定位方式。

  3. 执行计划是最好的老师CSEK2(聚集索引扫描)、SSEK2(二级索引扫描)、BLKUP2(回表)三个操作符,把两种索引的结构差异展现得一清二楚。写 SQL、建索引时多看执行计划,是每个 DBA 和开发者的基本功。


参考资料:达梦数据库官方文档《管理索引》https://eco.dameng.com/document/dm/zh-cn/pm/manage-index.html

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服