关于磁盘sstable的多版本存储方式和buffer表原理的疑问

【 使用环境 】测试环境
【 OB or 其他组件 】OB
【 使用版本 】3.2.4

官方文档中对于 buffer 表的定义有一个描述:“有大量的索引列更新”
于是有一个问题:为什么对非索引列的大量更新不会产生buffer 表问题?

以下是基于对 lsm-tree 的 key-value 存储格式进行的推理:

  • 更新索引列,索引列的值会变,lsm-tree 是 key-value 存储,对于索引,它的 key 类似于:tableid_{索引列的值}_{主键列的值},key 变了,lsm-tree 中会有一个新的记录。大批量反复更新,会导致lsm-tree中新增很多记录;

  • 更新非索引列,实际是对主表数据进行更新,主表的 key 类似于:tableid_{主键列的值},由于主键列的值没有更新,key 就不会更新,lsm-tree 中不会产生新的记录。大批量反复更新,不会导致 lsm-tree中新增记录。

这个解释有个问题,对照 rocksdb 的解释,lsm-tree 里存储的 key 实际上还有个 version 部分(实际就是事务的提交版本号),类似于 tableid{主键列的值}_version,对于更新非索引列的场景,key 其实也会更新,表现在 _version 的变化。

这样推理下来,需要深究 OB 中如何存储多版本数据,设计了一个测试:

  1. 表本来有 13000 行数据,每次 update 主键值相同的 3000 行数据,更新非索引字段

  2. update 3000 行,全表扫描:16000 行(MEMSTORE_READ_ROW_COUNT:3000,SSSTORE_READ_ROW_COUNT:13000)

  3. 转储一次(mini merge),全表扫描:16000 行

  4. update 3000 行,全表扫描:19000 行(MEMSTORE_READ_ROW_COUNT:3000,SSSTORE_READ_ROW_COUNT:16000)

  5. 转储一次(mini merge),全表扫描:19000 行。说明 2 个 mini sstable 中各有 3000 行记录,加上 major sstable 中的 13000 行,加起来要扫 19000 行

  6. update 3000 行,全表扫描:22000 行(MEMSTORE_READ_ROW_COUNT:3000,SSSTORE_READ_ROW_COUNT:19000)

  7. 再转储一次,触发 mini minor merge,全表扫描:16000 行。说明 mini sstable 进行 compaction 时,对于同一行数据只会保留一个记录,所以扫描行数= (major sstable里的 13000 行)+(mini sstable 里的 3000 行) 。这里一个记录不是指一个版本,如果只保留最新一个版本,如果有事务要读取在 undo_retention 时间内的旧版本,会报错,但可以在实验开始前设置隔离级别为 serializable 并启动一个事务,读取表的数据,在第7步再次读取不会报错,证明旧版本数据没有被清理。这里似乎可以证明 sstable 中数据行的多版本还是以 MvccRow 链表保存的。

根据实验结果,是否能够说明 OB 的存储实现里,sstable 存储多版本的格式中,key 部分是 table_id_{主键列的值},没有 _version 部分,而是和 memstable 中一样用了 MvccRow 链表?

那么,对于大量 delete + insert 场景,如果表有显示主键,每次insert 的行主键不变(比如每次都是 insert into t1 select * from t2),由于 rowkey 不变,也不会产生buffer表问题。

所以这个帖子最终想请教两个结论:

  1. OB 中 sstable 数据行的多版本存储形式是 table_id_{主键列的值}_version,对同一“行”数据,如果有多个版本,就会存有多“行”;还是和 memstable 中一样用了 MvccRow 链表,对于同一“行”数据,如果有多个版本,只存一“行”链表?
  2. buffer 表的形成条件比较多,对于大量 delete + insert 场景,最典型的是使用全局临时表,因为一般不会有显示主键;对于普通表,如果有显示主键,场景是 insert into t1 select * from t2,把 t1 表当临时表来用,其实不会有 buffer 表问题?

关于OceanBase中的“Buffer表”,它并不是一个实际存在的表类型,而是一种特殊业务场景下才会触发的一种SQL执行异常现象。具体来说:

  1. 触发条件:Buffer表现象通常发生在数据执行INSERT后,且绝大部分数据很快会被DELETE的情况下。这意味着表存量数据很小,正常情况下该表上的SQL执行计划都是主表全表扫描。但由于短时间内INSERT和DELETE的数据量级非常大,且表的数据块高水位没有及时回收,或者INSERT量级大于DELETE量级导致数据积压,使得真实扫描的表数据块数据较多,从而导致SQL性能下降。

  2. 影响因素:索引字段的超高频率执行UPDATE也会一定概率触发索引表Buffer情况,因为索引字段的UPDATE是通过INSERT和DELETE来维护索引表的。

  3. 优化建议:对于Buffer表现象,建议在日常情况下不做特殊处理,因为这属于低频小概率场景,触发条件苛刻。但在应急情况下可以通过outline绑定settle_id索引来优化。如果为了保证SQL性能稳定,可以在Hint中指定settle_id字段的索引,尽管这可能会牺牲一些日常性能。

高水位、数据积压这些描述不太符合 lsm-tree,我还是想从多版本的存储格式这个角度探讨下,顺便解决下实验中疑问: gv$sql_audit 里 MEMSTORE_READ_ROW_COUNT、SSSTORE_READ_ROW_COUNT 两个指标能不能体现多版本的“行”数

  1. MEMSTORE_READ_ROW_COUNT:
  • 这是在内存存储中读取的行的数量。内存存储是用于加速查询和减少物理IO的结构。
  1. SSSTORE_READ_ROW_COUNT:
  • 这是在磁盘存储(即SSSTORE)中读取的行的数量。SSSTORE通常指的是有关冷数据或不常用数据的存储。
    体现不出来多版本行数

1.SSTable 和 Memtable 的数据存储形式有一些不同。具体来说,SSTable 使用了一个多版本存储的形式,每个版本的行数据是通过 table_id_{主键列的值}_version 的形式存储的。因此,如果同一“行”有多个版本,SSTable 中会存储多行,每一行对应一个特定的版本。
而在 Memtable 中,OceanBase 使用了类似 MVCC 的存储结构,多个版本的数据会以链表的形式存储在一个“行”中,这样可以有效地对同一行的多个版本进行管理。
2. 对于有显示主键的普通表,使用 INSERT INTO t1 SELECT * FROM t2 时,通常不需要担心 buffer 表的问题。不过,在高并发和大量数据变更的场景中,仍然需要考虑优化策略,以减少对性能的影响。 如果可能,建议在设计表结构时尽量避免使用无主键的表,以提高数据操作的效率和稳定性。

1 个赞

感谢答复~
既然是这样,那还是不太理解“为什么对索引列的大量更新会产生 buffer 表问题,对非索引列的大量更新却不会?”
楼上老师说因为索引字段的UPDATE是通过INSERT和DELETE来维护索引表;那非索引字段的 update 是在主表上操作:写入一个更新后的记录,不是 delete + insert。

意味着只有 SSTable 中存在大量 delete 标记的数据才会造成 buffer 表;而单纯的多次 update 后 SSTable 上对同一“行”数据存储了多行版本数据,并不会造成 buffer 表问题?

@轻松的鱼 对非索引列的大量更新也可能会产生buffer 表问题

buffer表,表示的就是频繁插入删除的表,对非索引执行大量更新也会产生。
LSM-Tree增量数据被删除的数据是标记删除(包含索引和非索引字段)
同一行数据存储多行版本产生buffer表没见过,这个没必要深究。理论上产生buffer原因说的是大量更新操作并未说多行更新限制。

get