ValueVector#
ValueVector
接口 (在 C++ 实现中称为 Array,在规范中也是如此) 是一种抽象,用于存储单个列中具有相同类型的值序列。 在内部,这些值由一个或多个缓冲区表示,缓冲区的数量和含义取决于向量的数据类型。
对于规范中描述的每种基本数据类型和嵌套类型,都有 ValueVector
的具体子类。 在命名方面与规范中描述的类型名称存在一些差异:具有非直观名称的表格 (BigInt = 64 位整数等)。
重要的是,向量在尝试读取或写入之前进行分配,ValueVector
“应该”努力保证这种操作顺序:创建 > 分配 > 变更 > 设置值计数 > 访问 > 清除(或分配以重新开始该过程)。 我们将在下一节中通过一个具体的示例来演示每个操作。
向量生命周期#
如上所述,每个向量在其生命周期中都经过多个步骤,并且每个步骤都由向量操作触发。 特别是,我们有以下向量操作
1. 向量创建:我们通过向量构造函数等方式创建一个新的向量对象。 以下代码通过构造函数创建一个新的 IntVector
RootAllocator allocator = new RootAllocator(Long.MAX_VALUE);
...
IntVector vector = new IntVector("int vector", allocator);
到目前为止,已创建了一个向量对象。 但是,尚未分配任何底层内存,因此我们需要以下步骤。
2. 向量分配:在此步骤中,我们为向量分配内存。 对于大多数向量,我们有两种选择:1) 如果我们知道最大向量容量,则可以通过调用 allocateNew(int)
方法来指定它; 2) 否则,我们应该调用 allocateNew()
方法,并为其分配默认容量。 对于我们的运行示例,我们假设向量容量永远不会超过 10
vector.allocateNew(10);
3. 向量变更:现在我们可以使用所需的值填充向量。 对于所有向量,我们可以通过向量编写器填充向量值(将在下一节中给出一个示例)。 对于原始类型,我们还可以通过 set 方法更改向量。 set 方法分为两类:1) 如果我们可以确定向量具有足够的容量,则可以调用 set(index, value)
方法。 2) 如果我们不确定向量容量,则应调用 setSafe(index, value)
方法,如果容量不足,该方法将自动处理向量重新分配。 对于我们的运行示例,我们知道向量具有足够的容量,因此我们可以调用
vector.set(/*index*/5, /*value*/25);
4. 设置值计数:对于此步骤,我们通过调用 setValueCount(int)
方法来设置向量的值计数
vector.setValueCount(10);
完成此步骤后,向量进入不可变状态。 换句话说,我们不应再更改它。 (除非我们通过再次分配来重用该向量。 这将在稍后讨论。)
5. 向量访问:现在可以访问向量值了。 同样,我们有两种选择来访问值:1) get 方法和 2) 向量读取器。 向量读取器适用于所有类型的向量,而 get 方法仅适用于原始向量。 向量读取器的具体示例将在下一节中给出。 下面是通过 get 方法访问向量的示例
int value = vector.get(5); // value == 25
6. 向量清除:当我们完成向量的操作后,应清除它以释放其内存。 这可以通过调用 close()
方法来完成
vector.close();
关于上述步骤的一些注意事项
这些步骤不一定按线性顺序执行。 相反,它们可以位于循环中。 例如,当向量进入访问步骤时,我们也可以返回到向量更改步骤,然后设置值计数、访问向量等等。
我们应该尽量确保按顺序执行上述步骤。 否则,向量可能处于未定义状态,并且可能会发生一些意外行为。 但是,此限制并不严格。 这意味着我们可能会违反上述顺序,但仍然获得正确的结果。
通过 set 方法更改向量值时,我们应尽可能首选
set(index, value)
方法而不是setSafe(index, value)
方法,以避免处理向量容量时产生不必要的性能开销。所有向量都实现了
AutoCloseable
接口。 因此,当不再使用它们时,必须显式关闭它们,以避免资源泄漏。 为了确保这一点,建议将向量相关操作放入 try-with-resources 块中。对于固定宽度向量(例如 IntVector),我们可以按任意顺序在不同的索引处设置值。 但是,对于可变宽度向量(例如 VarCharVector),我们必须按索引的非递减顺序设置值。 否则,设置位置之后的值将变为无效。 例如,假设我们使用以下语句来填充可变宽度向量
VarCharVector vector = new VarCharVector("vector", allocator);
vector.allocateNew();
vector.setSafe(0, "zero");
vector.setSafe(1, "one");
...
vector.setSafe(9, "nine");
然后我们再次设置位置 5 的值
vector.setSafe(5, "5");
之后,向量位置 6、7、8 和 9 处的值将变为无效。
构建 ValueVector#
请注意,当前的实现不会强制执行 Arrow 对象是不可变的规则。 可以直接使用 new 关键字创建 ValueVector
实例,存在用于填充值的 set/setSafe API 和 FieldWriter 的具体子类。
例如,下面的代码演示了如何构建一个 BigIntVector
,在本例中,我们构建一个范围为 0 到 7 的向量,其中应保存第四个值的元素为空
try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE);
BigIntVector vector = new BigIntVector("vector", allocator)) {
vector.allocateNew(8);
vector.set(0, 1);
vector.set(1, 2);
vector.set(2, 3);
vector.setNull(3);
vector.set(4, 5);
vector.set(5, 6);
vector.set(6, 7);
vector.set(7, 8);
vector.setValueCount(8); // this will finalizes the vector by convention.
...
}
BigIntVector
保存两个 ArrowBufs。 第一个缓冲区保存 null 位图,此处由单个字节组成,其位为 1|1|1|1|0|1|1|1(如果该值不为 null,则该位为 1)。 第二个缓冲区包含所有上述值。 由于第四个条目为 null,因此缓冲区中该位置的值未定义。 请注意,与 set API 相比,setSafe API 会在设置值之前检查值容量,并在必要时重新分配缓冲区。
以下是如何使用编写器构建向量
try (BigIntVector vector = new BigIntVector("vector", allocator);
BigIntWriter writer = new BigIntWriterImpl(vector)) {
writer.setPosition(0);
writer.writeBigInt(1);
writer.setPosition(1);
writer.writeBigInt(2);
writer.setPosition(2);
writer.writeBigInt(3);
// writer.setPosition(3) is not called which means the fourth value is null.
writer.setPosition(4);
writer.writeBigInt(5);
writer.setPosition(5);
writer.writeBigInt(6);
writer.setPosition(6);
writer.writeBigInt(7);
writer.setPosition(7);
writer.writeBigInt(8);
}
存在 get API 和 FieldReader
的具体子类用于访问向量值,需要声明的是,编写器/读取器不如直接访问有效
// access via get API
for (int i = 0; i < vector.getValueCount(); i++) {
if (!vector.isNull(i)) {
System.out.println(vector.get(i));
}
}
// access via reader
BigIntReader reader = vector.getReader();
for (int i = 0; i < vector.getValueCount(); i++) {
reader.setPosition(i);
if (reader.isSet()) {
System.out.println(reader.readLong());
}
}
构建 ListVector#
ListVector
是一个向量,它保存每个索引的值列表。 使用一个向量需要处理与上述相同的步骤(创建 > 分配 > 变更 > 设置值计数 > 访问 > 清除),但是你完成此操作的详细信息略有不同,因为你需要同时创建向量并为每个索引设置值列表。
例如,以下代码演示了如何使用编写器 UnionListWriter
构建一个 int 的 ListVector
。 我们构建一个从 0 到 9 的向量,每个索引都包含一个值列表 [[0, 0, 0, 0, 0], [0, 1, 2, 3, 4], [0, 2, 4, 6, 8], …, [0, 9, 18, 27, 36]]。 可以按任何顺序添加列表值,因此编写一个列表(例如 [3, 1, 2])也是有效的。
try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE);
ListVector listVector = ListVector.empty("vector", allocator)) {
UnionListWriter writer = listVector.getWriter();
for (int i = 0; i < 10; i++) {
writer.startList();
writer.setPosition(i);
for (int j = 0; j < 5; j++) {
writer.writeInt(j * i);
}
writer.setValueCount(5);
writer.endList();
}
listVector.setValueCount(10);
}
可以通过 get API 或通过读取器类 UnionListReader
访问 ListVector
值。 要读取所有值,首先枚举索引,然后枚举内部列表值。
// access via get API
for (int i = 0; i < listVector.getValueCount(); i++) {
if (!listVector.isNull(i)) {
ArrayList<Integer> elements = (ArrayList<Integer>) listVector.getObject(i);
for (Integer element : elements) {
System.out.println(element);
}
}
}
// access via reader
UnionListReader reader = listVector.getReader();
for (int i = 0; i < listVector.getValueCount(); i++) {
reader.setPosition(i);
while (reader.next()) {
IntReader intReader = reader.reader();
if (intReader.isSet()) {
System.out.println(intReader.readInteger());
}
}
}
字典编码#
字典编码是一种压缩形式,其中一种类型的值被较小类型的值替换:整数数组替换字符串数组就是一个常见的例子。 原始值和替换之间的映射保存在“字典”中。 由于字典只需要每个较长值的一个副本,因此字典和较小值数组的组合可能会使用更少的内存。 原始数据越重复,节省的越多。
FieldVector
可以进行字典编码,以提高性能或提高内存效率。 如果有很多值,但唯一值很少,则几乎可以对任何类型的向量进行编码。
编码过程涉及几个步骤
创建一个常规的、未编码的向量并填充它
创建一个与未编码向量类型相同的字典向量。 该向量必须具有相同的值,但未编码向量中的每个唯一值只需要在此处出现一次。
创建一个
Dictionary
。 它将包含字典向量,以及一个DictionaryEncoding
对象,该对象保存编码的元数据和设置值。创建一个
DictionaryEncoder
。在
DictionaryEncoder
上调用 encode() 方法,以生成原始向量的编码版本。(可选)在编码向量上调用 decode() 方法以重新创建原始值。
编码后的值将是整数。根据您拥有的唯一值的数量,您可以使用 TinyIntVector
、SmallIntVector
、IntVector
或 BigIntVector
来保存它们。您在创建 DictionaryEncoding
实例时指定类型。您可能想知道这些整数来自哪里:字典向量是一个常规向量,因此该值在向量中的索引位置用作其编码值。
DictionaryEncoding
中的另一个关键属性是 id。了解 id 的使用方式非常重要,因此我们将在本节稍后介绍。
此结果将是一个新向量(例如,一个 IntVector
),它可以代替原始向量(例如,一个 VarCharVector
)。当您以 Arrow 格式写入数据时,写入的是新的 IntVector
和字典:您稍后需要该字典才能检索原始值。
// 1. create a vector for the un-encoded data and populate it
VarCharVector unencoded = new VarCharVector("unencoded", allocator);
// now put some data in it before continuing
// 2. create a vector to hold the dictionary and populate it
VarCharVector dictionaryVector = new VarCharVector("dictionary", allocator);
// 3. create a dictionary object
Dictionary dictionary = new Dictionary(dictionaryVector, new DictionaryEncoding(1L, false, null));
// 4. create a dictionary encoder
DictionaryEncoder encoder = new DictionaryEncoder.encode(dictionary, allocator);
// 5. encode the data
IntVector encoded = (IntVector) encoder.encode(unencoded);
// 6. re-create an un-encoded version from the encoded vector
VarCharVector decoded = (VarCharVector) encoder.decode(encoded);
我们尚未讨论的一件事是如何从原始的未编码值创建字典向量。这留给库用户来处理,因为自定义方法可能比通用实用程序更有效。由于字典向量只是一个普通向量,因此您可以使用标准 API 填充其值。
最后,您可以将多个字典打包在一起,这在您使用带有多个字典编码向量的 VectorSchemaRoot
时非常有用。这是使用名为 DictionaryProvider
的对象完成的,如下例所示。请注意,我们不会将字典向量放在与数据向量相同的 VectorSchemaRoot
中,因为它们通常具有更少的值。
DictionaryProvider.MapDictionaryProvider provider =
new DictionaryProvider.MapDictionaryProvider();
provider.put(dictionary);
DictionaryProvider
只是标识符到 Dictionary
对象的映射,其中每个标识符都是一个长整数值。在上面的代码中,您会看到它作为 DictionaryEncoding
构造函数的第一个参数。
这就是 DictionaryEncoding
的 ‘id’ 属性的用处。该值用于使用 DictionaryProvider
将字典连接到 VectorSchemaRoot
的实例。 这是它的工作原理:
VectorSchemaRoot
具有一个包含Field
对象列表的Schema
对象。该字段具有一个名为 ‘dictionary’ 的属性,但它保存的是
DictionaryEncoding
而不是Dictionary
。如前所述,
DictionaryProvider
保存由长整数值索引的字典。 此值是来自您的DictionaryEncoding
的 id。要检索
VectorSchemaRoot
中向量的字典,您需要获取与该向量关联的字段,获取其 dictionary 属性,并使用该对象的 id 在 provider 中查找正确的字典。
// create the encoded vector, the Dictionary and DictionaryProvider as discussed above
// Create a VectorSchemaRoot with one encoded vector
VectorSchemaRoot vsr = new VectorSchemaRoot(List.of(encoded));
// now we want to decode our vector, so we retrieve its dictionary from the provider
Field f = vsr.getField(encoded.getName());
DictionaryEncoding encoding = f.getDictionary();
Dictionary dictionary = provider.lookup(encoding.getId());
正如您所看到的,DictionaryProvider
对于管理与 VectorSchemaRoot
关联的字典非常有用。 更重要的是,它有助于在写入 VectorSchemaRoot
时打包字典。 类 ArrowFileWriter
和 ArrowStreamWriter
都接受可选的 DictionaryProvider
参数以实现此目的。 您可以在 (Reading/Writing IPC formats) 的文档中找到有关写入字典的示例代码。ArrowReader
及其子类也实现了 DictionaryProvider
接口,因此您可以在读取文件时检索实际的字典。
切片#
与 C++ 实现类似,可以制作向量的零拷贝切片,以通过 TransferPair
获得指向数据某些逻辑子序列的向量
IntVector vector = new IntVector("intVector", allocator);
for (int i = 0; i < 10; i++) {
vector.setSafe(i, i);
}
vector.setValueCount(10);
TransferPair tp = vector.getTransferPair(allocator);
tp.splitAndTransfer(0, 5);
IntVector sliced = (IntVector) tp.getTo();
// In this case, the vector values are [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] and the sliceVector values are [0, 1, 2, 3, 4].