表格#
注意:表格 API 是实验性的,可能会发生变化。 请参阅下面的限制列表。
表格是一个基于 FieldVector 的不可变表格数据结构。 与 VectorSchemaRoot 类似,Table
是一个由 Arrow 数组支持的柱状数据结构,或者更具体地说,由 FieldVector
对象支持。 它与 VectorSchemaRoot
的主要区别在于它是完全不可变的,并且缺少对批量操作的支持。 在管道中处理表格数据批次的任何人都应继续使用 VectorSchemaRoot
。 最后,Table
API 主要是面向行的,因此在某些方面它更像 JDBC API,而不是 VectorSchemaRoot
API,但您仍然可以使用 FieldReaders
以柱状方式处理数据。
表格和 VectorSchemaRoot 中的修改#
VectorSchemaRoot
在保存其数据的向量上提供了一个简单的包装器。 可以从向量模式根目录检索单个向量。 这些向量具有用于修改其元素的设置器,使得 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
或向量的集合或数组导入。
表格 API#
与 VectorSchemaRoot
类似,表格包含一个 Schema 和一个有序的 FieldVector
对象集合,但它被设计为通过面向行的接口访问。
从 VectorSchemaRoot 创建表格#
表格从 VectorSchemaRoot
创建,如下所示。 保存数据的内存缓冲区从向量模式根转移到新表格中的新向量,从而清除源向量。 这确保了新表格中的数据永远不会被更改。 由于缓冲区是转移而不是复制,因此这是一个非常低开销的操作。
Table t = new Table(someVectorSchemaRoot);
如果您现在更新 VectorSchemaRoot
保存的向量(使用 ValueVector#setSafe()
的某个版本),它会反映这些更改,但表格 *t* 中的值不变。
从 FieldVectors 创建表格#
可以使用“var-arg”数组参数从 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#
您可以为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()
检查值是否为空。 如果向量包含任何空值,这一点很重要,因为在某些情况下,从向量请求值可能会导致 NullPointerExceptions。
boolean name0isNull = row.isNull("name");
您还可以获取当前行号
int row = row.getRowNumber();
将值读取为对象#
对于任何给定的向量类型,基本的 get() 方法尽可能返回一个原始值。 例如,getTimeStampMicro() 返回一个编码时间戳的 long 值。 要在 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() 方法将 Table 转换为向量模式根。 缓冲区将传输到向量模式根,并且源表将被清除。
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);