Table¶
注意:Table API 目前处于实验阶段,可能会发生变更。请参阅下方的限制列表。
Table 是一种基于 FieldVector 的不可变表格数据结构。与 VectorSchemaRoot 一样,Table 是一种由 Arrow 数组(更具体地说是 FieldVector 对象)支持的列式数据结构。它与 VectorSchemaRoot 的主要区别在于它是完全不可变的,且不支持批处理操作。在流水线中处理表格数据批次的开发者应继续使用 VectorSchemaRoot。最后,Table API 主要面向行,因此在某些方面它更类似于 JDBC API 而不是 VectorSchemaRoot API,但您仍然可以使用 FieldReaders 以列式方式处理数据。
Table 和 VectorSchemaRoot 中的可变性¶
VectorSchemaRoot 为保存其数据的向量提供了一个轻量级包装器。可以从向量架构根中检索各个向量。这些向量具有用于修改其元素的 setter 方法,使得 VectorSchemaRoot 仅在约定上是不可变的。修改向量的协议记录在 ValueVector 接口中:
值需要按顺序写入(例如索引 0, 1, 2, 5)
在写入任何内容之前,空向量的所有值初始都为 null
对于可变宽度类型,偏移量向量在写入前应全为零
在读取向量之前,必须调用 setValueCount
一旦向量被读取,就不应再对其进行写入。
API 并未强制执行这些规则,因此程序员有责任确保遵循这些规则。否则可能会导致运行时异常。
另一方面,Table 是不可变的。底层的向量不会被暴露。当从现有向量创建表格时,它们的内存会被传输到新向量中,因此后续对原始向量的更改不会影响新表格的值。
功能与限制¶
目前提供了一套基本的表格功能
通过向量或
VectorSchemaRoot创建表格按行遍历表格,或直接设置当前行索引
以原始类型、对象和/或可为空的 ValueHolder 实例(取决于类型)访问向量值
获取任意向量的
FieldReader添加和删除向量以创建新表格
使用字典编码对表格的向量进行编码和解码
导出表格数据供原生代码使用
将具有代表性的数据打印为 TSV 字符串
获取表格的 Schema
切片表格
将表格转换为
VectorSchemaRoot
11.0.0 版本中的限制
不支持
ChunkedArray或任何形式的行组(row-group)。未来版本将考虑支持分块数组或行组。不支持 C-Stream API。对流式 API 的支持取决于对分块数组的支持。
不支持直接从 Java POJO 创建表格。表格持有的所有数据必须通过
VectorSchemaRoot,或从向量的集合或数组中导入。
Table API¶
与 VectorSchemaRoot 一样,表格包含一个 Schema 和一个有序的 FieldVector 对象集合,但其设计目的是通过面向行的接口进行访问。
从 VectorSchemaRoot 创建 Table¶
如下所示,表格可以从 VectorSchemaRoot 创建。保存数据的内存缓冲区会从向量架构根传输到新表格中的新向量中,在此过程中会清除源向量。这确保了新表格中的数据永远不会被更改。由于缓冲区是传输而不是复制,因此这是一个开销极低的操作。
Table t = new Table(someVectorSchemaRoot);
如果您现在更新 VectorSchemaRoot 持有的向量(使用某个版本的 ValueVector#setSafe()),它会反映这些更改,但表格 t 中的值保持不变。
从 FieldVectors 创建 Table¶
如下所示,可以使用“变长参数”数组参数从 FieldVectors 创建表格:
IntVector myVector = createMyIntVector();
VectorSchemaRoot vsr1 = new VectorSchemaRoot(myVector);
或通过传递集合创建:
IntVector myVector = createMyIntVector();
List<FieldVector> fvList = List.of(myVector);
VectorSchemaRoot vsr1 = new VectorSchemaRoot(fvList);
在多个向量架构根之间共享向量通常不是一个好主意,在向量架构根和表格之间共享它们更是不妥。从向量列表创建 VectorSchemaRoot 不会导致向量的引用计数增加。除非您手动管理计数,否则下面的代码会导致引用数超过引用计数,这可能会导致问题。这里存在一个隐含假设,即向量是为一个 VectorSchemaRoot 而创建的,而此代码违背了这一假设。
不要这样做
IntVector myVector = createMyIntVector(); // Reference count for myVector = 1
VectorSchemaRoot vsr1 = new VectorSchemaRoot(myVector); // Still one reference
VectorSchemaRoot vsr2 = new VectorSchemaRoot(myVector);
// Ref count is still one, but there are two VSRs with a reference to myVector
vsr2.clear(); // Reference count for myVector is 0.
发生的情况是,引用计数器在比 VectorSchemaRoot 接口更低的级别上工作。引用计数器计算的是对控制内存缓冲区的 ArrowBuf 实例的引用,而不是对持有这些 ArrowBuf 的向量的引用。在上面的示例中,每个 ArrowBuf 由一个向量持有,因此只有一个引用。当您调用 VectorSchemaRoot 的 clear() 方法时,这种区别变得模糊,该方法会释放其引用的每个向量所持有的内存,即使另一个实例也引用了相同的向量。
当您从向量创建表格时,假设这些向量没有外部引用。为了确保这一点,这些向量底层的缓冲区会被传输到新表格中的新向量中,原始向量会被清除。
也不要这样做,但请注意与上面的区别:
IntVector myVector = createMyIntVector(); // Reference count for myVector = 1
Table t1 = new Table(myVector);
// myVector is cleared; Table t1 has a new hidden vector with the data from myVector
Table t2 = new Table(myVector);
// t2 has no rows because myVector was just cleared
// t1 continues to have the data from the original vector
t2.clear();
// no change because t2 is already empty and t1 is independent
使用表格时,内存会在实例化时显式传输,因此表格持有的缓冲区仅由该表格持有。
创建带有字典编码向量的表格¶
另一个区别点是 VectorSchemaRoot 不了解其向量的任何字典编码,而表格持有一个可选的 DictionaryProvider 实例。如果源数据中的任何向量已编码,则必须设置 DictionaryProvider 以对值进行解码。
VectorSchemaRoot vsr = myVsr();
DictionaryProvider provider = myProvider();
Table t = new Table(vsr, provider);
在 Table 中,字典的使用方式与向量中的相同。要解码向量,用户需提供要解码的向量名称和字典 ID:
Table t = new Table(vsr, provider);
ValueVector decodedName = t.decode("name", 1L);
要对表格中的向量进行编码,使用类似的方法:
Table t = new Table(vsr, provider);
ValueVector encodedName = t.encode("name", 1L);
显式释放内存¶
表格使用堆外内存,必须在不再需要时释放。Table 实现了 AutoCloseable,因此创建它的最佳方式是在 try-with-resources 块中:
try (VectorSchemaRoot vsr = myMethodForGettingVsrs();
Table t = new Table(vsr)) {
// do useful things.
}
如果您不使用 try-with-resources 块,则必须手动关闭表格:
try {
VectorSchemaRoot vsr = myMethodForGettingVsrs();
Table t = new Table(vsr);
// do useful things.
} finally {
vsr.close();
t.close();
}
手动关闭应在 finally 块中执行。
获取 Schema¶
您可以像处理向量架构根一样获取表格的 schema:
Schema s = table.getSchema();
添加和删除向量¶
Table 提供了添加和删除向量的功能,这些功能模拟了 VectorSchemaRoot 中的相同功能。这些操作返回新实例,而不是就地修改原始实例。
try (Table t = new Table(vectorList)) {
IntVector v3 = new IntVector("3", intFieldType, allocator);
Table t2 = t.addVector(2, v3);
Table t3 = t2.removeVector(1);
// don't forget to close t2 and t3
}
切片表格¶
Table 支持 slice() 操作,源表格的一个切片是第二个 Table,它引用源中单个连续的行范围。
try (Table t = new Table(vectorList)) {
Table t2 = t.slice(100, 200); // creates a slice referencing the values in range (100, 200]
...
}
这提出了一个问题:如果您创建一个包含源表格中 所有 值的切片(如下所示),那它与使用与源相同的向量构建的新 Table 有何不同?
try (Table t = new Table(vectorList)) {
Table t2 = t.slice(0, t.getRowCount()); // creates a slice referencing all the values in t
// ...
}
区别在于,当您 构建 一个新表格时,缓冲区会从源向量传输到目标中的新向量。使用切片时,两个表格共享相同的底层向量。不过这没关系,因为两个表格都是不可变的。
使用 FieldReaders¶
您可以为 Table 中的任何向量获取一个 FieldReader,并传入 Field、向量索引或向量名称作为参数。签名与 VectorSchemaRoot 中的相同。
FieldReader nameReader = table.getReader("user_name");
行操作¶
基于行的访问由 Row 对象支持。Row 提供了按向量名称和向量位置获取值的 get() 方法,但没有 set() 操作。
必须认识到,行并没有被具体化为对象,而是像游标一样操作,可以使用同一个 Row 实例查看(一次一个)表格中众多逻辑行的数据。有关在表格中导航的信息,请参阅下方的“在行之间移动”。
获取行¶
在任何表格实例上调用 immutableRow() 都会返回一个新的 Row 实例。
Row r = table.immutableRow();
在行之间移动¶
由于行是可迭代的,您可以使用标准的 while 循环遍历表格:
Row r = table.immutableRow();
while (r.hasNext()) {
r.next();
// do something useful here
}
Table 实现了 Iterable<Row>,因此您可以在增强型 for 循环中直接从表格访问行:
for (Row row: table) {
int age = row.getInt("age");
boolean nameIsNull = row.isNull("name");
...
}
最后,虽然行通常按底层数据向量的顺序进行迭代,但它们也可以使用 Row#setPosition() 方法定位,因此您可以跳转到特定行。行号从 0 开始。
Row r = table.immutableRow();
int age101 = r.setPosition(101); // change position directly to 101
对位置的任何更改都会应用于表格中的所有列。
请注意,在通过行访问值之前,必须调用 next() 或 setPosition()。否则会导致运行时异常。
使用行的读取操作¶
可以使用向量名称和向量索引来获取值,其中索引是向量在表格中从 0 开始的位置。例如,假设‘age’是‘table’中的第 13 个向量,以下两个 get 操作是等效的:
Row r = table.immutableRow();
r.next(); // position the row at the first value
int age1 = r.get("age"); // gets the value of vector named 'age' in the table at row 0
int age2 = r.get(12); // gets the value of the 13th vector in the table at row 0
您还可以使用可为空的 ValueHolder 获取值。例如:
NullableIntHolder holder = new NullableIntHolder();
int b = row.getInt("age", holder);
这可用于检索值,而无需为每个值创建一个新对象。
除了获取值外,您还可以使用 isNull() 检查值是否为 null。如果向量包含任何 null 值,这一点很重要,因为在某些情况下,从向量请求值可能会导致 NullPointerException。
boolean name0isNull = row.isNull("name");
您还可以获取当前行号:
int row = row.getRowNumber();
将值读取为对象¶
对于任何给定的向量类型,基本的 get() 方法尽可能返回原始值。例如,getTimeStampMicro() 返回一个编码时间戳的长整型值。要获取在 Java 中表示该时间戳的 LocalDateTime 对象,提供了另一个名称后附加了 ‘Obj’ 的方法。例如:
long ts = row.getTimeStampMicro();
LocalDateTime tsObject = row.getTimeStampMicroObj();
此命名方案的例外情况是复杂向量类型(List、Map、Schema、Union、DenseUnion 和 ExtensionType)。这些类型总是返回对象而不是原始类型,因此不需要“Obj”扩展。预计一些用户可能会继承 Row 以添加更符合其特定需求的 getter。
读取 VarChars 和 LargeVarChars¶
Arrow 中的字符串表示为使用 UTF-8 字符集编码的字节数组。您可以获得 String 结果或实际的字节数组。
byte[] b = row.getVarChar("first_name");
String s = row.getVarCharObj("first_name"); // uses the default encoding (UTF-8)
将 Table 转换为 VectorSchemaRoot¶
可以使用 toVectorSchemaRoot() 方法将表格转换为向量架构根。缓冲区被传输到向量架构根中,源表格被清除。
VectorSchemaRoot root = myTable.toVectorSchemaRoot();
使用 C-Data 接口¶
许多 Arrow 功能都需要与原生代码交互的能力。本节介绍如何导出表格以供原生代码使用:
导出通过将数据转换为 VectorSchemaRoot 实例并使用现有设施传输数据来完成。您可以自己执行此操作,但这并不理想,因为转换为向量架构根会破坏不可变性保证。使用 Data 类中的 exportTable() 方法可避免此问题。
Data.exportTable(bufferAllocator, table, dictionaryProvider, outArrowArray);
如果表格包含字典编码的向量并且是使用 DictionaryProvider 构建的,则可以省略 exportTable() 的 provider 参数,届时将使用表格的 provider 属性。
Data.exportTable(bufferAllocator, table, outArrowArray);