#

注意: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)

  • 空向量在写入任何内容之前,所有值都为空值

  • 对于可变宽度类型,在写入之前偏移向量应全部为零

  • 在读取向量之前,必须调用 setValueCount

  • 读取向量后,绝不应该写入向量。

API 不会强制执行这些规则,因此程序员负责确保遵守这些规则。 不这样做可能会导致运行时异常。

另一方面,Table 是不可变的。 底层向量不公开。 从现有向量创建表时,它们的内存将传输到新向量,因此对原始向量的后续更改不会影响新表的值。

特性和限制#

目前提供了一套基本的表功能

  • 从向量或 VectorSchemaRoot 创建表

  • 按行迭代表,或直接设置当前行索引

  • 将向量值访问为基元、对象和/或可为空的 ValueHolder(值持有者) 实例(取决于类型)

  • 获取任何向量的 FieldReader

  • 添加和删除向量,创建新表

  • 使用字典编码对表的向量进行编码和解码

  • 导出表数据以供本机代码使用

  • 将代表性数据打印到 TSV 字符串

  • 获取表的模式

  • 切片表

  • 将表转换为 VectorSchemaRoot

11.0.0 版本中的限制

  • 不支持 ChunkedArray(分块数组) 或任何形式的行组。 对分块数组或行组的支持将在未来版本中考虑。

  • 不支持 C-Stream API。 对流式 API 的支持取决于分块数组支持

  • 不支持直接从 Java POJO 创建表。 表中保存的所有数据都必须通过 VectorSchemaRoot 或向量集合或数组导入。

Table API#

VectorSchemaRoot 类似,表包含一个 Schema(模式) 和一个有序的 FieldVector 对象集合,但它被设计为通过面向行的接口进行访问。

从 VectorSchemaRoot 创建表#

如下所示,表是从 VectorSchemaRoot 创建的。 保存数据的内存缓冲区将从向量模式根传输到新表中的新向量,并在此过程中清除源向量。 这可确保新表中的数据永远不会更改。 由于缓冲区是传输而不是复制的,因此这是一个非常低开销的操作。

Table t = new Table(someVectorSchemaRoot);

如果现在更新 VectorSchemaRoot 持有的向量(使用某种版本的 ValueVector#setSafe()),它将反映这些更改,但表 *t* 中的值保持不变。

从 FieldVectors 创建表#

可以使用“可变参数”数组参数,如下所示,从 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 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()* 操作,其中源表格的切片是第二个表格,它引用源表格中单个连续的行范围。

try (Table t = new Table(vectorList)) {
    Table t2 = t.slice(100, 200); // creates a slice referencing the values in range (100, 200]
    ...
}

这就引出了一个问题:如果您创建一个包含源表格中*所有*值的切片(如下所示),这与使用与源表格相同的向量构造的新表格有何不同?

try (Table t = new Table(vectorList)) {
    Table t2 = t.slice(0, t.getRowCount()); // creates a slice referencing all the values in t
    // ...
}

区别在于,当您*构造*一个新表格时,缓冲区会从源向量传输到目标中的新向量。使用切片时,两个表格共享相同的底层向量。不过,这没关系,因为两个表格都是不可变的。

使用 FieldReaders#

您可以获取表格中任何向量的 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() 检查值是否为空。如果向量包含任何空值,这一点很重要,因为在某些情况下,从向量请求值可能会导致 NullPointerExceptions。

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 子类化以添加更 specific to their needs 的 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)

将表格转换为 VectorSchemaRoot#

可以使用 *toVectorSchemaRoot()* 方法将表格转换为向量模式根。缓冲区将传输到向量模式根,并且源表格将被清除。

VectorSchemaRoot root = myTable.toVectorSchemaRoot();

使用 C 数据接口#

许多 Arrow 功能都需要使用原生代码。本节介绍如何导出表格以与原生代码一起使用。

导出通过将数据转换为 VectorSchemaRoot 实例并使用现有工具传输数据来工作。您可以自己完成,但这并不理想,因为转换为向量模式根会破坏不变性保证。使用 Data 类中的 exportTable() 方法可以避免这种担忧。

Data.exportTable(bufferAllocator, table, dictionaryProvider, outArrowArray);

如果表格包含字典编码的向量,并且是使用 DictionaryProvider 构造的,则可以省略 exportTable() 的 provider 参数,并将使用表格的 provider 属性。

Data.exportTable(bufferAllocator, table, outArrowArray);