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
类型类似, ShortArrayBlock
、ByteArrayBlock
和 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不一致,如下图所示:
下面有几条重要的规则:
- 若当前的
DictionaryBlock
是compacted,那么其嵌套类型不能是DictionaryBlock
; - 若当前的
DictionaryBlock
是sequential,那么该Block必须是compacted的(同时也表明其嵌套类型不会是DictionaryBlock
类型); - 若当前的
DictionaryBlock
是compacted,那么其sizeInBytes和uniqueIds都可以直接获取了,否则,需要调用calculateCompactedSize()方法才能计算。
Page类型
乍一看,Page
类型与RowBlock
有几分相似,其内部都引用了一个Block数组,不过Page其实是比Block更高层次的封装。Page
与 RowBlock
的区别是如下图所示:
- RowBlock内部嵌套Block数组固定,而Page内部嵌套Block数组是不固定的,可以通过appendColumn()方法和prependColumn()对其增加数据,并引入channel的概念,内部嵌套的一个Block对象就是一个channel,其索引编号是column;
- 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