引言
“帮我分析一下全国所有门店的销售数据”——这句话背后的真实工作量,往往是让人头疼的。
现实世界中,数据很少整整齐齐地躺在一张表里。更常见的情况是:每个店铺一张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+家店) |
获客渠道
精准渠道
- 在知乎/小红书发帖:“XX行业门店销售数据自动汇总方案”
- 在GitHub开源一个基础版本,README中留联系方式
- 在V2EX/ProductHunt发布工具帖
杠杆渠道
- 与ERP实施公司合作:他们做系统安装,你做数据方案
- 与财税代账公司合作:他们已有客户群
- 在餐饮/零售行业社群做知识分享
内容引流
- 写行业垂类博文(如"茶饮品牌数据管理从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 代码的工作。核心要点:
- read_csv_auto 通配符 + filename=true:一键读取、来源可追溯
- 正则提取 + strptime:从文件名反解元信息
- 中文列名原生支持:降低团队使用门槛
- strftime 时间聚合:灵活的时间维度分析
- Parquet 导出:为后续分析提速10倍
这套工作流不仅适用于多店铺销售数据,任何多文件、多来源、需要定期整合的场景——如多仓库库存、多站点日志、多门店客流——都可以直接套用。