DuckDB + SQLite 零成本搭建轻量级 BI 仪表盘
在做数据分析产品时,很多人第一反应就是 PostgreSQL、MySQL、ClickHouse……但对于独立开发者和小团队来说,这些方案太重了:要部署、要运维、要花钱买云数据库。
今天教你一套零服务器成本的 BI 方案:SQLite 管业务写入,DuckDB 做分析查询,两者通过 ATTACH 无缝连接,Python 把整个流程串起来。

为什么选这个组合?
传统 BI 架构需要至少三个组件:
- 数据库:存数据(PostgreSQL / MySQL)
- 分析引擎:跑查询(ClickHouse / Presto / Druid)
- 服务层:暴露 API(Flask / FastAPI)
而 DuckDB + SQLite 的方案只需要一个 Python 进程:
| 组件 | 传统方案 | DuckDB + SQLite |
|---|---|---|
| 数据存储 | PostgreSQL | SQLite(文件级) |
| 分析引擎 | ClickHouse | DuckDB(ATTACH SQLite) |
| API 服务 | FastAPI + SQLAlchemy | FastAPI + 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 定时执行导出,或者在每次业务数据变更后触发。
七、变现路径
这套方案的商业价值在于极低的交付成本:
- 中小企业报表服务:帮传统企业搭一套 SQLite + DuckDB 报表系统,一次性收费 ¥5,000-20,000
- SaaS 内置分析:在你的 SaaS 里加分析模块,DuckDB 不需要额外服务器,省下的就是利润
- 数据产品模板:把这套代码打包成模板,卖给需要快速搭建报表的开发者
- 咨询+部署:帮客户从 Excel 迁移到这套架构,按项目收费
核心优势:整个分析层就是一个 Python 库,不需要 DBA,不需要运维,部署成本几乎为零。
总结
DuckDB + SQLite 的组合非常适合以下场景:
- 独立开发者的数据产品 MVP
- 小团队的内部报表系统
- 需要快速交付的客户项目
- 移动端/边缘设备的本地分析
如果你正在寻找一个不依赖云服务、不养 DBA、开箱即用的分析方案,这个组合值得试试。
💡 想深入学习 DuckDB 从分析引擎到完整数据产品的搭建方法?duckdblab.org 上有从 SQLite 集成、Parquet 性能优化到生产部署的完整教程系列,适合想真正做出产品的开发者。