Featured image of post DuckDB Automated Weekly Reports: Build a Data Product for SMEs, Earn ¥5,000/Month Extra

DuckDB Automated Weekly Reports: Build a Data Product for SMEs, Earn ¥5,000/Month Extra

Step-by-step guide to building automated weekly reporting systems for SMEs using DuckDB + Python. A real monetization path charging ¥3,000-8,000/month per client.

The Undervalued Opportunity: SME Data Pain Points

Many data analysts have a misconception: their skills can only be used for “big companies, big projects.”

The reality is exactly the opposite — SMEs (Small and Medium Enterprises) are the most willing-to-pay, least-competitive goldmine.

Why? Because SME bosses check data every day, but they don’t have a data team. Excel can’t handle it anymore, outsourcing is too expensive, and hiring a full-time analyst costs ¥20,000+/month.

If you can build them an automated weekly report system with DuckDB, charging ¥3,000-8,000/month, that’s 360,000-960,000 RMB per year. And you only need 1-2 people.

Here’s the complete breakdown of this monetization path.

Automated Weekly Report System Architecture


Step 1: Understand What Clients Need

Let’s assume your client is a chain restaurant brand with 5 stores. The boss needs a weekly report containing:

  • Total revenue, order count, and average order value per store
  • Week-over-week comparison
  • Top 10 best-selling dishes
  • Dishes with the highest complaint rates

This data usually lives in CSV exports from POS systems or simple databases.

The boss spends 2 hours manually compiling Excel every week, makes frequent errors, and always delays sending it until Monday morning.

Your value proposition: Turn “manual 2 hours” into “check WeChat for the result.”


Step 2: Build the Analysis Engine with DuckDB

First, simulate the client’s data structure. CSV exports from restaurant POS systems look like this:

-- Simulated order data (in practice, read from CSV)
CREATE TABLE orders AS
SELECT * FROM (VALUES
    ('2026-06-23', 'Beijing Chaoyang Store', 1001, 'Kung Pao Chicken', 32, 1),
    ('2026-06-23', 'Beijing Chaoyang Store', 1002, 'Mapo Tofu', 22, 2),
    ('2026-06-23', 'Shanghai Pudong Store', 1003, 'Kung Pao Chicken', 32, 3),
    ('2026-06-23', 'Shanghai Pudong Store', 'Fish Hot Pot', 58, 1),
    ('2026-06-24', 'Beijing Chaoyang Store', 1005, 'Fish Hot Pot', 58, 2),
    ('2026-06-24', 'Guangzhou Tianhe Store', 1006, 'Kung Pao Chicken', 32, 1),
    ('2026-06-24', 'Guangzhou Tianhe Store', 1007, 'Braised Pork', 48, 2),
    ('2026-06-24', 'Beijing Chaoyang Store', 1008, 'Mapo Tofu', 22, 3)
) AS t(order_date, store, order_id, dish, price, quantity);

-- Simulated review data
CREATE TABLE reviews AS
SELECT * FROM (VALUES
    ('2026-06-23', 'Beijing Chaoyang Store', 'Kung Pao Chicken', 5),
    ('2026-06-23', 'Shanghai Pudong Store', 'Fish Hot Pot', 4),
    ('2026-06-23', 'Beijing Chaoyang Store', 'Mapo Tofu', 3),
    ('2026-06-24', 'Guangzhou Tianhe Store', 'Kung Pao Chicken', 5),
    ('2026-06-24', 'Beijing Chaoyang Store', 'Fish Hot Pot', 2),
    ('2026-06-24', 'Guangzhou Tianhe Store', 'Braised Pork', 4)
) AS t(review_date, store, dish, rating);

Now write the core analysis query — a complete weekly report SQL:

-- ========== Part 1: Store Performance Overview ==========
WITH weekly_store_metrics AS (
    SELECT 
        store,
        COUNT(DISTINCT order_id) AS order_count,
        SUM(price * quantity) AS total_revenue,
        ROUND(SUM(price * quantity) / COUNT(DISTINCT order_id), 2) AS avg_order_value,
        SUM(quantity) AS total_items_sold
    FROM orders
    WHERE order_date >= '2026-06-23' AND order_date <= '2026-06-29'
    GROUP BY store
),
prev_week_store_metrics AS (
    SELECT 
        store,
        SUM(price * quantity) AS prev_revenue
    FROM orders
    WHERE order_date >= '2026-06-16' AND order_date <= '2026-06-22'
    GROUP BY store
)

SELECT 
    s.store,
    s.order_count,
    s.total_revenue,
    s.avg_order_value,
    COALESCE(p.prev_revenue, 0) AS prev_week_revenue,
    CASE 
        WHEN COALESCE(p.prev_revenue, 0) > 0 
        THEN ROUND((s.total_revenue - p.prev_revenue) / p.prev_revenue * 100, 1)
        ELSE NULL
    END AS wow_change_pct,
    CASE 
        WHEN COALESCE(p.prev_revenue, 0) > 0 
             AND (s.total_revenue - p.prev_revenue) / p.prev_revenue > 0.05 
        THEN '📈'
        WHEN COALESCE(p.prev_revenue, 0) > 0 
             AND (s.total_revenue - p.prev_revenue) / p.prev_revenue < -0.05 
        THEN '📉'
        ELSE '➡️'
    END AS trend
FROM weekly_store_metrics s
LEFT JOIN prev_week_store_metrics p ON s.store = p.store
ORDER BY s.total_revenue DESC;

This SQL output is the core table of the weekly report — revenue, order count, average order value per store, and week-over-week trends. 📈 means growth over 5%, 📉 means decline over 5%.

Best-Selling Dishes + Complaint Alerts

The boss also cares about dish-level data. Window functions handle this elegantly:

-- ========== Part 2: Dish Rankings + Complaint Alerts ==========
WITH dish_performance AS (
    SELECT 
        o.dish,
        SUM(o.quantity) AS total_sold,
        SUM(o.price * o.quantity) AS total_revenue,
        ROUND(AVG(r.rating), 1) AS avg_rating,
        COUNT(r.rating) AS review_count
    FROM orders o
    LEFT JOIN reviews r 
        ON o.store = r.store AND o.dish = r.dish
        AND r.review_date BETWEEN '2026-06-23' AND '2026-06-29'
    WHERE o.order_date BETWEEN '2026-06-23' AND '2026-06-29'
    GROUP BY o.dish
),
ranked_dishes AS (
    SELECT *,
        RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank,
        RANK() OVER (ORDER BY avg_rating ASC) AS rating_rank,
        CASE 
            WHEN avg_rating IS NOT NULL AND avg_rating < 3.0 THEN '⚠️ Low Rating Alert'
            WHEN avg_rating IS NOT NULL AND avg_rating >= 4.5 THEN '⭐ Top Rated'
            ELSE '✅ Normal'
        END AS status
    FROM dish_performance
)

SELECT 
    dish,
    total_sold,
    ROUND(total_revenue, 0)::INTEGER AS revenue,
    avg_rating,
    review_count,
    revenue_rank,
    status
FROM ranked_dishes
ORDER BY revenue_rank;

The output is crystal clear:

Kung Pao Chicken: Sold 6, Revenue 192, Rating 5.0, Reviews 2, Rank #1 → ⭐ Top Rated
Fish Hot Pot: Sold 4, Revenue 232, Rating 3.0, Reviews 2, Rank #2 → ⚠️ Low Rating Alert
Mapo Tofu: Sold 5, Revenue 110, Rating 3.0, Reviews 1, Rank #3 → ⚠️ Low Rating Alert
Braised Pork: Sold 2, Revenue 96, Rating 4.0, Reviews 1, Rank #4 → ✅ Normal

The boss immediately sees: Fish Hot Pot ranks #2 in revenue but only has a 3.0 rating — needs attention.


Step 3: Python Automation — From SQL to Product

The SQL is ready. Now wrap it into an automated product with Python:

import duckdb
import smtplib
from email.mime.text import MIMEText
from datetime import datetime, timedelta
from jinja2 import Template

class WeeklyReportGenerator:
    def __init__(self, db_path="data/orders.duckdb"):
        self.con = duckdb.connect(db_path)
    
    def generate_report(self):
        """Generate complete weekly report"""
        today = datetime.now()
        week_start = (today - timedelta(days=today.weekday())).strftime('%Y-%m-%d')
        week_end = (today + timedelta(days=6 - today.weekday())).strftime('%Y-%m-%d')
        
        # Store metrics
        store_report = self.con.execute("""
            WITH weekly_store_metrics AS (
                SELECT store,
                    COUNT(DISTINCT order_id) AS order_count,
                    SUM(price * quantity) AS total_revenue,
                    ROUND(SUM(price * quantity) / COUNT(DISTINCT order_id), 2) AS avg_order_value
                FROM orders
                WHERE order_date >= ? AND order_date <= ?
                GROUP BY store
            ),
            prev_week_store_metrics AS (
                SELECT store, SUM(price * quantity) AS prev_revenue
                FROM orders
                WHERE order_date >= date_sub(?, INTERVAL 7 DAY) 
                  AND order_date <= date_sub(?, INTERVAL 7 DAY)
                GROUP BY store
            )
            SELECT s.store, s.order_count, s.total_revenue, s.avg_order_value,
                COALESCE(p.prev_revenue, 0) AS prev_week_revenue,
                CASE 
                    WHEN COALESCE(p.prev_revenue, 0) > 0 
                    THEN ROUND((s.total_revenue - p.prev_revenue) / p.prev_revenue * 100, 1)
                    ELSE NULL
                END AS wow_change_pct,
                CASE 
                    WHEN COALESCE(p.prev_revenue, 0) > 0 
                         AND (s.total_revenue - p.prev_revenue) / p.prev_revenue > 0.05 
                        THEN '📈'
                    WHEN COALESCE(p.prev_revenue, 0) > 0 
                         AND (s.total_revenue - p.prev_revenue) / p.prev_revenue < -0.05 
                        THEN '📉'
                    ELSE '➡️'
                END AS trend
            FROM weekly_store_metrics s
            LEFT JOIN prev_week_store_metrics p ON s.store = p.store
            ORDER BY s.total_revenue DESC
        """, [week_start, week_end, week_start, week_start]).fetchdf()
        
        # Dish rankings
        dish_report = self.con.execute("""
            WITH dish_performance AS (
                SELECT o.dish,
                    SUM(o.quantity) AS total_sold,
                    SUM(o.price * o.quantity) AS total_revenue,
                    ROUND(AVG(r.rating), 1) AS avg_rating,
                    COUNT(r.rating) AS review_count
                FROM orders o
                LEFT JOIN reviews r ON o.store = r.store AND o.dish = r.dish
                    AND r.review_date >= ? AND r.review_date <= ?
                WHERE o.order_date >= ? AND o.order_date <= ?
                GROUP BY o.dish
            ),
            ranked_dishes AS (
                SELECT *,
                    RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank,
                    CASE 
                        WHEN avg_rating IS NOT NULL AND avg_rating < 3.0 
                            THEN '⚠️ Low Rating Alert'
                        WHEN avg_rating IS NOT NULL AND avg_rating >= 4.5 
                            THEN '⭐ Top Rated'
                        ELSE '✅ Normal'
                    END AS status
                FROM dish_performance
            )
            SELECT dish, total_sold, 
                   ROUND(total_revenue, 0)::INTEGER as total_revenue,
                   avg_rating, review_count, revenue_rank, status
            FROM ranked_dishes
            ORDER BY revenue_rank
        """, [week_start, week_end, week_start, week_end]).fetchdf()
        
        return store_report, dish_report
    
    def render_html(self, store_df, dish_df):
        """Render as HTML email"""
        html_template = """
        <h2>📊 Weekly Store Report ({{ week }})</h2>
        
        <h3>🏪 Store Overview</h3>
        <table>
        <tr><th>Store</th><th>Orders</th><th>Revenue</th><th>Avg Order</th><th>WoW Change</th><th>Trend</th></tr>
        {% for row in store_rows %}
        <tr>
            <td>{{ row.store }}</td>
            <td>{{ row.order_count }}</td>
            <td>¥{{ "%.0f"|format(row.total_revenue) }}</td>
            <td>¥{{ "%.2f"|format(row.avg_order_value) }}</td>
            <td>{{ "%.1f"|format(row.wow_change_pct) if row.wow_change_pct else '-' }}%</td>
            <td>{{ row.trend }}</td>
        </tr>
        {% endfor %}
        </table>
        
        <h3>🍽️ Dish Rankings</h3>
        <table>
        <tr><th>Dish</th><th>Sold</th><th>Revenue</th><th>Rating</th><th>Status</th></tr>
        {% for row in dish_rows %}
        <tr>
            <td>{{ row.dish }}</td>
            <td>{{ row.total_sold }}</td>
            <td>¥{{ row.total_revenue }}</td>
            <td>{{ row.avg_rating }}</td>
            <td>{{ row.status }}</td>
        </tr>
        {% endfor %}
        </table>
        """
        template = Template(html_template)
        week_str = f"{store_df['order_date'].min()} ~ {store_df['order_date'].max()}"
        return template.render(
            week=week_str,
            store_rows=store_df.to_dict('records'),
            dish_rows=dish_df.to_dict('records')
        )
    
    def send_report(self, store_df, dish_df, to_email="[email protected]"):
        """Send email report"""
        html_body = self.render_html(store_df, dish_df)
        msg = MIMEText(html_body, 'html', 'utf-8')
        msg['Subject'] = f'📊 Weekly Store Report - {datetime.now().strftime("%Y-%m-%d")}'
        msg['From'] = '[email protected]'
        msg['To'] = to_email
        
        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.starttls()
        server.login('[email protected]', 'your_password')
        server.send_message(msg)
        server.quit()

# One-click execution
reporter = WeeklyReportGenerator("data/orders.duckdb")
stores, dishes = reporter.generate_report()
reporter.send_report(stores, dishes)

Step 4: Deployment Options & Pricing

Three Deployment Models

1. Local Version (¥3,000/month)

Run cron tasks on your VPS to auto-generate and email reports weekly. Suitable for clients with fewer than 5 stores.

2. Web Dashboard Version (¥5,000/month)

Build a simple web page with Streamlit or Gradio. Clients log in to see real-time data. DuckDB query results feed directly to the frontend.

# Streamlit dashboard core code
import streamlit as st
import duckdb

con = duckdb.connect("data/orders.duckdb")

st.title("📊 Store Dashboard")

week_start = st.date_input("Start Date", ...)
week_end = st.date_input("End Date", ...)

st.subheader("Store Performance")
store_df = con.execute("""
    SELECT store, COUNT(*) as order_count, 
           SUM(price * quantity) as total_revenue
    FROM orders 
    WHERE order_date BETWEEN ? AND ?
    GROUP BY store
""", [str(week_start), str(week_end)]).fetchdf()

st.dataframe(store_df)

3. SaaS Multi-Tenant Version (¥8,000+/month)

One system serving multiple restaurant brands, each with independent databases. Use DuckDB’s ATTACH feature:

ATTACH 'client_a.duckdb' AS client_a;
ATTACH 'client_b.duckdb' AS client_b;

SELECT 'client_a' AS client, * FROM client_a.orders
UNION ALL
SELECT 'client_b' AS client, * FROM client_b.orders;

Customer Acquisition Channels

  • Search “data analysis outsourcing” on Xianyu/Taobao, proactively contact merchants still using Excel
  • Give free presentations at local restaurant associations, demo DuckDB analysis live
  • Post “saved a restaurant boss 10 hours/week” case studies on Xiaohongshu (Little Red Book)

Step 5: Why This Business Model Works

DimensionDuckDBExcelPython + PandasBI Tools (Tableau/Power BI)
Learning Curve⭐ Very Low⭐ Low⭐⭐ Medium⭐⭐⭐ High
Million-row Processing⚡ Seconds❌ Crashes✅ OK✅ OK
Deployment Cost¥200/mo VPSFree¥200/mo VPS¥500-2000/mo
Automation✅ Perfect❌ Needs VBA✅ Perfect⚠️ Limited
Client Perceived Value⭐⭐⭐ Professional⭐ Basic⚠️ Needs UI⭐⭐ Dashboard
Maintenance CostVery LowHighLowHigh

DuckDB’s core advantage: delivering professional data products at minimal cost.

  1. Low technical barrier, high perceived value: DuckDB’s SQL covers 90% of analysis needs, but you deliver a “professional report received automatically every week” — the perceived value far exceeds your cost.

  2. High retention rate: Once a boss gets used to receiving automated weekly reports, they won’t cancel. This is a classic “can’t go back” product.

  3. Highly scalable: After one client succeeds, the marginal cost to replicate for 10 more clients is near zero. DuckDB runs in memory — a ¥200/month VPS can serve dozens of clients.

  4. Differentiated competition: Most data analysts compete with big tech companies. The SME automated reporting market is virtually empty.


Monetization Path Summary

Find 1 SME with data
  → Build analysis queries with DuckDB
  → Wrap in Python automation
  → Schedule weekly delivery
  → Charge ¥3,000-8,000/month

No complex architecture needed. No big data clusters required. One DuckDB instance is enough.

The next client could be someone you reach out to tonight.


Further Learning

The complete code templates (including Streamlit dashboards, multi-industry adaptations, and cron scheduling configurations) have been organized into a full project. If you want to dive deeper into DuckDB applications in business scenarios — including more industry cases and delivery SOPs — visit duckdblab.org for the complete tutorial series.

💡 More DuckDB monetization strategies and complete project templates → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.