背景

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

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

基于 C 接口

因为有 rusqlite 作为参考,所以很快实现了基于 C 接口的版本。简单来说,主要是通过 cbindgen、build.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. python_appium 之使用Appium Inspector定位工具进行元素识别,编写验证demo

    一.前提条件 appium环境搭建完成,模拟器Genymotion 安装完成,且已经下载成功了模拟设备 二.元素识别操作步骤及demo 1.进入appium如下菜单 2.填写Desired Capab ...

  2. noip2009 总结

    潜伏者 原题 R 国和 S 国正陷入战火之中,双方都互派间谍,潜入对方内部,伺机行动.历尽艰险后,潜伏于 S 国的 R 国间谍小 C 终于摸清了 S 国军用密码的编码规则:1. S 国军方内部欲发送的 ...

  3. NOIP模拟测试24「star way to hevaen·lost my music」

    star way to heaven 题解 大致尝试了一下并查集,记忆化搜索,最小生成树 最小生成树是正解,跑最小生成树然后找到最大的值 欧几里德距离最小生成树学习 prim楞跑 至于为什么跑最小生成 ...

  4. Unity项目代码书写规范

    以Google的代码规范为主,稍加改动 https://google.github.io/styleguide/csharp-style.html 书写规范 基础写法 Pascal和驼峰混用,参数用驼 ...

  5. Redis之阻塞分析

    Redis是典型的单线程架构,所有的读写操作都是在一条主线程中完成的.当Redis用于高并发场景时,这条线程就变成了它的生命线.如果出现阻塞,哪怕是很短时间,对于我们的应用来说都是噩梦.导致阻塞问题的 ...

  6. <5人公司极简研发方案

    人过35,被年轻人卷走了一大半,还停留在这个行业的,不是在创业,就是在创业的路上. 创业很难,刚开始没钱没人,啥都要自己干,一个字累.好处是地基是自己搭的,心里有底.不过博主最近健忘的毛病愈发严重了, ...

  7. SpringBoot Validation优雅的全局参数校验

    前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...

  8. 49、django工程(cookie+session)

    49.1.介绍: 1.cookie不属于http协议范围,由于http协议无法保持状态,但实际情况,我们却又需要"保持状态",因此cookie就是在这样一个场景下诞生. cooki ...

  9. ubuntu 替换某一内核模块

    流程 方法一 以下配置仅执行一次,并以 linux kernel 3.13.0 为例 $ cd ~ $ apt-get source linux-source-3.13.0 $ cd linux-3. ...

  10. js--你需要知道的字符串使用方法(含es6及之后)

    前言 字符串作为 JavScript 的基本数据类型,在开发以及面试过程中作为程序员对基础掌握情况的重要考点,本文来总结一下字符串的相关属性以及用法.包含了ES6中的一些新语法特性. 正文 1.字符串 ...