使用 Apache Arrow 进行内存分析——理解 Arrow 数据库连接 (ADBC)

在之前的几个章节中,我们讨论了许多使用 Apache Arrow 处理和交互数据的方法。我们甚至还介绍了如何利用一些 Arrow 库(如 Dataset 库)来获取和分析数据。但是,如果你的日常工作涉及数据处理,有一个我们无法避免的东西:结构化查询语言(SQL)。

SQL 仍然是大多数人与数据库交互的首选标准,那么连接这些数据库呢?随着新型数据库系统不断涌现,工程师们不得不频繁管理这些系统的各种连接器。如果你曾经处理过这些问题,那么你一定能理解它带来的混乱和头疼。

为了简化这一过程,已经存在一些常见的标准,每种标准都有各自的优缺点。本章将介绍 Arrow 社区在改善这一领域所做的努力,即 ADBC(Arrow Database Connectivity)标准,并将其与目前最常用的标准------开放数据库连接(ODBC)和 Java 数据库连接(JDBC)进行比较。

简而言之,我们将讨论以下主题:

  • ODBC 和 JDBC 是什么?我们是如何走到今天这一步的?
  • ADBC 为生态系统提供了哪些功能来简化数据系统之间的连接
  • 如何在你的应用程序中使用 ADBC 并创建 ADBC 驱动程序

技术要求

本章将会包含大量代码,所以请做好准备!按惯例,确保你的计算机上安装了以下内容:

  • Python 3+,并且已安装并可导入 pyarrow 模块
  • Go 1.21+ 版本
  • 支持 C++17 或更高版本的 C++ 编译器
  • Docker(请参阅第7章"探索 Apache Arrow Flight RPC"中的"设置性能测试"部分,了解安装说明)

代码示例可以在本书的 GitHub 仓库中的 chapter8 文件夹找到,链接为 GitHub repository

ODBC 被箭射中膝盖

ODBC 是一种标准化的应用程序编程接口(API),最初设计和构建于20世纪90年代初,用于访问数据库。ODBC 的开发旨在通过提供一个由数据库特定驱动程序实现的标准化 API,使应用程序不再依赖其底层数据库。这使得开发人员可以编写他们的应用程序,并通过简单地指定不同的驱动程序,轻松地将其迁移到其他数据库。到1997年,JDBC API 也被开发出来,提供了一个通用的 Java API,使程序能够管理多个驱动程序,并通过 ODBC 连接或其他类型的连接(例如本地供应商库或纯 Java 连接)进行连接,每种连接方式都有其优缺点。将近30年后,这些技术仍然是与 SQL 数据库通信的事实标准。

然而,在这段时间里,计算技术,尤其是数据领域,发生了显著变化。当时的系统是高度单体化的,处理器核心数量和计算能力都远远低于今天。随着大数据的兴起,分布式系统的增加以及数据科学作为一门职业的出现,ODBC 和 JDBC 在性能和可扩展性方面的承诺已经出现裂痕。我们需要推动这些老旧工具转向利用像 Arrow 这样的新技术,不仅仅作为一种实现细节,还需要支持将其作为直接的输出格式!

在数据科学的背景下,大多数开发人员在将数据加载到脚本或工具中进行分析时都会接触到 ODBC 或 JDBC。商业智能(BI)工具几乎普遍接受 ODBC 和 JDBC 作为与数据源交互的主要方式。这是有道理的,因为 BI 工具只需要一次实现基于 ODBC 标准的代码,就能访问任何发布 ODBC 驱动程序的数据源。在这一领域中的一些大牌工具包括 Tableau 和微软的 Power BI,它们都支持 ODBC 数据源访问。支持原生的 Arrow 数据、Parquet 文件以及其他通信格式将意味着更快的数据访问、更快速的计算以及为用户提供更灵敏的互动式仪表盘。它是如何做到的呢?通过在每个层面上直接支持 Arrow,从而减少甚至消除数据的翻译和复制。例如,我们在第2章《与关键 Arrow 规范一起工作》中提到的 Arrow-JDBC 适配器就是一个案例。

翻译的困境

即使系统使用相同的标准协议,在底层仍可能发生大量的翻译和数据复制。尽管 ODBC 有很多优点,但它还是在一个主要请求宽表(列多行少)而不是现代数据分析所需的窄表(行多列少)的时代设计的。尽管 ODBC 实现了不同系统之间的连接,但为了让一切正常工作,ODBC 驱动程序在处理时仍会进行大量的翻译和数据复制。图8.1对比了使用典型 ODBC 或 JDBC 的标准数据工作流与 Arrow-JDBC 适配器的工作流:

首先,让我们看一下图8.1左侧显示的典型 ODBC/JDBC 用例。在这个过程中,有三个需要在格式之间进行数据转换的点:

  1. 首先,数据在 JDBC/ODBC 驱动程序内部从数据库的原生格式转换为 JDBC/ODBC 标准格式。(A)
  2. 接着,数据会被转换为你所使用的编程语言和环境所需的对象或内存格式。例如,如果你使用 Python 和 pandas,你需要使用 ODBC 或 Python 原生驱动程序(性能较低),或者使用 JDBC 驱动程序,这时还需要将 JDBC 的 Java 对象转换为 Python 对象。这个过程成本非常高。(B)
  3. 最后,无论你通过哪种方式将数据导入环境中,如果接口没有直接输出 Arrow 格式的数据,那么在你能与 pandas 或 Polars 进行交互之前,数据还需要再次转换为 Arrow 格式。(当然,你可以直接使用 pandas,但我们之前已经展示过,从 Arrow 创建 pandas 或 Polars 的 DataFrame 具有可忽略甚至为零的成本,因此这样做并不会节省你任何资源。)(C)

让我们与图表右侧的工作流进行比较:

  1. 如果底层数据库不支持原生 Arrow 数据格式,数据库的数据格式会被转换为 JDBC 对象。(A)
  2. 然后,Arrow-JDBC 适配器会直接将 JDBC 对象转换为内存中不受 Java 虚拟机(JVM)管理的 Arrow 向量。(B)
  3. 这些向量的内存地址可以直接传递给 Python,Python 可以直接引用这些内存位置(正如我们在第2章《与关键 Arrow 规范一起工作》中展示的那样),而无需再次转换或复制数据。(C)

通过减少数据的翻译和复制,我们减少了 CPU 使用、内存占用和运行时间。简而言之,这种方式可以显著提高速度并减少资源消耗!如果工具如 Tableau 和 Power BI 原生支持 Arrow,这种工作流可以直接应用到它们中。

在 ODBC 驱动程序中的 Arrow 采用

随着时间的推移,越来越多的公司开始在他们的 ODBC 和 JDBC 驱动程序中启用 Arrow 作为内存格式。随着 Arrow 生态系统的不断扩展,工具和系统利用现有工具来提高性能和扩展其影响范围变得越来越有意义。例如,以下是一些已经在其客户端、驱动程序和连接器中构建了 Arrow 支持的工具:

  • Snowflake 的 ODBC 驱动程序和 Python 客户端使用 Arrow 进行数据传输。根据具体用例,他们在切换后性能提升了 5 倍到 10 倍【来源】。
  • Google BigQuery 添加了使用 Arrow 记录批次拉取数据的支持,用户在从 BigQuery Storage API 中将数据检索到 pandas DataFrame 时,性能提升达到了 15 倍到 31 倍【来源】。
  • Dremio Sonar 的 JDBC 和 ODBC 驱动程序一直使用 Arrow 来将数据传输给用户,因为 Arrow 本身就是 Dremio Sonar 的内部内存格式!从数据读取到结果集返回给客户端,期间无需进行数据转换。

最终,即使在 ODBC 和 JDBC 驱动程序中使用 Arrow 作为底层实现,也仅仅是一种过渡性方案。这种方式是在一个已经不再适合这些工作流的旧技术上强行加装新功能。那么,有没有替代方案呢?一些数据库发布了专有的协议和 SDK 来克服 ODBC 和 JDBC 的缺点,但这又带来了另一个问题。它重新引发了 ODBC 最初要解决的问题:连接性

连接标准的好处

标准本身并不引人注目。它们本身并不会让大多数人感到兴奋。标准的兴奋点往往来自于在它们基础上建立的东西。开放和免费的标准对于系统的运作至关重要,其中最具代表性的系统就是互联网。以下是万维网发明者 Tim Berners-Lee 的一段话,他发明了 HTML 和 HTTP 等使网络成为可能的技术:

"HTML 的标准化使网络能够腾飞。这不仅仅是因为它是标准的,而且是因为它是开放的和免版税的。[...] 是的,我们需要标准,因为竞争的焦点不在技术层面,而在于你在这些标准之上构建的业务和应用。"

如果你还没注意到,你现在正在阅读一本关于某种标准------Apache Arrow------的第八章。在整个生态系统中有无数的数据库和系统,它们可能使用不同的 API 和协议来进行通信。如果你正在开发一个需要访问多个数据源和系统的应用程序,你基本上只有两个选择:

  1. 数据源是否允许你使用标准化的客户端 API 访问?太好了!你可以对多个数据源使用这个标准化 API。
  2. 它不支持标准化 API?那你就需要为这个数据源构建一个特定的连接器。

如果你不能重用标准化客户端,支持大量数据源将对开发团队构成重大挑战。你不仅需要为这些源构建自定义的连接器,还需要随着时间的推移对它们进行维护。看看开源分布式查询引擎 Trino 支持的不同连接器的数量(trino.io/docs/curren...)。在撰写本文时,我至少能数出 38 种不同的数据源连接器。你认为其中有多少利用了标准化 API,使代码在连接器之间可以复用?

此外,数据翻译和复制的故事还有另一个问题:列式数据库

一般来说,大多数与 ODBC 或 JDBC 集成的应用程序在执行查询时,其结构类似于图 8.2 中所示的情况:

图 8.2 中标记的步骤按顺序如下:

  1. 应用程序通过 JDBC/ODBC API 提交 SQL 查询。
  2. 查询传递给加载的驱动程序。
  3. 驱动程序根据需要翻译查询,并使用数据库特定的协议将查询传递给底层数据库。
  4. 数据库执行查询并以数据库特定的格式返回结果。
  5. 驱动程序将结果转换为 JDBC/ODBC API 所需的格式。
  6. 应用程序使用 JDBC/ODBC API 遍历返回的数据行。

注意

上图和步骤改编自 ADBC 向社区介绍的原始博客文章:arrow.apache.org/blog/2023/0...。如果需要更多信息,可以访问该链接!

现代最受欢迎和高性能的分析系统通常使用列式存储的方法进行计算。正如你可能记得的,我们在第 1 章《Apache Arrow 入门》中介绍了列式存储方法的所有好处。不过,在 ODBC 和 JDBC 中这会成为一个问题。JDBC 明确只支持行式 API,而虽然 ODBC 可以支持列式数据,但其类型系统和数据表示方式并不容易映射到 Arrow。这意味着在前面提到的第 5 步中,列式数据通常会被转换为行数据,这个过程成本昂贵。

一些最知名和广泛使用的系统是列式数据库,例如 ClickHouse、Dremio Sonar、DuckDB、Google BigQuery 和 Snowflake。对于消费数据的客户端来说,像 pandas、Polars 和 Apache Spark 这样的工具如果直接获取列式数据会更好,而不必将行数据转换回列数据。图 8.3 直观地展示了这个问题:

请注意,我们无法完全消除这个问题。始终会有一些行式数据库系统(如 PostgreSQL)仍然非常受欢迎,并且这些系统的客户端会希望从中获取数据。但如果在可能的情况下,我们可以消除这些数据转换的成本,这将带来显著的性能提升。

由此,我们引出了 ADBC(Arrow Database Connectivity)。

ADBC 规范

你可以将 ADBC 理解为类似于 ODBC/JDBC,但它是 Arrow 原生 的。ADBC 定义了一个统一的客户端 API,允许通过不同的驱动程序实现,使应用程序能够以后端无关的方式与不同的数据源交互。让我们再看看之前提到的图 8.2,但这次使用 ADBC 的流程:

在图 8.4 中,我们再次看到了六个标记的步骤,但涉及的工作有所不同,甚至有些是可选的:

  1. 应用程序通过 ADBC API 提交查询。
  2. 查询传递给加载的/所需的 ADBC 驱动程序。
  3. 驱动程序仍将根据需要翻译查询,并使用数据库特定的协议将其发送到数据库。
  4. 数据库执行查询并以数据库特定的格式返回结果集。理想情况下,这些数据已经是 Arrow 格式。
  5. 仅在必要时,驱动程序才将结果数据转换为 Arrow 格式。如果结果已经是 Arrow 格式的数据,它可以直接将其转发给 API,无需任何转换或复制!
  6. 最后,应用程序遍历一系列 Arrow 记录批次。

与 ODBC/JDBC 类似,应用程序只需要处理单个 API,但现在无论底层数据源是什么,它只处理 Arrow 数据。是不是很酷?现在是时候深入了解了!

与 C 数据接口和 Arrow Flight RPC 不同,我不会尝试涵盖整个 ADBC 规范。你可能已经猜到,它相当庞大。如果你有兴趣直接研究代码,可以查看主要的 C/C++ 定义 adbc.h。还有多种语言的实现,包括 Go、Python、Java、R 和 C#/.NET。相反,我将介绍 ADBC 提供的一般功能集。

ADBC 处理四个主要对象:

  • 数据库
  • 连接
  • 语句
  • 错误

每个对象代表用户可能需要交互的特定逻辑对象。

ADBC 数据库

Database 对象保存可能在多个连接之间共享的任何状态。在大多数情况下,这将是常见的配置设置和缓存。对于内存数据库(例如 SQLite),它为内存数据库实例的所有权提供位置。对 ADBC Database 实例主要有以下三种操作:

  • 在数据库上设置选项,以修改任何连接的默认配置。
  • 从配置中获取现有选项的值。
  • 打开与数据库的新连接,检索 ADBC Connection 对象。

ADBC 连接

顾名思义,Connection 对象表示与数据库的单个逻辑连接。获得 Connection 对象后,你可以通过提供的 API 与所需的数据库后端进行各种方式的通信。除了能够获取或设置连接级别的选项(并非所有驱动程序都支持所有选项)之外,连接还允许你检索有关数据库的大量元数据。规范中的一些功能如下(所有结果都以 Arrow 记录批流的形式返回):

  • ConnectionGetInfo: 检索有关服务器的信息、它支持的 SQL 功能以及其他重要元数据。
  • ConnectionGetObjects: 检索数据库中可用的目录、数据库架构、表和列的层次信息。它允许你按特定的对象类型或模式进行筛选。
  • ConnectionGetTableSchema: 以 Arrow 架构的形式返回请求表的架构。
  • ConnectionGetTableTypes: 提供数据库中表类型的列表。
  • ConnectionSetOption: 在连接对象上设置或更新选项;有些选项只能在初始化连接之前设置。有关驱动程序特定选项的更多信息,请参阅各个驱动程序的文档。

注意

默认情况下,连接将在 "自动提交" 模式下运行。使用连接执行的查询不会包装在事务中,而是立即生效。如果你更喜欢使用手动提交和回滚调用,可以通过将 "adbc.connection.autocommit" 连接选项设置为 "false" 来禁用此功能。然而,目前并非所有驱动程序和实现都支持它。

ADBC 还公开了一个函数来检索各种表和列的统计数据。可用的精确统计信息因驱动程序而异,但通常包括行数、唯一行数、最小/最大值等。这样设计的原因是 ADBC 能够很好地在联合场景中工作,其中 Arrow 数据可以从一个查询引擎传递到另一个查询引擎。提供统计数据可以使利用 ADBC 从数据源提取数据的查询规划器做出更好的选择,关于连接顺序,甚至可以完全跳过读取。例如,查看以下 SQL 查询:

sql 复制代码
SELECT SUM(t.i) FROM t INNER JOIN t_2 ON (t_2.k = t.i)

如果表 tt_2 都来自 ADBC 源,查询规划器可以选择进行连接的最佳顺序,前提是它能够访问统计数据。怎么做到的呢?假设 tt_2 具有以下属性:

  • t 有 10^8 行,范围从 0 到 10^8。
  • t_2 有 10 行,范围从 0 到 10。

在这里,连接的顺序可以对查询的性能产生巨大影响。如果没有可用的统计信息,查询计划可能看起来像图 8.5 所示:

有了可用的统计信息,查询规划器就能够知道它应该调整连接的顺序,这样虽然等效,但性能会更高。结果将类似于图 8.6 所示的内容:

注意到,这两者之间的唯一区别是连接参数的顺序------也就是说,做 k = ii = k。设置如上所述的表并通过本地引擎(如 DuckDB)运行查询后,我发现通过调整连接顺序,性能提高了近 16 倍 !具体来说是 0.73125 秒 对比 0.04556 秒。因此,提供这些统计信息非常重要。

ADBC 语句

语句是你执行查询的方式,因为它们保存与执行查询和检索结果相关的所有状态。语句对象可以表示一次性查询,也可以作为预准备语句多次重复使用。

重要提示

语句对象始终可以重复使用,但这样做会使与该对象相关的任何先前结果集失效。因此,请确保在重复使用语句之前消费掉你的结果!

好了,现在我们终于有了所有的组件和构建模块。此时,我们可以通过图表来可视化 ADBC 的基本使用。请参见图 8.7 和图 8.8。

一旦你有了你的 Statement 对象,你就可以执行一些简单的查询了:

在图 8.8 中,虚线部分表示可选调用。Prepare 调用仅在你打算多次执行同一个查询时才有意义。请注意,如果你希望使用 Arrow 记录批作为查询的输入参数绑定,GetParameterSchema/Bind 将会很有用。

重要提示

前面图示中的方法名称表示所调用的操作,而不应被理解为代码中的实际函数名称。具体的函数名将根据你使用的编程语言绑定而有所不同。稍后我们会在本章的代码示例中详细介绍!

正如你可能记得,在第4章《跨越语言障碍:Arrow C 数据 API》中,我们讨论了如何处理流,特别是 ArrowArrayStream 。由于 ADBC 所定义的 API 本质上是一个 C API,因此我们将利用它来检索和消费从调用 ExecuteQuery 执行查询时产生的结果集。幸运的是,这非常容易实现。图 8.9 展示了基本流程:

对于没有结果集的查询(例如 INSERTUPDATE 语句),你可以简单地使用 ExecuteUpdate 替代 ExecuteQuery,如果底层数据库系统支持,该方法将返回受影响的行数。当然,如果你需要插入大量数据,ADBC 也能帮上忙!

对于支持批量导入的数据库,ADBC 提供了显式的批量导入工具。这避免了典型的绑定插入语句循环带来的开销,从而显著加快数据加载速度。它还通常允许用户不需要知道其数据库的正确 SQL 语法。用户只需要提供一条 Arrow 记录批流,然后设置相应的选项即可。与图 8.8 中展示的流程相比,这将成为一个简单的调用序列:

  1. AdbcStatementNew
  2. SetOption(ADBC_INGEST_OPTION_TARGET_TABLE, "tablename")
  3. GetParameterSchema
  4. Bind
  5. ExecuteUpdate
  6. AdbcStatementRelease

接下来,我们将介绍与 Statement 对象相关的最后一个主题------处理分区结果集。回顾前一章《探索 Apache Arrow Flight RPC》,我们了解到 FlightFlight SQL 都支持返回多个端点,这样客户端可以将结果集的检索分配到多个线程、进程或机器上进行。ADBC 允许驱动程序直接向客户端暴露这种分区和/或分布式的结果集。

ADBC 定义了一个 ExecutePartitions 函数,驱动程序可以实现该函数以返回分区列表。每个分区都可以传递给 ReadPartition 方法,它将返回 ArrowArrayStream 来消费结果集,从而允许并行读取分区。如果需要,还可以实现查询的增量执行。如前所述,首先对语句调用 SetSqlQuery,然后使用 ExecutePartitions 替代 ExecuteQuery。当然,这仅在驱动程序支持的情况下有效。如果不支持,驱动程序应该返回"未实现"错误代码。

现在让我们学习一下 ADBC 如何管理错误。

ADBC 错误处理

获取 AdbcError 对象的具体方式会因不同的实现而有所不同。在 C 语言中,大多数方法通过引用获取一个 AdbcError 对象,并在发生错误时对其进行填充。而在 Go 语言中,大多数方法会返回一个错误对象,该对象可以被转换为 AdbcError 对象,依此类推。

无论你如何获取错误对象,它总是会包含以下内容:

  • 状态码(在 ADBC 规范中定义的值)
  • 错误信息
  • 可选的供应商代码(供应商特定的状态码)
  • 可选的 5 个字符的 SQLSTATE 代码(类似于 SQL 的供应商特定代码)

驱动程序还可以暴露额外的错误元数据,以丰富错误信息并提供其他结构化信息。这些额外的元数据被称为 错误详情(Error Details) ,它们只是一些键值对,其中值是任意的、不透明的二进制数据。

现在我们已经了解了所有重要的概念,让我们开始一些代码示例吧!

使用 ADBC 实现高性能和适应性

ADBC 有多种语言的绑定实现。像往常一样,我们将重点关注 C/C++、Python 和 Go 绑定的示例。如果你对 C#、Java 或 R 的绑定也感兴趣,可以在 ADBC 文档中找到安装说明和示例:arrow.apache.org/adbc/

使用 C/C++ 的 ADBC

我们将从使用 C/C++ 来利用 ADBC 开始,因此我们需要安装一些包。如果你希望从源代码进行构建,你可以从 GitHub 仓库中下载源码:github.com/apache/arro...。但是,为了方便起见,如果你不想从源代码构建,可以通过几种方法安装这些包。

最简单的方式是使用 conda/mamba 安装各个包。对于 Windows 用户来说,这可能是最简单的方法:

ruby 复制代码
$ mamba install libadbc-driver-flightsql
$ mamba install libadbc-driver-postgresql
$ mamba install libadbc-driver-sqlite
$ mamba install libadbc-driver-snowflake

在使用 conda/mamba 时,确保根据 CONDA_PREFIX 设置必要的环境变量,以便能够找到库。这包括 PKG_CONFIG_PATH 和可能的 CMAKE_PREFIX_PATH

如果你使用的是基于 Debian GNU/Linux 或 Ubuntu 的发行版,或者使用 apt 包管理器的系统,安装过程相对简单。首先,准备并添加 Apache Arrow APT 仓库:

shell 复制代码
$ sudo apt update
$ sudo apt install --y --V ca-certificates lsb-release wget
$ sudo wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
$ sudo apt install --y --V ./apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
$ rm ./apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
$ sudo apt update

仓库设置完成后,可以安装所需的包:

ruby 复制代码
$ sudo apt install libadbc-driver-flightsql-dev
$ sudo apt install libadbc-driver-postgresql-dev
$ sudo apt install libadbc-driver-sqlite-dev
$ sudo apt install libadbc-driver-snowflake-dev

对于基于 yum 仓库的 Linux 发行版,你可以在以下平台上使用 dnf

  • AlmaLinux 8/9
  • Oracle Linux 8/9
  • Red Hat Enterprise Linux 8/9

首先,准备并启用 Apache Arrow Yum 仓库:

shell 复制代码
$ sudo dnf install --y epel-release || sudo dnf install --y oracle-epel-release-el$(cut --d: -f5 /etc/system-release-cpe | cut --d. -f1) || sudo dnf install --y https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(cut -d: -f5 /etc/system-release-cpe | cut --d. -f1).noarch.rpm
$ sudo dnf install --y https://apache.jfrog.io/artifactory/arrow/almalinux/$(cut -d: -f5 /etc/system-release-cpe | cut --d. -f1)/apache-arrow-release-latest.rpm
$ sudo dnf config-manager --set-enabled epel || :
$ sudo dnf config-manager --set-enabled powertools || :
$ sudo dnf config-manager --set-enabled crb || :

仓库设置完成后,安装所需的包:

ruby 复制代码
$ sudo dnf install adbc-driver-flightsql-devel
$ sudo dnf install adbc-driver-postgresql-devel
$ sudo dnf install adbc-driver-sqlite-devel
$ sudo dnf install adbc-driver-snowflake-devel

安装完 ADBC 库后,我们就可以开始了。我们第一个示例将使用 SQLite(www.sqlite.org),这是一个小型、可下载的本地 SQL 数据库引擎。我们还需要 ADBC 头文件,你可以直接从 ADBC 仓库下载(raw.githubusercontent.com/apache/arro...),也可以在本书的示例代码库中找到。

一个简单的 ADBC 示例

此示例代码假设 adbc.h 文件与我们将要编写的 C++ 文件位于同一目录下。我们将创建一个包含表的 SQLite 数据库,并向其中插入一些数据。请注意,ADBC 主要是 C API,所以代码有点冗长。

我们将从创建数据库实例开始:

arduino 复制代码
#include "adbc.h"
int main(int argc, char** argv) {
    struct AdbcDatabase database = {};
    AdbcDatabaseNew(&database, nullptr);
    // 执行操作
    AdbcDatabaseRelease(&database, nullptr);
}

别忘了调用 AdbcDatabaseRelease,否则可能会导致内存泄漏。记住,AdbcDatabase 对象是驱动程序的容器。因此,我们需要为 SQLite 数据库文件指定 URI 位置:

scss 复制代码
AdbcDatabaseNew(&database, nullptr);
AdbcDatabaseSetOption(&database, ADBC_OPTION_URI,
                      "file:data.db", nullptr);
AdbcDatabaseInit(&database, nullptr);

当然,我们一直将 nullptr 作为这些函数调用的最后一个参数传递。我们可能需要为此添加一些状态和错误检查,对吧?

go 复制代码
AdbcError error;
if (AdbcDatabaseSetOption(&database, ADBC_OPTION_URI,
    "file:data.db", &error) != ADBC_STATUS_OK) {
        // 输出 error.message
        error.release(&error);
        return 1;
}

为了简洁起见,我们不会为每个函数调用重复这个错误处理代码。示例代码库定义了一个方便的 AbortNotOk 函数来封装状态检查,因此我们将在后面的示例中使用它。

假设安装正确,你可以使用 pkg-config 编译可执行文件:

css 复制代码
$ g++ adbc_sqlite.cc -o adbc-sqlite $(pkg-config adbc-driver-sqlite --libs --cflags)

或者,你可以使用 cmake 构建它:

shell 复制代码
$ cmake --S ./cpp -B ./build && cmake --build build

然后运行 ./adbc-sqlite,这将创建一个名为 data.db 的数据库文件。

现在,我们可以继续执行查询并处理数据连接与语句对象了。

使用 ADBC 驱动管理器

由于 ADBC 只定义了一组自由函数,它非常适合在构建时直接链接到驱动程序。在这种简单的情况下,调用 API 函数非常简单,因为所有函数都是由驱动程序明确定义的。这可以用图 8.10 来描述:

如果你不想或不能直接链接到驱动程序,那么这种方法就行不通了。

那么,什么时候会发生这种情况呢?例如,如果你想动态加载一个或多个驱动程序?这通常是在将驱动程序加载到其他语言的绑定中时,例如在 Python 中。如果你有多个驱动程序,每个驱动程序都定义了 ADBC 函数,并且这些定义会相互冲突。为了解决这种情况,ADBC 提供了一个作为 AdbcDriver 结构的函数指针表,并且有一个 API 可以从驱动程序请求该表。

图 8.11 展示了应用程序使用此功能的第一步。它通过动态加载驱动程序并调用一个入口点函数来获取该表:

在这个例子中,入口点被称为 AdbcDriverInit,但实际上它可以根据驱动程序的需求命名。应用程序一旦获取了 AdbcDriver 结构中的函数指针表,就可以调用该表中的函数来使用驱动程序。图 8.12 展示了这个过程:

尽管这种方法可以轻松扩展到多个驱动程序,但在实际操作中,管理起来非常复杂且难以处理。因此,ADBC 提供了 Driver Manager,它表现为一个单一的驱动程序,能够像普通驱动程序一样被链接和使用。在内部,它会管理函数表并跟踪每个对象所需的实际驱动程序库。通过使用 Driver Manager,动态加载驱动程序并在同一个应用程序中使用多个驱动程序变得更加简单和高效:

图8.13展示了Driver Manager如何在之前的图中适配,并为应用程序提供了一个透传机制,让它将其视为单一的驱动程序。现在,经过概念上的讲解,我们可以在代码中使用它。猜猜看?相比于前面的例子,只需要额外加一行代码!

scss 复制代码
...
AdbcDatabaseNew(&database, nullptr);
AdbcDatabaseSetOption(&database, "driver", "adbc_driver_sqlite", nullptr);
AdbcDatabaseSetOption(&database, ADBC_OPTION_URI, "file:data.db", nullptr);
AdbcDatabaseInit(&database, nullptr);
...

由于ADBC的SQLite驱动使用默认的入口函数名AdbcDriverInit,我们无需显式指定它。这意味着在上述代码块中,唯一的新增内容就是标记出来的行,在那里我们指定了要加载的驱动程序。由于我们使用的是"adbc_driver_sqlite"来指定驱动程序,系统会根据需要自行解析。在大多数UNIX风格的机器上,系统会查找名为libadbc_driver_sqlite.so的库,可能在本地目录、标准库路径中,或者在由LD_LIBRARY_PATH环境变量定义的路径中查找。对于macOS系统,它会使用.dylib作为扩展名,而在Windows系统中,它会查找adbc_driver_sqlite.dll。当然,也可以指定要加载的具体共享库的绝对路径作为驱动程序。

最后一步是更改我们的编译命令。我们不再链接adbc-driver-sqlite,而是链接adbc-driver-manager。因此,我们需要安装driver-manager包。以下是Windows的安装命令:

ruby 复制代码
$ mamba install libadbc-driver-manager

对于Debian/Ubuntu系统,安装命令如下:

ruby 复制代码
$ sudo apt install libadbc-driver-manager-dev

对于使用Yum包管理器的系统,安装命令如下:

ruby 复制代码
$ sudo dnf install adbc-driver-manager-devel

然后,我们需要相应地调整编译命令:

css 复制代码
$ g++ adbc_sqlite.cc -o adbc-sqlite $(pkg-config adbc-driver-manager --libs --cflags)

就是这么简单!接下来我们将介绍如何在Python中使用ADBC,同时还会涵盖如何连接到SQLite以外的系统。

重要提示

无论你使用哪种语言来调用ADBC,传递的具体选项都是一样的,因此你可以随时回到我们的C++示例,尝试使用新选项连接到不同的数据源。不用担心------到时候我会指出这些内容!

使用 Python 的 ADBC

首先,我们将确认之前的 C++ 示例是否成功将数据写入我们测试的 SQLite 数据库中。ADBC 的 Python 驱动已发布在 PyPi 和 conda-forge 上,因此安装起来非常方便。

通过 PyPi 安装:

ruby 复制代码
$ pip install adbc-driver-sqlite

通过 conda-forge/mamba 安装:

ruby 复制代码
$ mamba install adbc-driver-sqlite

只要 PyArrow 已安装,你就可以使用 ADBC 提供的高层 API,遵循 DBAPI 标准(定义于 PEP 249)。在最简单的情况下,我们可以使用两行代码创建 SQLite 连接:

python 复制代码
>>> import adbc_driver_sqlite.dbapi
>>> conn = adbc_driver_sqlite.dbapi.connect("file:../cpp/data.db")

为了防止内存泄漏,你必须在不再使用连接时通过 .close() 方法关闭连接。为了确保不会忘记关闭连接,强烈建议将连接作为上下文管理器来使用。一旦有了连接,你就需要一个游标来执行查询:

scss 复制代码
>>> cursor = conn.cursor()
>>> cursor.execute('SELECT * FROM foo')
>>> cursor.fetchone()
('bar',)

如果你已经运行了之前的 C++ 示例,数据库文件中应该有一个名为 'foo' 的表,并至少包含一行数据(具体取决于你执行了多少次)。如你所见,通过 DBAPI 标准将数据作为 Python 对象获取非常简单。但这是 Arrow 数据库连接,所以我们可以将结果作为 Arrow 数据来获取:

lua 复制代码
>>> cursor.execute('SELECT * FROM foo')
>>> cursor.fetch_arrow_table()
Pyarrow.Table
col: string
-----
col: [["bar"]]

通过非标准的 fetch_arrow_table 方法,整个结果集会以 PyArrow Table 对象的形式返回。当然,缺点是所有结果都会在内存中物化。然而,这证明了我们之前 C++ 示例中的插入操作成功了,因为返回的行包含字符串 "bar"。

现在,我们连接到另一种类型的数据库。通过 Docker,我们可以轻松启动一个本地的 PostgreSQL 数据库服务器,并使用 ADBC 进行连接。在继续之前,确保你已经安装了 Docker!如果需要说明,你可以回顾第 7 章《探索 Apache Arrow Flight RPC》中关于 Docker 安装的部分。

要启动本地的 Postgres 实例,请运行以下命令:

css 复制代码
$ docker run --rm --e POSTGRES_PASSWORD=mysecret --p 5431:5432 postgres

一旦数据库启动并运行,安装对应的 ADBC 驱动包(你也可以选择使用 conda/mamba):

ruby 复制代码
$ pip install adbc-driver-postgresql

现在,我们只需要将导入从 adbc_driver_sqlite 更改为 adbc_driver_postgresql,并更新连接的 URI。将此代码示例中的高亮部分与之前的 SQLite 示例进行比较:

python 复制代码
>>> import adbc_driver_postgresql.dbapi
>>> uri = "postgresql://postgres:mysecret@0.0.0.0:5431/postgres"
>>> conn = adbc_driver_postgresql.dbapi.connect(uri)
>>> cursor = conn.cursor()
>>> cursor.execute('SELECT 1')
>>> cursor.fetchone()
(1,)

看到需要更改的部分多么少了吗?只需要更改导入和连接字符串。如果我们回到 C++ 示例中,我们也可以通过更新 URI 和驱动名称,在我们的驱动管理器示例中完成相同的操作:

arduino 复制代码
struct AdbcDatabase database = {};
AdbcDatabaseNew(&database, nullptr);
AdbcDatabaseSetOption(&database, "entrypoint", "AdbcDriverInit", nullptr);
AdbcDatabaseSetOption(&database, "driver", "adbc_driver_postgresql", nullptr);
AdbcDatabaseSetOption(&database, "uri", "postgresql://postgres:mysecret@0.0.0.0:5431/postgres", nullptr);
AdbcDatabaseInit(&database, nullptr);

我们还需要更新 SQL 语法,以确保查询在 Postgres 中有效。ADBC 不会执行查询转换,因此应用程序需要确保正确的 SQL 方言更改,就像使用 ODBC 一样:

erlang 复制代码
...
AdbcStatementSetSqlQuery(&stmt,
    "CREATE TABLE IF NOT EXISTS foo ( col varchar(80) )",
    nullptr);
...

我们还能用这个方便的 ADBC 连接做些什么呢?请继续阅读!

使用 ADBC 批量导入数据

之前提到过,ADBC 包含一个批量导入 API。让我们来尝试一下吧!

为此,我们将使用第 6 章《使用 Arrow Datasets API》中的纽约出租车示例数据。你是否还保存着 yellow_tripdata_2015-01.parquet 文件?在继续之前,看看你是否还记得如何将该文件读取为内存中的 pyarrow.Table 表。试试看,然后再查看以下代码示例。

记起来了吗?如果需要帮助,请参考以下代码:

python 复制代码
>>> import pyarrow as pa
>>> import pyarrow.parquet as pq
>>> tbl = pq.read_table('<path/to>/yellow_tripdata_2015-01.parquet')

现在,使用我们的连接和 Arrow 数据表,尝试通过 adbc_ingest 方法将数据添加到 Postgres 数据库:

less 复制代码
>>> cursor.adbc_ingest('taxi-sample', tbl)
Traceback (most recent call last):
  .....
adbc_driver_manager.NotSupportedError: NOT_IMPLEMENTED: [libpq] Field #18 ('congestion_surcharge') has unsupported type for ingestion na

等等,发生了什么?它失败了,但为什么呢?

看一下错误提示中的高亮部分,我们看到它在尝试导入 congestion_surcharge 列时出错了,该列的类型是 na。这是什么意思?让我们看看表的架构:

vbnet 复制代码
>>> tbl.schema
...
congestion_surcharge: null
airport_fee: null
-- schema metadata --
pandas: '{"index_columns": [], "column_indexes": [],
          "columns": [{"name"}:' + 2478

现在我们明白了。查看输出中高亮的行,错误中提到的列 congestion_surcharge 和接下来的列 airport_fee 都是 null 类型。我们可以轻松处理这一问题------在导入数据时删除这些 null 类型的列:

arduino 复制代码
>>> cursor.adbc_ingest('taxi-sample',
        tbl.drop_columns(['congestion_surcharge',
            'airport_fee']))
12741035

成功了!不过,过程似乎花费了一些时间。为什么会这么慢?因为 Postgres 是一个面向行的数据库,我们为 12,741,035 行的列式数据转置为行式数据付出了代价,以便将其插入到 Postgres 中。如果我们插入的是一个列式数据库,ADBC 驱动程序可以利用我们提供的列式 Arrow 数据,显著加快这一过程!稍后我们将对此进行演示,但现在让我们先看看批量导入时可用的选项。

在文档中,你会发现 adbc_ingest 可以使用以下四种模式:

  1. Append: 将新数据插入到已存在的表中。如果表不存在,将发生错误。
  2. Create: 创建一个新表并插入数据。如果表已经存在,将发生错误(这是默认模式)。
  3. Replace: 如果具有相同名称的表已经存在,删除并重新创建该表,并基于输入数据的模式插入新数据。
  4. create_append: 如果表不存在,创建它并插入新数据。如果表已经存在,向其中插入新数据。如果现有模式与新数据不兼容,将发生错误。

现在,让我们回到刚才提到的导入过程因 Postgres 是行式数据库而变慢的问题。我们可以尝试使用 DuckDB,这是一种列式的内存数据库,并且支持与 Arrow 的零拷贝交互。它还可以通过简单的 pip 命令轻松安装:

ruby 复制代码
$ pip install duckdb

我们还需要确保安装了 Driver Manager,因为 DuckDB 实现了 ADBC 接口:

ruby 复制代码
$ pip install adbc_driver_manager

现在,我们只需再次切换导入内容并更新连接代码。由于 DuckDB 像 SQLite 一样是本地进程数据库,我们只需提供一个文件名来作为数据库存储,而不需要完整的 URI。然后,我们可以再次尝试使用 adbc_ingest

python 复制代码
>>> import adbc_driver_duckdb.dbapi
>>> conn = adbc_driver_duckdb.dbapi.connect("test.db")
>>> cursor = conn.cursor()
>>> cursor.adbc_ingest("taxi_sample", tbl)
0
>>> cursor.execute('select count(*) from "taxi_sample"')
>>> cursor.fetchone()
(12741035,)

快得多了,对吧?我们甚至可以非常快地取回整个表:

scss 复制代码
>>> cursor.execute('select * from "taxi_sample"')
>>> tbl2 = cursor.fetch_arrow_table()

现在你看到了,在使用列式数据库时,导入速度快了很多。

最后一件事 ------ 在数据库之间进行流式传输!

在我们开始使用 Go 之前,还有最后一件很酷的事情想给你展示:我们可以使用 ADBC 在两个数据库之间进行数据流式传输!

首先,通过 docker 命令重新启动你的 Postgres 实例,并使用 adbc_ingest 方法重新将样本 Parquet 文件中的数据填充到一个表中。由于我们使用了 Docker 且没有配置共享驱动器,Postgres 数据并没有被持久化。因此,如果你曾经停止并重新启动 Postgres 实例,之前创建的表将不存在。

确保已导入 Postgres 和 DuckDB 驱动程序,并且它们都已建立了活动连接:

python 复制代码
>>> import adbc_driver_postgresql.dbapi
>>> import adbc_driver_duckdb.dbapi
>>> conn_postgres = adbc_driver_postgresql.dbapi.connect(...)
>>> conn_duck = adbc_driver_duckdb.dbapi.connect(...)

接下来,我们需要在 Postgres 实例上执行一个 select 语句。但这次我们不会将数据完全加载到内存中作为一个单独的表,而是使用 RecordBatchReader 逐批流式传输数据:

ini 复制代码
>>> cur = conn_postgres.cursor()
>>> cur.execute('select * from "taxi-sample"')
>>> rdr = cur.fetch_record_batch()

最后,我们可以使用这个 RecordBatchReader 通过 adbc_ingest 方法将数据填充到 DuckDB 实例中的一个表中。通过这种方式使用 reader 可以最大限度地减少内存使用,因为我们避免了一次性将整个结果集作为一个表加载到内存中,而是逐个小批量地跨 ADBC 接口传递数据:

python 复制代码
>>> cur2 = conn_duck.cursor()
>>> cur2.adbc_ingest('taxi2', rdr)
>>> cur2.execute('select count(*) from "taxi2"')
>>> cur2.fetchone()
(12741035,)

瞧!你觉得怎么样?你可以随时查看 ADBC 文档以获取完整的 Python API 函数列表,但我认为我们已经涵盖了足够的信息,应该能让你理解其工作方式了。接下来,让我们试试在 Go 中实现这一操作吧!

使用 ADBC 和 Go

和我们之前的例子一样,我们从使用 SQLite 驱动开始。如果你还没有安装 ADBC 的 SQLite 驱动,请按照之前在 C++ 中使用 ADBC 的部分进行安装。然后,你只需要导入 Go 的 ADBC 包:

go 复制代码
$ go get github.com/apache/arrow-adbc/go/adbc@latest

一切就绪后,我们可以深入了解如何使用 Go 接口与 ADBC 交互。与 C++ 和 Python 类似,Go 也有一个 Driver Manager 包,可以加载实现 ADBC 接口的任意库。在我们的第一个例子中,我们将像在 C++ 中一样加载 SQLite 驱动。

首先,我们需要导入包:

go 复制代码
import (
    "context"
    "fmt"
    "github.com/apache/arrow-adbc/go/adbc/drivermgr"
)

接下来,我们开始编写例子,就像我们之前一样,首先创建我们的 Database 对象:

go 复制代码
func main() {
    var drv drivermgr.Driver
    db, err := drv.NewDatabase(map[string]string{
        "driver": "adbc_driver_sqlite",
        "uri": "file:data.db",
    })
    if err != nil {
        panic(err)
    }
    defer db.Close()
}

这些高亮的行看起来是否很熟悉?它们与我们在 C++ 示例中初始化 Driver Manager 时使用的选项相同。"driver" 选项指定要加载的库,而 "uri" 选项指定 SQLite 要加载的数据库文件。我们还使用了 Go 的 defer 关键字来确保在退出之前关闭数据库,这是我们会经常使用的模式。

现在,让我们打开与 SQLite 数据库的连接:

go 复制代码
    ctx := context.Background()
    cnxn, err := db.Open(ctx)
    if err != nil {
        panic(err)
    }
    defer cnxn.Close()

非常简单明了!因为我们使用的是之前 C++ 示例中创建的相同文件,现在我们来获取我们在那个示例中命名为 foo 的表的模式(schema):

go 复制代码
    sc, err := cnxn.GetTableSchema(ctx, nil, nil, "foo")
    if err != nil {
        panic(err)
    }
    fmt.Println(sc)

注意,我们为 catalogschema 参数传递了 nil。大多数数据库系统是分层的,这些参数允许我们指定要查找的表的位置。由于我们的表不是在任何特定的目录或架构位置创建的,因此我们可以忽略它们。

运行这段代码会给我以下输出,请确保在我们之前创建的 data.db 文件所在的目录中运行此代码!

bash 复制代码
$ go run ./chapter8/go
schema:
    fields: 1
    - col: type=utf8, nullable

这很合理!有一个名为 col 的字符串类型列,正是我们之前创建的。让我们通过对该表执行 SELECT 语句来确认我们获得了正确的结果。与之前一样,我们首先创建一个 Statement 对象,然后使用它来执行查询:

go 复制代码
    stmt, err := cnxn.NewStatement()
    // check err and handle it
    defer stmt.Close()
    stmt.SetSqlQuery("SELECT * FROM foo")
    rdr, _, err := stmt.ExecuteQuery(ctx)
    // check err and handle it
    defer rdr.Release()
    for rdr.Next() {
       fmt.Println(rdr.Record())
    }

看看高亮的几行代码,这正是你所期望的------我们在语句上设置查询,然后执行它。你会注意到,我们忽略了其中一个返回值,即返回的行数。在撰写本文时,SQLite 驱动并不返回结果中的总行数,而是返回 -1。其余的代码应该让你感到熟悉,因为它与我们之前的例子一样,用 RecordReader 循环打印结果。

整理好一切准备开始

根据之前的示例,您能否弄清楚如何使用 Postgres 驱动将 ADBC 连接到 Go 中的 Postgres?如果您猜对了,只需要将 "file

.db" 值替换为我们之前使用的 URI 就可以了!

但是,要在 Go 中复制我们之前使用 Python 进行的从 Postgres 到 DuckDB 的数据流传输示例,可能会稍微复杂一些。第一步是使用 ADBC 的批量导入功能将 Parquet 文件中的数据流传输到 Postgres。我们来一步步走过这个过程:

首先,我们需要添加一些导入:

go 复制代码
import (
    ...
    "os"
    ...
    "github.com/apache/arrow/go/v18/parquet"
    "github.com/apache/arrow/go/v18/parquet/file"
    "github.com/apache/arrow/go/v18/parquet/pqarrow"
    "github.com/apache/arrow/go/v18/arrow"
    "github.com/apache/arrow/go/v18/arrow/memory"
    ...
)

接下来,我们必须使用 pqarrow 包来直接从 Parquet 文件中生成 Arrow 记录。请注意,我们必须启用并行列读取并提供批量大小,以控制每个记录批次包含的最大行数。这些设置可以帮助我们控制内存使用量:

go 复制代码
rdr, err := file.OpenParquetFile(
     "path/to/yellow_tripdata_2015-01.parquet", false)
 // handle if err != nil
 defer rdr.Close()
 pqrdr, err := pqarrow.NewFileReader(rdr,
     pqarrow.ArrowReadProperties{
             Parallel: true,
             BatchSize: 102400},
     memory.DefaultAllocator)
 // handle if err != nil

您可能还记得,Parquet 文件的最后两列是 null 列,Postgres 无法处理。所以,我们需要创建一个只包含我们想从文件中获取的列索引的列表,跳过 null 列:

go 复制代码
cols := make([]int, 0)
for _, f := range pqrdr.Manifest.Fields {
    if f.Field.Type.ID() != arrow.NULL {
        cols = append(cols, f.ColIndex)
    }
}

现在,我们可以创建一个 RecordReader 类来从文件中流式读取 Arrow 记录批次。我们将最后一个参数传递为 nil,因为我们想读取所有的行组。如果我们不跳过任何列,也可以使用 nil 来表示我们想要所有列,但现在我们会传递我们的列索引切片:

go 复制代码
recrdr, err := pqrdr.GetRecordReader(ctx, cols, nil)
 // handle if err != nil
 defer recrdr.Release()

接着,我们需要像之前在 SQLite 中那样设置与 Postgres 数据库的连接。使用我们之前用来连接的相同 URI:

go 复制代码
db, err := drv.NewDatabase(map[string]string{
    "driver": "adbc_driver_postgresql",
    "entrypoint": "AdbcDriverInit",
    adbc.OptionKeyURI: "<postgres_uri>"})
// handle if err != nil
defer db.Close()
cnxn, err := db.Open(ctx)
// handle if err != nil
defer cnxn.Close()

最后,我们必须创建一个 Statement 并将记录批次读取器绑定为输入。同时,我们需要设置必要的导入选项,以定义我们想要使用的模式和要创建的表名。幸运的是,ADBC 库为我们提供了常量来用于选项键和值:

go 复制代码
stmt, err := cnxn.NewStatement()
// handle if err != nil
defer stmt.Close()
stmt.SetOption(adbc.OptionKeyIngestMode,
               adbc.OptionValueIngestModeReplace)
stmt.SetOption(adbc.OptionKeyIngestTargetTable,
               "taxisample")
if err := stmt.BindStream(ctx, recrdr); err != nil {
   panic(err)
}

剩下的就是执行导入!我们还会打印出导入的行数:

go 复制代码
n, err := stmt.ExecuteUpdate(ctx)
// handle if err != nil
fmt.Println(n)

运行更新后的代码时,它应该输出与我们在 Python 示例中看到的相同的行数:12741035

接下来的步骤是为 DuckDB 创建驱动程序和连接。基于目前为止的示例,代码应该相当简单。不过,有一个小问题:我们需要确保有 DuckDB 库可供链接。您可以通过几种不同的方法获取它,说明也可以在 DuckDB 网站上找到:DuckDB 安装说明。最方便的选项是使用 conda/mamba 安装:

ruby 复制代码
$ mamba install libduckdb

无论您是通过什么方式获取 libduckdb.so 文件,请确保它位于您运行的本地目录中,或者在 LD_LIBRARY_PATH 中引用的目录中,这样系统就可以找到并加载它。现在,我们可以像之前一样创建驱动程序:

go 复制代码
duckdb, err := drv.NewDatabase(map[string]string{
    "driver":     "duckdb",
    "entrypoint": "duckdb_adbc_init",
    "path":       "test.db"})
// handle if err != nil
defer duckdb.Close()
duckCon, err := duckdb.Open(ctx)
// etc.....

注意事项

  1. DuckDB 的 ADBC 实现中有两个需要注意的点。首先,DuckDB 期望一个名为 "path" 的参数,而不是 "uri"。其次,"entrypoint" 参数指定了用于填充函数指针表的函数。对于 DuckDB,我们指定入口点为 "duckdb_adbc_init",该函数会导出并返回所需的函数表。

由于我们已经讨论了如何设置导入操作,接下来要做的就是从 Postgres 获取记录流,并将其用作导入 DuckDB 的输入:

go 复制代码
// cnxn 是我们的 postgres 连接
stmt, err := cnxn.NewStatement()
// handle if err != nil
defer stmt.Close()
stmt.SetSqlQuery("SELECT * FROM taxisample")
results, n, err := stmt.ExecuteQuery(ctx)
// handle if err != nil
defer results.Release()

调用 ExecuteQuery 将返回记录读取器、记录数(如果已知)和错误。在我们的例子中,results 是我们的记录读取器,可以传递给 DuckDB 连接上的 Statement 对象的 BindStream 方法。您可以在本书的 GitHub 仓库中找到此代码的其余部分,但我建议您首先自己尝试编写代码!我们已经涵盖了完成这项任务所需的所有部分------我相信您能够做到。

最后一件事------使用 database/sql

Go 标准库提供了一个用于与基于 SQL 的数据库交互的标准化接口,任何包都可以实现这个接口。其好处在于,许多包都是根据这个称为 database/sql 的标准库接口编写的,可以被任何数据源使用。这与 ADBC 的有用性类似,但它是 Go 语言特有的。

尽管这个接口是面向行的,但 ADBC Go 包实现了一个适配器,使任何 ADBC 驱动程序都可以自动且轻松地通过这个接口使用。在本章的最后一个代码示例中,我们将展示如何使用这个有用的适配器:

像往常一样,我们从新的导入行开始:

go 复制代码
import (
    "database/sql"
    ...
    "github.com/apache/arrow-adbc/go/adbc/sqldriver"
)

接下来,我们只需要将我们的驱动程序注册到 Go 的 database/sql 包中。在这个例子中,我们将注册驱动程序管理器(Driver Manager),但我们也可以直接注册用 Go 原生编写的任何 ADBC 驱动程序:

less 复制代码
  sql.Register("adbc", sqldriver.Driver{Driver:
      &drivermgr.Driver{}})

现在,我们只需要打开一个连接并使用它。database/sql 包将为我们处理连接池和线程安全,因此我们只需让它在底层管理事务,代价是需要将列转换为行:

go 复制代码
db, err := sql.Open("adbc",
    "driver=duckdb;
    entrypoint=duckdb_adbc_init;
    path=test.db")
 // handle if err != nil
 defer db.Close()
 rows, err := db.Query("SELECT ?", 1)
 // handle if err != nil
 defer rows.Close()
 for rows.Next() {
     var v int64
     if err := rows.Scan(&v); err != nil {
         panic(err)
     }
     fmt.Println(v)
 }

在继续之前,您可以查看 ADBC 文档中列出的其他可用功能!为了节省您翻页的麻烦,这里是链接:ADBC 文档

总结

到目前为止,我们一直在讨论数据访问的客户端 API 并对其进行标准化(ODBC 和 JDBC 就是这样做的)。但实际上,在连接性方面有两个重要的标准化领域:客户端 API 和传输协议(Wire Protocol)。无论是 ADBC、ODBC 还是 JDBC,都没有定义驱动程序与数据库之间发生的事情,它们只是定义了应用开发人员作为客户端 API 所调用的 API。

在第 7 章《探索 Apache Arrow Flight RPC》中提到的 FlightSQL 是一种传输协议。它准确地定义了如何与数据库通信并执行各种操作,例如身份验证、创建预处理语句或执行查询。另一个流行的传输协议示例是 PostgreSQL 的传输协议。这些标准的存在使可重用组件成为可能。尽管我们没有详细讨论这一点,但实际上已经存在一个用于 ADBC 的 FlightSQL 驱动程序。这意味着任何已经使用 Arrow FlightSQL 作为其传输协议的数据库都可以通过 ADBC 进行访问和利用,而无需数据库开发人员进行任何进一步的开发。拥抱 Arrow、ADBC 和 FlightSQL 等标准,使得我们的工具和应用程序变得模块化且可组合。这让工程师可以灵活搭配不同的组件,使用最适合当前任务的库。

下一章将讨论如何将 Arrow 与机器学习工作流结合使用。由于生态系统已经拥抱了这些开放标准,开发人员可以选择大量高性能且有用的工具和库。翻页继续,让我们一起整合我们所讨论的概念,并了解当前流行的库是如何使用 Arrow 的!

相关推荐
Leoysq18 分钟前
深度学习领域的主要神经网络架构综述
深度学习·神经网络·架构
中东大鹅2 小时前
分布式数据存储基础与HDFS操作实践
大数据·linux·hadoop·分布式·hbase
Y编程小白2 小时前
Git版本控制工具--基础命令和分支管理
大数据·git·elasticsearch
殇淋狱陌3 小时前
第四章 列表(List)&元组(Tuple)以及代码格式基础语法及操作
数据结构·python·数据分析·list·元组
不爱学习的YY酱4 小时前
【操作系统不挂科】<内存管理-文件系统实现(18)>选择题(带答案与解析)
java·大数据·数据库
guanpinkeji4 小时前
陪诊小程序搭建,打造一站式陪诊服务
大数据·小程序·小程序开发·陪诊·陪诊小程序
胡耀超5 小时前
如何从全局视角规划项目与战略决策(“精准接送”案例、技术架构设计与选型、业务逻辑及产品商业模式探讨)
大数据·数据挖掘·软件架构·商业模式·数据管理
标贝科技5 小时前
标贝科技受邀出席2024ADD数据应用场景大会 共议数据要素发展新契机
大数据·数据库·人工智能·科技·语言模型·数据挖掘
drbool5 小时前
AI驱动的数据分析:利用自然语言实现数据查询到可视化呈现
人工智能·信息可视化·数据分析
出发行进6 小时前
Hive其五,使用技巧,数据查询,日志以及复杂类型的使用
大数据·hive·数据分析