Featured image of post DuckDB 数组聚合实战:用 ARRAY_AGG 构建区域销售热力看板

DuckDB 数组聚合实战:用 ARRAY_AGG 构建区域销售热力看板

深入掌握 DuckDB 的 ARRAY_AGG、QUANTILE_CONT 等高级分析函数,用一条 SQL 实现多维度交叉分析,将日报从'看数据'升级为'发现商机'的数据产品。

从"做报表"到"造产品":你的数据能力值多少钱?

很多数据分析师每天都在做同样的事情:拉数据、算汇总、画图表、写结论。然后老板说"明天继续"。

但真正值钱的不是"做了日报",而是**“从日报里发现了能变现的机会”**。

今天我们要聊的是 DuckDB 的两个被严重低估的高级分析函数——ARRAY_AGGQUANTILE_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 同时输出三个维度的洞察:

  1. 产品维度:哪个产品最赚钱?平均单价是多少?收入分布是否健康?
  2. 时间维度:今天的销售额相比昨天涨还是跌?趋势信号是什么?
  3. 地域维度:哪个地区贡献了最多的头部产品销量?

第一步:用 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 BYARRAY_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

总结:四步方法论

  1. 用分位数代替均值——看到数据的全貌,而不是被平均值骗了
  2. 用窗口函数算环比——日报的灵魂是"变化率",不是"绝对值"
  3. 用数组聚合做交叉分析——一行 SQL 替代多步 Python 处理
  4. 用 COPY 导出 Parquet——SQL 分析完直接喂给下游系统

掌握了这四步,你的数据能力就从"辅助工具"升级成了"盈利引擎"。


📖 本文所有代码已在 DuckDB 1.5.4 环境验证通过。想系统学习 DuckDB 从入门到实战的完整教程?duckdblab.org 上有系列专栏,涵盖 CSV/Parquet 读写、窗口函数、数组操作、HTTPFS 远程查询等核心技能,配套可运行代码和实战案例。学习更多 DuckDB 实战经验 → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

⚠️ 本站为独立社区项目,与 DuckDB 基金会及 DuckDB 官方项目无任何从属、背书或赞助关系。

"DuckDB" 是 DuckDB 基金会的注册商标,本站仅以事实描述方式使用该名称。

本站内容仅供教育与社区推广用途,不构成任何商业服务。