?MotherDuck: 大数据已“死”,逆行者DuckDB

大家好,我是Tim。

最近在做些关于嵌入式数据库的事情,经常会刷到关于DuckDB的相关文章,其实我在很早就关注过DuckDB,今天我们就聊一聊DuckDB的来龙去脉,以及其原理和用法。

上次刷到DuckDB还是在去年的时候,那时DuckDB被疯狂的炒作,其github的关注度也一直在上升。

其母公司MortherDuck提出了非常吸睛的言论:”事实是,‘大数据’已经死了,理解数据的简单性和易用性比数据的大小重要得多“。”今天的笔记本电脑比数据仓库更快,随着硬件的进步,大多数工作负载不再需要分布式计算“。

MotherDuck 团队认为,他们是数据组织中的”逆行者“,他们违背了行业和研究中的普遍智慧,即只有大规模和分布式数据处理将是前进的方向。

他们相信,虽然存在大量数据集,但它们大多存在于已经拥有处理它们的技术专长的组织中。他们将押注于高效且符合人体工程学的单节点分析

当然,他们的这些话语中,不乏炒作的痕迹。

什么是DuckDB?

在2019年的时候,荷兰国家计算科学与数学机构 CWI发表了一篇关于DuckDB的论文:DuckDB: an Embeddable Analytical Database。

提出了在OLAP领域建立一个嵌入式的数据库,主要解决交互式数据分析和边缘计算的问题。

在论文中,他把DuckDB总结为一个进程内 SQL OLAP 数据库管理系统。让我们试着解读一下这个句子,因为它包含了相关的信息:

  1. 进程内SQL 表示DuckDB的功能在您的应用程序中运行,而不是连接到您的应用程序的外部进程。换句话说:没有客户端发送指令,也没有服务器读取和处理它们。和SQLite的工作方式相同,而与PostgreSQL、MySQL等则不同。

  2. OLAP 代表OnLine Analytical Processing,Microsoft将其定义为组织大型业务数据库并支持复杂分析的技术。它可以用于执行复杂的分析查询,而不会对事务处理系统产生负面影响。另一个OLAP数据库管理系统的例子是Terradata。

我们把DuckDB称作是SQLite的向量化实现,准确的说是OLAP 领域的 SQLite。没错,这个SQLite就是你所想象的那个———我们在开发手机软件时所使用最多的关系型数据库软件。

所以说要聊 DuckDB,我们先来看看 SQLite。

SQLite可以称得上世界上使用最多的关系型数据库系统,我们几乎在每台手机、每个浏览器和操作系统上都能找到它的身影。它是一个嵌入式、小型化的数据库,它几乎绑定了每种语言,所以其使用非常方便。

但SQLite 是为 OLTP 而设计的,采用行存储,不能利用内存来加快计算速度,查询优化器非常有限,所以对于分析任务来说非常不友好。

DuckDB正是基于此,打造了一个进程内嵌入式数据库。

嵌入式数据库,我们俗称为链接库,即通过链接到其他程序中运行。它不像传统MySQL等服务型数据库,需要配置client端和server端,而嵌入式数据库一般是单进程的,属于进程内的数据库。

DuckDB是一个开源的进程内数据库,在DuckDB之前,针对OLAP场景的尚未出现,或者说在 OLAP 领域缺少一个嵌入式的数据库管理系统。

要建立一个OLAP领域的嵌入式数据库需要满足以下几个需求:

  1. 高性能的OLAP workload,且不会过多牺牲OLTP性能
    很多场景中,既有高并发使用OLTP的更新,也有并行的OLAP查询存在。

  2. 高度稳定性
    如果embeded database挂掉,会影响其宿主直接宕机。

  3. 高效的数据传输
    因为embeded database和application运行在同一个进程中,共享相同的地址空间,因此这一点比较容易实现。

  4. 嵌入性和可移植性
    该database可以在任何环境中都能正常运行,不能依赖任何编译时或周二运行时外部库,同时也不能修改系统的Signal handling。

DuckDB则是一款完全符合上述几个要求的OLAP场景embeded database。

DuckDB的特点

DuckDB 没有使用革命性的组件,而是将成熟的,适合其场景的技术方法进行组合。

作为一个嵌入式的数据库,它没有客户端协议接口,或者服务端进程。而是使用 C++ 语言的 API。

Duckdb 提供了SQLite 的兼容层,允许之前使用 SQLite 的重新连接到 DuckDB,可见DuckDB也可以直接应用于场景的SQLite场景。

DuckDB的实现主要分如下几个部分:parser、logical planner、optimizer、physical planner以及execution engine。

  1. Parser

    DuckDB的SQL parser是从Postgres的libpg_query精简而来的。这样做的好处在于既可以提供一个功能丰富的、又稳定的的Parser实现。

    由于Postgres是C语言写的,通过这个SQL parser会得到一个C语言结构体表示的parse tree。DuckDB会立即将其转换成自己内部的C++对象,以尽量减少Postgres中的数据结构渗透到DuckDB代码中的多个角落。

  2. Logical Planner
    DuckDB的Logical planner由binder和plan generator两部分组成:

    binder将parse tree与schema的信息(列名、类型等)绑定起来。

    plan generator将parse tree转换成一颗逻辑查询操作符(scan, filter, project等)树。

    经过planning阶段,我们可以获得一个fully type-resolved逻辑执行计划(logical plan)。

    另外,DuckDB保存了所存储数据的统计信息,这些数据在Logical Planner阶段通过不同的expression trees向下传播,这些统计信息则用于Optimizer阶段。

  3. Optimizer
    DuckDB实现了基于规则(rule-based)和基于代价(cost-based)的优化器。Optimizer的主要任务是优化SQL,它将前面logical planner生成的logical plan转换成一个等价但执行代价更小的logical plan。常见的优化方式有谓词下推(predicate pushdown)、表达式重写(expression rewriting)、调整 join 顺序(join ordering)等。

  4. Physical Panner
    Physical planner将Logical plan转换成physical plan。Physical plan是一个真正可以执行的物理计划。在生成physical plan过程中,会选择合适的实现,例如:根据join predicates来选择是使用hash join或者merge join

  5. Execution Engine
    DuckDB实现了一个向量化(vectorized)的解释型执行引擎。向量化可以利用 CPU 提供的 SIMD 指令加速计算。这可能会带来可移植性问题的原因而没有选择JIT的实现方式,是因为JIT需要依赖编译组件,比如LLVM。

    Execution engine是以向量火山的模式开始执行查询计划的。Query execution以从物理执行计划的root节点拉取chunk data开始。chunk data是结果集中间表或者基表的水平子集。该节点将会递归的从子节点拉取数据,最终到达scan operator,该scan operator将会通过从persistent tables中读取以获取chunk data。当到达root节点的chunk是空时则代表该计划已经执行完。

  6. Transaction
    DuckDB通过MVCC提供了ACID兼容性。DuckDB实现了HyPer的MVCC可序列化变体,其实专门针对HTAP场景设计的。该变体以update in place的方式更新数据,并将previous states保存在一个单独的undo buffer,用于并发事务及中止。之所以使用MVCC,而不是其他更简单的方案(例如乐观并发控制),是因为DuckDB虽然是一个OLAP场景存储,但是并发更新也是一个比较频繁的需求。

  7. Storage
    DuckDB使用列式存储,并使用了读取优化的数据所存储布局。逻辑表被水平划分为chunks of columns,这些chunk of columns使用轻量的压缩方法压缩成physical block。Block中会保存min/max索引,用于在查询时判断该Block是否相关。另外,block稍微每个column保存了一个轻量的索引,用于进一步限制扫描的数据量。

DuckDB的局限性

那么我们是否应该放弃 Spark、BigQuery 和 Trino 并用 DuckDB 替换它们?

虽然说DuckDB爆火的原因是因为它轻巧,很快与Python 配合得非常好,包括深度 Pandas 集成等。但事实的体验下来,你会发现它并没有那么快。

DuckDB 确实有其局限性。一个重要的问题是它是为在一台机器上使用而设计的。如果你的数据无法容纳在一台机器上,那么你就完蛋了

其次,DuckDB 本质上是一种单人游戏体验。它在磁盘上的单个文件上运行,多人或团队很难共享数据、确保数据是最新的并同时开发模型。

DuckDB 与 Python 结合使用

您需要做的件事是使用 pip 安装 duckdb 库,

pip install duckdb

先下载本例所用的虚拟销售数据 https://support.spatialkey.com/spatialkey-sample-csv-data/

然后编写代码将 CSV 中的数据加载到表中,

import duckdb
conn = duckdb.connect('sales.duckdb')
cursor = conn.cursor()

cursor.execute("""
CREATE TABLE sales(
   Transaction_date date
  ,Product          varchar
  ,Price            bigint
  ,Payment_Type     varchar
  ,Name             varchar
  ,City             varchar
  ,State            varchar
  ,Country          varchar
  ,Account_Created  varchar
  ,Last_Login       varchar
  ,Latitude         double
  ,Longitude        double
)
"
""
)

cursor.execute("COPY sales FROM 'sales.csv' (HEADER)")

print(cursor.execute('select count(*) from sales').fetchall())
cursor.close()
conn.close()

在上面显示的代码片段中,我们连接到数据库并执行了一些查询。首先,我们创建一个销售表,然后将它们从 CSV 文件复制到表中,后进行计数查询。

如果运行此脚本,将看到如下所示的输出。

(venv) ➜  duckdb python app.py
[[998]]

sales.csv有 998 条记录。

我们也可以执行分组查询。

query = """select city, sum(price)
from sales
group by city"
""
print(cursor.execute(query).fetchdf())

上面将返回一个数据帧,如下所示。

                             city  sum(price)
0                   Prince Albert        3600
1                       Kobenhavn        1200
2                      Belleville        1200
3                          Aardal        1200
4                        Den Haag       19500
..                            ...         ...
754              Fort Saint James        1200
755                      Mettlach        1200
756                        Bergen        2400
757  Billings                            1200
758  Norcross                            1200

[759 rows x 2 columns]

我们还可以执行聚合查询,如下所示,

SELECT MIN(cnt), AVG(cnt),MAX(cnt) FROM
    (select count(*) as cnt FROM sales
    GROUP BY EXTRACT(DOY FROM Transaction_date::DATE),
    EXTRACT(HOUR FROM Transaction_date)
    ) stats

dbt + Duckdb 代替 Spark

十多年来,Apache Spark 一直是执行数据转换的首选。然而,Spark就一定是正确的唯一选择吗?

就像Data Minded 的首席执行官 Kris所说的,”一方面Spark是一个复杂的野兽,具有许多配置选项和陷阱。另一方面, 90% 的数据管道仅处理中等规模的数据集。“

即大部分的公司并没有大数据。如果我们的数据集只适合一台机器,我们是否需要像 Spark 这样复杂的分布式系统来处理我们的数据?

使用dbt-duckdb来处理那些中小型数据集任务或者转换相对简单的任务,有实验表明其性能完全优于Spark,由于篇幅这里就不贴具体的实验数据。

总结

随着大数据向量化的推进,Apache Arrow和DuckDB近两年都获得了较大的关注。对于DuckDB来说,其不仅仅是向量化的SQLite,也不仅仅是一个”缝合怪“。

它将各种优势的能力进行了巧妙的整合,创建了OLAP领域的嵌入式数据库。正如Mühleisen所说,”DuckDB 之所以得名,是因为我曾经养过一只宠物鸭,鸭子是神奇的动物。它们能飞、能走、能游泳,而且对环境挑战的适应能力很强。因此,它们是多功能且有弹性的数据管理系统的完美吉祥物。

随着机器性能的提升,我们对大数据的处理提出了更高的要求,而老一代的引擎架构或者处理方式,是否还能在新的时代提供大数据处理的”核动力“,这确实要打一个大大的问号。

也许以前我们不敢想象的计算处理方式,随着算力和硬件的提升,已经在慢慢的改变我们的认知。

DuckDB也许值得一试。



如果觉得这篇文章对你有所帮助,
请点一下或者在看,是对我的肯定和支持~


请使用浏览器的分享功能分享到微信等