
Why Do You Need Incremental Updates?
Every morning at 9 AM, you spend 1-2 hours re-running yesterday’s sales data analysis — exporting CSVs, cleaning, aggregating, generating reports, sending to WeChat groups. Day after day, year after year.
This isn’t “hard work” — it’s low tool efficiency.
Assume you process 100,000 orders daily, which is 3 million per month. The problems with full re-runs:
- You read all historical data every day, wasting 90%+ of compute resources
- Report generation time grows linearly with data volume — in six months, it could go from 30 seconds to 10 minutes
- You can’t achieve “real-time” — you can only run at fixed daily intervals
The core idea of incremental updates: Instead of re-running everything from scratch, only process new data and merge it incrementally into the main table.
Initialize Database and Table Schema
import duckdb
from datetime import datetime, timedelta
import random
# Create database (first run only)
con = duckdb.connect("daily_report.db")
# Create main orders table
con.execute("""
CREATE TABLE IF NOT EXISTS orders (
order_id VARCHAR PRIMARY KEY,
order_date DATE,
product_id VARCHAR,
product_name VARCHAR,
quantity INTEGER,
unit_price DECIMAL(10,2),
region VARCHAR,
channel VARCHAR,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
# Create daily summary report table
con.execute("""
CREATE TABLE IF NOT EXISTS daily_summary (
report_date DATE PRIMARY KEY,
total_orders INTEGER,
total_revenue DECIMAL(12,2),
avg_order_value DECIMAL(10,2),
top_product VARCHAR,
top_product_revenue DECIMAL(12,2),
region_revenue JSON,
channel_revenue JSON,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
# Create staging table for incremental data
con.execute("""
CREATE TABLE IF NOT EXISTS daily_staging (
order_id VARCHAR,
order_date DATE,
product_id VARCHAR,
product_name VARCHAR,
quantity INTEGER,
unit_price DECIMAL(10,2),
region VARCHAR,
channel VARCHAR
);
""")
print("✅ Database initialized")
Simulate Daily New Orders
def generate_daily_orders(days=30):
"""Generate simulated order data for specified days"""
products = [
('P001', 'iPhone 15', 7999),
('P002', 'MacBook Pro', 14999),
('P003', 'AirPods Pro', 1899),
('P004', 'iPad Air', 4799),
('P005', 'Apple Watch', 2999),
('P006', 'AirTag', 229),
('P007', 'Magic Keyboard', 999),
('P008', 'Studio Display', 11999),
]
regions = ['East', 'South', 'North', 'Southwest', 'Central', 'Northeast', 'Northwest']
channels = ['Tmall', 'JD', 'Pinduoduo', 'Douyin', 'Official App']
all_orders = []
for day_offset in range(days):
date = datetime.now() - timedelta(days=day_offset)
num_orders = random.randint(80, 200)
for _ in range(num_orders):
product = random.choice(products)
quantity = random.randint(1, 5)
order_id = f"ORD{date.strftime('%Y%m%d')}{random.randint(1000, 9999)}"
all_orders.append({
'order_id': order_id,
'order_date': date,
'product_id': product[0],
'product_name': product[1],
'quantity': quantity,
'unit_price': product[2],
'region': random.choice(regions),
'channel': random.choice(channels),
})
return all_orders
# Generate 30 days of historical data
print("📊 Generating historical data...")
all_orders = generate_daily_orders(30)
print(f"✅ Generated {len(all_orders)} order records")
Core: MERGE UPSERT for Idempotent Loading
On the first run, use MERGE to ensure idempotency — running it repeatedly won’t create duplicates:
def initial_load(orders):
"""Initial full data load"""
# Batch insert into staging table
for order in orders:
con.execute("""
INSERT INTO daily_staging
(order_id, order_date, product_id, product_name, quantity, unit_price, region, channel)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", [
order['order_id'], order['order_date'], order['product_id'],
order['product_name'], order['quantity'], order['unit_price'],
order['region'], order['channel']
])
# Use MERGE for idempotent loading (UPSERT)
con.execute("""
MERGE INTO orders AS target
USING daily_staging AS source
ON target.order_id = source.order_id
WHEN NOT MATCHED THEN
INSERT (order_id, order_date, product_id, product_name,
quantity, unit_price, region, channel)
VALUES (source.order_id, source.order_date, source.product_id,
source.product_name, source.quantity, source.unit_price,
source.region, source.channel)
WHEN MATCHED THEN
UPDATE SET
quantity = source.quantity,
unit_price = source.unit_price,
updated_at = CURRENT_TIMESTAMP
""")
# Clear staging table
con.execute("TRUNCATE TABLE daily_staging")
loaded = con.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
print(f"✅ Initial load complete: {loaded} orders")
return loaded
Incremental Update: Only Process New Data
This is the core of the entire system — every day only new data is processed, then the report is updated incrementally via MERGE:
def incremental_update(new_orders):
"""Incremental update: only process new data, update reports"""
# 1. Insert new data into staging
for order in new_orders:
con.execute("""
INSERT INTO daily_staging
(order_id, order_date, product_id, product_name, quantity, unit_price, region, channel)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", [
order['order_id'], order['order_date'], order['product_id'],
order['product_name'], order['quantity'], order['unit_price'],
order['region'], order['channel']
])
# 2. Incremental load to main table (only insert non-existent records)
con.execute("""
MERGE INTO orders AS target
USING daily_staging AS source
ON target.order_id = source.order_id
WHEN NOT MATCHED THEN
INSERT (order_id, order_date, product_id, product_name,
quantity, unit_price, region, channel)
VALUES (source.order_id, source.order_date, source.product_id,
source.product_name, source.quantity, source.unit_price,
source.region, source.channel)
""")
con.execute("TRUNCATE TABLE daily_staging")
# 3. Incremental update daily report (key step)
con.execute("""
MERGE INTO daily_summary AS target
USING (
SELECT
order_date AS report_date,
COUNT(*) AS total_orders,
ROUND(SUM(quantity * unit_price), 2) AS total_revenue,
ROUND(AVG(quantity * unit_price), 2) AS avg_order_value,
(SELECT product_name FROM orders o2
WHERE o2.order_date = orders.order_date
GROUP BY product_name
ORDER BY SUM(quantity * unit_price) DESC
LIMIT 1) AS top_product,
(SELECT ROUND(SUM(quantity * unit_price), 2)
FROM orders o2
WHERE o2.order_date = orders.order_date
GROUP BY product_name
ORDER BY SUM(quantity * unit_price) DESC
LIMIT 1) AS top_product_revenue,
(SELECT json_group_object(region, ROUND(SUM(quantity * unit_price), 2))
FROM orders o3 WHERE o3.order_date = orders.order_date
) AS region_revenue,
(SELECT json_group_object(channel, ROUND(SUM(quantity * unit_price), 2))
FROM orders o4 WHERE o4.order_date = orders.order_date
) AS channel_revenue
FROM orders
WHERE order_date = (SELECT MAX(order_date) FROM orders)
GROUP BY order_date
) AS source
ON target.report_date = source.report_date
WHEN NOT MATCHED THEN
INSERT (report_date, total_orders, total_revenue, avg_order_value,
top_product, top_product_revenue, region_revenue, channel_revenue)
VALUES (source.report_date, source.total_orders, source.total_revenue,
source.avg_order_value, source.top_product, source.top_product_revenue,
source.region_revenue, source.channel_revenue)
WHEN MATCHED THEN
UPDATE SET
total_orders = source.total_orders,
total_revenue = source.total_revenue,
avg_order_value = source.avg_order_value,
top_product = source.top_product,
top_product_revenue = source.top_product_revenue,
region_revenue = source.region_revenue,
channel_revenue = source.channel_revenue,
updated_at = CURRENT_TIMESTAMP
""")
print("✅ Incremental update complete")
Performance Comparison
| Metric | Full Re-run | Incremental Update |
|---|---|---|
| Daily data processed | 30,000 (full) | 1,000 (new only) |
| Report generation time | 2 hours → grows over time | < 30 seconds → constant |
| Resource consumption | High (full read every time) | Low (only new data) |
| Data consistency | Recalculated each time | Incremental merge + weekly full verification |
| Best for | Data < 10K rows | Continuously growing datasets |
For a scenario processing 300K records monthly, incremental updates can reduce daily report generation from 30 seconds to under 2 seconds.
Advanced: Materialized Views for Speed
For reports that need frequent querying, create materialized views to cache results:
-- Create materialized view caching last 30 days of daily reports
CREATE MATERIALIZED VIEW IF NOT EXISTS mv_30d_summary AS
SELECT
report_date,
total_orders,
total_revenue,
avg_order_value,
top_product,
top_product_revenue,
region_revenue,
channel_revenue
FROM daily_summary
WHERE report_date >= CURRENT_DATE - INTERVAL '30' DAY;
Query directly from the materialized view for 10x+ speed improvement:
SELECT * FROM mv_daily_trend ORDER BY report_date DESC LIMIT 7;
Automation Scheduling
#!/bin/bash
# daily_report_incremental.sh — Auto incremental update at 3 AM daily
cd ~/daily-report-system
# 1. Fetch yesterday's new data (from API or data warehouse sync)
python3 fetch_yesterday_orders.py
# 2. Run incremental update
python3 incremental_update.py
# 3. Generate today's brief and send notification
python3 send_report.py
# 4. Log execution
echo "[$(date)] Incremental update complete" >> /var/log/daily_report.log
Crontab configuration:
# Auto incremental update on workdays at 3 AM
0 3 * * 1-5 /home/user/daily-report-system/daily_incremental.sh
# Full verification every Sunday at 4 AM (ensure data consistency)
0 4 * * 0 /home/user/daily-report-system/daily_full_verify.py
💰 Monetization Suggestions
Sell reporting services: Many SMBs lack data teams and spend $300-700/month on manual reporting. Deploy this system once, charge $70-140/month maintenance per client. 10 clients = $700-1,400/month.
Sell the automation system: Package this as a “Daily Report Auto-Generator” for a one-time fee of $400-1,000, targeting SMBs with data needs.
Build a SaaS product: Turn the incremental update + materialized view approach into a SaaS. Monthly subscription where clients upload their own data and the system auto-generates reports.
The core selling point: “Your daily report, from 2 hours to 30 seconds.”
🎯 Action Plan for Tonight
- Create a DuckDB database and set up the basic framework using the code above
- Replace simulated data with your real data and test incremental updates
- Set up crontab for daily automated execution
- Send the generated reports to a WeChat group or enterprise WeChat to experience the satisfaction of automation
Next step: Expose incremental update results via FastAPI as an API, building a real-time data product.
📺 More DuckDB tutorials → youtube.com/@duckdblab