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 物件是不可變的規則。ValueVector
實例可以直接使用 new 關鍵字建立,有 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。第一個緩衝區保存空值位元圖,此處由單個位元組組成,位元為 1|1|1|1|0|1|1|1(如果值為非空值,則位元為 1)。第二個緩衝區包含以上所有值。由於第四個條目為空值,因此該位置在緩衝區中的值未定義。請注意,與 set API 相比,setSafe API 將在設定值之前檢查值容量,並在必要時重新分配緩衝區。
以下是如何使用 writer 建構向量
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
的具體子類別用於存取向量值,需要聲明的是 writer/reader 不如直接存取有效率
// 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
是一個向量,它為每個索引保存一個值列表。使用它時,您需要處理與上述相同的步驟(建立 > 分配 > 變更 > 設定值計數 > 存取 > 清除),但是您完成此操作的細節略有不同,因為您需要同時建立向量並為每個索引設定值列表。
例如,以下程式碼顯示如何使用 writer 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);
}
ListVector
值可以透過 get API 或透過 reader 類別 UnionListReader
存取。若要讀取所有值,請先列舉索引,然後列舉內部列表值。
// 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
中向量的字典,您可以取得與向量關聯的欄位,取得其字典屬性,並使用該物件的 id 在提供者中查閱正確的字典。
// 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
引數。您可以在 (讀取/寫入 IPC 格式) 的文件中找到用於寫入字典的範例程式碼。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].