ITPub博客

首页 > 数据库 > 国内数据库 > 胖头鱼的技术专栏-483 索引里也掺了水分(20261028)

胖头鱼的技术专栏-483 索引里也掺了水分(20261028)

原创 国内数据库 作者:yhw1809 时间:2026-10-27 12:04:02 0 删除 编辑

胖头鱼的技术专栏-483 索引里也掺了水分(20261028)

作者:胖头鱼的鱼缸(尹海文)
Oracle ACE Pro: Database
PostgreSQL ACE
10年+数据库行业经验
拥有OCM 11g/12c/19c、MySQL 8.0 OCP、Exadata、CDP等认证
墨天轮MVP,ITPUB认证专家
圈内拥有“总监”称号,非著 名社恐(社交恐怖分子)
全网同名:胖头鱼的鱼缸
ITPUB:yhw1809
除授权转载并标明出处外,均为“非法”抄袭

914fcc7ad57defa7868c3be1ca7fb4f5.jpg

一张 300 万行的订单表,status 列只有 8 个取值。某天顺手查了下它的索引大小:65 MB。

8 种值,65 MB。这个数字盯上三秒,很难不上火——里面到底装了什么?

没装错,B-Tree 就是这么老实。表里每一行,它都规规矩矩地在叶子上放一条记录——8 种值不假,但 300 万行也是 300 万行,重复的键值,被一笔一画抄了 300 万遍。

说白了,索引里掺着水分。

翻金仓 KES V9 新版说明书(V009R002C016)的时候,在可用性那一节看到一行:

新增B-Tree 索引数据去重功能,支持通过deduplicate_items 存储参数灵活配置,有效节省存储空间并降低磁盘 I/O 开销。

一句话,别的什么都没说。到底能省多少、有没有代价——摆一局实测,把数记全。

一、水分是怎么进去的

众所周知,索引的作用是"帮你找到每一行",所以每条索引记录里有两样东西:索引列的值、指向表里那一行的位置。再加一点内部开销,就是一条记录的全部。

两样里,位置是必须的——每一行都得能被指到。冤的是"值":status 就 8 种取值,值本身几十个字节打得住了,但它出现 300 万次,每一次出现都从头到尾自带一份完整拷贝。像一份 8 页的说明书,每页都把封面重印一遍。

KES索引去重配图1叶页布局对比.png

叶页里的记录是按键排好序的,排在一起的那些记录,键相同的机会很大——status 这种列上,一个叶子页里大部分键都一样。于是就有了"掺水"的结构性原因: 同一个页内,重复的键各存各的。

把账摊开算:那张表 300 万行,索引 67,870,720 字节,摊到每行是 22.6 个字节。

那"不掺水"的版本该长什么样?想象一下:每个值只存一份,行位置一个不少地全留着。这么算下来,每行只需要摊到那几个字节的行位置,加上很少的摊销开销。

实测里这个数是 7.8 个字节。

22.6 到 7.8,多出来的那 14.8 个,就是水分。

二、挤一下:65 MB → 22 MB

V009R002C016 给 B-Tree 加的去重能力,说穿了很朴素: 让重复的键只存一份,后面挂一串行位置。参数名叫  deduplicate_items。

先把测试的摆法交代清楚,再上数字。这轮一共测了四组:索引体积、查询 I/O、写入耗时,还有不同重复度下的收益变化——

  • 环境:一台测试环境上的 KES V009R002C016
  • 数据:一张 300 万行的表,在它身上造了多列不同重复度的数据——从"每值平均重复 150 万条"一路到"唯一列"
  • 对象:每档建一个  on 索引,和"不去重"的基准比体积;8 个值那列多建两个——明确  off 的,和 什么都不写的(文档没写默认值,那就别猜,建出来比大小)
  • 怎么比:体积直接比字节数和页数——同表同数据,只差建索引参数这一个变量;查询 I/O 用一对"各只挂单一索引"的表对照;耗时类的操作(建索引、插入、更新)都交替多轮、取稳定轮次

一句丑话放前面: 单台测试环境、单次测量,下面的数字只代表这套环境下的量级——换台机器,绝对值会有出入,比例关系才是要看的。

KES索引去重配图2体积对比.png

结果很干脆: 65 MB 变 22 MB,省掉 65.5%;页数从 8,285 掉到 2,857,差了接近三倍。

但这轮测试里最重要的发现不是省了多少,是图上第二行:

什么都不写,跟 off 一字节不差(67,870,720 字节,页数一模一样)。

也就是说: 这个能力默认是关的。 升级到新版本不会自动生效,新建索引也不会自动去重——想用,得明确写上  WITH (deduplicate_items = on)。

三、重复少到多少,就挤不出水了

去重干的是"页内合并相同的键",那顺理成章的问题:重复少到什么程度,这招就不灵了?

我把 300 万行造出不同重复度的列,一档一档量下来,结果跟直觉不太一样:

KES索引去重配图3收益曲线.png

  • 平均每值重复  30 条到 150 万条(六个档位):收益全在  -65% 上下——一条平台线
  • 平均重复  10 条:-58%;平均重复  3 条:-27%
  • 唯一列(每行一个值):0%(顺路验了下:唯一索引上写这个参数不报错、也没影响——它本来就轮不到)

最值得记下的结论是: 去重挑的不是"重复的总量",而是"有没有重复"。

B-Tree 里相同的键天然排在一起,页内总能凑出可合并的邻居——从"平均每值重复 30 条"往上,收益就不再变化。真正零收益的只有一种: 事实唯一的列——订单号、流水号、UUID 这类每行一个新值的。

所以挑对象的口径也跟着换了:不是"低基数列才值得开",而是——

  • 值得看的: 任何有重复的列(现实里大多数列都在这个范围)。重复够密更好,稀疏一点(平均每条值十条以上)也照样有赚头
  • 不用折腾的: 事实唯一的列——没有水可挤,写不写参数一个样

四、省了空间,代价呢

空间省下来了,下一个问题:查起来、写起来有变化吗?

查:读的页少了

同一条查询,我放到两张结构完全一样的表上——一张只挂去重索引,一张只挂普通索引。(不这么摆的话,优化器从多个索引里挑谁用是随机的,对比没法归因。)VACUUM 之后跑同一条等值查询,命中 37.5 万行:

KES索引去重配图4查询IO对比.png

  • 去重索引:读 356 页,28.1 毫秒
  • 普通索引:读 1,032 页,33.4 毫秒

读页数少了 65.5%,跟体积一个比例——索引小了,同样的扫描读的页就少,这很好理解。

但耗时只快不到两成(16%)。为什么没跟着页数同比例降?因为省的是 I/O——这条查询里还有一块不缩水的开销:把 37.5 万行数一遍的 CPU。索引再小,数数的活儿一分没少。

写:基本相当

KES索引去重配图5耗时对比.png

  • 建索引(300 万行):两对对照都在 0.6~0.95 秒区间,没有系统性差异
  • 带索引批量插入(300 万行):去重 5.30 秒,不去重 5.86 秒——同一水平。(无索引光插的参照是 2.34 秒:索引维护这块大头,两种索引都要付)
  • 批量 UPDATE(每次 30 万行、交替 5 轮):中位数 1.32 秒对 1.24 秒

这几组数得把口径说清楚:单台环境、单次测量,而且这个量级的操作,测量本身的波动不小(同一个操作两遍能差出两三成)。所以准确的说法是—— 写入侧没有肉眼可见的代价,不是"更快"。

五、给自家索引挤水

本期跟随总监,把要用的 SQL 摆出来。三步:看、开、验。

第一步,看:哪些索引里有水

按大小排个队,从占地方最多的往下看:

SELECT indexrelname,
       sys_size_pretty(sys_relation_size(indexrelid)) AS size
FROM sys_stat_user_indexes
WHERE relname = '你的表名'
ORDER BY sys_relation_size(indexrelid) DESC;

队伍排出来之后,看一眼列上的重复情况——只要不是事实唯一(订单号、UUID 这类),都够得着收益;排在前面的,就是最值得先挤的。

第二步,开

新索引,建的时候写明:

CREATE INDEX idx_xxx ON 表名 (列名) WITH (deduplicate_items = on);

存量索引,先改参数、再 REINDEX:

ALTER INDEX idx_xxx SET (deduplicate_items = on);
REINDEX INDEX idx_xxx;

两步缺一不可。实测里: 光 ALTER 改参数,体积一动不动——reloptions 已经显示  on 了,65 MB 还是 65 MB;只有 REINDEX 之后,才真正变成 22 MB。ALTER 登记的是"下次重建时用新参数",物理结构得重建才落地。

KES索引去重配图6存量索引开启.png

第三步,验

参数看 reloptions:

SELECT relname, reloptions
FROM sys_class
WHERE relname = 'idx_xxx';

体积看  \di+,或者把第一步的查询再跑一遍,前后一对就清楚了。

KES索引去重配图7实操清单.png

总结

回到开头那 65 MB。

去重没什么魔法,就是把叶页里重复的键合并成一份,行位置一个不少地留着。效果实在:300 万行、8 个值的场景,65 MB 变 22 MB,同样的查询少读 65.5% 的页。

实测里还有几个容易略过的点,我觉得比"省了 65%"本身更值得记住:

  • 受益面比想象宽——不是"低基数列专属",只有事实唯一的列完全没收益;平均每值重复到十条以下,收益才开始明显缩水
  • 默认是关的——升级版本、建新索引,都不会自动去重,得把参数明确写上
  • 改参数不等于生效——存量索引 SET 完还是老样子,REINDEX 之后才落地

所以我的建议:

  • 有重复的列都值得看一眼(现实里大多数列都是),重复密、占地方大的优先
  • 事实唯一的列不用折腾——订单号、UUID 这类,写不写参数一个样
  • 存量索引:先按大小排队,挑真正占地方的补上;重建有成本,挑业务低峰做

老规矩,知道写了些啥。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31466763/viewspace-3186773/ ,如需转载,请注明出处,否则将追究法律责任。

下一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2017-06-07

  • 博文量
    387
  • 访问量
    408230