背景

duckdb 是一个 C++ 编写的单机版嵌入式分析型数据库。它刚开源的时候是对标 SQLite 的列存数据库,并提供与 SQLite 一样的易用性,编译成一个头文件和一个 cpp 文件就可以在程序中使用,甚至提供与 SQLite 兼容的接口,因此受到了很多人的关注

本文介绍笔者近期开发的 duckdb-rs 库,让大家可以很方便地在 rust 代码库中使用 duckdb 的功能。

libduckdb-sys

了解过 rust 的同学可能知道,rust 提供了 ffi 的方式与其他语言互通。因为 duckdb 本身是 C++ 编写的,想要在 rust 里面使用 duckdb,就需要考虑 ffi 的问题。而基于 ffi 对其他语言程序封装的基础库,一般会被命名为 libxxx-sys,这也就是 libduckdb-sys 的由来。

为了方便大家使用,duckdb 提供了 C++ 原生接口,C 接口,以及与 SQLite3 兼容的 C 接口。我在做 libduckdb-sys 的时候对这三种接口都尝试过,相关的讨论可以参见 Rust Support,我这里介绍一下当时的情况。

基于 SQLite3 接口

最开始我使用的是 SQLite3 的接口,原因主要有三个:

  1. 我对 SQLite 比较熟悉,想必用起来会比较方便;
  2. 觉得 SQLite 的接口被广泛使用,接口比较稳定,以后不至于大改;
  3. 也许是最重要的一点,市面上已经有 SQLite 的 rust 封装rusqlite,基于 SQLite 的接口应该能最大程度复用 rusqlite 的代码。

尝试之后确实发现很快能把程序跑起来,基本的功能也能使用。但是随着进一步的深入以及对 duckdb 更多的了解,发现了一些弊端:

  1. 虽说 duckdb 是想最大程度兼容 SQLite,但是毕竟一个是行存一个是列存,有区别在所难免,接口肯定也没办法做到 100% 兼容;
  2. 有一个区别需要特别提出来,SQLite 是动态数据类型,而 duckdb 是静态类型,也就是说在 SQLite 中你可以认为所有的数据都是存成 Text,在读取的时候根据 schema 来解析数据;而 duckdb 是会根据数据类型来存储数据,并且根据列存的特性做一些存储优化。有了这个区别之后,如果我们使用 SQLite 的接口的话,会做一些不必要的数据格式转换,性能有损,程序也不直观。
  3. duckdb 可以被编译成一个 so 使用,如果想使用 SQLite 的接口,需要再编译一个 sqlite3_api_wrapper 出来,两个库合作才能使用 SQLite 的接口,这给程序分发引入了额外的负担;另外目前 duckdb 在 release 的时候没有自带 sqlite3_api_wrapper,需要用户自己去编译,使用上又多了一些不便。
  4. 由于上面的封装的问题,数据类型的问题,以及通过 SQLite 接口查询 duckdb 的数据时候,结果集会被复制一遍,资源占用必定上升。

基于上面一些原因,我最终放弃了基于 SQLite 接口来开发,转而尝试使用原生的 C++ 或者 C 接口。

基于 C++ 接口

既然为了性能和接口丰富性,使用 C++ 接口当然是首选,毕竟 duckdb 本身主要都是拿 C++ 开发的,duckdb 的 python 封装 也是拿 C++ 接口来做的。

市面上也有方便 rust 与 C++ 交互的一些代码库,比如 cxxautocxx。其中 autocxx 入手门槛低使用上更简单,而 cxx 的可定制性更强,功能更丰富。在尝试了几次之后发现了一些问题,主要还是 rust ffi 只能支持部分的 C++ 语法,大部分情况下可能是够用的,但是对于 duckdb 这样比较大型的数据库代码,还是有很多不支持的地方。除非自己再基于现有的 C++ 接口封装一份支持 cxx 的版本,否则就算这一次编译过了,也很难保证以后 duckdb 的作者以后不会引入其他的特性导致不能兼容。

而 rust 基于 C 语言的 ffi 是原生支持的,所以最终还是下定决心基于 C 接口来开发。

基于 C 接口

因为有 rusqlite 作为参考,所以很快实现了基于 C 接口的版本。简单来说,主要是通过 cbindgenbuild.rs 和 rust 的 features 功能来实现。其中:

  • cbindgen 用于生成基于 C 接口的 rust 代码,方便 rust 其他程序使用
  • build.rs 和 features 用于控制整个编译流程,用户可以根据需要是当场编译依赖库,还是使用机器上已经安装好的版本
  • build.rs 中还可以选择使用 cc 来实时编译 duckdb 实现,这样其他使用 rust 封装的人不用关心 duckdb 的安装问题

应该说这是一个很通用的提供 C 接口 rust 封装的解决方案,感兴趣的同学可以 参考

duckdb-rs

完成了 libduckdb-sys 之后其实只是第一步,因为这样生成的代码都是 unsafe 代码,具体的使用例子可以参考 lib.rs 中的测试代码。但是我们使用 rust 主要是为了他的安全性,rust 希望我们尽量减少 unsafe 的使用。所以一般的 rust 封装都会基于 libxxx-sys 提供一个内存安全的版本,这就是 duckdb-rs 的部分。

小试牛刀

还是因为有 rusqlite 的参考,所以花了一些时间终于实现了最初始的版本,并且我已经把这个版本发布到 crates.io 上了。这个版本的目标是基于 rusqlite 做最小的改动,并删掉 SQLite 特有的功能,让整个程序跑起来。完成之后效果不错,下面是文档中给的一个使用范例:

use duckdb::{params, Connection, Result};

#[derive(Debug)]
struct Person {
id: i32,
name: String,
data: Option<Vec<u8>>,
} fn main() -> Result<()> {
let conn = Connection::open_in_memory()?; conn.execute_batch(
r"CREATE SEQUENCE seq;
CREATE TABLE person (
id INTEGER PRIMARY KEY DEFAULT NEXTVAL('seq'),
name TEXT NOT NULL,
data BLOB
);
")?; let me = Person {
id: 0,
name: "Steven".to_string(),
data: None,
};
conn.execute(
"INSERT INTO person (name, data) VALUES (?, ?)",
params![me.name, me.data],
)?; let mut stmt = conn.prepare("SELECT id, name, data FROM person")?;
let person_iter = stmt.query_map([], |row| {
Ok(Person {
id: row.get(0)?,
name: row.get(1)?,
data: row.get(2)?,
})
})?; for person in person_iter {
println!("Found person {:?}", person.unwrap());
}
Ok(())
}

可以看到,接口设计非常优雅,代码也非常符合 rust 的风格,使用上也非常方便。实现过程中发现有些 duckdb 的 C 接口还不支持的部分,我也通过提 issue 或者 PR 去解决了。这里必须要提一点,duckdb 的维护者非常耐心,不管是回答问题还是 review 代码都非常专业。

剩下的问题有一个是之前提到的,duckdb 是静态类型的数据,所以需要支持很多数据类型,这里面工作量不小。另外,因为我之前也有关注 Apache Arrow,做过 OLAP 数据库的同学可能知道,Apache Arrow 是一个通用的列式内存格式,方便在内存中做大数据量的计算或者传输,有很多 OLAP 数据引擎都在用。刚好 duckdb 也支持 arrow 格式,所以就想尝试使用 arrow 格式来查询数据,这样至少有两个好处,一个是这样我们就可以暴露 arrow 格式的数据给用户,在使用的时候就可以用上 arrow 生态的其他功能,有可能会产生一些化学反应;另外 arrow 也是有丰富的数据类型和明确的定义,反正我们是要支持很多数据类型的,现在的 C 接口本身也不完善,用 arrow 格式反而更加清晰。

通过 Apache Arrow 查询数据

基于上面的考虑,我把目标又看向了 arrow-rs,并给 duckdb 的 C 接口也加上了 arrow 的功能,最终在 duckdb-rs 中实现了通过 Arrow 格式来查询数据,实现参见 这里

实现之后,之前通过行来读取数据的接口完全不变,还能直接查询到 Arrow 格式的数据,下面是一个测试的例子:

fn test_query_arrow_record_batch_large() -> Result<()> {
let db = Connection::open_in_memory().unwrap();
db.execute_batch("BEGIN TRANSACTION")?;
db.execute_batch("CREATE TABLE test(t INTEGER);")?;
for _ in 0..300 {
db.execute_batch("INSERT INTO test VALUES (1); INSERT INTO test VALUES (2); INSERT INTO test VALUES (3); INSERT INTO test VALUES (4); INSERT INTO test VALUES (5);")?;
}
db.execute_batch("END TRANSACTION")?;
let rbs = db.query_arrow("select t from test order by t", [])?;
assert_eq!(rbs.len(), 2);
assert_eq!(rbs.iter().map(|rb| rb.num_rows()).sum::<usize>(), 1500);
assert_eq!(
rbs.iter()
.map(|rb| rb
.column(0)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.iter()
.map(|i| i.unwrap())
.sum::<i32>())
.sum::<i32>(),
4500
);
Ok(())
}

可以看到,我们查询到 Arrow 格式的数据之后,还能通过 arrow-rs 中提供的其他能力做进一步的计算,十分方便。

总结

本文主要介绍了 duckdb-rs 的设计和实现,笔者之前有一些开发 OLAP 数据的经验,但是对于 rust 算是新手,之前虽然写过一些但是没有深入学习,做这个项目也有一个目的是为了重新学习一下 rust。好在有 rusqlite 作为参考,所以没有碰到特别多语言层面的问题。

希望这篇文章对于其他对 rust 和数据库感兴趣的同学有一些帮助。同时这个库还有很多没解决的问题,比如支持更多的数据类型,支持连接池,支持更快的数据导入接口等等,我已经建了一些 issues,感兴趣的同学可以回复 issue 认领,我也会竭力提供需要的帮助,大家一起讨论和学习。

参考

基于 apache-arrow 的 duckdb rust 客户端的更多相关文章

  1. 基于 Apache Mahout 构建社会化推荐引擎

    基于 Apache Mahout 构建社会化推荐引擎 http://www.ibm.com/developerworks/cn/views/java/libraryview.jsp 推荐引擎利用特殊的 ...

  2. WebService学习之旅(五)基于Apache Axis2发布第一个WebService

    上篇博文介绍了如何將axis2 webservice引擎安装到Web容器中,本节开始介绍如何基于apache axis2发布第一个简单的WebService. 一.WebService服务端发布步骤 ...

  3. 基于Apache Thrift的公路涵洞数据交互实现原理

    基于Apache Thrift的公路涵洞数据交互实现原理 Apache Thrift简介 Apache Thrift(以下简称为“Thrift”) 是 Facebook 实现的一种高效的.支持多种编程 ...

  4. Tomcat:基于Apache+Tomcat的集群搭建

    根据Tomcat的官方文档说明可以知道,使用Tomcat配置集群需要与其它Web Server配合使用才可以完成,典型的有Apache和IIS. 这里就使用Apache+Tomcat方式来完成基于To ...

  5. 项目源码--Android基于LBS地理位置信息应用的客户端

    下载源码 技术要点: 1. LBS应用框架客户端实现 2. 登录与注册系统 3. TAB类型UI实现 4. HTTP通信模块 5. 源码带详细的中文注释 ...... 详细介绍: 1. LBS应用框架 ...

  6. 基于Apache搭建Nagios图形监控

    基于apache 的稍微简单一点么?实验一下子就OK了... 环境: System: [root@losnau etc]# cat /etc/issueRed Hat Enterprise Linux ...

  7. Apache Arrow 内存数据

    1.概述 Apache Arrow 是 Apache 基金会全新孵化的一个顶级项目.它设计的目的在于作为一个跨平台的数据层,来加快大数据分析项目的运行速度. 2.内容 现在大数据处理模型很多,用户在应 ...

  8. 在Linux(CentOS 6.6)服务器上安装并配置基于Apache的SVN服务器

    #!/bin/bash # # 在Linux(CentOS 6.6)服务器上安装并配置基于Apache的SVN服务器: # # .安装服务 # .创建svn版本库 # .创建svn用户 # .配置sv ...

  9. 基于Android的小巫新闻客户端开发系列教程

    <ignore_js_op> 141224c6n6x7wmu1aacap7.jpg (27.51 KB, 下载次数: 0) 下载附件  保存到相册 23 秒前 上传   <ignor ...

随机推荐

  1. redis 记一次搭建高可用redis集群过程,问题解决;Node 192.168.184.133:8001 is not configured as a cluster node

    ------------恢复内容开始------------ 步骤 1:每台redis服务器启动之后,需要将这几台redis关联起来, 2: 关联命令启动之后 报错: Node 192.168.184 ...

  2. Spring Cloud06: Ribbon 负载均衡

    一.使用背景 前面的学习中,我们已经使用RestTemplate来实现了服务消费者对服务提供者的调用,如果在某个具体的业务场景下,对某个服务的调用量突然大幅提升,这个时候就需要对该服务实现负载均衡以满 ...

  3. Spring Cloud02:Eureka Server注册中心

    一.Eureka是什么 Eureka是Netflix开源的基于REST的服务治理方案,Spring Cloud集成了Eureka,提供服务治理和服务发现功能,可以和基于Spring Boot搭建的微服 ...

  4. 深入理解java虚拟机笔记Chapter2

    java虚拟机运行时数据区 首先获取一个直观的认识: 程序计数器 线程私有.各条线程之间计数器互不影响,独立存储. 当前线程所执行的字节码行号指示器.字节码解释器工作时通过改变这个计数器值选取下一条需 ...

  5. LeetCode 每日一题「判定字符是否唯一」

    我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章,回复[资料],即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板. 题目 ...

  6. LeetCode:322. 零钱兑换

    链接:https://leetcode-cn.com/problems/coin-change/ 标签:动态规划.完全背包问题.广度优先搜索 题目 给定不同面额的硬币 coins 和一个总金额 amo ...

  7. (5)使用自定Web根目录

    调整 Web 站点 http://server0.example.com 的网页目录,要求如下: 1) 新建目录 /webroot,作为此站点新的网页文件根目录 # mkdir /webroot # ...

  8. linux安装后配置

    1.1 系统设置(自测用,公司不需要) 1.1.1 Selinux系统安全保护 Security-Enhanced Linux – 美国NSA国家安全局主导开发,一套增强Linux系统安 全的强制访问 ...

  9. MySQL 最佳实践 —— 高效插入数据

    当你需要在 MySQL 数据库中批量插入数百万条数据时,你就会意识到,逐条发送 INSERT 语句并不是一个可行的方法. MySQL 文档中有些值得一读的 INSERT 优化技巧. 在这篇文章里,我将 ...

  10. PRVF-0002: could not retrieve local nodename报错解决