编程 用 DuckDB mysql 扩展直连线上 MySQL:ATTACH 写法、secret 管理与 pushdown 边界

2026-10-21 21:31:04

用 DuckDB mysql 扩展直连线上 MySQL:ATTACH 写法、secret 管理与 pushdown 边界

项目信息:

  • 扩展文档:
  • 仓库:
  • 性能相关 issue:
  • DuckDB 多库支持背景:

适合的场景是:不想把线上数据整表同步过来,直接 ATTACH 上去做一次性分析,结果落成本地 Parquet。前提是接受它不走 MySQL 执行器,pushdown 有限。

安装与连接

INSTALL mysql;  -- 一次性
LOAD mysql;     -- 首次使用会自动加载
ATTACH 'host=localhost user=root port=0 database=mysql' AS mysqldb (TYPE mysql);
USE mysqldb;

连接串是 PostgreSQL 风格的 key=value 列表,不是 MySQL 的 DSN 格式(不要写成 mysql://user:pass@host/db)。未给出的项回落到默认值或环境变量。

对线上库一律加只读:

ATTACH 'host=... user=... database=...' AS mysqldb (TYPE mysql, READ_ONLY);

密码不要写进连接串

官方明确警告:连接出错时,完整连接串(含密码)会出现在错误信息里。改用 secret:

CREATE SECRET (
TYPE mysql,
HOST '127.0.0.1',
PORT 0,
DATABASE mysql,
USER 'mysql',
PASSWORD ''
);
ATTACH '' AS mysql_db (TYPE mysql);

CREATE PERSISTENT SECRET ... 可跨会话保留;临时 secret 默认只在当前会话有效。

连接串仍可覆盖 secret 里的单项,例如临时换库:

ATTACH 'database=my_other_db' AS mysql_db (TYPE mysql);

环境变量与 SSL

变量对应项
MYSQL_DATABASE库名
MYSQL_HOST主机
MYSQL_PWD密码
MYSQL_TCP_PORTTCP 端口
MYSQL_UNIX_PORTUnix socket 端口
MYSQL_USER用户名

SSL 由 ssl_mode 控制,取值 disabled / required / verify_ca / verify_identity / preferred,默认 preferred。

读表、物化与导出

ATTACH 之后按普通 DuckDB 表查,数据在查询时直接从 MySQL 读,不在本地留副本:

SELECT * FROM mysqldb.orders WHERE created_at >= '2026-10-01';

同一张大表反复读不划算时,先物化到本地:

CREATE TABLE local_orders AS FROM mysqldb.orders;

导出:

COPY (SELECT ...) TO 'orders.parquet' (FORMAT parquet);

关键 settings

  • mysql_pool_size:每个 catalog 的最大连接数,默认按 CPU 数自动推导。
  • mysql_pool_acquire_mode:force(忽略上限)/ wait / try。
  • mysql_enable_filter_pushdown:默认 true。
  • mysql_debug_show_queries:打印发给 MySQL 的每条 SQL。排查「到底推了什么」时打开。
  • mysql_incomplete_dates_as_nulls:默认 false,MySQL 的零日期会原样返回,不会自动变成 NULL。
  • mysql_session_time_zone:新建连接的会话时区,默认空。
  • mysql_tinyint1_as_boolean / mysql_bit1_as_boolean:默认 true。
  • mysql_time_as_time:默认 false。
  • mysql_pool_connection_idle_timeout_millis:默认 60000。

schema 缓存

DuckDB 会缓存表结构。别人(另一条连接或其它工具)加了列之后,本地缓存会过期,直接清:

CALL mysql_clear_cache();

执行任意 SQL

SELECT * FROM mysql_query('mysqldb', 'SELECT * FROM cars LIMIT 3');   -- 只读查询
CALL mysql_execute('mysqldb', 'CREATE TABLE my_table (i INTEGER)');    -- 可含 DDL

mysql_query 支持 stream_results=TRUE,流式不可用时会直接报错,而不是悄悄全量载入内存。

性能边界

以下结论来自 duckdb-mysql issue #55,维护者 staticlibs 在 2025-12 的说明:

  1. 不对聚合做 pushdown,带聚合的查询会把整张表数据读进 DuckDB 再计算。
  2. 只有当一个 attached catalog 上只有一个 scan 时才流式返回结果集,多数情况下结果集先完整载入内存。
  3. 一个连接同一时刻只跑一条查询,没有并行。
  4. pushdown 支持有限,可能读到比实际需要更多的数据。
  5. 通过 scanner 读取比 mysql CLI 慢约 2 倍,可以拿这个当基线。
  6. 复杂类型(尤其 DECIMAL)在大结果集上有明显额外开销,必要时在 mysql_query 里 CAST 成基本类型。

对应的用法:聚合留在 DuckDB 侧,用 ATTACH 外表扫描;过滤尽量用 mysql_query 子查询把条件下压到 MySQL;大结果集可以用多个连接(同一个库 ATTACH 多次)拿到流式;复杂类型先 CAST。

可复现的工作流

过滤推到 MySQL、聚合留 DuckDB、结果落 Parquet。DuckDB 支持 $$ 包裹字符串字面量,写内嵌单引号的 SQL 时比转义省事。

-- 1. 过滤和列裁剪下压到 MySQL,只回传需要的行
CREATE OR REPLACE TABLE filtered AS
FROM mysql_query('mysqldb', $$
SELECT id, user_id, amount, created_at
FROM orders
WHERE created_at >= '2026-10-01'
AND status = 'paid'
$$);

-- 2. 聚合留在 DuckDB 侧做
CREATE OR REPLACE TABLE daily AS
SELECT created_at::DATE AS d,
count(*)        AS n,
sum(amount)     AS amt
FROM filtered
GROUP BY 1
ORDER BY 1;

-- 3. 结果落 Parquet
COPY (SELECT * FROM daily) TO 'daily.parquet' (FORMAT parquet);

金额类字段如果是 DECIMAL,在第 1 步里就 CAST 成基本类型,避免第 2 步在大结果集上付第 6 条那笔开销。想确认过滤有没有真的推下去,会话里 SET mysql_debug_show_queries=true; 看一遍发出的 SQL。

事务限制

DuckDB 单条语句默认各自一个事务;跨多个 attached 数据库的写在同一个事务里不支持,会直接报错。另外 MySQL 侧的 DDL 本身不是事务性的,mysql_execute 里执行 DDL 时不要指望能回滚。

复制全文 生成海报 DuckDB MySQL SQL 数据分析 Parquet

推荐文章

程序员茄子在线接单