Introduction: What’s Your Time Worth?
Do you also experience this scenario—every Monday morning, opening Excel, manually copying last weekend’s sales data, piecing together a report, then sending it to your boss. After that, your boss says “add a few more dimensions,” and you have to start over.
If you automate this workflow with DuckDB, the same work goes from one day to 10 minutes, generates automatically every week, and never makes mistakes. More importantly—this system is sellable.
Today I’ll walk through a real paid project: building an automated e-commerce weekly report system using DuckDB + Python. I’ve used this methodology for three e-commerce clients, charging each 3000-5000 RMB per month.

1. Project Architecture: Why DuckDB?
Traditional approach: Python script → MySQL → Airflow → Jupyter → Email. Long development cycle, high maintenance cost, no DBA at the client side.
DuckDB approach: Python + DuckDB, one script does it all, zero operations.
Four key advantages of DuckDB:
- Analytics performance接近列式数据库: Columnar storage architecture, aggregation 10x faster than pandas
- Read CSV/JSON/Parquet in-place: No ETL needed, just
read_csv_auto()to load - Seamless Python integration: duckdb library executes SQL directly, results convert to pandas
- Single-file database: Copy and use anywhere, deployment cost approaches zero
2. Building the Data Pipeline: From CSV to DuckDB
Assume the e-commerce platform exports CSV files containing orders, products, and users tables.
import duckdb
import pandas as pd
from pathlib import Path
from datetime import datetime, timedelta
# Connect to DuckDB database (persistent storage)
db_path = Path("ecommerce.duckdb")
con = duckdb.connect(str(db_path))
# Read CSV in-place (no need to load into memory first)
con.execute("""
CREATE TABLE IF NOT EXISTS orders AS
SELECT * FROM read_csv_auto('orders.csv')
""")
con.execute("""
CREATE TABLE IF NOT EXISTS products AS
SELECT * FROM read_csv_auto('products.csv')
""")
con.execute("""
CREATE TABLE IF NOT EXISTS users AS
SELECT * FROM read_csv_auto('users.csv')
""")
# Check table structure
print(con.execute("DESCRIBE orders").fetchdf())
print(con.execute("SELECT COUNT(*) FROM orders").fetchone())
Key points:
read_csv_auto()automatically infers column types, no manual specification neededCREATE TABLE IF NOT EXISTSensures idempotency, safe to run repeatedly- DuckDB reads CSV directly, no need to import into database first
3. Core Analysis Logic: Weekly Report Metric Design
The core of a weekly report is metric tree design, making it understandable for non-technical clients.
# Create weekly sales view (auto-slices by week)
con.execute("""
CREATE VIEW IF NOT EXISTS v_weekly_sales AS
SELECT
DATE_TRUNC('week', order_date) AS week,
COUNT(*) AS total_orders,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order_value,
COUNT(DISTINCT user_id) AS active_users,
COUNT(DISTINCT product_id) AS unique_products
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '12 weeks'
GROUP BY DATE_TRUNC('week', order_date)
ORDER BY week DESC
""")
# Verify view
print(con.execute("SELECT * FROM v_weekly_sales LIMIT 5").fetchdf())
Metric design principles:
- Put core metrics first: orders, revenue, average order value, active users
- Add week-over-week comparison: clients can see trends at a glance
- Keep 12 weeks of data: sufficient, doesn’t waste storage
4. Generating Weekly Reports: Complete Python Code
def generate_weekly_report(db_path: str) -> dict:
"""Generate weekly business report"""
con = duckdb.connect(db_path)
# Get current week and previous week data
current_week = con.execute("""
SELECT * FROM v_weekly_sales
WHERE week = DATE_TRUNC('week', CURRENT_DATE)
""").fetchone()
prev_week = con.execute("""
SELECT * FROM v_weekly_sales
WHERE week = (
SELECT MAX(week) FROM v_weekly_sales
WHERE week < DATE_TRUNC('week', CURRENT_DATE)
)
""").fetchone()
# Week-over-week calculation function
def pct_change(curr, prev):
if prev is None or prev == 0:
return None
return round((curr - prev) / prev * 100, 2)
report = {
"week": current_week[0].strftime('%Y-%m-%d') if current_week else None,
"total_orders": current_week[1] if current_week else 0,
"total_revenue": round(current_week[2], 2) if current_week else 0,
"revenue_growth": pct_change(current_week[2], prev_week[2]) if current_week and prev_week else None,
"avg_order_value": round(current_week[3], 2) if current_week else 0,
"avg_order_growth": pct_change(current_week[3], prev_week[3]) if current_week and prev_week else None,
"active_users": current_week[4] if current_week else 0,
"user_growth": pct_change(current_week[4], prev_week[4]) if current_week and prev_week else None,
}
# Top 5 categories by revenue
report["top_categories"] = con.execute("""
SELECT p.category, SUM(o.amount) as revenue
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE DATE_TRUNC('week', o.order_date) = DATE_TRUNC('week', CURRENT_DATE)
GROUP BY p.category
ORDER BY revenue DESC
LIMIT 5
""").fetchall()
# User retention analysis (window functions)
report["retention"] = con.execute("""
SELECT
first_week,
COUNT(DISTINCT user_id) as new_users,
SUM(CASE WHEN weeks_active >= 2 THEN 1 ELSE 0 END) as retained_2w,
SUM(CASE WHEN weeks_active >= 3 THEN 1 ELSE 0 END) as retained_3w
FROM (
SELECT
user_id,
DATE_TRUNC('week', MIN(order_date)) as first_week,
COUNT(DISTINCT DATE_TRUNC('week', order_date)) as weeks_active
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '12 weeks'
GROUP BY user_id
)
GROUP BY first_week
ORDER BY first_week DESC
LIMIT 4
""").fetchall()
con.close()
return report
5. Exporting Reports: Multiple Formats for Different Scenarios
import json
from pathlib import Path
def export_report(report: dict, output_dir: str):
"""Export in multiple formats"""
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
# JSON format (for API calls)
(out / "report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2)
)
# Markdown format (can be sent directly via WeChat/Feishu)
md = f"""# Weekly Report {report['week']}
## Core Metrics
- **Orders**: {report['total_orders']:,} ({'↑' if report['revenue_growth'] and report['revenue_growth'] > 0 else '↓'} {abs(report['revenue_growth']):.1f}%)
- **Revenue**: ${report['total_revenue']:,.2f} ({'↑' if report['revenue_growth'] and report['revenue_growth'] > 0 else '↓'} {abs(report['revenue_growth']):.1f}%)
- **Avg Order Value**: ${report['avg_order_value']:.2f} ({'↑' if report['avg_order_growth'] and report['avg_order_growth'] > 0 else '↓'} {abs(report['avg_order_growth']):.1f}%)
- **Active Users**: {report['active_users']:,}
## Top 5 Categories
"""
for i, (cat, rev) in enumerate(report['top_categories'], 1):
md += f"{i}. {cat}: ${rev:,.2f}\n"
(out / "report.md").write_text(md)
print(f"✅ Report generated: {out}")
# Usage example
if __name__ == "__main__":
report = generate_weekly_report("ecommerce.duckdb")
export_report(report, "reports/2026-W31")
6. Complete Workflow: One-Click Weekly Report Generation
# main.py
from pathlib import Path
import duckdb
from datetime import datetime
def main():
# 1. Load data (idempotent)
con = duckdb.connect("ecommerce.duckdb")
# Auto incremental update
csv_files = list(Path("data").glob("orders_*.csv"))
for f in sorted(csv_files)[-1:]: # Only load the latest file
con.execute(f"INSERT INTO orders SELECT * FROM read_csv_auto('{f}')")
# 2. Generate report
report = generate_weekly_report("ecommerce.duckdb")
# 3. Export
week_str = report['week'].replace('-', '')
export_report(report, f"reports/{week_str}")
con.close()
print("🎉 Weekly report generated")
if __name__ == "__main__":
main()
7. Monetization: What’s This Product Worth?
Pricing Strategy
| Model | Price | Use Case |
|---|---|---|
| One-time customization | 5000-15000 RMB | Small-medium e-commerce businesses |
| Monthly subscription | 2000-5000 RMB/month | Brand owners, agencies |
| Template SaaS | 99-299 RMB/month | Multiple reusable clients |
Key Moats
- Metric system design: What metrics clients actually care about (not tech, but business understanding)
- Anomaly detection logic: Finding problems is more valuable than displaying data
- Delivery experience: Format, frequency, interpretability
Advanced Directions
- Connect to real-time data sources (Kafka + DuckDB Streaming)
- Add prediction modules (Prophet / LightGBM)
- Build web interface (Streamlit / Gradio)
- Multi-tenant isolation (one DuckDB file per client)
8. Comparison with Traditional Approaches
| Dimension | Traditional (MySQL + Airflow) | DuckDB Approach |
|---|---|---|
| Development cycle | 2-4 weeks | 1-2 days |
| Maintenance cost | Need DBA | Zero ops |
| Deployment cost | Server + database | Single machine |
| Query performance | Need indexes | Columnar storage, ready to use |
| Data update | Complex ETL pipeline | CSV direct read, plug and play |
| Portability | Tied to specific database | Single file, copy and use |
9. Real Case: What I Charged My Client
Last month, I built this system for an e-commerce client:
- Data source: Shopify exported CSV, about 300K rows/month
- Metrics: Orders, revenue, average order value, active users, retention rate
- Deliverable: Automatically sent Markdown report every week
- Fee: One-time 8000 RMB + monthly maintenance 2000 RMB/month
Client feedback: “Much faster than the outsourced solution I had before, and easier to modify.”
10. Summary
Building an e-commerce weekly report automation system with DuckDB comes down to three steps:
- Read data:
read_csv_auto()搞定 with one line of code - Calculate metrics: Window functions + views, SQL = analytics
- Export reports: Python formatting, dual Markdown/JSON formats
This methodology can be replicated to:
- Financial monthly reports
- Ad spend ROI tracking
- SaaS subscription analysis
- Inventory warning systems
Remember: The value of data products lies not in technology, but in business understanding. The same code, applied to a different industry, becomes another product.
📖 The complete tutorial (including data simulation, anomaly detection, Streamlit deployment) is published on duckdblab.org. Bookmark it for systematic learning.
💡 Want to learn more about DuckDB applications in e-commerce? duckdblab.org has a complete practical tutorial series, covering everything from beginner to monetization.