Featured image of post DuckDB End-to-End Automated Report Pipeline: From Data Source to Email Delivery

DuckDB End-to-End Automated Report Pipeline: From Data Source to Email Delivery

Build a complete DuckDB + Python automated reporting pipeline from scratch. Covers batch CSV ingestion, SQL aggregation, formatted Excel output, scheduled execution, and email delivery.

DuckDB End-to-End Automated Report Pipeline: From Data Source to Email Delivery

Toolchain | For: Data analysts who manually compile reports every week


The Pain Point

You spend 3 hours every Monday morning manually compiling Excel reports — repetitive, boring, and error-prone. With a DuckDB-based automated reporting system, this workflow compresses to 5 minutes, or even runs fully autonomously.

This article walks you through building a complete DuckDB + Python automated report pipeline, covering data ingestion, analytical queries, formatted output, scheduled execution, and email delivery.


System Architecture Overview

Data Sources (Excel/CSV/SQL)
 ↓
DuckDB (Unified Query Layer)
 ↓
Python Processing (Cleaning/Aggregation)
 ↓
Output (Excel/PDF/Email Delivery)

Key advantages: DuckDB reads Excel and CSV files natively without intermediate conversion; SQL表达能力 strong for complex aggregations in a single query; combined with Python’s ecosystem, it can string together an entire workflow.


Step 1: Environment Setup

pip install duckdb openpyxl pandas

Verify installation:

import duckdb
print(duckdb.__version__) # Should print the version number

Step 2: Connect to Data Sources

Assume you have an e-commerce sales data folder sales_data/ containing multiple CSV files, each representing one month of sales records.

import duckdb
import pandas as pd
from pathlib import Path

# Connect to a persistent DuckDB database (data saved to disk)
con = duckdb.connect('sales_automation.duckdb')

# Batch-read all CSV files (auto-merge, no need to read individually)
con.execute("""
 CREATE TABLE all_sales AS
 SELECT * FROM read_csv_auto('sales_data/*.csv', hive_partitioning=true)
""")

# Preview the data structure
result = con.execute("DESCRIBE all_sales").fetchdf()
print(result)

Key techniques:

  • read_csv_auto automatically infers column types — no need to manually specify schemas
  • hive_partitioning=true supports automatic partitioning by directory structure; dates in filenames are extracted as partition columns
  • DuckDB’s columnar storage makes aggregation queries 10x faster than Pandas

Step 3: Core Analytical Queries

3.1 Monthly Sales Summary

monthly_summary = con.execute("""
 SELECT 
 DATE_TRUNC('month', sale_date) AS month,
 SUM(revenue) AS total_revenue,
 COUNT(DISTINCT customer_id) AS active_customers,
 AVG(order_value) AS avg_order_value,
 SUM(CASE WHEN region = 'East' THEN revenue ELSE 0 END) AS east_region_revenue
 FROM all_sales
 GROUP BY DATE_TRUNC('month', sale_date)
 ORDER BY month DESC
""").fetchdf()

print(monthly_summary)

3.2 Top Products Ranking (with window functions)

top_products = con.execute("""
 SELECT 
 product_name,
 category,
 SUM(quantity) AS total_qty,
 SUM(revenue) AS total_revenue,
 RANK() OVER (ORDER BY SUM(revenue) DESC) AS revenue_rank
 FROM all_sales
 GROUP BY product_name, category
 HAVING SUM(revenue) > 1000
 ORDER BY total_revenue DESC
 LIMIT 20
""").fetchdf()

3.3 Customer Segmentation

customer_segment = con.execute("""
 SELECT 
 customer_id,
 COUNT(*) AS order_count,
 SUM(revenue) AS total_spend,
 MAX(sale_date) AS last_purchase,
 CASE 
 WHEN SUM(revenue) > 10000 THEN 'VIP'
 WHEN SUM(revenue) > 5000 THEN 'High-Value'
 WHEN SUM(revenue) > 1000 THEN 'Regular'
 ELSE 'Potential'
 END AS segment
 FROM all_sales
 GROUP BY customer_id
 ORDER BY total_spend DESC
""").fetchdf()

Step 4: Generate Formatted Reports

4.1 Excel Format (Styled)

import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment

def generate_excel_report(df_dict, output_path='report.xlsx'):
 with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
 for sheet_name, df in df_dict.items():
 df.to_excel(writer, sheet_name=sheet_name, index=False)
 
 # Style the header row
 ws = writer.sheets[sheet_name]
 header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
 header_font = Font(bold=True, color='FFFFFF')
 
 for cell in ws[1]:
 cell.fill = header_fill
 cell.font = header_font
 cell.alignment = Alignment(horizontal='center')
 
 # Auto-fit column widths
 for column in ws.columns:
 max_length = 0
 column_letter = column[0].column_letter
 for cell in column:
 try:
 if len(str(cell.value)) > max_length:
 max_length = len(str(cell.value))
 except:
 pass
 adjusted_width = min(max_length + 2, 50)
 ws.column_dimensions[column_letter].width = adjusted_width
 
 print(f'Report generated: {output_path}')

# Generate multi-sheet report
report_data = {
 'Monthly Summary': monthly_summary,
 'Top Products': top_products,
 'Customer Segments': customer_segment
}

generate_excel_report(report_data, 'weekly_report.xlsx')

4.2 Plain Text Format (Minimal)

def generate_text_report(df_dict, output_path='report.txt'):
 with open(output_path, 'w', encoding='utf-8') as f:
 f.write('=' * 60 + '\n')
 f.write('Weekly Data Report\n')
 f.write('=' * 60 + '\n\n')
 
 for sheet_name, df in df_dict.items():
 f.write(f'\n[{sheet_name}]\n')
 f.write(df.to_string(index=False))
 f.write('\n' + '-' * 60 + '\n')
 
 print(f'Text report generated: {output_path}')

Step 5: Scheduled Execution (Three Options)

Option A: Python schedule library (local execution)

import schedule
import time
from datetime import datetime

def run_weekly_report():
 print(f'{datetime.now()} - Starting weekly report...')
 
 # Reload latest data
 con.execute("DROP TABLE IF EXISTS all_sales")
 con.execute("""
 CREATE TABLE all_sales AS
 SELECT * FROM read_csv_auto('sales_data/*.csv', hive_partitioning=true)
 """)
 
 # Execute analysis queries
 monthly_summary = con.execute("""
 SELECT DATE_TRUNC('month', sale_date) AS month,
 SUM(revenue) AS total_revenue, COUNT(DISTINCT customer_id) AS active_customers
 FROM all_sales GROUP BY DATE_TRUNC('month', sale_date)
 ORDER BY month DESC
 """).fetchdf()
 
 # Output report
 report_data = {'Monthly Summary': monthly_summary}
 generate_excel_report(report_data, f'report_{datetime.now().strftime("%Y%m%d")}.xlsx')
 print('Weekly report generated!')

# Run every Monday at 9:00 AM
schedule.every().monday.at("09:00").do(run_weekly_report)

while True:
 schedule.run_pending()
 time.sleep(60)

Option B: System Cron (Linux/Mac)

crontab -e

Add this line (runs every Monday at 9:00 AM):

0 9 * * 1 cd /path/to/project && python3 weekly_report.py

Option C: GitHub Actions (cloud-based, free)

Create .github/workflows/report.yml:

name: Weekly Report
on:
 schedule:
 - cron: '0 9 * * 1' # Monday 9:00 UTC
 workflow_dispatch:

jobs:
 build-report:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v3
 - name: Set up Python
 uses: actions/setup-python@v4
 with:
 python-version: '3.11'
 - name: Install dependencies
 run: pip install duckdb openpyxl pandas
 - name: Generate report
 run: python3 weekly_report.py
 - name: Upload artifact
 uses: actions/upload-artifact@v3
 with:
 name: weekly-report
 path: report_*.xlsx

Step 6: Email Delivery (Optional)

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders

def send_email_report(to_email, report_path, subject='Weekly Report'):
 msg = MIMEMultipart()
 msg['From'] = '[email protected]'
 msg['To'] = to_email
 msg['Subject'] = subject
 
 body = 'Please find this week\'s data report attached.'
 msg.attach(MIMEText(body, 'plain', 'utf-8'))
 
 with open(report_path, 'rb') as f:
 attachment = MIMEBase('application', 'octet-stream')
 attachment.set_payload(f.read())
 encoders.encode_base64(attachment)
 attachment.add_header('Content-Disposition', f'attachment; filename={report_path}')
 msg.attach(attachment)
 
 with smtplib.SMTP('smtp.example.com', 587) as server:
 server.starttls()
 server.login('[email protected]', 'your_password')
 server.send_message(msg)
 
 print(f'Email sent to {to_email}')

Comparison with Traditional Approaches

DimensionTraditional Pandas ApproachDuckDB + Python Approach
Multi-file readingMust read_csv + concat one by oneread_csv_auto('*.csv') in one line
Memory usageAll data loaded into memoryColumnar storage, compute-on-demand
Query performanceNoticeably slow above 1M rowsSub-second response for millions of rows
File supportRequires additional librariesNative support for CSV/Parquet/JSON/Excel/SQLite/PostgreSQL
Deployment complexityDependencies on Pandas + multiple libsSingle DuckDB dependency

本文信息

项目内容
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