绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Trino中的Block类型
2022-05-12 15:24:06

在使用分布式SQL查询引擎Trino的时候,不可避免地要接触Block 类型,Trino查询获取数据的单元——页(Page)也是以Block类型为基础的, 深入了解Block结构有助于理解Trino内部的数据组织。

Block在Trino中以接口形式出现,Trino定义了几个基本的实现类, 作为基础的Block结构,然后再定义其他更加高层的Block结构,其内部是其他Block的组合,可以这么理解:当你手上有一个Block的数据,其内部的真实数据可能是引用其他的一个或多个Block。本质上Block是个套娃结构,Block对象是个实现了Block接口的嵌套数据对象,Block存储的底层数据都指向了基本的Block实现类。

Block基本实现类

Block基本实现类是指内部包含直接真实数据,不是以嵌套其他Block对象的形式出现的数据类型。其内部的数据以基本类型数组或者Slice对象出现,这里的Slice指的是一块连续内存区域,后面会讲到。

Block的基本实现类有IntArrayBlock, ShortArrayBlock, ByteArrayBlock, LongArrayBlock,Int96ArrayBlock, Int128ArrayBlock, VariableWidthBlock .

IntArrayBlock

IntArrayBlock 内部以整型数组存放该Block的真实底层数据。具体的内存形式如下图:

IntArrayBlock 的底层数据其实就是int[] values boolean[] valueIsNull 这两个数组,这里boolean[] valueIsNull的出现是为了表示某个位置上的值是否为null,毕竟values 是一个基本类型数组,而不是包装类型Integer数组,所以还是需要有个额外的布尔类型数组表示某个位置是否是null。

这里要注意,values 数组和 valueIsNull 数组的长度不一定相同,因为IntArrayBlock只会使用其中的一部分数据,即索引为 [arrayOffset, arrayOffset+positionCount)的区域。所以这个Block在计算其底层数据大小(sizeInByte)的时候,只会统计自己使用的那一部分。

在计算整个类型占用的字节大小(retainedSizeInByte)时,会计算values valueIsNull占用的全部内存,然后再加上这个类自身使用的内存大小(这里要注意,数组是引用类型,在类中只保存了一个引用标记,所以需要额外计算)。

IntArrayBlock类型类似, ShortArrayBlockByteArrayBlock  LongArrayBlock 无非就是把int[]换成了short[] byte[] Long[], 这几个类型此处就不再赘述了。

Int96ArrayBlock和Int128ArrayBlock

由于java中的整型长是64字节,即long类型,那么如何表示更长字节的整型呢?

Trino使用了long+int来表示一个96位的整型。同样,这里的high数组 和 low数组 的长度也没有必要相同,Block对象只会使用其一部分。

由于没有基本类型可以表示96位的整型,所以从Int96ArrayBlock获取数据需要调用getLong()getInt() 两个方法,才能将一个完整的数据取出。并且由于high  low表示的数据高低位不一样,所以在获取数据时要有明确的offset。只能这么调用:

long high = getLong(position, );
int low = getInt(position, 8);

既然long+int可以表示一个96位的整型,那么long+long可以表示一个128位的整型。

这里values数组中的两个元素表示Block底层数据的一个元素,对应一个valueIsNull的值。 个人感觉这里的代码不如分为两个数组(long[] high  long[] low)来的清晰。

同样,想要获取128位的整型数据,需要调用两次方法:

long high = getLong(position, );
long low = getLong(position, 8);

VariableWidthBlock

VariableWidthBlock使用的底层数据是Slice类型,Slice是Facebook在另一个代码库中定义的数据类型,不仅仅在Trino中使用。

Slice类型是一个内存切片,Slice底层是一段连续的内存空间, 有点像python中列表的切片。Trino使用Slice来高效地操作内存。Slice中可以存储各类基本数据,不过主要使用场景是用来存储字符串。

VariableWidthBlock 实际上是一段内存切片,然后通过offsets数组,分位postionCount个小的内存切片。与前面一样,offsets数组 和 valueIsNull数组 的长度也是可以不等的,VariableWidthBlock 只会使用其一部分。

由于VariableWidthBlock的内部元素(即一小段Slice)的字节长度不定,所以offsets的实际可用元素要比positionCount多一个,用于确定后一个元素的末尾偏移量。 后面的嵌套类型也是如此,元素的字节长度不定,offsets的可用长度必须比positionCount多一个。

Block嵌套类型

所谓Block嵌套类型,就是该Block的内部数值是存储在另一个或多个的Block里。可以说是对Block基本实现类的无限次封装,以实现不同的功能。

ArrayBlock

ArrayBlock 表示的是一个Block数组,其内部结构如图:

ArrayBlock将一个完整的内嵌Block通过offsets数组分隔成一个个小的Block块,可以当成一个Block[] 

这里要注意retainedSizeInByte的计算方式,由于采用了Block嵌套结构,其内部保存的retainedSizeInByte不再包含其内嵌Block占用的内存大小,需要调用getRetainedSizeInBytes()方法,然后该方法会调用其内嵌Block的getRetainedSizeInBytes(),直到终的计算完成。

MapBlock

MapBlock 表示的是一个 { Block -> Block } 的映射结构,其内部结构如下:

MapBlock将两个Block分别存储映射类型的 key 和 value,并且一一对应。那么,根据外部输入,如何找到对应的键值对呢?总不能每次都扫描一遍keyBlock吧?

这里就要提到MapBlock 的一个包装类型了—— SingleMapBlock.

SingleMapBlock

SingleMapBlock  MapBlock 的hashTables利用起来了。

由于SingleMapBlock在内部面对的是整个`AbstractMapBlock(MapBlock的基类),而不是单独的 keyBlock 和 valueBlock , 所以positionCount是其嵌套的AbstractMapBlock的两倍。

SingleMapBlock 利用hashTables找到该映射类型的键值对,具体步骤如下:

SingleMapBlock 会根据外部输入,从hashTable 中找到keyposition的位置,由于然后再指示出得到的键值对在SingleMapBlock中的位置——键的位置是 2 * keyPosition,值的位置是 2 * keyPosition + 1.

RowBlock

RowBlock 将多个具有相同大小(即positionCount相同)的Block对象组合在一起。

SingleRowBlock

SingleRowBlock 的结构与RowBlock 结构类似,同样是内部嵌套了一个Block数组,但是不同的是,SingleRowBlock使用其内部嵌套数组各个元素的一个小单元。具体结构如下:

SingleRowBlock内部存有一个整数——rowIndex,表示SingleRowBlock只使用其嵌套的fieldBlock数组中偏移量位rowIndex的一小段,而不是像其他Block类型那样有个offsets数组表示使用了嵌套Block的一长段。

DictionaryBlock

DictionaryBlock 是一个非常重要的Block类型。首先概览 DistionaryBlock的内存结构:

DistionaryBlock 有两个非常重要的状态——sequential 和 compacted。

sequential 表示内嵌Block和ids数组指向的偏移量是依序的,如下图所示:

compacted表示ids的大小和内嵌Block的positionCount不一致,如下图所示:

下面有几条重要的规则:

  1. 若当前的DictionaryBlock是compacted,那么其嵌套类型不能是DictionaryBlock;
  2. 若当前的DictionaryBlock是sequential,那么该Block必须是compacted的(同时也表明其嵌套类型不会是DictionaryBlock类型);
  3. 若当前的DictionaryBlock是compacted,那么其sizeInBytes和uniqueIds都可以直接获取了,否则,需要调用calculateCompactedSize()方法才能计算。

Page类型

乍一看,Page 类型与RowBlock有几分相似,其内部都引用了一个Block数组,不过Page其实是比Block更高层次的封装。Page  RowBlock的区别是如下图所示:

  1. RowBlock内部嵌套Block数组固定,而Page内部嵌套Block数组是不固定的,可以通过appendColumn()方法和prependColumn()对其增加数据,并引入channel的概念,内部嵌套的一个Block对象就是一个channel,其索引编号是column;
  2. RowBlock对其内部嵌套的Block数组只能进行整体操作,而Page可以将其内部Block取出,getBlock(int channel) 方法取出一个单独的Block,getColumns(int column)方法单独取出一个Block对象并生成一个单Block的Page;

总的来说,RowBlock对其内部嵌套的Block数组的每一个元素都是一视同仁的,而Page对其内部嵌套的Block数组中的每个元素是区别对待的。

Page类型是Trino中获取数据的基本数据结构。在Trino中,获取数据有两种方式,一种是通过RecordCursor获取终二维表的一条记录,另一种就是利用ConnectorPageSource的getNextPage()方法,获取一个Page的数据,一个Page表示的是二维表的多条记录。

来自:https://zhuanlan.zhihu.com/p/443839491

分享好友

分享这个小栈给你的朋友们,一起进步吧。

Trino
创建时间:2022-04-12 14:37:38
Trino
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 飘絮絮絮丶
    专家
戳我,来吐槽~