多CSV文件合并分析实战:用DuckDB轻松搞定多店铺销售数据

在实际数据分析工作中,数据往往分散在几十个甚至上百个CSV文件中。本文以多店铺销售数据合并为例,手把手教你用DuckDB的 read_csv_auto 通配符匹配、中文列名处理、strftime 时间聚合和 Parquet 导出,一站式完成数据整合与分析。

引言

“帮我分析一下全国所有门店的销售数据”——这句话背后的真实工作量,往往是让人头疼的。

现实世界中,数据很少整整齐齐地躺在一张表里。更常见的情况是:每个店铺一张CSV,每天生成一个文件,文件名混乱、列名不统一、编码不标准。传统做法——逐文件合并、手动清洗、写Python脚本——既慢又容易出错。

这正是 DuckDB 大显身手的场景。本文用一个真实的多店铺销售数据合并案例,带你走完从零散CSV到多维度分析报告的全流程。

场景设定

假设某连锁品牌有 5 家店铺,每家店每天产生一个 CSV 销售报表。文件格式如下:

data/
├── store_001_daily_20260501.csv
├── store_001_daily_20260502.csv
├── store_002_daily_20260501.csv
├── store_002_daily_20260502.csv
├── store_003_daily_20260501.csv
├── store_003_daily_20260502.csv
├── store_004_daily_20260501.csv
├── store_004_daily_20260502.csv
├── store_005_daily_20260501.csv
├── store_005_daily_20260502.csv

每个CSV文件内容结构相同,使用中文列名:

订单号,商品名称,单价,数量,金额,销售日期,收银员
ORD001,咖啡拿铁,32.00,2,64.00,2026-05-01,张三
ORD002,美式咖啡,25.00,1,25.00,2026-05-01,李四

第一步:用通配符一键读取所有CSV

传统方法:写一个 Python 脚本遍历目录、逐文件读取、拼接 DataFrame。DuckDB 的方法:一行 SQL。

-- 用通配符匹配所有CSV文件,DuckDB自动推断schema
CREATE TABLE raw_sales AS
SELECT * FROM read_csv_auto('data/*.csv');

-- 查看合并后的数据
SELECT COUNT(*) AS 总行数, COUNT(DISTINCT 订单号) AS 总订单数 FROM raw_sales;

read_csv_auto 是 DuckDB 的秘密武器:

  • * 通配符自动匹配目录下所有CSV文件
  • 自动推断列名、数据类型、分隔符
  • 支持 glob 模式:**/*.csv 递归匹配子目录
  • 如果列结构不一致,可以用 union_by_name=true 自动按列名合并
-- 更健壮的写法:自动按列名合并
CREATE TABLE raw_sales AS
SELECT * FROM read_csv_auto('data/*.csv', union_by_name=true);

-- 查看自动推断的schema
DESCRIBE raw_sales;

第二步:提取店铺信息和日期

文件名中包含了店铺ID和日期信息,我们用 DuckDB 丰富的文件路径函数来提取:

-- 从文件名提取店铺ID和销售日期
CREATE TABLE sales_with_meta AS
SELECT
  filename,
  regexp_extract(filename, 'store_(\d+)', 1) AS 店铺ID,
  regexp_extract(filename, '(\d{8})\.csv', 1) AS 日期字符串,
  *
FROM read_csv_auto('data/*.csv', filename=true, union_by_name=true);

-- 转换日期格式
CREATE TABLE sales_clean AS
SELECT
  店铺ID,
  strptime(日期字符串, '%Y%m%d')::DATE AS 销售日期,
  订单号,
  商品名称,
  单价,
  数量,
  金额,
  收银员
FROM sales_with_meta;

filename=true 参数会自动添加一个 filename 列,记录每行数据来自哪个文件,这是多文件合并的调试利器。

第三步:中文列名下的数据分析

DuckDB 对中文列名有原生支持,无需任何特殊配置即可直接引用:

-- 各店铺销售排行
SELECT
  店铺ID,
  SUM(金额) AS 总销售额,
  COUNT(DISTINCT 订单号) AS 订单数,
  SUM(数量) AS 总销量,
  ROUND(AVG(金额), 2) AS 客单价
FROM sales_clean
GROUP BY 店铺ID
ORDER BY 总销售额 DESC;

-- 热销商品TOP10
SELECT
  商品名称,
  SUM(数量) AS 总销量,
  SUM(金额) AS 总销售额,
  COUNT(DISTINCT 店铺ID) AS 铺货店铺数
FROM sales_clean
GROUP BY 商品名称
ORDER BY 总销售额 DESC
LIMIT 10;

-- 收银员业绩排名
SELECT
  收银员,
  店铺ID,
  COUNT(*) AS 处理订单数,
  SUM(金额) AS 经手金额
FROM sales_clean
GROUP BY 收银员, 店铺ID
ORDER BY 经手金额 DESC;

第四步:strftime 时间维度聚合

时间聚合是销售分析的灵魂。DuckDB 的 strftime 函数提供了类似 Python strftime 的灵活格式化能力:

-- 按日聚合
SELECT
  strftime(销售日期, '%Y-%m-%d') AS ,
  SUM(金额) AS 日销售额
FROM sales_clean
GROUP BY 
ORDER BY ;

-- 按周聚合
SELECT
  strftime(销售日期, '%Y-W%W') AS ,
  SUM(金额) AS 周销售额,
  COUNT(DISTINCT 销售日期) AS 营业天数
FROM sales_clean
GROUP BY 
ORDER BY ;

-- 按月聚合(含同比)
SELECT
  strftime(销售日期, '%Y-%m') AS 月份,
  SUM(金额) AS 月销售额,
  SUM(数量) AS 月销量,
  ROUND(AVG(金额), 2) AS 日均销售额
FROM sales_clean
GROUP BY 月份
ORDER BY 月份;

-- 按小时段(假设有具体时间)
-- 判断销售高峰时段
SELECT
  CASE
    WHEN strftime(销售日期, '%H') BETWEEN '06' AND '09' THEN '早餐时段'
    WHEN strftime(销售日期, '%H') BETWEEN '10' AND '13' THEN '午餐时段'
    WHEN strftime(销售日期, '%H') BETWEEN '14' AND '17' THEN '下午茶时段'
    ELSE '晚餐时段'
  END AS 时段,
  SUM(金额) AS 销售额
FROM sales_clean
GROUP BY 时段
ORDER BY 销售额 DESC;

strftime 的常用格式符:

格式符含义示例
%Y四位年份2026
%m两位月份05
%d两位日期11
%W年周数19
%w星期几(0-6)1
%H小时(00-23)14

第五步:导出 Parquet 格式

分析完成后,将结果导出为 Parquet 格式——比 CSV 快 10 倍、体积小 5 倍,且支持列式存储和压缩:

-- 将清洗后的数据导出为Parquet
COPY sales_clean TO 'output/sales_clean.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD);

-- 导出分析报表
COPY (
  SELECT
    店铺ID,
    strftime(销售日期, '%Y-%m') AS 月份,
    strftime(销售日期, '%W') AS 周数,
    商品名称,
    SUM(数量) AS 总销量,
    SUM(金额) AS 总金额
  FROM sales_clean
  GROUP BY 店铺ID, 月份, 周数, 商品名称
  ORDER BY 店铺ID, 月份, 周数, 总金额 DESC
) TO 'output/daily_report.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD, ROW_GROUP_SIZE 100000);

-- Parquet直接查询
SELECT 店铺ID, SUM(总金额) AS 汇总金额
FROM read_parquet('output/*.parquet')
GROUP BY 店铺ID
ORDER BY 店铺ID;

Parquet 的额外优势:

  • 压缩率惊人:ZSTD 压缩后通常仅为 CSV 的 20%
  • 列式存储:只需读取查询涉及的列,而非整行
  • Schema 自描述:类型信息嵌在文件中,不再需要 DDL
  • DuckDB 原生优化:投影下推、谓词下推、延迟物化

完整工作流脚本

把以上步骤整合为一个可重复执行的 SQL 脚本:

-- merge_analysis.sql
-- 多店铺销售数据合并分析完整流程

-- 1. 导入数据
CREATE TABLE raw AS
SELECT * FROM read_csv_auto('data/*.csv', filename=true, union_by_name=true);

-- 2. 清洗与变换
CREATE TABLE clean AS
SELECT
  regexp_extract(filename, 'store_(\d+)', 1) AS 店铺ID,
  strptime(regexp_extract(filename, '(\d{8})\.csv', 1), '%Y%m%d') AS 销售日期,
  订单号, 商品名称, 单价, 数量, 金额, 收银员
FROM raw;

-- 3. 聚合分析
CREATE TABLE monthly_summary AS
SELECT
  店铺ID,
  strftime(销售日期, '%Y-%m') AS 月份,
  COUNT(DISTINCT 订单号) AS 订单数,
  SUM(金额) AS 销售额,
  SUM(数量) AS 销量,
  ROUND(AVG(金额), 2) AS 客单价
FROM clean
GROUP BY 店铺ID, 月份
ORDER BY 店铺ID, 月份;

-- 4. 导出
COPY clean TO 'output/clean_data.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
COPY monthly_summary TO 'output/monthly_summary.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);

-- 5. 快速验证
SELECT '总行数' AS 指标, COUNT(*)::VARCHAR AS 数值 FROM clean
UNION ALL
SELECT '总店铺数', COUNT(DISTINCT 店铺ID)::VARCHAR FROM clean
UNION ALL
SELECT '日期范围', MIN(销售日期)::VARCHAR || ' ~ ' || MAX(销售日期)::VARCHAR FROM clean;

执行方式:

duckdb < merge_analysis.sql
# 或者
duckdb -c ".read merge_analysis.sql"

变现SOP:从技术到商业

做数据分析的人很多,但能把数据整合成可交付的商业洞察的人不多。以下是一套完整的变现方案。

定价策略

服务层级内容价格适合客户
基础层一次性的多CSV合并+基础报表(3张表)¥500-800小微商户(1-5家店)
标准层多数据源合并+周报/月报模板+Parquet导出¥2,000-3,000中型连锁(5-20家店)
专业层全流程自动化+自定义Dashboard+定期维护¥5,000-10,000/月大型连锁(20+家店)

获客渠道

  1. 精准渠道

    • 在知乎/小红书发帖:“XX行业门店销售数据自动汇总方案”
    • 在GitHub开源一个基础版本,README中留联系方式
    • 在V2EX/ProductHunt发布工具帖
  2. 杠杆渠道

    • 与ERP实施公司合作:他们做系统安装,你做数据方案
    • 与财税代账公司合作:他们已有客户群
    • 在餐饮/零售行业社群做知识分享
  3. 内容引流

    • 写行业垂类博文(如"茶饮品牌数据管理从0到1")
    • 录制短视频教程(抖音/B站)
    • 做免费线上分享会

交付模板

## 交付清单
1. ✅ SQL自动化脚本(命名:merge_analysis.sql)
2. ✅ 数据词典文档(Excel/PDF)
3. ✅ 清洗后数据(Parquet格式)
4. ✅ 月度/周度报表模板
5. ✅ 操作指南(README)
6. ✅ 远程支持(1周内免费答疑)

交付时附上一份数据健康度检查报告,包含:

  • 数据完整性:是否有空值、异常值
  • 数据一致性:是否有重复记录、订单号冲突
  • 性能评估:当前流程耗时,优化建议

进阶变现方向

方向说明客单价
数据看板用 Streamlit/Grafana 搭建实时看板¥8,000-15,000
AI分析报告结合 LLM 自动生成周报文字解读¥3,000-5,000/月
异常预警销售额异常波动自动预警通知¥1,000-2,000/月
数据API为POS系统提供标准化数据接口¥2,000-5,000/月

客户常见拒绝及应对

“我们自己用Excel也行” 回应:Excel打开10万行就卡,且无法自动合并每日新增文件。DuckDB方案从读取到出报表不超过5秒,每天自动更新。

“太贵了” 回应:先将工作量化——如果每天花30分钟手工合并,一个月15小时。按最低工资算,3个月就超过我们的费用。

“我们没需求” 回应:先免费帮他们做一次数据合并,让他们看到数据集中后的商业洞察(哪个店最赚钱、哪个商品最畅销),需求自然就有了。

常见问题

Q1:文件编码有问题怎么办?

-- 指定编码为UTF-8或GBK
SELECT * FROM read_csv_auto('data/*.csv', encoding='utf-8');
SELECT * FROM read_csv_auto('data/*.csv', encoding='gbk');

Q2:列名有的有空格有的没有?

-- 统一规范化列名
SELECT * FROM read_csv_auto('data/*.csv', normalize_names=true);

Q3:文件太多,内存不够?

-- 分批读取+直接写入数据库
CREATE TABLE sales AS
SELECT * FROM read_csv_auto('data/2026-01/*.csv');
INSERT INTO sales SELECT * FROM read_csv_auto('data/2026-02/*.csv');
-- 逐月追加...

Q4:每天都要合并怎么自动化?

用 cron job 定时运行脚本:

# crontab -e
# 每天凌晨2点运行
0 2 * * * cd /path/to/project && duckdb < merge_analysis.sql

总结

从零散的多店铺CSV文件到可分析的Parquet数据集,DuckDB 用寥寥几行 SQL 就完成了传统方法需要几十行 Python 代码的工作。核心要点:

  1. read_csv_auto 通配符 + filename=true:一键读取、来源可追溯
  2. 正则提取 + strptime:从文件名反解元信息
  3. 中文列名原生支持:降低团队使用门槛
  4. strftime 时间聚合:灵活的时间维度分析
  5. Parquet 导出:为后续分析提速10倍

这套工作流不仅适用于多店铺销售数据,任何多文件、多来源、需要定期整合的场景——如多仓库库存、多站点日志、多门店客流——都可以直接套用。