来源:https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/

MariaDB 的 DuckDB 存储引擎:当海狮学会嘎嘎叫

对 MariaDB 的 DuckDB 存储引擎的早期预览——列式、向量化分析,与您的事务型表紧密相邻。

问题所在

MariaDB 的 InnoDB 在其设计目标上表现出色:事务处理。逐行插入、更新、点查询、强一致性。但是,当您要求它扫描数千万行以进行多路连接和若干聚合操作时,行存储就必须付出巨大的努力。

通常的解决方案是搭建一个独立的分析系统,然后构建 ETL 管道将数据复制进去。这样一来,您需要维护两个系统,并且还要忍受它们之间的延迟。

如果分析引擎就位于同一台服务器内部呢?—— 相同的 SQL,相同的客户端,相同的数据目录?

我们构建了什么

MariaDB 的 DuckDB 存储引擎正是做到了这一点。使用 ENGINE=DuckDB 创建表,其数据以 DuckDB 原生的列式格式存储;针对它的分析查询则通过 DuckDB 的向量化、多核引擎运行——在 MariaDB 服务器内部进行。

无需单独的集群。无需 ETL。无需新的协议。使用熟悉的 mariadb 客户端,一种 SQL 方言,InnoDB 表和 DuckDB 表在同一个数据库中共存。

关于 MariaDB ColumnStore 的说明

MariaDB 已经提供了 ColumnStore,这是一个用于大规模分析的成熟列式引擎——通常部署为横向扩展的多节点数据仓库。DuckDB 引擎则针对不同的应用场景:单节点、进程内、零运维的分析引擎,与您的 InnoDB 表相邻,用于轻量级嵌入式分析和 HTAP。两者是互补而非竞争关系——当您需要分布式数据仓库时使用 ColumnStore,当您希望进行进程内分析且无需额外组件时,则使用 DuckDB 引擎。

30 秒快速体验

像创建任何其他表一样创建 DuckDB 表:

CREATE TABLE sales (
  id        BIGINT PRIMARY KEY,
  product   VARCHAR(64),
  amount    DECIMAL(12,2),
  sold_at   TIMESTAMP
) ENGINE=DuckDB;

使用普通 SQL 查询它——整个 SELECT 语句会被下推到 DuckDB:

SELECT product, SUM(amount) AS revenue
  FROM sales
 GROUP BY product
 ORDER BY revenue DESC
 LIMIT 10;

而使其真正有用的部分是——跨引擎连接。单个查询可以同时连接 DuckDB 分析表和 InnoDB 操作表。端到端示例如下——两个不同引擎的表,在一个语句中连接:

-- 分析数据在 DuckDB 中,操作数据在 InnoDB 中
CREATE TABLE analytics.orders   (id BIGINT PRIMARY KEY, product_id INT, amount DECIMAL(12,2)) ENGINE=DuckDB;
CREATE TABLE inventory.products (id INT PRIMARY KEY, name VARCHAR(64))                          ENGINE=InnoDB;

INSERT INTO inventory.products VALUES (1, 'Wireless Keyboard'), (2, 'Mechanical Switch');
INSERT INTO analytics.orders   VALUES (4217, 1, 1299.00), (4218, 2, 42.00);

-- 一个查询,两种引擎
SELECT d.id, d.amount, i.name
  FROM analytics.orders   d        -- ENGINE=DuckDB
  JOIN inventory.products i        -- ENGINE=InnoDB
    ON d.product_id = i.id
 WHERE d.amount > 1000;

DuckDB 处理连接、聚合和排序;InnoDB 行通过 MariaDB 优化器按需生成。无需复制,无需 ETL。

性能表现

我们在 TPC-H SF10(约 11 GB,8660 万行,lineitem 表 6000 万行)上对该引擎进行了基准测试。分析引擎有两个关键指标:数据加载速度和查询速度。

硬件/环境:Intel Core i7-13700H(14 核 / 20 线程),64 GB RAM,NVMe SSD。MariaDB 11.4.13 内置 DuckDB v1.5.2,duckdb_memory_limit=8 GiBthreads=20。预热运行,稳定在约 ±15% 误差。

查询延迟——全部 22 个 TPC-H 查询

完整套件预热运行约需 4.3 秒

查询耗时 (秒)查询耗时 (秒)查询耗时 (秒)
q010.253q090.337q170.116
q020.076q100.253q180.318
q030.134q110.049q190.207
q040.135q120.152q200.166
q050.142q130.606q210.486
q060.070q140.128q220.113
q070.133q150.101总计4.30
q080.145q160.178

每个查询还需支付少量固定成本(约 40 毫秒),用于客户端连接和下推设置——在最轻量的查询上比较明显,而在重分析查询上可以忽略不计,因为 DuckDB 的向量化执行占据主导。

批量加载

路径总加载时间
引擎内 COPY (run_in_duckdb,8 GB 内存)33 秒
LOAD DATA LOCAL INFILE~400 秒(6.7 分钟)

通过 run_in_duckdb() 函数,在进程内使用 DuckDB 原生的并行读取器加载外部 CSV,可在约 33 秒内摄取全部 8660 万行数据。标准的 LOAD DATA LOCAL INFILE 路径大约慢 12 倍,因为每一行都经过 MariaDB 的 SQL/handler 层路由,串行化了 COPY 并行执行的工作。对于批量加载,建议通过 run_in_duckdb 使用引擎内 COPY,并为大表提高 duckdb_memory_limit

工作原理(简述)

DuckDB 的速度基于三大支柱:列式存储(只读取所需的列)、向量化执行(以缓存友好的批量处理数据)和并行性(利用所有核心)。

在 MariaDB 内部,该引擎接入两个地方:

  • 查询下推——整个 SELECT 语句(包括 WHERE、JOIN、GROUP BY、ORDER BY)通过 MariaDB 的 select_handler 接口交给 DuckDB,由 DuckDB 的优化器规划整个查询。
  • 跨引擎扫描——当查询混合了 DuckDB 和非 DuckDB 表时,非 DuckDB 表通过完整的 MariaDB 执行管道按需流入 DuckDB,因此索引访问和谓词评估仍发生在 MariaDB 端。

这是简要版本。更深入的架构文章即将发布。

为什么采用插件形式

DuckDB 引擎以 MariaDB 插件的形式提供——这是一个经过深思熟虑的重要选择。MariaDB 的可插拔架构使得这样的组件可以拥有独立于服务器本身的成熟度。

  • 对服务器稳定性无影响——因为存储引擎插件是按需加载的,并且通过定义良好的接口进行隔离,它可以被添加到一个 GA 版本中,而不会危及核心服务器的稳定性。如果您不加载插件,对您没有任何影响。
  • 按自身节奏成熟——该插件可以按照自己的节奏迭代、稳定并达到生产就绪状态,与服务器的发布周期解耦。早期采用者今天就可以选择加入;其他人则不受影响。
  • 干净的按需启用——当您需要该功能时安装插件,不需要时则不安装。长期以来为 InnoDB、Spider 和 ColumnStore 等引擎提供支持的相同机制,现在也为 DuckDB 提供了支持。

这正是让 MariaDB 能够安全地提供雄心勃勃的新功能的模式:需要时功能强大,不需要时无感隐身。

适用场景

良好适用

  • HTAP —— InnoDB 用于事务,DuckDB 用于分析,均在同一个数据库中。
  • 即席分析查询 —— 大数据量的连接、聚合、窗口函数,无需导出数据。
  • 消除 ETL —— 分析引擎在进程内运行。

当前限制(仍处于早期阶段)

  • DECIMAL 精度上限为 38 位(DuckDB 的限制)。
  • DuckDB 引擎的表需要 PRIMARY KEY,并拒绝非 UTF-8 字符集。
  • 某些 MariaDB 函数尚不支持下推(例如 GROUP_CONCAT()DATE_FORMAT()),将回退到 MariaDB 执行。
  • 比 MariaDB 默认 sql_mode 更严格的 GROUP BY 要求。
  • 不支持 XA 事务;跨引擎外部扫描目前是单线程的。
  • 对于非 UTF-8 字符集,排序规则是近似的。

路线图

这仅仅是开始。我们接下来的方向:

  • 分析型 GIS —— 开放 DuckDB 的 Spatial 扩展,使地理空间分析能在 MariaDB 内部运行。
  • 仅 InnoDB 数据上的更快 HTAP —— 通过 DuckDB 的向量化执行对纯 InnoDB 表运行查询,即使不涉及 DuckDB 表也能加速分析。
  • 部分查询下推 —— 实现一个派生 handler,将复杂查询的部分下推到 DuckDB,使混合工作负载可以卸载其分析子查询。
  • 数据湖访问 —— 通过 DuckDB 的扩展生态系统访问外部数据湖协议和格式。

尝试一下

获取预构建包

最快路径:MariaDB 的社区 CI 在 ci.mariadb.org/68929/ 发布预构建包。选择与您的平台和架构匹配的目录——每个操作系统/架构组合都有一个,因此您需要选择正确的路径。对于 Ubuntu 24.04 (amd64),路径是:

https://ci.mariadb.org/68929/amd64-ubuntu-2404-deb-autobake/

将该目录用作软件包仓库(或直接从其中下载 .deb 文件)即可安装,无需编译。对于其他目标,请替换为对应的文件夹——例如 amd64-rhel-9-rpm-autobake/、aarch64-ubuntu-2404-deb-autobake/ 等。请注意,这些包未签名,仅适用于测试目的。

从源码构建

另外,该引擎在 storage/duckdb/ 目录下随树提供。克隆与您的目标 MariaDB 版本匹配的分支并进行构建:

git clone --recurse-submodules -b 11.4 https://github.com/MariaDB/server.git mariadb-server
cd mariadb-server

./storage/duckdb/build.sh -D   # 安装构建依赖 (需要 root)
./storage/duckdb/build.sh      # 构建 + 安装

自行构建软件包

更愿意自己制作可分发的软件包?在安装构建依赖(build.sh -D)之后,build.sh -p 会为您的平台生成 RPM 或 DEB。详情请参见引擎 README 中的"构建软件包"部分:

./storage/duckdb/build.sh -p   # 构建 RPM (Rocky/Fedora/Amazon Linux) 或 DEB (Debian/Ubuntu)

启用引擎

DuckDB 引擎必须在服务器启动时加载——此步骤是必须的。将 duckdb.cnf 文件放入您服务器的 config-include 目录:

  • Debian / Ubuntu: /etc/mysql/mariadb.conf.d/duckdb.cnf
  • RHEL / Fedora / openSUSE: /etc/my.cnf.d/duckdb.cnf
[mysqld]
plugin-maturity=alpha
plugin-load-add=ha_duckdb.so
duckdb-memory-limit=8G        # 默认只有 1G —— 在测试前值得调高

plugin-load-add 加载引擎,而 plugin-maturity=alpha 是必需的,因为该插件目前处于 alpha 成熟度级别。注意默认的 duckdb-memory-limit 只有 1 GB —— 对于任何认真测试,都值得先将其调高,因为它是大负载和查询的主要瓶颈。重启服务器后,您就可以创建 ENGINE=DuckDB 表了。

这是一个预览版——我们欢迎测试者、错误报告和反馈。

许可证

该引擎基于 GPL v2 许可(与 MariaDB 服务器相同),而 DuckDB 本身基于宽松的 MIT 许可证分发。

致谢

这项工作借鉴了 DuckDB 项目——其简洁、可嵌入、MIT 许可的引擎使这样的集成成为可能——也借鉴了阿里巴巴的 AliSQL,其 2025 年 12 月将 DuckDB 集成到 MySQL 兼容服务器中的版本是一个有价值的参考和验证点。

后续还有更多内容——包括更深入的架构探讨和更全面的基准研究。

更多推荐