
从"做报表"到"造产品":你的数据能力值多少钱?
很多数据分析师每天都在做同样的事情:拉数据、算汇总、画图表、写结论。然后老板说"明天继续"。
但真正值钱的不是"做了日报",而是**“从日报里发现了能变现的机会”**。
今天我们要聊的是 DuckDB 的两个被严重低估的高级分析函数——ARRAY_AGG 和 QUANTILE_CONT——以及它们如何帮你把一份普通的销售日报,变成一个可以收费的商业洞察产品。
场景设定:一份真实的电商销售数据
假设你运营着一个小型电商业务,有一份销售数据 sales.csv:
date,product,revenue,quantity,region
2026-01-01,Widget A,1500.00,10,North
2026-01-01,Widget B,2300.50,15,South
2026-01-01,Widget C,950.00,8,East
2026-01-02,Widget A,1800.00,12,North
2026-01-02,Widget B,2100.00,14,West
2026-01-02,Widget C,1200.00,10,South
...
传统做法是用 GROUP BY 求均值,但这会掩盖数据的真实分布。你的目标应该是用一条 SQL 同时输出三个维度的洞察:
- 产品维度:哪个产品最赚钱?平均单价是多少?收入分布是否健康?
- 时间维度:今天的销售额相比昨天涨还是跌?趋势信号是什么?
- 地域维度:哪个地区贡献了最多的头部产品销量?
第一步:用 QUANTILE_CONT 看穿数据的真相
传统做法是用 AVG(revenue) 看平均收入,但均值是最容易被误导的指标。如果某天有个大客户下了巨额订单,均值就会虚高,让你误以为业务在增长。
QUANTILE_CONT 能告诉你数据的真实分布:
SELECT
product,
SUM(revenue) AS total_revenue,
ROUND(AVG(revenue), 2) AS avg_daily_revenue,
COUNT(DISTINCT date) AS active_days,
ROUND(SUM(revenue) / SUM(quantity), 2) AS avg_unit_price,
QUANTILE_CONT(revenue, 0.25) AS q25,
QUANTILE_CONT(revenue, 0.50) AS median,
QUANTILE_CONT(revenue, 0.75) AS q75,
QUANTILE_CONT(revenue, 0.95) AS p95
FROM read_csv_auto('sales.csv')
GROUP BY product
ORDER BY total_revenue DESC;
为什么这比均值更有价值?
假设 Widget A 的均值是 1787,但中位数只有 1650——这意味着有几天的大额订单拉高了均值。你的收入不稳定!你需要找到那几天发生了什么:是促销活动?是大客户?还是季节性因素?
这种"发现异常再深挖"的思路,才是客户愿意为你付费的核心能力。你不是在"做报表",你是在提供商业诊断服务。
QUANTILE_CONT vs 传统统计函数的对比
| 指标 | AVG() | QUANTILE_CONT(x, 0.5) | 实际意义 |
|---|---|---|---|
| 抗极端值能力 | 差 | 强 | 中位数不受个别大额订单影响 |
| 数据覆盖率 | 单一值 | 可看 25%/50%/75%/95% 分位 | 了解全貌而非一个点 |
| 异常检测 | 需要额外逻辑 | 直接观察 Q1-Q3 间距 | IQR 法识别离群值 |
| 业务解释性 | “平均每天卖 1787” | “50% 的日子收入低于 1650” | 后者更能指导决策 |
第二步:用窗口函数 LAG() 计算环比增长率
日报的灵魂是"变化",不是"绝对值"。用 LAG() 窗口函数计算环比增长率:
WITH daily_summary AS (
SELECT
date,
SUM(revenue) AS daily_revenue,
SUM(quantity) AS daily_quantity,
COUNT(DISTINCT product) AS sku_count
FROM read_csv_auto('sales.csv')
GROUP BY date
)
SELECT
date,
daily_revenue,
daily_quantity,
sku_count,
LAG(daily_revenue) OVER (ORDER BY date) AS prev_day_revenue,
ROUND(
(daily_revenue - LAG(daily_revenue) OVER (ORDER BY date))
/ NULLIF(LAG(daily_revenue) OVER (ORDER BY date), 0) * 100,
1
) AS growth_pct
FROM daily_summary
ORDER BY date;
注意 NULLIF(..., 0) 的使用——避免除以零报错。这是生产环境 SQL 的基本素养。
实战建议:把这段 SQL 封装成 Python 脚本里的一个函数,配合 schedule 库或 cron 每天凌晨跑一次,结果直接写入 Parquet 文件。第二天上班打开就是最新数据,不用等 BI 工具刷新。
第三步:ARRAY_AGG —— 一行 SQL 替代多步 Python 处理
这是 DuckDB 最被低估的能力之一。数组聚合可以把一天的所有产品销售情况压缩成一个数组,一眼看出全貌。
SELECT
date,
region,
ARRAY_AGG(product ORDER BY revenue DESC) AS top_products,
ARRAY_AGG(revenue ORDER BY revenue DESC) AS revenues,
ARRAY_AGG(quantity ORDER BY revenue DESC) AS quantities,
ARRAY_LENGTH(ARRAY_AGG(product ORDER BY revenue DESC)) AS product_count
FROM read_csv_auto('sales.csv')
GROUP BY date, region
ORDER BY date, region;
输出示例:
date | region | top_products | revenues
2026-01-01 | North | [Widget A, Widget C] | [1500.0, 950.0]
2026-01-01 | South | [Widget B] | [2300.5]
2026-01-02 | West | [Widget B, Widget A] | [2100.0, 1800.0]
ARRAY_AGG 的高级用法
不只是简单聚合,你还可以对数组进行各种操作:
-- 取数组前 N 个元素(TOP N 产品)
SELECT
date,
region,
ARRAY_SLICE(top_products, 0, 3) AS top_3_products,
ARRAY_SUM(revenues) AS total_region_revenue
FROM (
SELECT
date,
region,
ARRAY_AGG(product ORDER BY revenue DESC) AS top_products,
ARRAY_AGG(revenue ORDER BY revenue DESC) AS revenues
FROM read_csv_auto('sales.csv')
GROUP BY date, region
)
-- 用 UNNEST 展开数组做进一步分析
SELECT
date,
region,
UNNEST(top_products) AS ranked_product,
UNNEST(revenues) AS ranked_revenue,
ROW_NUMBER() OVER (PARTITION BY date, region ORDER BY revenues DESC) AS rank
FROM (
SELECT
date,
region,
ARRAY_AGG(product ORDER BY revenue DESC) AS top_products,
ARRAY_AGG(revenue ORDER BY revenue DESC) AS revenues
FROM read_csv_auto('sales.csv')
GROUP BY date, region
) sub;
ARRAY_AGG 与传统 GROUP BY 的对比
| 需求 | 传统方法 | DuckDB ARRAY_AGG |
|---|---|---|
| 每个地区的 TOP 3 产品 | 自连接 + 行号过滤 | ARRAY_SLICE(ARRAY_AGG(...), 0, 3) |
| 数组长度统计 | COUNT(*) + 子查询 | ARRAY_LENGTH(ARRAY_AGG(...)) |
| 数组元素去重 | ARRAY_UNIQUE(ARRAY_AGG(...)) | 内置函数一行搞定 |
| 数组排序后再聚合 | 需要先 ORDER BY 再 GROUP BY | ARRAY_AGG(col ORDER BY ...) |
第四步:一键导出 Parquet,接入下游系统
日报的最终目的不是"看",而是"用"。把分析结果写入 Parquet,可以无缝对接任何大数据工具:
COPY (
WITH daily_summary AS (
SELECT
date,
SUM(revenue) AS daily_revenue,
SUM(quantity) AS daily_quantity,
LAG(SUM(revenue)) OVER (ORDER BY date) AS prev_day_revenue,
ROUND(
(SUM(revenue) - LAG(SUM(revenue)) OVER (ORDER BY date))
/ NULLIF(LAG(SUM(revenue)) OVER (ORDER BY date), 0) * 100, 1
) AS growth_pct
FROM read_csv_auto('sales.csv')
GROUP BY date
)
SELECT *,
CASE WHEN growth_pct > 10 THEN '🟢 高速增长'
WHEN growth_pct > 0 THEN '🟡 平稳增长'
WHEN growth_pct > -10 THEN '🟠 小幅下滑'
ELSE '🔴 大幅下滑'
END AS trend_signal
FROM daily_summary
) TO 'daily_report.parquet' (FORMAT PARQUET);
一行 COPY 命令,结果直接变成 Parquet 文件。接下来你可以:
- 用 Python 读出来发企业微信/钉钉机器人
- 用 Metabase 连接 Parquet 做可视化
- 用 FastAPI 暴露成一个 API 给前端调用
Python 集成:完整的日报生成脚本
import duckdb
import json
from datetime import datetime
def generate_daily_report(csv_path='sales.csv'):
"""生成带趋势信号的日报"""
conn = duckdb.connect()
# 主分析查询
report = conn.execute("""
WITH daily_summary AS (
SELECT
date,
SUM(revenue) AS daily_revenue,
SUM(quantity) AS daily_quantity,
LAG(SUM(revenue)) OVER (ORDER BY date) AS prev_day_revenue,
ROUND(
(SUM(revenue) - LAG(SUM(revenue)) OVER (ORDER BY date))
/ NULLIF(LAG(SUM(revenue)) OVER (ORDER BY date), 0) * 100, 1
) AS growth_pct
FROM read_csv_auto(?)
GROUP BY date
)
SELECT *,
CASE WHEN growth_pct > 10 THEN '🟢 高速增长'
WHEN growth_pct > 0 THEN '🟡 平稳增长'
WHEN growth_pct > -10 THEN '🟠 小幅下滑'
ELSE '🔴 大幅下滑'
END AS trend_signal
FROM daily_summary
ORDER BY date DESC
""", [csv_path]).fetchdf()
# 区域 × 产品交叉分析
regional_insight = conn.execute("""
SELECT
date,
region,
ARRAY_AGG(product ORDER BY revenue DESC) AS top_products,
ARRAY_AGG(revenue ORDER BY revenue DESC) AS revenues
FROM read_csv_auto(?)
GROUP BY date, region
ORDER BY date, region
""", [csv_path]).fetchdf()
# 导出 Parquet
report.to_parquet('daily_report.parquet')
# 返回 JSON 格式供 API 使用
return {
'generated_at': datetime.now().isoformat(),
'daily_report': report.to_dict(orient='records'),
'regional_insights': regional_insight.to_dict(orient='records')
}
# 使用示例
result = generate_daily_report()
print(json.dumps(result['daily_report'][0], indent=2, ensure_ascii=False))
变现思路:把 SQL 能力打包成数据产品
掌握了 ARRAY_AGG 和 QUANTILE_CONT,你就不再是"做报表的工具人",而是"用数据驱动决策的产品人"。以下是几个具体的变现方向:
方向一:区域销售热力看板(SaaS 产品)
这个功能可以直接做成一个"区域销售热力看板"——每个地区的 TOP 产品一目了然。中小电商老板不需要懂 SQL,你只需要提供一个前端接口调用这个查询即可。
技术栈:DuckDB + FastAPI + Streamlit 定价:基础版 ¥99/月,专业版 ¥299/月 获客渠道:电商社群、闲鱼、小红书
方向二:数据监控即服务
每天自动运行 SQL 分析,当发现异常趋势时通过微信/邮件通知客户。你卖的不是"报表",而是"预警"。
技术栈:DuckDB + cron + 企业微信机器人 定价:按监控数据源数量收费,¥199-599/月 客户画像:拥有多个店铺的小微商
方向三:商业诊断咨询
用 QUANTILE_CONT 分析客户的收入分布,找出"均值被掩盖的不稳定性",然后给出改进建议。这不是报表,这是诊断报告,价值完全不同。
定价:单次诊断 ¥500-2000,月度顾问 ¥3000-8000
总结:四步方法论
- 用分位数代替均值——看到数据的全貌,而不是被平均值骗了
- 用窗口函数算环比——日报的灵魂是"变化率",不是"绝对值"
- 用数组聚合做交叉分析——一行 SQL 替代多步 Python 处理
- 用 COPY 导出 Parquet——SQL 分析完直接喂给下游系统
掌握了这四步,你的数据能力就从"辅助工具"升级成了"盈利引擎"。
📖 本文所有代码已在 DuckDB 1.5.4 环境验证通过。想系统学习 DuckDB 从入门到实战的完整教程?duckdblab.org 上有系列专栏,涵盖 CSV/Parquet 读写、窗口函数、数组操作、HTTPFS 远程查询等核心技能,配套可运行代码和实战案例。学习更多 DuckDB 实战经验 → duckdblab.org