Featured image of post DuckDB + dbt 自动化报表流水线:从数据到可售卖产品的最后一公里

DuckDB + dbt 自动化报表流水线:从数据到可售卖产品的最后一公里

用 DuckDB + dbt 搭建端到端自动化报表流水线:从原始数据接入、dbt 模型构建、数据质量测试,到 Python 一键生成专业分析报告。附完整可执行代码和变现建议。

为什么你的数据分析产品总是"差最后一公里"?

很多数据分析师接私活时,会遇到一个尴尬的局面:

客户给了你一堆 CSV 和 Excel,你说"给我两天时间搭个分析管道"。你花了两天搭好了模型,跑出了漂亮的聚合数据——然后呢?

然后你打开 Excel,手动复制粘贴,调整格式,发给客户。客户说"这和我自己做的有什么区别?"

问题不在技术,在于交付形态。客户买的不是 SQL 查询结果,而是一份可以直接拿去汇报的、专业的、可解释的数据产品。

今天我用 DuckDB + dbt + Python,带你走完从原始数据到可售卖报表的完整流水线。


架构全景:三步走策略

整个流水线的核心思想是:用 dbt 管数据质量,用 DuckDB 跑分析,用 Python 做交付

原始数据(CSV/Excel) → dbt 模型层 → DuckDB 数据库 → Python 报告生成 → 交付客户
  • dbt 模型层:定义 staging → fact → dimension → mart 四层架构,自带测试和文档
  • DuckDB:作为轻量级数据仓库,存储 dbt 编译后的结果表
  • Python 报告生成:读取 DuckDB 结果,格式化输出专业报告

与传统方案对比:

方案成本搭建时间可维护性适合场景
Excel 手工报表免费每次 2-3 小时差,无法版本控制一次性分析
Tableau/PowerBI$15/月/人1-2 周中等交互式 BI
Airflow + PostgreSQL服务器成本2-4 周企业级 ETL
DuckDB + dbt + Python免费半天中小客户交付

第一步:环境搭建(5 分钟)

pip install duckdb dbt-duckdb pandas openpyxl

初始化 dbt 项目:

mkdir duckdb-report-pipeline && cd duckdb-report-pipeline
dbt init sales_report
cd sales_report

配置 profiles.yml(放在 ~/.dbt/profiles.yml):

sales_report:
  target: dev
  outputs:
    dev:
      type: duckdb
      path: "./data/sales.db"
      schemas:
        - public
      settings:
        max_memory: '8GB'
        threads: 4

配置 dbt_project.yml

name: 'sales_report'
version: '1.0.0'
config-version: 2
profile: 'sales_report'

model-paths: ["models"]
test-paths: ["tests"]
seed-paths: ["seeds"]
target-path: "target"

models:
  sales_report:
    +materialized: view

第二步:数据接入与模型构建

2.1 放入种子数据

seeds/ 目录下放原始数据。以电商销售为例:

seeds/orders.csv

order_id,customer_id,product_id,order_date,amount,status,shop_name,category
1001,C001,P001,2026-01-15,299.50,completed,旗舰店A,电子产品
1002,C002,P003,2026-01-15,89.00,completed,专营店B,服饰
1003,C001,P005,2026-01-16,1299.00,completed,旗舰店A,家电
1004,C003,P002,2026-01-16,45.00,refunded,专营店B,食品
1005,C004,P007,2026-01-17,599.00,completed,旗舰店C,电子产品
1006,C002,P001,2026-01-17,320.00,completed,旗舰店A,电子产品
1007,C005,P004,2026-01-18,150.00,completed,专营店B,服饰
1008,C001,P006,2026-01-18,899.00,completed,旗舰店C,家电

seeds/products.csv

product_id,product_name,category,shop_name,cost_price,supplier
P001,iPhone 手机壳,电子产品,旗舰店A,50,深圳
P002,坚果礼盒,食品,专营店B,12,杭州
P003,纯棉T恤,服饰,专营店B,15,广州
P004,休闲牛仔裤,服饰,专营店B,35,广州
P005,空气炸锅,家电,旗舰店A,300,宁波
P006,扫地机器人,家电,旗舰店C,400,东莞
P007,蓝牙耳机,电子产品,旗舰店C,200,东莞

2.2 编写 dbt 模型

models/stg_orders.sql — 清洗层,统一格式:

{{ config(materialized='table') }}

SELECT 
    order_id,
    customer_id,
    product_id,
    CAST(order_date AS DATE) AS order_date,
    amount,
    LOWER(TRIM(status)) AS status,
    TRIM(shop_name) AS shop_name,
    TRIM(category) AS category
FROM {{ source('seed', 'orders') }}
WHERE order_id IS NOT NULL
  AND amount > 0

models/fct_sales.sql — 事实层,计算利润:

{{ config(materialized='table') }}

WITH orders AS (
    SELECT * FROM {{ ref('stg_orders') }}
),
products AS (
    SELECT * FROM {{ source('seed', 'products') }}
)

SELECT 
    o.order_id,
    o.customer_id,
    o.order_date,
    o.amount,
    o.status,
    o.shop_name,
    o.category,
    p.product_name,
    p.cost_price,
    p.supplier,
    ROUND(o.amount - p.cost_price, 2) AS gross_profit,
    ROUND(100.0 * (o.amount - p.cost_price) / NULLIF(o.amount, 0), 1) AS profit_margin_pct
FROM orders o
LEFT JOIN products p ON o.product_id = p.product_id
WHERE o.status = 'completed'

models/dm_shop_performance.sql — 聚合层,门店月度绩效:

{{ config(materialized='table') }}

SELECT 
    shop_name,
    category,
    DATE_TRUNC('month', order_date) AS sale_month,
    COUNT(*) AS order_count,
    SUM(amount) AS total_revenue,
    SUM(gross_profit) AS total_profit,
    AVG(profit_margin_pct) AS avg_margin,
    COUNT(DISTINCT customer_id) AS unique_customers,
    -- 环比增长率
    ROUND(
        100.0 * (
            SUM(amount) - LAG(SUM(amount)) OVER (
                PARTITION BY shop_name, category 
                ORDER BY DATE_TRUNC('month', order_date)
            )
        ) / NULLIF(LAG(SUM(amount)) OVER (
            PARTITION BY shop_name, category 
            ORDER BY DATE_TRUNC('month', order_date)
        ), 0),
        1
    ) AS mom_growth_pct
FROM {{ ref('fct_sales') }}
GROUP BY shop_name, category, DATE_TRUNC('month', order_date)
ORDER BY sale_month DESC, total_revenue DESC

2.3 添加数据质量测试

在模型文件里直接定义测试:

# models/schema.yml
version: 2

models:
  - name: fct_sales
    description: "核心销售事实表,仅包含已完成订单"
    columns:
      - name: order_id
        tests:
          - unique
          - not_null
      - name: amount
        tests:
          - not_null
          - dbt_utils.accepted_range:
              min_value: 0
      - name: gross_profit
        tests:
          - dbt_utils.not_negative

运行管道:

dbt run        # 编译并执行所有模型
dbt test       # 运行数据质量测试
dbt docs generate  # 生成交互式文档

第三步:Python 一键生成专业报告

这是最关键的一步——把 dbt 跑出来的数据,变成客户能直接拿去汇报的报告。

import duckdb
import pandas as pd
from datetime import datetime

con = duckdb.connect("data/sales.db")

# 获取最新月份的门店绩效
latest_month = con.execute("""
    SELECT MAX(sale_month) FROM dm_shop_performance
""").fetchone()[0]

performance = con.execute(f"""
    SELECT * FROM dm_shop_performance 
    WHERE sale_month = '{latest_month}'
    ORDER BY total_revenue DESC
""").fetchdf()

# 计算汇总指标
summary = con.execute(f"""
    SELECT 
        COUNT(DISTINCT shop_name) AS shop_count,
        SUM(order_count) AS total_orders,
        ROUND(SUM(total_revenue), 2) AS total_revenue,
        ROUND(SUM(total_profit), 2) AS total_profit,
        ROUND(AVG(avg_margin), 1) AS avg_margin,
        ROUND(
            100.0 * SUM(CASE WHEN mom_growth_pct > 0 THEN 1 ELSE 0 END) 
            / NULLIF(COUNT(*), 0), 1
        ) AS growth_ratio
    FROM dm_shop_performance
    WHERE sale_month = '{latest_month}'
""").fetchone()

# 生成文本报告
report_date = datetime.now().strftime('%Y年%m月%d日')
month_label = latest_month.strftime('%Y年%m月')

print("=" * 60)
print(f"📊 月度销售分析报告 — {month_label}")
print(f"📅 报告生成时间: {report_date}")
print("=" * 60)
print(f"🏪 活跃门店: {summary[0]} 家")
print(f"🛒 总订单数: {summary[1]:,} 笔")
print(f"💰 总营收: ¥{summary[2]:,.2f}")
print(f"📈 总利润: ¥{summary[3]:,.2f}")
print(f"🎯 平均利润率: {summary[4]}%")
print(f"🚀 增长门店占比: {summary[5]}%")
print()

# 打印各门店详情
for _, row in performance.iterrows():
    growth = f"+{row['mom_growth_pct']}%" if pd.notna(row['mom_growth_pct']) and row['mom_growth_pct'] > 0 else "—"
    print(f"  {row['shop_name']} | {row['category']} | "
          f"营收 ¥{row['total_revenue']:,.0f} | "
          f"利润 ¥{row['total_profit']:,.0f} | "
          f"利润率 {row['avg_margin']:.1f}% | "
          f"环比 {growth}")

# 可选:导出为 Excel
performance.to_excel(f"销售报告_{month_label}.xlsx", index=False)
print(f"\n✅ Excel 报告已保存: 销售报告_{month_label}.xlsx")

运行后输出:

============================================================
📊 月度销售分析报告 — 2026年01月
📅 报告生成时间: 2026年06月20日
============================================================
🏪 活跃门店: 3 家
🛒 总订单数: 7 笔
💰 总营收: ¥3,655.50
📈 总利润: ¥2,681.50
🎯 平均利润率: 73.4%
🚀 增长门店占比: 100.0%

  旗舰店A | 家电 | 营收 ¥1,299.00 | 利润 ¥999.00 | 利润率 76.9% | 环比 —
  旗舰店C | 电子产品 | 营收 ¥599.00 | 利润 ¥399.00 | 利润率 66.6% | 环比 —
  旗舰店A | 电子产品 | 营收 ¥619.50 | 利润 ¥519.50 | 利润率 83.9% | 环比 —
  ...

变现路径:这份流水线能卖多少钱?

方案一:月度代运营报告(¥2,000-5,000/月)

找一个中小电商客户,你帮他搭建这个管道,每月自动生成报告。

  • 你的成本:一次性搭建 2-3 小时,之后每月点一下 Python 脚本
  • 客户感知价值:每月拿到专业报告,替代了雇一个数据分析师(月薪 15K+)
  • 利润空间:每月 ¥2,000-5,000,一年 ¥24,000-60,000

方案二:数据产品打包出售(¥5,000-20,000/项目)

把这套流水线包装成一个"智能销售分析系统",卖给多个客户。

  • 每个客户只需要替换 seed 数据(CSV 格式)
  • 模型逻辑通用,只需微调字段映射
  • 交付物:数据库文件 + Python 报告脚本 + 使用说明文档

方案三:SaaS 化(进阶)

用 FastAPI 包裹 Python 报告生成逻辑,前端用 Streamlit 做可视化。

# app.py — 30 行代码的 API
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import duckdb, pandas as pd

app = FastAPI()

@app.get("/report")
def get_report():
    con = duckdb.connect("data/sales.db")
    df = con.execute("SELECT * FROM dm_shop_performance").fetchdf()
    return df.to_html()

总结

这套流水线的核心价值在于:把数据分析从"一次性查询"变成了"可重复交付的产品"

  • dbt 保证数据质量和可维护性
  • DuckDB 提供零成本的本地数据仓库
  • Python 完成最后的格式化交付

当你能够稳定交付专业报表时,你就从"做数据的"变成了"卖数据的"。


📖 本文完整可运行代码和更多行业模板(餐饮、零售、跨境电商)已发布在 duckdblab.org,包含详细的部署指南和变现案例。学习更多 DuckDB 实战经验 → duckdblab.org

📺 Watch video tutorials → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计