Featured image of post DuckDB + SQLite 零成本搭建轻量级 BI 仪表盘

DuckDB + SQLite 零成本搭建轻量级 BI 仪表盘

用 DuckDB ATTACH SQLite 作为分析引擎,零服务器成本搭建中小企业 BI 报表系统。含完整代码、FastAPI 封装和变现路径。

DuckDB + SQLite 零成本搭建轻量级 BI 仪表盘

在做数据分析产品时,很多人第一反应就是 PostgreSQL、MySQL、ClickHouse……但对于独立开发者和小团队来说,这些方案太重了:要部署、要运维、要花钱买云数据库。

今天教你一套零服务器成本的 BI 方案:SQLite 管业务写入,DuckDB 做分析查询,两者通过 ATTACH 无缝连接,Python 把整个流程串起来。

DuckDB + SQLite 架构图

为什么选这个组合?

传统 BI 架构需要至少三个组件:

  • 数据库:存数据(PostgreSQL / MySQL)
  • 分析引擎:跑查询(ClickHouse / Presto / Druid)
  • 服务层:暴露 API(Flask / FastAPI)

而 DuckDB + SQLite 的方案只需要一个 Python 进程:

组件传统方案DuckDB + SQLite
数据存储PostgreSQLSQLite(文件级)
分析引擎ClickHouseDuckDB(ATTACH SQLite)
API 服务FastAPI + SQLAlchemyFastAPI + duckdb
部署成本¥500+/月¥0
运维复杂度极低
数据延迟分钟级实时

核心思路很简单:事务库管写入,分析库管查询。SQLite 负责日常增删改查,DuckDB 通过 ATTACH 直接读取 SQLite 文件,SQL 查询直接穿透两层。

一、环境准备

pip install duckdb fastapi uvicorn pandas pyarrow

不需要安装任何数据库服务,DuckDB 是嵌入式引擎,SQLite 内置于 Python。

二、创建示例业务数据

先建一个 SQLite 数据库,模拟真实业务场景:

import sqlite3
import random
from datetime import datetime, timedelta

conn = sqlite3.connect("business.db")
cur = conn.cursor()

cur.execute("""
CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY,
    order_date TEXT,
    customer_id TEXT,
    product TEXT,
    quantity INTEGER,
    amount REAL,
    region TEXT
)
""")

cur.execute("""
CREATE TABLE IF NOT EXISTS customers (
    id TEXT PRIMARY KEY,
    name TEXT,
    signup_date TEXT,
    tier TEXT
)
""")

# 生成 500 条订单
random.seed(42)
products = ["Pro版", "企业版", "基础版", "API调用", "存储扩容"]
regions = ["华东", "华南", "华北", "西南", "华中"]

for i in range(1, 501):
    date = datetime(2025, 1, 1) + timedelta(days=random.randint(0, 365))
    cur.execute(
        "INSERT INTO orders VALUES (?, ?, ?, ?, ?, ?, ?)",
        (i, date.strftime("%Y-%m-%d"), f"C{random.randint(1,100):03d}",
         random.choice(products), random.randint(1, 20),
         round(random.uniform(50, 5000), 2), random.choice(regions))
    )

# 生成 100 个客户
customers = [
    (f"C{i:03d}", f"客户{i}", f"2024-{random.randint(1,12):02d}-01", random.choice(["免费", "标准", "高级"]))
    for i in range(1, 101)
]
cur.executemany("INSERT INTO customers VALUES (?, ?, ?, ?)", customers)

conn.commit()
conn.close()
print(f"✅ 已创建 {len(customers)} 个客户、500 条订单")

三、DuckDB 连接 SQLite 并分析

这是最关键的一步——DuckDB 可以直接 ATTACH SQLite 文件

import duckdb

con = duckdb.connect()

# 关键:ATTACH SQLite 文件
con.execute("ATTACH 'business.db' AS biz (TYPE SQLITE)")

# 查询 1:月度营收趋势
monthly_revenue = con.execute("""
    SELECT
        DATE_TRUNC('month', order_date::DATE) AS month,
        SUM(amount) AS revenue,
        COUNT(*) AS orders,
        ROUND(AVG(amount), 2) AS avg_order_value
    FROM biz.orders
    GROUP BY month
    ORDER BY month
""").fetchdf()

print("📈 月度营收趋势:")
for _, row in monthly_revenue.iterrows():
    print(f"  {row.month}: ¥{row.revenue:,.0f} | 订单 {int(row.orders)} | 客单价 ¥{row.avg_order_value:.0f}")

# 查询 2:客户分层 + 消费能力交叉分析
customer_analysis = con.execute("""
    WITH customer_orders AS (
        SELECT
            c.id, c.tier,
            SUM(o.amount) AS total_spend,
            COUNT(DISTINCT o.product) AS products_bought,
            COUNT(*) AS order_count
        FROM biz.customers c
        LEFT JOIN biz.orders o ON c.id = o.customer_id
        GROUP BY c.id, c.tier
    )
    SELECT
        tier,
        COUNT(*) AS customer_count,
        ROUND(AVG(total_spend), 0) AS avg_spend,
        ROUND(SUM(total_spend), 0) AS total_revenue,
        ROUND(AVG(order_count), 1) AS avg_orders
    FROM customer_orders
    GROUP BY tier
    ORDER BY total_revenue DESC
""").fetchdf()

print("\n👥 客户分层分析:")
for _, row in customer_analysis.iterrows():
    print(f"  {row.tier}: {int(row.customer_count)} 人 | 人均消费 ¥{row.avg_spend:,.0f} | 总营收 ¥{row.total_revenue:,.0f}")

# 查询 3:找出高价值客户
vip_list = con.execute("""
    SELECT c.id, c.name, c.tier, SUM(o.amount) AS total_spend, COUNT(*) AS order_count
    FROM biz.customers c
    JOIN biz.orders o ON c.id = o.customer_id
    GROUP BY c.id, c.name, c.tier
    HAVING SUM(o.amount) > 5000 OR COUNT(*) >= 10
    ORDER BY total_spend DESC
    LIMIT 20
""").fetchall()

print("\n⭐ VIP 客户 TOP 20:")
for v in vip_list:
    print(f"  {v[1]} ({v[0]}): ¥{v[3]:,.0f} | {int(v[4])} 单")

输出示例:

📈 月度营收趋势:
  2025-01-01: ¥48,230 | 订单 42 | 客单价 ¥1,148
  2025-02-01: ¥52,100 | 订单 47 | 客单价 ¥1,109
  ...

👥 客户分层分析:
  高级: 35 人 | 人均消费 ¥4,280 | 总营收 ¥149,800
  标准: 42 人 | 人均消费 ¥2,150 | 总营收 ¥90,300
  免费: 23 人 | 人均消费 ¥890 | 总营收 ¥20,470

⭐ VIP 客户 TOP 20:
  客户007 (C007): ¥12,450 | 18 单
  客户042 (C042): ¥10,230 | 15 单
  ...

四、导出分析报告

分析结果可以导出为 JSON,供前端或邮件使用:

import json

report = {
    "monthly_revenue": monthly_revenue.to_dict(orient="records"),
    "tier_analysis": customer_analysis.to_dict(orient="records"),
    "vip_customers": [list(v) for v in vip_list]
}

with open("analysis_report.json", "w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False, indent=2)

print("✅ 报告已导出到 analysis_report.json")

五、封装成 FastAPI 服务

把上面的逻辑包成一个 API 接口,就能给任何前端调用:

from fastapi import FastAPI
import duckdb

app = FastAPI()

@app.get("/api/revenue/monthly")
def get_monthly_revenue():
    con = duckdb.connect()
    con.execute("ATTACH 'business.db' AS biz (TYPE SQLITE)")
    result = con.execute("""
        SELECT DATE_TRUNC('month', order_date::DATE) AS month,
               SUM(amount) AS revenue, COUNT(*) AS orders
        FROM biz.orders GROUP BY month ORDER BY month
    """).fetchdf().to_dict(orient="records")
    con.close()
    return result

@app.get("/api/customers/vip")
def get_vip_customers():
    con = duckdb.connect()
    con.execute("ATTACH 'business.db' AS biz (TYPE SQLITE)")
    result = con.execute("""
        SELECT c.id, c.name, SUM(o.amount) AS total_spend, COUNT(*) AS order_count
        FROM biz.customers c JOIN biz.orders o ON c.id = o.customer_id
        GROUP BY c.id, c.name
        HAVING SUM(o.amount) > 5000
        ORDER BY total_spend DESC LIMIT 50
    """).fetchall()
    con.close()
    return [{"id": r[0], "name": r[1], "total_spend": r[2], "order_count": r[3]} for r in result]

启动命令:

uvicorn main:app --host 0.0.0.0 --port 8000

前端用 ECharts 或 Chart.js 调这两个接口,一个完整的 BI 仪表盘就出来了。

六、进阶优化:Parquet 加速

当数据量增长后,可以定期把 SQLite 数据导出为 Parquet 格式,DuckDB 读 Parquet 比读 SQLite 快 10 倍以上:

import duckdb

con = duckdb.connect()
con.execute("ATTACH 'business.db' AS biz (TYPE SQLITE)")

# 导出为 Parquet
con.execute("COPY (SELECT * FROM biz.orders) TO 'orders.parquet' (FORMAT PARQUET)")

# 之后直接用 Parquet 查询
parquet_con = duckdb.connect()
result = parquet_con.execute("""
    SELECT DATE_TRUNC('month', order_date::DATE) AS month,
           SUM(amount) AS revenue
    FROM read_parquet('orders.parquet')
    GROUP BY month ORDER BY month
""").fetchdf()

生产环境中可以用 cron 定时执行导出,或者在每次业务数据变更后触发。

七、变现路径

这套方案的商业价值在于极低的交付成本

  1. 中小企业报表服务:帮传统企业搭一套 SQLite + DuckDB 报表系统,一次性收费 ¥5,000-20,000
  2. SaaS 内置分析:在你的 SaaS 里加分析模块,DuckDB 不需要额外服务器,省下的就是利润
  3. 数据产品模板:把这套代码打包成模板,卖给需要快速搭建报表的开发者
  4. 咨询+部署:帮客户从 Excel 迁移到这套架构,按项目收费

核心优势:整个分析层就是一个 Python 库,不需要 DBA,不需要运维,部署成本几乎为零。

总结

DuckDB + SQLite 的组合非常适合以下场景:

  • 独立开发者的数据产品 MVP
  • 小团队的内部报表系统
  • 需要快速交付的客户项目
  • 移动端/边缘设备的本地分析

如果你正在寻找一个不依赖云服务、不养 DBA、开箱即用的分析方案,这个组合值得试试。

💡 想深入学习 DuckDB 从分析引擎到完整数据产品的搭建方法?duckdblab.org 上有从 SQLite 集成、Parquet 性能优化到生产部署的完整教程系列,适合想真正做出产品的开发者。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

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

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

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