DuckDB – 笔记本电脑上的数据强力工具,现已支持 Clojure(2023)

Hacker News Top 工具

摘要

TechAscent 展示了如何通过 tech.ml.dataset(TMD)从 Clojure 中利用 DuckDB 的高性能矢量化 SQL 引擎,实现大型内存外连接,以及约两分钟内将 50GB CSV 数据摄入并压缩至 18GB。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/04 22:48

# TechAscent - DuckDB - 数据强力工具,现在支持 Clojure 了 来源:https://techascent.com/blog/just-ducking-around.html 2023\-09\-02 #### 需求背景 我们的内存列式数据处理平台 tech\.ml\.dataset (https://github.com/techascent/tech.ml.dataset)\(TMD\) 驱动着函数式数据科学的未来。当数据量大到无法全部装入内存时,仍可通过 TMD 对数据采样,或过滤出相关子集以适应当前运行环境的限制。此外,无论数据大小,都可以通过 nippy、arrow 或 parquet (https://techascent.com/blog/clojure-csv-parquet.html) 实现持久化。 当数据量变得非常大,例如约 100GB 且带有关系型特征的一组 \.csv 文件时,现有工具可能会变得笨重难用。人们很容易陷入非函数式的 Spark 集群泥潭。当然,保持一定的事务交互能力和简单的磁盘 IO 模型仍然是超级理想的。本地磁盘足够大,本地芯片足够快,没必要做什么仓促的事。 关系型数据库非常擅长超出内存容量的存储和快速关系查询——但如何在不放弃函数式编程优势和 TMD 列式处理模型的情况下利用这一点?JDBC 加上 Postgres 为这个问题提供了一个不错的初步答案,但通过低效、非批处理的 API 完成完整的行到列转换,以便将数据通过 JDBC 送入 TMD,实在令人恼火。 #### 新挑战者出现 DuckDB 于 2021 年 5 月通过一个 github issue (https://github.com/techascent/tech.ml.dataset/issues/241) 出现,到同年 12 月,tmducken (https://github.com/techascent/tmducken) 已与其 C 绑定进行了最小集成。在那个版本中,所有查询结果一次性返回,因此必须适合内存。此外,在 DuckDB 早期,还没有专门的的高性能追加或插入系统,因此 IO 限制了潜在性能,Postgres 仍然是 TMD 的辅助处理系统。自那时起,情况发生了很大变化。 在过去两年中,DuckDB 改进了很多。重要的是,C 接口现在为插入和查询都提供了批量系统,这使得处理非常大的连接成为可能——稍后会详细介绍。现在可以通过 TMD 在 Clojure 中利用这些改进的能力,访问 DuckDB 最先进的矢量化 SQL 执行引擎,这很棒。 #### 实际使用 基于我们之前的文章 (https://techascent.com/blog/clojure-csv-parquet.html),有一个 50 GB 的 \.csv 文件,包含 3 年的交易数据,总计 400,000,000 行: `` $ ll -h data.csv -rw-rw-r-- 1 harold harold 50G Aug 8 09:49 data.csv `` 将其加载到 DuckDB 中出奇地容易——不过,你需要等待 2 分钟: `` $ time duckdb data.ddb 'CREATE TABLE data AS FROM "data.csv";' 100% ▕████████████████████████████████████████████████████████████▏ real 1m50.091s user 21m42.693s sys 0m57.887s $ ll -h data.ddb -rw-rw-r-- 1 harold harold 18G Sep 6 10:57 data.ddb `` 这样,文件就缩减到了 18GB,其中包含了 DuckDB 自动创建的所有索引(!)。 数据就在里面: `` $ duckdb data.ddb v0.8.1 6536a77232 Enter ".help" for usage hints. D SELECT COUNT(*) AS n FROM data; ┌───────────┐ │ n │ │ int64 │ ├───────────┤ │ 400000000 │ └───────────┘ D DESCRIBE TABLE data; ┌────────────────┬─────────────┬─────────┬─────────┬─────────┬─────────┐ │ column_name │ column_type │ null │ key │ default │ extra │ │ varchar │ varchar │ varchar │ varchar │ varchar │ varchar │ ├────────────────┼─────────────┼─────────┼─────────┼─────────┼─────────┤ │ customer-id │ VARCHAR │ YES │ │ │ │ │ day │ BIGINT │ YES │ │ │ │ │ inst │ TIMESTAMP │ YES │ │ │ │ │ month │ BIGINT │ YES │ │ │ │ │ brand │ VARCHAR │ YES │ │ │ │ │ style │ VARCHAR │ YES │ │ │ │ │ sku │ VARCHAR │ YES │ │ │ │ │ year │ BIGINT │ YES │ │ │ │ │ transaction-id │ VARCHAR │ YES │ │ │ │ │ quantity │ BIGINT │ YES │ │ │ │ │ price │ DOUBLE │ YES │ │ │ │ ├────────────────┴─────────────┴─────────┴─────────┴─────────┴─────────┤ │ 11 rows 6 columns │ └──────────────────────────────────────────────────────────────────────┘ `` 从 Clojure 中通过 TMD 访问它同样很容易: `` user> (require '[tmducken.duckdb :as duckdb]) nil user> (require '[tech.v3.dataset :as ds]) nil user> (duckdb/initialize!) Sep 06, 2023 11:00:12 AM clojure.tools.logging$eval7454$fn__7457 invoke INFO: Attempting to load duckdb from "./binaries/libduckdb.so" true user> (def db (duckdb/open-db "data.ddb")) #'user/db user> (def conn (duckdb/connect db)) #'user/conn user> (time (duckdb/sql->dataset conn "SELECT COUNT(*) AS n FROM data")) "Elapsed time: 10.305756 msecs" :_unnamed [1 1]: | n | |----------:| | 400000000 | `` 现在,想象一下管理层让我们知道还有另一个数据集,它记录了每个 sku 对应的商品有哪些颜色。这个也需要放进数据库: `` user> (-> (let [colors ["red" "green" "blue" "yellow" "purple" "black" "white"]] (->> (for [brand (range 100) style (range 10) item (range 10)] (let [sku (format "sku-%s-%s-%s" brand style item) n (rand-int 8)] (for [color (take n (shuffle colors))] {"sku" sku "color" color}))) (apply concat))) (ds/->dataset {:dataset-name "colors"})) colors [35179 2]: | sku | color | |------------|--------| | sku-0-0-0 | red | | sku-0-0-0 | blue | | sku-0-0-0 | white | | sku-0-0-0 | yellow | | sku-0-0-0 | black | | sku-0-0-0 | green | | sku-0-0-1 | black | | sku-0-0-1 | yellow | | sku-0-0-1 | blue | | sku-0-0-1 | purple | | ... | ... | | sku-99-9-8 | yellow | | sku-99-9-8 | purple | | sku-99-9-8 | black | | sku-99-9-8 | red | | sku-99-9-8 | white | | sku-99-9-8 | blue | | sku-99-9-8 | green | | sku-99-9-9 | purple | | sku-99-9-9 | blue | | sku-99-9-9 | black | | sku-99-9-9 | green | user> (duckdb/create-table! conn *1) "colors" user> (duckdb/insert-dataset! conn *2) 35179 `` 你知道接下来要做什么了。我们需要将 4 亿笔交易(每笔都有一个 sku)与这些数据连接起来,这些数据表明每个 sku 大约有 3.51 种颜色。 幸运的是,他们的第一个请求相对简单。“2021 年 3 月每种颜色卖出了多少件?”,他们问,“我们什么时候能知道?” `` ;; 首先,既然能做到,就在你的笔记本上 2.5 秒内连接 14 亿行... user> (time (duckdb/sql->dataset conn "SELECT COUNT(*) FROM data INNER JOIN colors ON data.sku = colors.sku;")) "Elapsed time: 2486.620275 msecs" :_unnamed [1 1]: | count_star() | |-------------:| | 1416737859 | ;; 然后,回答他们的问题... user> (time (duckdb/sql->dataset conn "SELECT color, COUNT(*) FROM data INNER JOIN colors ON data.sku = colors.sku WHERE data.year='2021' AND data.month='3' GROUP BY color;")) "Elapsed time: 1077.723309 msecs" :_unnamed [7 2]: | color | count_star() | |--------|-------------:| | red | 5714223 | | yellow | 5652010 | | black | 5720753 | | blue | 5750846 | | white | 5689916 | | green | 5816652 | | purple | 5671959 | `` 我们现在就能知道,1 秒内。答案就在这里。 也许下一个问题不适合用 SQL 解决,用 Clojure 和 TMD 处理会更好。下面的示例对老板最喜欢的 sku 的*每笔*交易按时间顺序(1 秒内)进行归约: `` user> (time (reduce (fn [eax ds] (conj eax (ds/row-count ds))) [] (duckdb/sql->datasets conn "SELECT * FROM data WHERE sku='sku-50-5-5' ORDER BY inst"))) "Elapsed time: 1067.480751 msecs" [2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 2048 576 733] `` 当然,这个归约函数很简单,但它证明了这一点——通过归约,可以通过一种永远不会耗尽内存的机制,对实例化数据集进行任意处理。 DuckDB 还支持零拷贝查询路径 (https://duckdb.org/2021/12/03/duck-arrow.html)。如果查询结果的任何块都不需要离开归约函数,那么机器可能可以做更少的工作。在下面的示例中,通过传递 `{:reduce-type :zero-copy-imm}` 选项来启用和访问此功能。 当处理可以以这种方式表达时,这理论上是可用的最低内存路径。 `` user> (time (let [sql "SELECT * FROM data WHERE sku='sku-50-5-5' ORDER BY inst" options {:reduce-type :zero-copy-imm}] (reduce (fn [eax zc-ds] (conj eax (ds/row-count zc-ds))) [] (duckdb/sql->datasets conn sql options)))) "Elapsed time: 1037.480113 msecs" [2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 2048 531 1408] `` --- 希望这能让你感受到这个系统现在所提供的强大能力。 #### 一些有趣的 DuckDB 小知识 DuckDB 会自动将所有数值数据存储在 minmax 索引 (https://duckdb.org/docs/sql/indexes.html) 中——也称为 BRIN 索引。这些索引不会显著增加原始数据大小,但会大幅提升查询性能。对于唯一列或主键列,它还会自动创建 ART 索引。最后,用户还可以为类别列创建索引,但缺点是会增加磁盘存储空间,并可能减慢事务处理速度。 DuckDB 使用标准 C++11 编写,因此具有相当好的可移植性——他们很快就为 mac m-1 构建了一个变体,如果你有另一个平台并且想要一些特别的东西,我们会很乐意针对该平台编译和修改数据库。截至撰写本文时(2023 年 9 月),其代码库的 src 目录约有 100,000 行 C++ 代码: `` (base) chrisn@chrisn-lp2:~/dev/cnuernber/duckdb$ cloc src 1612 text files. 1612 unique files. 0 files ignored. github.com/AlDanial/cloc v 1.90 T=0.83 s (1936.0 files/s, 203773.8 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- C++ 831 13749 8912 99454 C/C++ Header 679 7754 9903 28287 CMake 101 49 0 1541 Markdown 1 7 0 15 ------------------------------------------------------------------------------- SUM: 1612 21559 18815 129297 ------------------------------------------------------------------------------- `` DuckDB 采用 MIT 许可证,拥有开放的 github 开发模式,他们的社区回答问题非常迅速。以如此开放的方式开发如此高质量的动力工具,令人敬佩。 #### 总结 试试我们的 duckdb 集成 (https://github.com/techascent/tmducken),或者雇佣我们为你完成。DuckDB 与 TMD 形成了良好的互补,极大地提升了小团队高效管理和处理大型数据集的能力,而无需诉诸昂贵的分布式解决方案。这种集成支持并代表了高质量、高效计算工具的价值,通过让人们在笔记本上就能实现函数式解决方案,而使用较笨拙工具的人则不得不求助于集群来管理,从而真正实现数据处理的大众化。 --- TechAscent: 让我们的鸭子排成一排。 联系我们 (https://techascent.com/contact.html)

相似文章

DuckDB: it's not quack science

Lobsters Hottest

DuckDB是一个开源嵌入式分析型数据库,支持直接查询文件、嵌入应用,并提供友好的SQL扩展,在数据分析场景下比传统Unix管道更高效。

DuckDB v2.0 预览

Hacker News Top

DuckDB v2.0,代号 Cyanoptera,预览了新功能,包括服务器模式、触发器、VARIANT 类型、异步 I/O 和新的 SQL 解析器,将于今年秋季发布。

@guizmaii: DuckDB 是数据工程的终极对手

X AI KOLs Timeline

Duckherder 是一个新的 DuckDB 扩展,它使用 Arrow Flight 在多个工作节点之间支持分布式查询执行,旨在通过并行处理增强数据工程工作流。