Apache Arrow提供了用于表示数据的基本数据结构:Array、ChunkedArray、RecordBatch和Table。本文将演示如何从基本数据类型构造这些数据结构;具体而言,我们将使用不同大小的整数表示天、月和年。我们将使用它们来创建以下数据结构:
Arrow Arrays
ChunkedArrays
从Arrays创建的RecordBatch
从ChunkedArrays创建的Table
先决条件
在继续之前,请确保已经具备以下条件:
安装了Arrow,您可以在这里设置:在您自己的项目中使用Arrow C++
了解如何使用基本的C++数据结构
了解基本的C++数据类型
设置
在尝试Arrow之前,我们需要填补一些空白:
我们需要包括必要的头文件。
需要一个main()函数来将一切粘合在一起。
包含
首先,像往常一样,我们需要一些包含。我们将获取输出的iostream,然后从api.h中导入Arrow的基本功能,如下所示:
#include <arrow/api.h> #include <iostream>
主函数
接下来,我们需要一个main()函数 - Arrow的常见模式如下:
int main() { arrow::Status st = RunMain(); if (!st.ok()) { std::cerr << st << std::endl; return 1; } return 0; }
这允许我们轻松使用Arrow的错误处理宏,如果发生失败,它将返回一个arrow::Status对象,然后这个main()函数将报告错误。请注意,这意味着Arrow从不引发异常,而是依赖于返回Status。关于这一点的更多信息,请阅读这里:Conventions。
为配合这个main()函数,我们有一个RunMain()函数,任何Status对象都可以从中返回 - 这是我们将编写程序的地方:
arrow::Status RunMain() { // code return arrow::Status::OK(); }
创建Arrow Array
构建int8 Arrays
考虑到我们在标准的C++数组中有一些数据,希望使用Arrow,我们需要将数据从这些数组中移动到Arrow数组中。在Array中,我们仍然保证内存的连续性,因此使用Array与C++数组相比不会导致性能损失。构建Array的最简单方法是使用ArrayBuilder。
以下代码初始化了一个ArrayBuilder,该Array将保存8位整数。具体来说,它使用AppendValues()方法,该方法存在于具体的arrow::ArrayBuilder子类中,以填充ArrayBuilder的内容。请注意使用ARROW_RETURN_NOT_OK。如果AppendValues()失败,该宏将返回到main(),main()将打印出失败的原因。
// Builders是在Arrow中从现有值创建Arrays的主要方法,这些值不是在磁盘上的。 // 在这种情况下,我们将创建一个简单的数组,并将其传递给它。 // 数据类型仍然很重要,每种兼容类型都有一个Builder; // 在这种情况下是int8。 arrow::Int8Builder int8builder; int8_t days_raw[5] = {1, 12, 17, 23, 28}; // 如其名称所示,AppendValues将days_raw中的5个值放入我们的Builder对象。 ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw, 5));
一旦ArrayBuilder中具有我们希望在Array中的值,我们可以使用ArrayBuilder::Finish()将最终结构输出到Array,具体而言,输出为std::shared_ptrarrow::Array。请注意在以下代码中使用的ARROW_ASSIGN_OR_RAISE。Finish()输出一个arrow::Result对象,ARROW_ASSIGN_OR_RAISE可以处理该对象。如果该方法失败,它将返回到main(),并解释出了什么问题。如果成功,它将将最终输出分配给左侧变量。
// 我们只有一个Builder,不是一个Array -- 以下代码将构建好的数据输出到一个适当的Array中。 std::shared_ptr<arrow::Array> days; ARROW_ASSIGN_OR_RAISE(days, int8builder.Finish());
一旦ArrayBuilder调用了其Finish方法,其状态将重置,因此它可以再次使用,就像它是全新的一样。因此,我们可以重复上面的过程,为我们的第二个数组做同样的操作:
// 由于Builder在填充Array时清除其状态,因此如果类型相同,我们可以重用Builder。 // 我们在这里重用Builder以获取月份值。 int8_t months_raw[5] = {1, 3, 5, 7, 1}; ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw, 5)); std::shared_ptr<arrow
构建int16数组
一旦在声明时指定了ArrayBuilder的类型,就无法更改其类型。当我们切换到需要至少16位整数的年份数据时,我们必须创建一个新的ArrayBuilder。当然,Arrow提供了相应的ArrayBuilder。它使用完全相同的方法,但具有新的数据类型:
// 现在我们切换到int16,我们使用该数据类型的Builder。 arrow::Int16Builder int16builder; int16_t years_raw[5] = {1990, 2000, 1995, 2000, 1995}; ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw, 5)); std::shared_ptr<arrow::Array> years; ARROW_ASSIGN_OR_RAISE(years, int16builder.Finish());
现在,我们有三个Arrow数组,类型上有一些差异。
创建RecordBatch
只有当您有一个表时,列式数据格式才真正发挥作用。因此,让我们创建一个。我们首先要制作的是RecordBatch - 这在内部使用Arrays,这意味着每个列内的所有数据将是连续的,但任何附加或连接都需要复制。制作RecordBatch有两个步骤,给定现有的Arrays:
定义模式
将模式和Arrays加载到构造函数中
定义模式
为了开始制作RecordBatch,我们首先需要定义列的特征,每个特征由一个Field实例表示。每个Field包含其关联列的名称和数据类型;然后,一个Schema将它们组合在一起,并设置列的顺序,如下所示:
// 现在,我们想要一个RecordBatch,其中包含列和列的标签。 // 这会得到我们在Arrow中想要的2D数据结构。 // 这些由模式定义,模式有字段 - 在这里我们准备好这两种对象类型。 std::shared_ptr<arrow::Field> field_day, field_month, field_year; std::shared_ptr<arrow::Schema> schema; // 每个字段都需要其名称和数据类型。 field_day = arrow::field("Day", arrow::int8()); field_month = arrow::field("Month", arrow::int8()); field_year = arrow::field("Year", arrow::int16()); // 可以从字段的矢量构建模式,我们在这里这样做。 schema = arrow::schema({field_day, field_month, field_year});
构建RecordBatch
具有来自上一节的Arrays中的数据以及先前步骤中模式的情况下,我们可以制作RecordBatch。请注意,列的长度是必需的,并且所有列共享相同的长度。
// 有了模式和充满数据的Arrays,我们就可以制作我们的RecordBatch!在这里,每列在内部是连续的。 // 这与我们将在下一节看到的Tables相反。 std::shared_ptr<arrow::RecordBatch> rbatch; // RecordBatch需要模式,列的长度,它们都必须匹配, // 以及实际的数据本身。 rbatch = arrow::RecordBatch::Make(schema, days->length(), {days, months, years}); std::cout << rbatch->ToString();
现在,我们将数据以漂亮的表格形式安全地放在RecordBatch中。我们将在后续的教程中讨论如何处理这些数据。
制作ChunkedArray
假设我们想要一个由子数组组成的数组,因为这可以在连接时避免数据复制,用于并行化工作,将每个块可爱地切分到缓存中,或者超过标准Arrow Array的2,147,483,647行限制。为此,Arrow提供了ChunkedArray,它可以由单独的Arrow Arrays组成。在此示例中,我们可以重用之前制作的数组的部分来组成我们的Chunked数组,从而可以扩展它们而无需复制数据。因此,让我们使用相同的构建器建立几个新的Arrays:
// 现在,让我们获取一些新的数组!它们的数据类型与上面相同,因此我们重用构建器。 int8_t days_raw2[5] = {6, 12, 3, 30, 22}; ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw2, 5)); std.shared_ptr<arrow::Array> days2; ARROW_ASSIGN_OR_RAISE(days2, int8builder.Finish()); int8_t months_raw2[5] = {5, 4, 11, 3, 2}; ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw2, 5)); std.shared_ptr<arrow::Array> months2; ARROW_ASSIGN_OR_RAISE(months2, int8builder.Finish()); int16_t years_raw2[5] = {1980, 2001, 1915, 2020, 1996}; ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw2, 5)); std.shared_ptr<arrow::Array> years2; ARROW_ASSIGN_OR_RAISE(years2, int16builder.Finish());
为了支持在ChunkedArray的构建中具有任意数量的Arrays,Arrow提供了ArrayVector。这为Arrays提供了一个向量,我们将在这里使用它来准备制作ChunkedArray:
// ChunkedArrays允许我们拥有一系列数组,它们之间不是连续的。 // 首先,我们获取一个数组向量。 arrow::ArrayVector day_vecs{days, days2};
为了充分利用Arrow,我们需要采取最后一步,进入ChunkedArray:
// 然后,我们使用它来初始化一个ChunkedArray,可以与Arrow中的其他函数一起使用!这很好,因为使用普通数组向量无法做到这一点。 std::shared_ptr<arrow::ChunkedArray> day_chunks = std::make_shared<arrow::ChunkedArray>(day_vecs);
对于我们的day值,现在我们只需要重复相同的过程来处理月份和年份数据:
// 重复处理月份。 arrow::ArrayVector month_vecs{months, months2}; std::shared_ptr<arrow::ChunkedArray> month_chunks = std.make_shared<arrow::ChunkedArray>(month_vecs); // 重复处理年份。 arrow::ArrayVector year_vecs{years, years2}; std::shared_ptr<arrow::ChunkedArray> year_chunks = std.make_shared<arrow::ChunkedArray>(year_vecs);
通过这样做,我们留下了三个不同类型的ChunkedArray。
创建Table
我们可以使用前一节中的ChunkedArrays创建Tables。与RecordBatch类似,Table存储表格数据。但是,由于它由ChunkedArrays组成,Table不保证连续性。这对于逻辑、并行化工作、将块适应缓存或超出Array和因此RecordBatch中的2,147,483,647行限制可能很有用。
如果您阅读了RecordBatch的部分,您可能会注意到以下代码中的Table构造函数实际上是相同的,只是将列的长度放在位置3,并创建一个Table。我们重用了之前的模式,并制作了我们的Table:
// Table是我们需要的结构,用于这些非连续的列,并将它们都放在一个地方,以便我们可以像它们是正常数组一样使用它们。 std::shared_ptr<arrow::Table> table; table = arrow::Table::Make(schema, {day_chunks, month_chunks, year_chunks}, 10); std::cout << table->ToString();
现在,我们将数据以漂亮的表格形式安全地放在Table中。我们将在后续的教程中讨论如何处理这些数据。
结束程序 最后,我们只需返回Status::OK(),这样main()函数就知道我们已经完成了,一切都很好。
return arrow::Status::OK();
总结 通过这样做,您已经创建了Arrow中的基本数据结构,并可以继续在下一篇文章中使用文件I/O将它们输入和输出到程序中。
完整代码:
#include <arrow/api.h> #include <iostream> arrow::Status RunMain() { // Builder 是从不在磁盘上的现有值创建 Arrow 数组的主要方法。在这种情况下,我们将创建一个简单的数组,并将其提供。 // 数据类型仍然很重要,每个兼容类型都有一个 Builder; // 在这种情况下,是 int8。 arrow::Int8Builder int8builder; int8_t days_raw[5] = {1, 12, 17, 23, 28}; // 正如所调用的 AppendValues 一样,将来自 days_raw 的 5 个值放入我们的 Builder 对象中。 ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw, 5)); // 但是我们只有一个 Builder,没有数组 —— 以下代码将构建好的数据输出到正规的数组中。 std::shared_ptr<arrow::Array> days; ARROW_ASSIGN_OR_RAISE(days, int8builder.Finish()); // 每次填充数组后,Builder 会清除其状态,因此如果类型相同,我们可以重复使用该 Builder。 // 在这里,我们用于月份值。 int8_t months_raw[5] = {1, 3, 5, 7, 1}; ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw, 5)); std.shared_ptr<arrow::Array> months; ARROW_ASSIGN_OR_RAISE(months, int8builder.Finish()); // 现在,我们切换到 int16,使用该数据类型的 Builder。 arrow::Int16Builder int16builder; int16_t years_raw[5] = {1990, 2000, 1995, 2000, 1995}; ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw, 5)); std.shared_ptr<arrow::Array> years; ARROW_ASSIGN_OR_RAISE(years, int16builder.Finish()); // 现在,我们想要一个 RecordBatch,它具有列和相应列的标签。 // 这使我们得到了 Arrow 中想要的二维数据结构。 // 这些由模式定义,模式具有字段 —— 在这里我们准备好了这两种对象。 std.shared_ptr<arrow::Field> field_day, field_month, field_year; std.shared_ptr<arrow::Schema> schema; // 每个字段都需要其名称和数据类型。 field_day = arrow::field("Day", arrow::int8()); field_month = arrow::field("Month", arrow::int8()); field_year = arrow::field("Year", arrow::int16()); // 模式可以从字段向量构建,我们在这里这样做。 schema = arrow::schema({field_day, field_month, field_year}); // 使用模式和充满数据的数组,我们可以制作 RecordBatch!在这里, // 每个列在内部是连续的。这与 Tables 相反,我们将会看到下一步。 std.shared_ptr<arrow::RecordBatch> rbatch; // RecordBatch 需要模式、列的长度,所有这些必须匹配, // 以及实际数据本身。 rbatch = arrow::RecordBatch::Make(schema, days->length(), {days, months, years}); std::cout << rbatch->ToString(); // 现在,让我们获得一些新的数组!数据类型与上面相同,因此我们可以重复使用生成器。 int8_t days_raw2[5] = {6, 12, 3, 30, 22}; ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw2, 5)); std::shared_ptr<arrow::Array> days2; ARROW_ASSIGN_OR_RAISE(days2, int8builder.Finish()); int8_t months_raw2[5] = {5, 4, 11, 3, 2}; ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw2, 5)); std::shared_ptr<arrow::Array> months2; ARROW_ASSIGN_OR_RAISE(months2, int8builder.Finish()); int16_t years_raw2[5] = {1980, 2001, 1915, 2020, 1996}; ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw2, 5)); std::shared_ptr<arrow::Array> years2; ARROW_ASSIGN_OR_RAISE(years2, int16builder.Finish()); // ChunkedArrays 允许我们拥有一组不连续的数组,它们之间没有紧密排列。 // 首先,我们获取一个数组的向量。 arrow::ArrayVector day_vecs{days, days2}; // 然后,我们使用它来初始化一个 ChunkedArray,可以与 Arrow 中的其他函数一起使用! // 这非常有用,因为拥有普通的数组向量不会让我们走太远。 std::shared_ptr<arrow::ChunkedArray> day_chunks = std::make_shared<arrow::ChunkedArray>(day_vecs); // 重复处理月份。 arrow::ArrayVector month_vecs{months, months2}; std::shared_ptr<arrow::ChunkedArray> month_chunks = std::make_shared<arrow::ChunkedArray>(month_vecs); // 重复处理年份。 arrow::ArrayVector year_vecs{years, years2}; std::shared_ptr<arrow::ChunkedArray> year_chunks = std::make_shared<arrow::ChunkedArray>(year_vecs); // 表是我们需要的结构,用于处理这些不连续的列,并将它们保存在一个地方,以便我们可以像处理普通数组一样使用它们。 std::shared_ptr<arrow::Table> table; table = arrow::Table::Make(schema, {day_chunks, month_chunks, year_chunks}, 10); std::cout << table->ToString(); return arrow::Status::OK(); } int main() { arrow::Status st = RunMain(); if (!st.ok()) { std::cerr << st << std::endl; return 1; } return 0; }