导读:索引是数据库性能优化的核心,而"聚集索引 vs 普通索引"又是索引知识中最基础也最容易混淆的概念。本文不空谈理论,而是通过三个 SQL 场景的执行计划对比,一步步验证聚集索引与普通索引的底层差异。
很多开发者在建索引时都会遇到这样的困惑:
CSEK2、SSEK2、BLKUP2 都是什么意思?在 DM8 中,除位图索引、位图连接索引、全文索引外,索引数据均采用 B+ 树结构存储。
两个最基础的概念:
| 概念 | 说明 |
|---|---|
| B+ 树 | 一种多路平衡查找树,叶子节点有序存储键值,内部节点只存键用于路由。查找、插入、删除都是 O(log n) 级别 |
| ROWID | 数据行的物理存储地址(类似文件中的"偏移量"),通过 ROWID 可以精确定位到一行数据 |
理解这两点后,核心问题就变成了:B+ 树的叶子节点里到底存了什么? 存整行 → 聚集索引;只存"索引列值 + 行地址" → 普通索引。这就是全部秘密。
| 索引类型 | 含义 |
|---|---|
| 聚集索引<br>CLUSTER INDEX | 索引叶子节点直接存储整行数据,索引即表数据;表主键默认就是聚集索引 |
| 普通二级索引<br>SECONDARY INDEX | 索引叶子节点只存索引列值 + 行地址(ROWID / 聚集键),不存完整行 |
聚集索引(主键 ID):
┌─────────────────────────────────────────────┐
│ B+ 树叶子节点:ID | 整行数据(所有列) │
│ ┌──────┬───────────────────────────────┐ │
│ │ ID=2 │ NAME │ DEPARTMENT │ SALARY │ │
│ └──────┴───────────────────────────────┘ │
│ —— 索引即数据,定位到叶子 = 拿到整行 │
└─────────────────────────────────────────────┘
普通二级索引(NAME 列):
┌─────────────────────────────────────────────┐
│ B+ 树叶子节点:NAME | 行地址(ROWID/主键) │
│ ┌───────────┬─────────────────────────┐ │
│ │ NAME │ ROWID(物理行地址) │ │
│ └───────────┴─────────────────────────┘ │
│ —— 只存索引列值 + 行地址,不存完整行 │
└─────────────────────────────────────────────┘
这就是两者最本质的区别:聚集索引的叶子节点是"完整的数据行",而普通索引的叶子节点只是"索引列值 + 一个指向数据行的指针"。
-- 员工表:ID 为主键(默认即聚集索引),NAME 上建立普通二级索引
CREATE TABLE T_EMP (
ID INT PRIMARY KEY, -- 聚集索引键
NAME VARCHAR(50),
DEPARTMENT VARCHAR(50),
SALARY NUMERIC(10,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;
-- 在 NAME 列创建普通索引(二级索引)
CREATE INDEX IDX_EMP_NAME ON T_EMP(NAME);
此时表上有两个索引:
| 索引 | 类型 | 键列 | 叶子节点存储内容 |
|---|---|---|---|
| 主键索引 | 聚集索引 | ID | ID + 整行数据(NAME/DEPARTMENT/SALARY) |
| IDX_EMP_NAME | 普通二级索引 | NAME | NAME + 行地址(ROWID/主键ID) |
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):查询所需的全部列都包含在索引中,就不用再去访问数据行。
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 * 需要的 DEPARTMENT、SALARY 等列不在二级索引叶子节点上,必须通过回表获取。这里的回表方式参数为 use_clu_addr(0),含义是:
注意:如果表显式创建了聚集索引(
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) |
| 叶子节点是否有全部所需列 | 是(存整行) | 是(含主键列) | 否(缺非索引列) |
| 访问路径 | 一步直达 | 一步直达 | 两步:索引定位 + 回表取行 |
一句话总结:
- 聚集索引:叶子节点存整行 → 查什么列都"够用",不需要回表;
- 普通索引:叶子节点只存"索引列 + 行地址" → 查主键(索引列)够用,不回表;查其他非索引列,必须回表。
NAME 有 1000 条重复值)时,回表会变成大量随机 IO,性能急剧下降;把查询需要的列"塞进"索引里,让二级索引叶子节点包含所有查询列:
-- 把 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 已包含在索引中,直接覆盖,无需回表。
SELECT 的列纳入复合索引;SELECT *,只取需要的列,减少回表概率;达梦支持通过 CREATE CLUSTER INDEX 显式指定聚集索引键(默认聚集索引键是 ROWID):
-- 按 ENAME 列物理组织表数据
CREATE CLUSTER INDEX clu_emp_name ON emp(ename);
| 要点 | 说明 |
|---|---|
| 每表仅一个 | 每个普通表有且仅有一个聚集索引,重复创建会报错 |
| 重建代价极大 | 新建聚集索引会重建整个表及其所有索引(包括主键索引),建议在建表时或数据量少时确定聚集索引键 |
| 删除自动还原 | 删除聚集索引时,会以 ROWID 为键重建聚集索引,同样要重建所有索引 |
| ROWID 索引不可删 | 默认的 ROWID 聚集索引不允许删除 |
| 适用范围受限 | 不能用于函数索引;不能在列存储表和堆表上新建;语句不能含分区子句 |
| 与位图索引互斥 | 存在 CLUSTER KEY 的表不支持位图索引、位图连接索引 |
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 带模式名前缀
通过三个场景的执行计划对比,我们可以清晰看到达梦数据库中索引工作的底层逻辑:
聚集索引"索引即数据":叶子节点直接存储整行数据,主键默认即聚集索引。WHERE ID = ? 的 SELECT * 通过 CSEK2 一步定位,无需回表——因为要什么列都在叶子节点上。
普通索引"只存目录":叶子节点只存"索引列值 + 行地址"。查询列恰好是主键(索引覆盖)时,SSEK2 直接返回,不回表;查询其他非索引列时,SSEK2 定位后还要通过 BLKUP2 回表,参数 use_clu_addr(0) 表明走 ROWID 物理定位方式。
执行计划是最好的老师:CSEK2(聚集索引扫描)、SSEK2(二级索引扫描)、BLKUP2(回表)三个操作符,把两种索引的结构差异展现得一清二楚。写 SQL、建索引时多看执行计划,是每个 DBA 和开发者的基本功。
参考资料:达梦数据库官方文档《管理索引》https://eco.dameng.com/document/dm/zh-cn/pm/manage-index.html
文章
阅读量
获赞
