tva
← 洞察

使用 DuckDB 进行即席分析:将数千个 CSV 转变为仪表盘

Amazon Seller Central 产生数据的速度超过了大多数分析人员所用工具的处理能力。流量报告、转化数据、库存健康摘要、业务绩效导出、广告活动细分数据,都以 CSV 或 TSV 文件交付,经过数月乃至数年,积累成包含数千个文件的目录。标准做法是将所有数据导入数据库。但实际上,对于这种本质上以读取为主、数据模式多变的分析工作负载,搭建、维护和查询 PostgreSQL 实例带来的运维开销,已经超过问题本身所需。

我们为一家在五个欧洲 Amazon 站点运营的卖家构建了报告管道。原始数据是六年的 Seller Central 导出文件,共 4,200 个,涵盖业务报告、广告效果、库存快照和 FBA 费用台账。最初采用 PostgreSQL 数据模式配合 ETL 脚本,用了三周才稳定下来;之后,随着 Amazon 在不同 API 版本中修改列名和报告结构,仍需持续维护。查询很快,维护却耗费精力。

DuckDB 改变了这种成本权衡。它是嵌入式分析数据库,与分析代码在同一进程中运行,无需服务器、守护进程或连接字符串,并且直接读取磁盘文件。查询接口采用标准 SQL,性能则来自针对大规模聚合优化的列式引擎。对于这种工作负载,DuckDB 不只是够用,而是合适的工具。

DuckDB 究竟是什么

最重要的区别在于列式存储与行式存储。PostgreSQL 按行组织数据:同一条记录的所有字段在磁盘上连续存储。执行 SELECT revenue, units_sold FROM orders WHERE marketplace = 'DE' 时,顺序扫描会读取所遍历的每行的所有字段,丢弃未请求的字段,返回所需的两个字段。索引(包括仅索引扫描)可以缩小读取范围,但对于覆盖表中大部分数据的广泛聚合,通常帮助不大。这种方式适合插入、更新和读取单条记录的事务型工作负载,却不太适合只涉及少数几列、但需要对数百万行数据进行聚合的分析查询。

DuckDB 按列组织数据。revenue 的所有值存储在一起,units_sold 的所有值也存储在一起。同样的查询只读取所需的三列(返回的两列,以及用于筛选的 marketplace),完全跳过其余部分。对于包含四十列的数据集,这并非微小优化:对于只涉及少数几列的查询,I/O 可减少约 90% 或更多。再结合向量化执行,即使用 SIMD CPU 指令,默认每批处理 2,048 个值,而非逐行处理,DuckDB 在分析工作负载上的查询性能可以超过相同硬件上经过充分调优的 PostgreSQL 实例,尤其是在使用 Parquet 等列式格式时。

嵌入式架构同样重要。无需网络往返,没有身份验证开销,也不必管理连接池。DuckDB 打开文件或目录,生成查询计划,然后执行。二进制文件小于 100MB。它可以运行在笔记本电脑上、Lambda 函数内,或由 cron 任务调用的 Python 脚本中。对于自托管分析管道,运维涉及的组件几乎为零。

直接用 SQL 查询文件

DuckDB 对 Seller Central 数据尤其有用的一项功能,是基于通配符的文件扫描。一条 SQL 语句即可同时查询数千个 CSV 文件:

SELECT
  date_trunc('month', "Date") AS month,
  SUM("Units Ordered")          AS units,
  SUM("Ordered Product Sales")  AS revenue
FROM read_csv_auto('/data/seller-central/business-reports/*.csv')
GROUP BY 1
ORDER BY 1;

read_csv_auto 根据文件内容推断数据模式:抽样读取行、识别列类型、处理不同分隔符,并在设置 union_by_name=true 时,按名称统一列集合不同的文件中的列(默认采用第一个文件的数据模式,并按位置匹配后续文件)。它不会自动匹配重命名的列;这些列仍需在查询中使用别名或 COALESCE 处理。对于 Seller Central 数据,这一点很重要,因为 Amazon 在过去三年中至少修改过四次列名。查询直接读取执行时目录内存在的文件,无需导入、迁移数据模式,也没有需要清除的过期缓存。

对于广告控制台导出的 TSV 文件,同样的方法也适用,只需显式指定格式:

FROM read_csv('/data/advertising/*.tsv', delim='	', header=true)

这不仅是一项便利功能,而是处理文件数据的另一种思路:文件系统成为数据库,数据模式通过推断获得,而非预先声明;查询可以跨越文件边界组合数据,无需任何预处理。对于问题变化速度快于 ETL 管道更新速度的即席分析,这种架构是合适的默认选择。

性能表现

在我们的参考数据集上,共 4,200 个文件、约 2.8GB 未压缩数据,按 Amazon 站点、ASIN 和月份分组汇总全部收入,在标准笔记本电脑(Apple M2、16GB RAM)上耗时 4.1 秒。等效的 PostgreSQL 查询在已导入并建立索引的数据上耗时 2.8 秒。但这种比较没有算入最初四十分钟的导入时间、Amazon 每次修改列名后所需的数据模式迁移,以及持续维护 ETL 的成本。按总运维成本计算,DuckDB 在这种工作负载上具有明显优势。

如果查询性能成为瓶颈,通常是在达到数 TB 规模时,DuckDB 原生支持读取 Parquet,现有 CSV 文件只需转换一次:

COPY (FROM read_csv_auto('/data/**/*.csv'))
TO '/data/consolidated.parquet'
(FORMAT PARQUET);

后续对 Parquet 文件的查询,比原始 CSV 快 8 至 12 倍。Parquet 存储列统计信息,即每个行组的最小值和最大值,DuckDB 利用这些信息,无需读取就能跳过整段数据。在已排序的 Parquet 文件上使用 marketplace = 'DE' 过滤条件,可能只需读取磁盘数据的不到百分之十。普通 CSV 文件无法提供这种谓词下推优化。

使用 Chart.js 构建独立仪表盘

DuckDB 查询的输出是数据。实际问题是如何使用这些数据。对于中小规模 Amazon 运营团队的内部报告,基于 Chart.js 的独立 HTML 文件往往比托管式仪表盘更实用:无需运行服务器、管理凭据,也无需维护 Superset 或 Metabase 实例。

这条管道分三步运行。首先,DuckDB 将查询结果导出为 JSON:

COPY (
  SELECT
    strftime(date_trunc('week', "Date"), '%Y-%m-%d') AS week,
    SUM("Units Ordered")         AS units,
    SUM("Ordered Product Sales") AS revenue
  FROM read_csv_auto('/data/**/*.csv')
  GROUP BY 1 ORDER BY 1
) TO 'weekly.json' (FORMAT JSON, ARRAY true);

其次,Python 脚本读取这些 JSON 文件,将数据直接嵌入通过 CDN 引用 Chart.js 的 HTML 模板。最后,在浏览器中打开 HTML 文件,无需 Web 服务器。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Seller Dashboard</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
  <canvas id="revenue"></canvas>
  <script>
    const rows = /* JSON_DATA_HERE */;
    new Chart(document.getElementById('revenue'), {
      type: 'line',
      data: {
        labels: rows.map(r => r.week),
        datasets: [{ label: 'Revenue', data: rows.map(r => r.revenue), tension: 0.3 }]
      }
    });
  </script>
</body>
</html>

生成的是单个 HTML 文件(它从 CDN 加载 Chart.js,因此除非将库直接内嵌到文件中,否则查看者需要联网),可以通过邮件共享、放到私有 S3 存储桶中,或直接从本地目录打开。对于需要每周报告、又不想承担实时仪表盘基础设施开销的运营团队,这种方式务实且易于维护。只需运行一个 shell 脚本即可重新生成仪表盘:DuckDB 查询 CSV,Python 构建 HTML,流程完成。无需服务器基础设施,也没有持续的托管费用,维护范围很小:查询和模板脚本。

DuckDB 何时优于 PostgreSQL

DuckDB 并非 PostgreSQL 的替代品。只有工作负载确实属于分析型,涉及大范围扫描、聚合和分组,且以读取而非写入为主时,这种比较才有意义。对于并发写入、参照完整性、行级访问控制和生产应用的完整运维需求,PostgreSQL 具有明显优势。对于由应用代码插入、更新和查询记录的多用户系统,它是合适的选择。

DuckDB 更有优势的情况包括:

  • 数据已经以文件形式存储在磁盘上,且没有计划进行导入
  • 事先无法完全确定数据模式,或数据模式经常变化
  • 查询是即席的,无法预先预测访问模式
  • 项目持续时间或规模不足以证明运行数据库服务器的运维开销是合理的

Amazon Seller Central 数据恰好满足这四个条件。数据模式随每次 Seller Central 发布而变化;导出后,文件已经在本地;业务问题随着每周的优化重点而变化。另一种方案,即持续维护 PostgreSQL ETL 管道,需要数周的工程投入,而 DuckDB 用一个下午就能解决这个问题。

实用原则是:如果您原本会用 Excel 或 Jupyter notebook 分析这些数据,可以改用 DuckDB。SQL 的表达能力更强,对于超过几千行的数据,性能通常要好得多;基于文件的架构也意味着数据仍留在原来的位置。

相关洞察

延伸阅读