注册
识别与消除回表
专栏/技术分享/ 文章详情 /

识别与消除回表

🌌 2026/09/23 262 0 0
摘要

执行计划里一旦出现 BLKUP2,就表示优化器已经走完索引定位,还要再回基表取列。索引已经建了,查询仍然慢,原因经常不在扫描本身,而在回表。


1. 数据在磁盘上怎么放

是否回表,取决于行数据与索引条目是否在同一处。

1.1 堆表(默认)

默认堆表按 ROWID 连续存放完整行:

ROWID | C1 | C2 || ROWID | C1 | C2 || ...

已知 ROWID 时,优化器可以用聚簇定位(计划中常见 CSEK)直接找到该行。

需要区分两个容易混用的概念:达梦默认的“按 ROWID 物理有序存放”不是经典 IOT。Oracle IOT、MySQL InnoDB 主键聚簇用的是显式主键;达梦这里的聚簇键是伪列 ROWID。每张表只允许一个聚簇索引。建表时默认按 ROWID 聚簇;一旦建立自定义聚簇索引,默认的 ROWID 聚簇即失效。

1.2 二级索引

二级索引只保存索引列和对应 ROWID,并按索引列排序:

C1 | ROWID || C1 | ROWID || ...

查询若只需索引列,读完索引即可结束。若还需要其他列,就要用 ROWID 回到基表读整行。这个“回到基表取数”的步骤,就是回表,对应计划中的 BLKUP / BLKUP2。

1.3 聚簇索引

聚簇索引会按索引键把整行数据物理重组:

C1 | C2 | ROWID || C1 | C2 | ROWID || ...

按聚簇键做范围查询时,叶子节点上已经有完整行,通常不必再回表。代价是:原 ROWID 直达通道消失,WHERE ROWID = ? 不能再走 CSEK。
省一次 BLKUP 并不等于聚簇索引永远优于二级索引,还要看查询列集合与选择性。

1.4 B+ 树高度与 IO

在 1KB 行、8KB 页的 DM8 实测下,B+ 树层数与容量大致如下:

层数 最大行数
1 层(仅叶子) 31
2 层 约 53,630
3 层 约 1.03 亿
4 层 约 21 亿

表超过约 1 亿行、树高到 4 层时,通常需要评估分区:每多一层就多一次索引 IO。页大小改为 16KB 或 32KB 时,单页容量相应增加。

回表之所以贵,主要是因为它引入额外存储 IO。常见介质延迟量级:

介质 延迟
NVMe SSD PCIe 5.0 50–60 μs
NVMe SSD PCIe 3.0 约 100 μs
企业级 SAS/SATA SSD 100–500 μs
HDD 毫秒级

在较理想的 NVMe 场景下,存储 IO 仍可占数据检索链路耗时的 90% 以上;生产环境中这一比例常常更高。回表次数一大,延迟会按随机 IO 叠加。


2. 在计划里认出 BLKUP2

官方定义可以概括为:二级索引通常只存“索引列 + ROWID”。若 SELECT 列表中还有索引未覆盖的列,优化器必须用 ROWID 回基表读行。BLKUP 的开销与参与回表的行数成正比。

典型形态是 SSEK2 上方挂着 BLKUP2:

3   #BLKUP2: [257, 250000, 68]; IDX$$_2(TX); use_clu_addr(0)
4     #SSEK2: [257, 250000, 68]; scan_type(ASC), IDX$$_2(TX), scan_range[999998,999998]

读法是自下而上:SSEK2 先在二级索引上按范围定位,得到一批 ROWID;BLKUP2 再用这些 ROWID 回基表取行。方括号中的三个数字依次是代价、估算行数、行宽估算,后文案例会反复用到。


3. 同一张表上的三次计划变化

以下取自官方文件中的调优实例。表 tx(c1, c2, c3) 约 1000 万行,查询条件为 WHERE c1 = 999998。

3.1 无索引:全表扫描

SELECT * FROM tx WHERE c1 = 999998; -- 已用时间: 378.143 毫秒
1   #NSET2: [1283, 250000, 68]
2     #PRJT2: [1283, 250000, 68]; exp_num(4)
3       #SLCT2: [1283, 250000, 68]; TX.C1 = 999998
4         #CSCN2: [1283, 10000000, 68]; INDEX33555476(TX)

CSCN2 表示按堆表/聚簇结构扫描整表,此处扫描 1000 万行,代价 1283。估算行数为 250000,这是缺少统计信息时的默认选择率,实际命中只有 1 行。过滤发生在 SLCT2:先读表,再按 C1 筛选。

3.2 二级索引:定位后再回表

CREATE INDEX IDX$$_2 ON "SYSDBA"."tx"("c1"); SELECT * FROM tx WHERE c1 = 999998; -- 已用时间: 12.248 毫秒
1   #NSET2: [257, 250000, 68]
2     #PRJT2: [257, 250000, 68]; exp_num(4)
3       #BLKUP2: [257, 250000, 68]; IDX$$_2(TX); use_clu_addr(0)
4         #SSEK2: [257, 250000, 68]; scan_type(ASC), IDX$$_2(TX), scan_range[999998,999998]

代价从 1283 降到 257,耗时约降为原来的 1/31。索引已经发挥作用,但 SELECT * 需要 c2、c3,因此 SSEK2 之后仍有 BLKUP2。

3.3 只查询索引列:回表消失

SELECT c1 FROM tx WHERE c1 = 999998; -- 已用时间: 1.949 毫秒

索引与过滤条件都未改,只是投影列从全部列改为 c1。索引条目已包含所需数据,BLKUP2 不再出现。耗时从 12.248 毫秒降到 1.949 毫秒,相对上一版约快 6 倍,相对全表扫描约快 194 倍。

本例只回表 1 行,回表仍把查询拖慢约 6 倍。业务 SQL 若回表数千、数万行,差距会继续放大。官方同时指出两点:全表扫描代价高,应建立正确的二级索引;SELECT * 会把不需要的列自下而上传递,列存储表上还会削弱按列 IO 的优势。


4. 回表的成本从何而来

4.1 回表次数

案例库中的关系可以写成:

BLKUP 回表次数 = 驱动行数 × 内表命中数

嵌套循环中这一点特别明显。外层 1000 行、内层每次命中 440 行,回表次数就是 44 万。单次回表看起来不重,乘上连接基数后会成为计划中的主要开销。

4.2 随机 IO

二级索引按索引列有序,但各条目上的 ROWID 在基表中通常并不连续(除非数据已按该列聚簇)。逐行回表等于在基表上随机寻址。一次随机 IO 约 50–500 μs,1 万次大约是 0.5–5 秒,且随存储介质变差而上升。

4.3 优化器何时放弃索引

当估算命中行数占表总量大约超过 5%–15% 时(行越宽、数据越散,阈值越低),逐行回表的随机 IO 会超过一次顺序全表扫描。CBO(基于代价的优化器)可能改选 CSCN2。这也是“估算行数是否准确,会直接改变计划形态”的物理原因:估算偏高,更容易跨过这条拐点,丢掉本可使用的索引。


5. 减少回表的四条路径

5.1 覆盖索引

把查询需要的列都放进同一棵索引,索引叶子即可给出结果。

-- 需要 status、amount,仅在 create_time 上建索引时通常要回表 CREATE INDEX idx_orders_ct ON orders(create_time, status, amount);

计划形态往往从 SSEK2 → BLKUP2 变为单独的 SSCN 或 SSEK2(下方不再挂 BLKUP)。覆盖索引是消除回表最稳妥的结构手段,代价是索引更宽、维护成本更高,需要按真实 SELECT 列表设计,而不是把所有列塞进索引。

5.2 只查询需要的列

若业务并不需要全部列,应写明确的列清单。这是成本最低的改法:上一节第 3 步已经说明,仅改变投影列就可以去掉 BLKUP2。除非接口约定必须返回整行,否则应避免 SELECT *。

5.3 索引过滤下推(ENABLE_INDEX_FILTER)

达梦提供类似 MySQL ICP(Index Condition Pushdown)的能力,常用 HINT 为 /*+ ENABLE_INDEX_FILTER(1) */。

目标是把过滤与回表的顺序从 BLKUP → SLCT 调整为 SLCT → BLKUP:先在索引层完成能够完成的过滤,再只对剩余行回表。

  • BLKUP → SLCT:候选行先全部回表,再过滤,回表行数大。
  • SLCT → BLKUP:索引层先缩小集合,再回表,回表行数小。

官方案例中,某查询从 44 秒降到 968 毫秒。适用与否取决于索引能否承担过滤、以及回表本身是否足够贵:

较适用 不太适用
索引能覆盖过滤条件,且筛选率高 索引列重复值多,筛选率低
表大、索引相对较窄,回表代价高 过滤列不在索引中,或表很小

5.4 聚簇索引

按最常用的查询键把整行物理重组后,该键上的范围扫描通常不再回表。限制同样明确:全表只能有一个聚簇索引,且 ROWID 直达失效。这更接近建表时的存储决策,而不是上线后随时可改的局部调优。


6. CSCN 与 SSCN:是否回表决定扫描对象

在“有没有合适索引、要不要回表”之间,优化器会在堆表/聚簇全扫(CSCN)和二级索引扫描(SSCN)中选择。官方判据的核心是:当前查询列是否已经在索引里。

查询 索引情况 常见选择 原因
SELECT C1 FROM T1 C1 上有二级索引 SSCN(索引全扫) 索引条目已含 C1,不必回表
SELECT C2 FROM T1 仅 C1 上有索引 CSCN 索引没有 C2,回表读完全表往往不如直接扫表

SSCN 扫描的是整棵二级索引,而不是堆表。索引通常比表窄(只有索引列 + ROWID),因此在不回表的前提下,它可以替代全表扫描。当索引宽度接近表宽度时,这一优势减弱,CBO 会回到 CSCN。


7. 与回表相关的 PLAN_OP_FLAG

PLAN_OP_FLAG 是会话级参数,也支持 HINT。与回表相关的位值如下:

位值 作用
4 仅优化 BLKUP:在 B 树中找到一行后,继续在当前页查找下一条,利用页内局部性
8 在 4 的基础上,于 BLKUP 下方增加 SORT,使送入回表的 ROWID 更紧凑;额外付出排序代价
12 在 4 的基础上,对每批送入 BLKUP 的数据做 bdta 排序,不额外增加排序操作符;适合数据较乱且批量较大的场景
512 回表行数多时提高回表代价,使优化器更不倾向选择回表路径
65536 禁止使用聚集索引记录地址来优化 BLKUP

512 的官方实测:同一条 SQL 回表 187500 行,PLAN_OP_FLAG(0) 时代价为 181,PLAN_OP_FLAG(512) 时升到 259。代价被抬高后,CBO 可能改选不回表的计划。该开关适合在怀疑“回表代价被低估”时做对照实验,而不是作为默认配置。


8. SSCN → BLKUP → SLCT 并不总是更差

存在一类例外:SLCT 条件是非等值且过滤性不强(例如 id1 > 5、LIKE '%x%'),若强制走某单列索引,计划可能变成 SSCN → BLKUP → SLCT。在需要尽快返回前几行、并配合 LIMIT / TOP 时,这条路径经常仍然合理。

原因是有序索引可以从扫描起点按 ORDER BY 顺序产出记录,不必先物化一个很大的中间结果再排序。即使回表之后大量行会被过滤掉,端到端延迟仍可能更低。

判断时同时看三件事:过滤条件的选择性、是否要求提前返回、以及 LIMIT 的大小。不能仅因“先回表再过滤”就否定该计划。


9. 有实例时的对照实验

DROP TABLE tx; CREATE TABLE tx(c1 INT, c2 INT, c3 VARCHAR(100)); INSERT INTO tx SELECT LEVEL+1, LEVEL+2, RPAD('B',10,'B') FROM DUAL CONNECT BY LEVEL<=10000000; COMMIT; BEGIN DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'TX'); END; / SELECT * FROM tx WHERE c1=999998; EXPLAIN SELECT * FROM tx WHERE c1=999998; CREATE INDEX IDX$$_2 ON tx(c1); BEGIN DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'TX'); END; / SELECT * FROM tx WHERE c1=999998; EXPLAIN SELECT * FROM tx WHERE c1=999998; SELECT c1 FROM tx WHERE c1=999998; EXPLAIN SELECT c1 FROM tx WHERE c1=999998;

记录三次实际耗时。

下面截图来自本地虚拟机:
image.png

无索引全表扫描:
image.png

image.png

二级索引 + 回表:

image.png
image.png

只查索引列:

image.png
image.png)

说明:A 远慢于 B(索引有效),B 有 BLKUP2、C 没有(覆盖有效)

10. 操作符速查

操作符 含义 是否回表
CSCN2 堆表 / 聚簇结构全扫 否
SSCN 二级索引全扫 查询列不在索引中时需要
SSEK2 二级索引等值或范围定位 查询列不在索引中时需要
CSEK2 聚簇定位(含 ROWID 直达) 否
BLKUP2 回表 该操作符本身就是回表

减少回表的顺序通常是:覆盖索引、收缩 SELECT 列表、ENABLE_INDEX_FILTER(1)、建表期考虑聚簇索引。相关参数见 PLAN_OP_FLAG 的 4 / 8 / 12(优化回表过程)、512(提高回表代价)、65536(禁用地址优化)。

结构或 HINT 调整之后,仍应以 EXPLAIN 与 ET() 对比前后计划与耗时。官方材料中存在“按理论改完反而变慢”的例子,最终依据是实测,而不是操作符是否从计划中消失。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服