Featured image of post DuckDB + SQLite: Build a Lightweight BI Dashboard at Zero Cost

DuckDB + SQLite: Build a Lightweight BI Dashboard at Zero Cost

Use DuckDB ATTACH SQLite as an analysis engine to build zero-server-cost BI dashboards for small businesses. Includes full code, FastAPI integration, and strategies

DuckDB + SQLite: Build a Lightweight BI Dashboard at Zero Cost

When building data analytics products, most people’s first instinct is PostgreSQL, MySQL, or ClickHouse. But for indie developers and small teams, these solutions are heavy: they require deployment, maintenance, and cloud database costs.

Today I’ll show you a zero-server-cost BI approach: SQLite handles transactional writes, DuckDB performs analytical queries, connected seamlessly via ATTACH, with Python tying everything together.

DuckDB + SQLite Architecture

Why This Combination?

Traditional BI architecture needs at least three components:

ComponentTraditional ApproachDuckDB + SQLite
Data StoragePostgreSQLSQLite (file-level)
Analytics EngineClickHouseDuckDB (ATTACH SQLite)
API ServiceFastAPI + SQLAlchemyFastAPI + duckdb
Maintenance ComplexityHighMinimal
Data LatencyMinutesReal-time

The core idea is simple: transactional databases handle writes, analytical engines handle reads. SQLite manages daily CRUD operations, while DuckDB reads directly from the SQLite file through ATTACH, allowing SQL queries to span both layers transparently.

One, Environment Setup

pip install duckdb fastapi uvicorn pandas pyarrow

No database services needed. DuckDB is an embedded engine, and SQLite is built into Python.

Two, Create Sample Business Data

First, build a SQLite database simulating real business scenarios:

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
)
""")

# Generate 500 orders
random.seed(42)
products = ["Pro", "Enterprise", "Basic", "API Calls", "Storage"]
regions = ["East", "South", "North", "West", "Central"]

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))
 )

# Generate 100 customers
customers = [
 (f"C{i:03d}", f"Customer {i}", f"2024-{random.randint(1,12):02d}-01",
 random.choice(["Free", "Standard", "Premium"]))
 for i in range(1, 101)
]
cur.executemany("INSERT INTO customers VALUES (?, ?, ?, ?)", customers)

conn.commit()
conn.close()
print(f"✅ Created {len(customers)} customers, 500 orders")

Three, Connect SQLite and Analyze with DuckDB

This is the critical step — DuckDB can ATTACH a SQLite file directly:

import duckdb

con = duckdb.connect()

# Key: ATTACH the SQLite file
con.execute("ATTACH 'business.db' AS biz (TYPE SQLITE)")

# Query 1: Monthly Revenue Trend
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("📈 Monthly Revenue Trend:")
for _, row in monthly_revenue.iterrows():
 print(f" {row.month}: ${row.revenue:,.0f} | Orders {int(row.orders)} | AOV ${row.avg_order_value:.0f}")

# Query 2: Customer Tier Cross-Analysis
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👥 Customer Tier Analysis:")
for _, row in customer_analysis.iterrows():
 print(f" {row.tier}: {int(row.customer_count)} users | Avg Spend ${row.avg_spend:,.0f} | Total Revenue ${row.total_revenue:,.0f}")

# Query 3: Identify VIP Customers
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 Customers TOP 20:")
for v in vip_list:
 print(f" {v[1]} ({v[0]}): ${v[3]:,.0f} | {int(v[4])} orders")

Example output:

📈 Monthly Revenue Trend:
 2025-01-01: $48,230 | Orders 42 | AOV $1,148
 2025-02-01: $52,100 | Orders 47 | AOV $1,109
 ...

👥 Customer Tier Analysis:

⭐ VIP Customers TOP 20:
 Customer 007 (C007): $12,450 | 18 orders
 Customer 042 (C042): $10,230 | 15 orders
 ...

Four, Export Analysis Reports

Export results as JSON for frontend consumption or email delivery:

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("✅ Report exported to analysis_report.json")

Five, Wrap as FastAPI Service

Package the logic into API endpoints callable by any frontend:

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]

Start command:

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

Connect your frontend to these endpoints using ECharts or Chart.js, and you have a complete BI dashboard.

Six, Advanced Optimization: Parquet Acceleration

As data grows, periodically export SQLite data to Parquet format. DuckDB reads Parquet 10x faster than SQLite:

import duckdb

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

# Export to Parquet
con.execute("COPY (SELECT * FROM biz.orders) TO 'orders.parquet' (FORMAT PARQUET)")

# Then query directly from 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()

In production, use cron to schedule exports, or trigger them after each business data update.

Summary

The DuckDB + SQLite combination is ideal for:

  • Indie developer data product MVPs
  • Small team internal reporting systems
  • Client projects requiring rapid delivery
  • Local analytics on mobile/edge devices

If you’re looking for an analytics solution that doesn’t depend on cloud services, doesn’t require a DBA, and works out of the box, this combination is worth trying.

💡 Want to learn more about building complete data products with DuckDB? duckdblab.org has a full tutorial series covering SQLite integration, Parquet performance optimization, and production deployment — perfect for developers who want to ship real products.


本文信息

项目内容
DuckDB 版本v1.5.x(部分功能基于 v2.0 Preview)
最后验证2026-09-12
测试环境Linux / x86_64 / 16GB RAM
官方文档DuckDB Documentation
GitHubpengzz9527/duckdb-blog

如发现错误,欢迎通过 GitHub Issue 或邮件 [email protected] 反馈。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials