DuckDB 常被一句很有效的话概括掉:面向分析场景的 SQLite。这句话之所以好用,是因为它同时说中了两件确实成立的事。DuckDB 可以嵌入,DuckDB 也把重心放在分析型工作负载上。可 Mark Raasveldt 的 Push-Based Execution in DuckDB 值得认真看,原因正在这里,这句口号说到这一步,精度已经开始松动。[1][4] 2019 年的 DuckDB 论文把项目描述成一套 embeddable analytical database,目标是在另一个宿主进程内部执行分析型 SQL,因为交互式分析和 edge 一类部署环境都需要分析能力,同时也希望绕开一整套 stand-alone server 的运维重量。[4] 当前官方 internals overview 又把执行层说得更直白:DuckDB 使用 push-based vectorized model,DataChunks 会在 operator tree 里被持续推送。[2]

这两层信息合在一起,才构成 DuckDB 真正的解释。它更像一台刻意放在进程内部的执行引擎:工作单元保持在 vectors 里,operators 被整理成 pipelines,parallelism 被处理成 scheduling 问题,client-server 往返因此退出了核心解释。[1][2][3][4] execution-format 文档把机械结构写得很清楚。DuckDB 的 operators 以固定大小的 vectors 为工作单位,默认的 STANDARD_VECTOR_SIZE 是 2048 tuples;与此同时,这些 vectors 也不需要在执行中退化成一种笨重的统一形态,因为 flat、constant、dictionary、sequence 这些表示方式都可以在查询过程中继续保留。[3] 顺着视频与文档一起读,更贴切的判断会慢慢清楚起来。DuckDB 的力量来自边界管理:它尽量把工作留在进程内部,也尽量把数据留在向量和 pipeline 形态里,等到查询形状真正逼迫它停下来的时候,再去物化、spill,或者把控制权交给下一段执行。[1][2][3][5]

这也是观看这支视频时最应该带在脑中的框架。假如只把它当成一场偏实现细节的技术演讲,真正的架构判断就会被压扁。更重要的故事在于,DuckDB 如何让一套 embedded analytics library 呈现出远比“linked library”更强的执行感:vectors 取代 rows,morsels 取代整块粗糙任务,sink 边界则指出一条 pipeline 在哪里必须停下,转入分段化系统。[1][3][4][5]

配图说明:题图使用 DuckDB 网站上的 Mark Raasveldt 真实作者照片。这个选择合适,因为本文围绕的是他本人对执行引擎的拆解。重点不在产品包装,而在他如何解释 DuckDB 的内部边界。[6]

大约从 0:57 开始,旧的 “vector volcano” 模型把 DuckDB 想离开的地方先照了出来

视频最先值得停下来的地方,是 Raasveldt 回头解释 DuckDB 早期 pull-based 模型,并把它叫作 “vector volcano” 的时候。[1] 这个说法很有分量,因为它一边还站在经典 iterator-style database execution 的谱系里,一边又已经承认 DuckDB 不想按单个 tuple 去搬运世界了。视频里的真正重点在于,工作单元已经从一行一行的 tuple,转成了沿着 pipeline 前进的 vectors。[1] execution-format 页面与这套结构完全对得上:DuckDB 的 operators 以固定大小的 vectors 作为优化单位,默认批大小是 2048 tuples。[3]

这听上去只是底层参数,实际上它改变了整个项目的阅读方式。只要执行单位变成 vector,函数求值、过滤、projection、解压缩,都可以围绕更适合缓存和批处理的块来组织。[3] vector 文档之所以重要,也在这里。constant vector 可以把重复值只存一次,dictionary vector 允许 dictionary-compressed 的表示方式继续留在执行过程中,sequence vector 则可以用偏移量和步长去表示规则递增的序列。[3] 所以,当视频一开始把 pull-based execution 与后面的 push-oriented model 做对照时,变化首先落在控制流上,更深的一层则是 DuckDB 希望分析引擎把紧凑的列式批次当成天然货币,让临时搬运工具那一层退到后面。[1][3]

到了这里,论文里那句 embeddable analytical database 也开始变得具体起来。[4] 嵌入宿主进程只是故事的一半。另一半在于,一旦查询进入 DuckDB,它立刻被交给一台以分析型批处理语义组织起来的引擎。这就使它很容易被导入,却一点也不轻飘。[3][4]

大约从 3:29 与 4:40 开始,morsel-driven parallelism 把一条查询拆成可以调度的 fragments

视频的第二个关键转折,出现在 Raasveldt 讲到 morsel-driven parallelism,随后说明 pipeline model 会把 query tree 拆成 linear fragments 的时候。[1] 到了这里,DuckDB 已经不再像“一个嵌入式引擎配一根线程”那样被理解,它更像一台调度器。Raавvelt 说得很清楚,目标不只是让不同 pipelines 并排运行,真正棘手的问题,是把 parallelism 压进那条最重、最拖慢总耗时的 pipeline 之中。[1]

这段内容很重要,因为它修正了人们对 embedded database 的直觉。很多人一听到 “embedded”,会先想到方便,再想到执行力度。DuckDB 的论文走的是反方向:交互式分析和 edge 场景之所以需要 embedded analytics,恰恰是因为它们需要真正的分析能力,同时又不想承受独立数据库服务器的摩擦。[4] morsel-driven parallelism 正是这套论证成立的原因之一。DuckDB 并没有把切分工作全丢给宿主应用,它自己会把 scan 与 operators 分解成更小的工作块,再在同一个进程里把这些工作块分配到多个核心上。[1][4]

顺着这个角度去看,DuckDB 的 in-process 设计就不只是部署上的轻量感。它实际上是把一层运维仪式拿掉,同时把真正的分析执行引擎保留在内部。linked library 让你进入进程,pipeline 与 morsel 则解释了为什么进入之后,它仍然是一台严肃的 analytical runtime。[1][4]

大约从 7:11 与 7:25 开始,sink 与 dependent pipelines 把“流动何时停止”这件事讲清楚了

视频里最见功力的一段,出现在 Raasveldt 说 sink 是 “a very push-based thing”,因为 chunks 可以直接被 push 进去,随后又解释 finalize 完成后会去 schedule dependent pipelines 的时候。[1] 也是在这里,架构第一次彻底承认分析系统最难处理的现实:查询终究会走到那些无法继续保持干净、连续、不断裂流动的阶段。

这一层之所以重要,是因为分析型查询总会遇到一些必须先累积、分区、哈希、排序,再把结果交给后续阶段的边界。当前 tuning guide 把这些边界明确命名成 blocking operators:grouping、joining、sorting、windowing 都要求先把输入缓冲起来,它们也是关系型系统里最吃内存的部分。[5] DuckDB 的做法,是把这些位置直接写进架构本身。查询有一部分可以保持为 vectorized operators 组成的 pipeline,另一部分则会终止在 sink 处,先 finalize 状态,再释放 dependent work。[1][5]

也正是在这里,DuckDB 的 larger-than-memory 叙述才真正站稳。性能文档写得很清楚,DuckDB 可以通过 spilling to disk 支持 larger-than-memory workloads,包括那些中间结果本身都放不进内存的查询。[5] 这是一条顺势长出来的结果。只要 joins、sorts、windows、grouped aggregations 被当成 pipeline-breaking stages 来认真处理,系统就能在需要时管理 temp storage,在需要时重启下游阶段,也能避免把整条查询讲成一条未曾中断的流。[5]

因此,观看这一段最好的方式,是把 “DuckDB 有 sinks” 继续往前推一步。更值得带走的判断在于,DuckDB 的优雅感正来自它知道优雅会在哪里停下。系统会尽量保持向量化、局部化、流动化;一旦条件变了,它就会分段、finalize、必要时 spill,再把执行权交给下一段 pipeline。[1][3][5]

DuckDB 看起来比一套库更大,因为“库”的边界没有包住它真正的执行边界

这支视频真正留下来的价值,就在这里。它解释了为什么 DuckDB 已经不只是 notebook 里的一个顺手 SQL 包。论文强调的是 embeddability、在分析环境中被直接调用的便利,以及对许多工作负载来说没有必要再起一台独立数据库服务器。[4] 可在这条边界里面,DuckDB 放进去的执行引擎远比营销口号显得更严格。internals overview 说 DataChunks 会被持续推过 operator tree。[2] execution-format 文档说明,这些 chunks 在内存里保持 vectorized,而且很多时候还能保留压缩或半压缩表示。[3] tuning guide 又进一步说明,在 larger-than-memory 工作负载里,系统会在 blocking operators 处硬化成 spill 与 staging 逻辑。[5]

把这些来源放在一起,视频的架构判断就会变得很清楚。DuckDB 的吸引力,集中在它把一套严肃的 analytical pipeline 做成了可导入的形态。带着这个判断回看整支视频,真正值得反复看的地方,落在 Raasveldt 反复划边界的时刻:vectors 对 rows,morsels 对巨型任务,sinks 对无限流动的幻想,dependent pipelines 对松散而无形的 operator graph。[1][2][3][4][5]

这也解释了为什么 DuckDB 会不断出现在一些传统 warehouse 过于沉重、而 row-store library 又显得过弱的地方。你导入进来的单位是一套 library,真正落到手里的,却是一台 pipeline engine。

来源

  1. Mark Raasveldt,《Push-Based Execution in DuckDB》,YouTube 视频,发布于 2021 年 11 月 26 日。
  2. DuckDB documentation,《Overview of DuckDB Internals》——parser 到 physical plan 的流程,以及以 DataChunks 为核心的 push-based 执行模型。
  3. DuckDB documentation,《Execution Format》——固定大小 vectors、STANDARD_VECTOR_SIZE = 2048,以及 flat / constant / dictionary / sequence 的表示方式。
  4. Mark Raasveldt 与 Hannes Mühleisen,《DuckDB: an Embeddable Analytical Database》,SIGMOD 2019 demo paper。
  5. DuckDB documentation,《Tuning Workloads》——larger-than-memory processing、disk spill 行为,以及 blocking operators。
  6. DuckDB,《Mark Raasveldt》作者照片文件——本文题图来源。