Table#

注意:Table API 尚在實驗階段,可能會有所變動。請參閱下方所列的限制。

Table 是一種基於 FieldVector 的不可變表格資料結構。如同 VectorSchemaRootTable 是一種以 Arrow 陣列為基礎的欄狀資料結構,更精確地說,是以 FieldVector 物件為基礎。它與 VectorSchemaRoot 的主要區別在於,它完全不可變,且缺乏對批次操作的支援。任何在管線中處理批次表格資料的人都應繼續使用 VectorSchemaRoot。最後,Table API 主要以列為導向,因此在某些方面,它更像 JDBC API,而非 VectorSchemaRoot API,但您仍然可以使用 FieldReaders 以欄狀方式處理資料。

Table 與 VectorSchemaRoot 中的變更#

VectorSchemaRoot 在保存其資料的向量上提供了一個輕薄的封裝。可以從向量綱要根目錄中檢索個別向量。這些向量具有用於修改其元素的設定器,使得 VectorSchemaRoot 僅依慣例是不可變的。向量變更的協定記錄在 ValueVector 介面中

  • 值需要依序寫入(例如,索引 0、1、2、5)

  • 在寫入任何內容之前,空值向量會以所有值皆為空值開始

  • 對於可變寬度類型,偏移向量在寫入之前應全部為零

  • 您必須先呼叫 setValueCount,才能讀取向量

  • 一旦向量被讀取,您就不應再寫入。

這些規則並未由 API 強制執行,因此程式設計人員有責任確保遵循這些規則。未能這樣做可能會導致執行階段例外。

Table 另一方面是不可變的。底層向量不會公開。當從現有向量建立表格時,它們的記憶體會傳輸到新的向量,因此後續對原始向量的變更不會影響新表格的值。

功能與限制#

目前提供一組基本的表格功能

  • 從向量或 VectorSchemaRoot 建立表格

  • 依列迭代表格,或直接設定目前的列索引

  • 以基本類型、物件和/或可為 Null 的 ValueHolder 實例存取向量值(取決於類型)

  • 取得任何向量的 FieldReader

  • 新增和移除向量,建立新表格

  • 使用字典編碼來編碼和解碼表格的向量

  • 匯出表格資料以供原生程式碼使用

  • 將代表性資料列印為 TSV 字串

  • 取得表格的綱要

  • 切割表格

  • 將表格轉換為 VectorSchemaRoot

11.0.0 版本中的限制

  • 不支援 ChunkedArray 或任何形式的列群組。未來版本將考慮支援分塊陣列或列群組。

  • 不支援 C-Stream API。串流 API 的支援取決於分塊陣列的支援

  • 不支援直接從 Java POJO 建立表格。表格保存的所有資料都必須透過 VectorSchemaRoot 或從向量的集合或陣列匯入。

Table API#

如同 VectorSchemaRoot,表格包含 SchemaFieldVector 物件的有序集合,但其設計為透過以列為導向的介面進行存取。

從 VectorSchemaRoot 建立 Table#

表格是從 VectorSchemaRoot 建立的,如下所示。保存資料的記憶體緩衝區會從向量綱要根目錄傳輸到新表格中的新向量,並在此過程中清除來源向量。這確保了新表格中的資料永遠不會變更。由於緩衝區是傳輸而非複製,因此這是一個非常低負擔的操作。

Table t = new Table(someVectorSchemaRoot);

如果您現在更新 VectorSchemaRoot 保存的向量(使用某個版本的 ValueVector#setSafe()),它會反映這些變更,但表格 t 中的值保持不變。

從 FieldVectors 建立 Table#

可以從 FieldVectors 建立表格,如下所示,使用「var-arg」陣列引數

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() 操作,其中來源表格的切片是第二個 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

您也可以使用可為 Null 的 ValueHolder 取得值。例如

NullableIntHolder holder = new NullableIntHolder();
int b = row.getInt("age", holder);

這可以用於檢索值,而無需為每個值建立新的 Object。

除了取得值之外,您還可以透過 isNull() 檢查值是否為 Null。如果向量包含任何 Null,這很重要,因為在某些情況下,要求從向量取得值可能會導致 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() 方法將表格轉換為向量綱要根目錄。緩衝區會傳輸到向量綱要根目錄,而來源表格會被清除。

VectorSchemaRoot root = myTable.toVectorSchemaRoot();

使用 C-Data 介面#

許多 Arrow 功能都需要能夠使用原生程式碼。本節說明如何匯出表格以供原生程式碼使用

匯出的運作方式是將資料轉換為 VectorSchemaRoot 實例,並使用現有的設施來傳輸資料。您可以自行執行,但這並非理想之選,因為轉換為向量綱要根目錄會破壞不可變性保證。使用 Data 類別中的 exportTable() 方法可以避免此問題。

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

如果表格包含字典編碼向量,且是使用 DictionaryProvider 建構的,則可以省略 exportTable() 的提供者引數,並將使用表格的提供者屬性

Data.exportTable(bufferAllocator, table, outArrowArray);